main
19 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
1c1fba0c0f |
fix(macos): place fresh windows correctly (#369)
* fix(macos): place fresh windows correctly - Separate restored, explicit, and default window placement from persistence policy. - Honor explicit origins and restore policy across platform seams and both macOS hosts. - Add placement coverage, update docs, and remove obsolete example workarounds. * fix(macos): honor window placement policies * fix(macos): correct window placement policies * fix(macos): preserve window placement contracts * fix(runtime): restore secondary window placement |
||
|
|
283ab804c0 |
Add cross-platform audio capture and voice memo example (#303)
* Add cross-platform audio capture and voice memo example - Add microphone and system-audio capture effects for macOS, Windows, and TypeScript core. - Add a polished voice memo example with private app-data WAV saving and playback. - Document platform support and packaging requirements with runtime and conformance coverage. Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com> * Fix audio capture teardown and permissions Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com> * Fix audio capture lifecycle and metadata Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com> * Fix audio capture lifecycle edge cases Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com> * fix: harden audio capture lifecycle Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com> --------- Co-authored-by: Marcus Schiesser <17126+marcusschiesser@users.noreply.github.com> |
||
|
|
8fc933b9db |
fix(windows): accelerate gpu surface presentation (#258)
* fix(windows): accelerate gpu surface presentation - Render retained binary canvas packets through Direct2D and DirectWrite. - Preserve dirty-region updates and GPU effects with a safe software fallback. - Wire and validate the renderer across supported Windows build paths. Co-authored-by: Omer Shatzberg <131801941+oshtz@users.noreply.github.com> * fix(windows): harden Direct2D presentation * fix(sdk): separate GPU backend request types * fix(windows): honor GPU surface fallback contracts * fix(windows): scale transformed blur kernels * fix: harden Windows GPU surface presentation * fix(windows): harden gpu blur and caption sampling * fix(runtime): bypass packets for software surfaces * fix(windows): preserve precise gpu surface updates * fix(windows): reconcile GPU presenter state * fix: preserve retained canvas resources * fix: preserve Windows canvas packet fidelity --------- Co-authored-by: Omer Shatzberg <131801941+oshtz@users.noreply.github.com> |
||
|
|
87fa3f92d0 |
Video playback: AVFoundation through the media surface, replayable end to end (#184)
* Add the video playback tier: platform seam, effects channel, journal v9
- One video channel mirroring audio end to end: loadVideo with the local-then-URL cascade and surface claim, transport verbs (play/pause/stop/seek/volume/mute/loop), key-stamped events, honest failed/rejected degrades, and automation-snapshot mirrors
- Pixels never enter the core: the platform decoder pushes RGBA8 through a copyable VideoFrameSink into the media-surface texture channel the load claimed
- Journal format v9: the .video effect-result kind (code 13) and platform-event tag (code 25) append the video fields after the v8 channel fields and journal every event verbatim for byte-identical replay with no producer attached
* Cover the video tier: lifecycle, frames, teardown, replay identity
- Fake and real executor batteries mirror the audio suite: request capture, event round trips, transport mirrors, rejection classes, cascade order, staged-host degrade, straggler swallowing, and the quit-while-playing stop hook
- Sink coverage proves decoded frames reach the claimed surface, replace/stop release the claim, and a stale sink push lands inert
- A recorded playback replays byte-identical into a decoder-less host with no producer attached, fingerprint and model equal; journal codecs round-trip the video event and effect shapes
* Export the video effect types and decline video on the embed host
- native_sdk.EffectVideo/EffectVideoEventKind/EffectVideoSource ride the SDK roots like their audio twins
- The mobile embed host declines video_playback until a shim registers a real decoder, the audio honesty rule
* Decode video on macOS with AVFoundation; teach on Windows and Linux
- One AVPlayer in the AppKit host: AVPlayerItemVideoOutput frames fitted to the sink's pixel budget, BGRA-to-RGBA vImage swizzle, a 1/60s run-loop frame pump plus the audio tier's 0.5s position clock, loop wraps without a completion, and paused seeks still paint their frame
- The frame sink crosses the C ABI as a callconv(.c) trampoline over the runtime's VideoFrameSink; a released claim answers 1 and the host stops its pump
- Windows and Linux stage the capability honestly: video_playback reports false and the load verbs answer a named teaching plus error.UnsupportedService; the CEF host stubs the video C ABI like audio
* Add Cmd.videoLoad and videoCtl to the TypeScript tier at opcodes 0x17/0x18
- videoLoad claims the named media-surface, resolves the local-then-URL cascade, and bakes autoplay/loop/muted into one flags byte; videoCtl drives play/pause/stop/seek/volume/muted/loop by verb ordinal with an f64 value
- The event arm is the seven-field record matched by NAME with the exact five-member state union both directions, the audio arm convention; refusals teach with the NS1027/NS1029/NS1030 vocabulary
- The host routes events on the TSVI key namespace, parks loads under the fake executor like audio, and covers the wire byte-for-byte in the package, host, and e2e suites
* Declare video from markup: the <video> element, house chrome, reconciler
- Element code 68 with flag attrs controls/autoplay/loop/muted at codes 82-85 (src rides the existing attr name; registry law keeps names unique); a leaf on all three surfaces — validator, interpreter, compiled engine — rejecting children like image
- ui.video composes the playback surface on the framework-owned surface id plus runtime-consumed transport chrome (ghost play/pause, proportional scrub slider, clipped time readouts in the built-in bar's register); presence IS playback: the ui-app reconciler loads on src change, applies loop/muted deltas, and stops when the element leaves the view
- Handler-less playback stays honest under replay: platform video events steer the channel mirrors while journaled effect records remain the only Msg source, so house-chrome sessions replay with live readouts and identical fingerprints
* Scope declarative video ownership to the playback it started
- The reconciler stops or retunes the channel only while the active key is the declaration's own: an update handler that loaded its own playback owns the single player, and a departing <video> element must not kill it
* Add the video-player example: house chrome and custom controls
- Player screen is one declarative ui.video with the house transport chrome; Custom screen composes its own bar (transport, +/-10s, proportional scrub, volume, mute, loop) from the command vocabulary over a bare media surface
- No bundled media: the launch argument or the source field names a local clip or http(s) URL, so a live macOS check is one command away
- Headless tests drive the whole transport with the fake executor's synthetic events, the automation widget path, and the null decoder behind the declarative screen
* Document the video element across the docs surfaces
- Components page, catalog entry, native-ui element table row, and the LSP/vocab doc strings for src/controls/autoplay/loop/muted
- Deterministic preview scenes render the placeholder with the house chrome (regenerated video webps only; the untouched catalog keeps its committed renders)
- Changelog fragment states the new element, the command vocabulary, the staged platforms, and the journal v9 break
* Resolve video attribute docs in the markup LSP hover path
- attributeDoc consults the video scoped table so controls/autoplay/loop/muted hover like every registry attribute; the coverage pin now names the video composite
* Make the video-player example's docs and status line honest
- README run instructions use the real CLI verbs: native dev for build-and-run, or native build plus the zig-out binary with the clip argument (the argument passthrough shape)
- The status line now matches each screen's ownership model: the declarative Player screen shows a static teaching (its transport state lives in the runtime-owned chrome, so an event-fed status there could only lie or stall), while the Custom screen keeps the event-fed loading/dimensions/finished/failed line it owns; tests pin both across load, playback, pause, and completion
- Direct-SDK examples handle the runtime's video event in their exhaustive event switches
* Stage video events non-lossily and deliver fed terminals under replay
- Video events leave the lossy pending ring for their own non-lossy stage (the image/channel discipline): a loop-side .rejected or .failed is its load call's only terminal and a fed event is one recorded delivery, so a burst past the ring's capacity must never evict one — pinned by a 40-rejection burst test
- Fed events capture the handler at feed time and deliver their journaled values verbatim when the channel no longer resolves them: under replay the platform .failed event that follows the record applies the channel reset first, and delivery-time resolution silently dropped the Msg the recording dispatched — pinned by a recorded mid-playback failure replaying fingerprint-identical
- loadVideo's deterministic refusal classes extract to the pure videoLoadRejected, one source of truth a caller-side validator can consult
* Refuse an invalid video_load before it re-routes the bridge entry
- The engine keeps the current playback when it rejects a load, but the bridge re-keyed its single routing entry optimistically first: a refused replacement left the surviving stream's events and transport verbs answering to the refused key and arm
- The bridge now consults the engine's own videoLoadRejected gate before committing the entry and stages the rejection Msg to the refused arm directly (stageLoopMsg, the channel-admission precedent), leaving the entry and the engine untouched
- Pinned: a rejected replacement delivers its rejection while the live stream keeps its events and its wire-key gate
* Drive the house video chrome from keys and rebuild it in every window
- Keyboard activation (Enter/Space) of the transport's play/pause control now drives the video channel exactly like the pointer release: the control advertises Play/Pause to focus and accessibility, so the intent must act instead of being consumed silently — pinned beside the pointer-toggle test
- Runtime-consumed control paths and handler-less video events rebuild through one helper that follows dispatch's discipline: the main canvas against ITS window (a control event from a secondary window used to target that window with the main canvas label and error) and the window slots after it, so a <video controls> declared in a secondary window's tree repaints from the moved mirrors
* Fail a macOS video load whose conversion buffer cannot allocate
- The frame tap's reusable BGRA-to-RGBA conversion buffer failing to allocate used to degrade to a silent zero-frame pump while the load still acknowledged LOADED: playback reported positions forever and could never deliver a pixel
- videoAttachOutputForItem now reports the failure and the status hop answers with the FAILED terminal instead of the acknowledgment - the honest degrade; an audio-only item (no video geometry) keeps its documented honest-absence path
* Journal handler-less video terminals and quarantine stale fed events
- Loop-side video terminals now stage and journal with no Msg handler bound (the image arm's rule): a declarative playback binds no handler, but its synchronous .failed is executor truth — the record is what replays the channel reset, and the staged delivery's wake re-renders the chrome
- A fed event whose staged key no longer names the live playback (replaced before its drain, or a replayed platform .failed already applied the terminal) delivers its staged values verbatim and leaves the live channel alone — applying a replaced stream's terminal would reset the replacement
- A journal-fed video record claiming a millisecond or dimension scalar at or past 2^53 refuses replay as a damaged record at the gate (no recorder writes one, and the TS tier's exact-integer widening would trap on it) — pinned by a hand-patched journal
* Route every video event by the key of the load that produced it
- The bridge's engine key now carries the issuing load's event-arm tag in its low byte (videoKeyForTag): a staged synchronous .failed that delivers AFTER a replacing load re-keyed the single entry still routes the arm of the load it answers, instead of handing the old stream's failure to the replacement's arm
- videoEventMsg routes by the event's own key tag, never the mutable entry's; the wire-key gate on transport verbs is unchanged
- Pinned: a replaced load's straggling terminal routes its own arm while the replacement's stream keeps its own; the request-key pins move to the tagged shape
* Reconcile <video src> from every window's tree and repaint Msg-less failures
- Secondary-window builds now feed the video reconciler: Ui.video promises that declaring the element IS the playback in every window's tree, but slot builds discarded the declaration — the main canvas wins when both declare (one player, one owner), the first declaring slot keeps ownership until it stops declaring or its window closes (reconcile-close included)
- The main build stamps the video mirrors it rendered; drainEffects compares that stamp after a Msg-less drain and re-renders the chrome when they moved — a handler-less declarative playback's synchronous failure no longer leaves controls advertising a playback that is gone
* Paint the poster frame for paused macOS video loads
- A load acknowledged while paused (autoplay = false, the documented poster-frame shape) ran no frame timer and never pumped its first decoded frame: the surface held the placeholder until the user played, paused, or seeked
- LOADED now arms a bounded first-frame hunt: the frame timer polls until the first frame pushes (a paused load then stops the timer; a playing one keeps it), surrendering honestly after ~3s if the output never yields one
* Restart the macOS frame timer when buffered playback actually begins
- The poster hunt could stop the pixel clock while AVPlayer was still waiting to play (a remote autoplay stream yielding its poster mid-buffer, or a hunt surrendering past its bound), and nothing restarted it when the waiting phase ended: audio played and positions ticked with no frames flowing
- The timeControlStatus hop now arms the frame timer whenever playback reports rolling (idempotent beside videoPlay's own arm), for local and remote sources alike
* Deliver a staged video terminal past stop instead of panicking
- A Cmd.batch([videoLoad, videoStop]) on a host whose load fails at once stages the .failed before stop retires the bridge entry; the drain then hit the entry gate's panic even though the terminal is the load call's only answer
- videoEventMsg drops the entry gate: every event reaching it was produced by a bridge-issued load and carries its arm tag in its own key (the engine swallows its post-stop stragglers itself), so routing needs no entry at all - pinned by the load-then-stop batch shape
* Retain every window's video declaration and promote on the owner's close
- The reconciler kept only the owning window's <video src>: closing that window (or its declaration vanishing) stopped playback and left another window's mounted video inactive until an unrelated rebuild
- Slot declarations now retain one entry per window (the table matches the window budget); when the owner closes or stops declaring, the next retained declaration promotes inside the same dispatch - pinned by a two-window close-promotes test with no on_close Msg and no rebuild
* Retry the frame wake for identical pushes still awaiting adoption
- A refused frame-wake request leaves pending clear so a retry can land, but the push-boundary fingerprint short-circuit returned before the wake: a static frame pushed again after a transient request_frame_fn refusal short-circuited forever and the staged bytes were never adopted
- An identical push now falls through to the wake while bytes are still staged; only an adopted frame's repeat stays a pure no-op
* Expose videoPlayback through the bridge alias and the TS feature type
- platformFeatureFromString gains the videoPlayback camel-case alias every other feature carries, so window.zero.platform.supports("videoPlayback") answers the platform truth instead of InvalidPlatformFeature
- NativeSdkPlatformFeature adds video_playback/videoPlayback so TS clients can compile the support query
* Stamp every platform video event with the load that produced it
- The engine mints a token per loadVideo, passes it through the load seam, and every host echoes it in each event: takeVideoMsg swallows an event whose token is not the current load's, so a replaced playback's queued terminal can neither reset the replacement, release its claim, nor route through its handler - pinned by a stale-failed-after-replace regression
- macOS hardening on the same theme: the item end/failure notification blocks re-check note.object against the current item (a removeObserver cannot recall a block already enqueued on the main queue), and the host carries the token across its terminal emits' teardown
- The journaled video platform event carries the token, so replay's mirror steering swallows stale recorded events exactly as live (the re-run loads mint the same deterministic sequence); the null platform echoes tokens like the real hosts and its fake position advance saturates instead of trapping on hostile deltas
* Pair replayed video deliveries with their events; token-gate fed entries; stop abandoned players
- A recorded video Msg dispatched synchronously inside its platform event's dispatch, but replay delivered the fed record at the NEXT drain: an update loading the next clip from its completion handler ran too late and the new clip's .loaded event was swallowed against the old load's token - takeVideoMsg under replay now pops the fed head (the journal's contiguity puts each event's record immediately before it) and dispatches it during the same event, pinned by a chained-load playlist session replaying byte-identical
- Pending video entries carry the load token that staged them, and delivery resolves against the live channel only while it still IS that load: the public key alone cannot tell two loads under one app key apart, and a stale fed terminal resolving against a same-key replacement would have reset it
- failVideoChannel silences the platform player it abandons (best effort): a load that succeeded before a later step refused kept its player decoding while the reset channel forgot it, and the inactive channel skipped it at teardown too
* Keep refused and uninstalled declarations out of the video reconciler
- A declared src the engine's own gates refuse (a malformed URL, say) no longer commits the reconciler's tracked ownership: the running playback keeps its identity - so removing the element later stops IT, instead of hashing the refused source and stranding the playback forever; the refused src is remembered separately and taught once, never re-attempted every rebuild - pinned by a refused-declaration ownership test
- A secondary window's declaration is captured only after its build pass and layout succeed and the tree installs: a build whose layout errors never displays, so its declaration never steers the playback either
* Link CoreVideo wherever appkit_host.m builds standalone
- The frame pump's CVPixelBuffer calls are real CoreVideo symbols: the central build gained the framework, but the generated-app template and the standalone example builds still linked only AVFoundation - a generated macOS project failed to link
- The template and every example build.zig that compiles the AppKit host now link CoreVideo beside AVFoundation
* Gate the poster hunt on a real push and clamp millisecond CMTimes
- videoPumpFrame initialized its push result to the success code, so a NULL base address or failed vImage permutation ended the poster hunt and stopped the frame timer with nothing on the surface; a sentinel now keeps the hunt alive until a push actually returns success
- NativeSdkCMTimeFromMs narrowed the u64 millisecond position straight into the signed CMTimeValue, turning absurd seeks negative; the value now clamps at INT64_MAX so AVFoundation clamps to the duration as documented
* Retire the null video player when a non-looping playback completes
- advanceVideo left the fake player loaded after its completion, so post-completion transport calls succeeded where a live host's torn-down player refuses them (and a replayed play could emit a second completion)
- the completion now unloads before the event returns, matching the live hosts' retire-before-emit order; tests pin the unload and the failed-play degrade path
* Stop the custom playback when an empty source commits
- loadCustom returned early on an empty committed source, leaving the previous video rolling under a status line that said "no source"
- an empty commit now stops the playback and resets the transport mirrors, with a test pinning the stopped channel and the honest status line
* Route replayed video records by the journaled load identity
- Every .video effect record now carries the producing load's token; replay feeds through feedVideoRecord, which resolves the token against the live channel or a retired-load park instead of binding the record to whatever the channel holds at feed time.
- loadVideo and stopVideo park the outgoing load's identity under replay, so a synchronous terminal staged inside the very dispatch that then replaced or stopped its playback still delivers the recorded Msg with the recorded identity - never EffectNotFound, never a reset of a same-key replacement the recording kept playing.
- The replay pairing in takeVideoMsg now token-gates fed entries against the platform event, and two session tests pin the batch shapes: load-then-stop and load-then-replace under one app key.
* Check the Core Video lock result before touching the pixel buffer
- A failed CVPixelBufferLockBaseAddress never maps the buffer, so reading the base address was undefined and the unconditional unlock unbalanced the lock count; the frame now drops with the buffer released.
- The poster hunt stays latched across the dropped frame - only an actual push verdict may end it, the converted-but-unpushed rule.
* Document why rate > 0 is transport intent at the playing derivations
- Per the AVPlayer.h contract, a playback waiting in AVPlayerTimeControlStatusWaitingToPlayAtSpecifiedRate keeps rate at the requested value ("not currently effective but instead indicates the rate at which playback will start or resume"), so a stalled-but-unpaused stream already reports playing=1 + buffering=1 and the transport control offers Pause.
- AVPlayer resets rate to 0.0 on its own only with waits-to-minimize-stalling disabled - the local-file configuration, where playback would not self-resume and offering Play is the honest affordance.
* Keep the controls-bearing video element zero-intrinsic
- The <video controls> wrap column now adopts the media sizing contract (WidgetLayoutStyle.zero_intrinsic): the transport bar's intrinsic size never leaks into the element, so an unsized element in a hug container measures zero like the bare surface instead of rendering a controls-only strip.
- The wrap clips its content, so chrome cannot paint past a box the layout granted nothing; declared width/height stay definite through the frame and min/max bounds.
- A ui test pins both shapes: zero in a hug container, surface-above-bar inside a declared 320x180 box.
* Bound video scalars at delivery, sweep replay parks, republish flag deltas
- Platform video events clamp position, duration, and dimensions into the exact-integer delivery window (max_effect_video_scalar_exclusive, 2^53) at takeVideoMsg, whatever a host or embedder reports - the engine-side guarantee behind replay's damage gate, so an honest recording can never be refused as damage; a past-window readout now records and replays clamped, pinned by test.
- Replay-side retired video identities release at the first drain-pass boundary after parking (any journaled record for them feeds before that pass's event dispatches), so a long replayed playlist parks and releases one entry per clip instead of accumulating them; pinned at the channel level.
- Same-src declarative loop/muted deltas republish the runtime mirror in the same reconcile, so an automation snapshot taken after the flip reports the new value instead of the one published before the rebuild.
* Journal the video cascade's resolved source for replay
- loadVideo now journals a Msg-less .video_load record (the .clock/.env discipline) carrying which source the recording host's cascade resolved - a missing local file that fell through to the url is filesystem truth the replayed fake load cannot re-probe.
- Replay queues each record and the replayed load consumes it by its deterministic token, so videoSnapshot() and the automation mirror report .stream with the optimistic buffering flag exactly as the recording did, handler-less declarative playbacks included.
- Pinned by a record/replay test on an assets-absent host; the handler-less house-chrome pin now expects exactly the one Msg-less record.
* Keep buffering honest across paused streams and widen the fake's loop wrap
- Buffering means an un-paused stream waiting for bytes: a fresh URL load now starts the flag from the autoplay intent (engine mirror and replayed cascade resolution alike), the macOS host derives it purely from timeControlStatus instead of presetting it, and pauseVideo clears it - a conforming host emits no pause acknowledgment that could.
- The macOS time-control observer stays silent when the transition lands on paused: pause emits nothing by contract and position reports are for playback in motion; waiting and playing transitions still emit.
- The null platform's advanceVideo runs its loop wrap in widened arithmetic so a past-u64 delta lands on the exact residue instead of a saturated one; pinned along with the paused-stream and pause-clears-buffering shapes.
* Spill the replayed cascade-resolution queue past its inline capacity
- Loads per dispatch are unbounded by contract and every .video_load record lands before the dispatch's event, so the replay-side queue now grows geometrically past its inline 64 (freed when it empties and at deinit) instead of refusing the 65th record as a false divergence - the pending stages' non-lossy discipline.
- Pinned by a record/replay burst one past the inline capacity on an assets-absent host.
* Cross-check the journaled key on every replayed video pairing
- Reminted tokens pair records with replayed loads by position; the journaled key now proves the load at that position is the one the recording issued - feedVideoRecord refuses a mismatch as divergence, and a cascade resolution consumed under a different key latches one for the finish check.
- Replay now ends with a consistency check (the .finish replay control): a structurally valid journal whose queued cascade resolutions the replayed timeline never claimed fails as ReplayEffectDivergence instead of reporting success.
- Pinned at the channel level: wrong-key feeds refuse, an unclaimed or misclaimed resolution fails finishReplay, and the honest pairing stays silent.
* Copy the captured video declaration and verify the load bijection under replay
- The main-canvas <video src> capture now copies the source out of the build arena into app-owned storage (the slot captures' rule): a later failed rebuild resets the arena the old capture borrowed, and a window-close reconcile could hash or load overwritten bytes.
- Every non-rejected real load now journals exactly one .video_load record - refused cascades included (failVideoLoad) - so the replayed loads and the journaled records form a bijection: a replayed load with no record at its position, and a record whose position was never issued, both latch divergence for the finish check.
- The journal semantic epoch bumps to 2: identical bytes, changed replay meaning - an earlier recording's failed loads would replay as false divergence, so old journals refuse with the re-record teaching. Pinned: the extra-load shape fails finishReplay and the captured src aliases the app-owned buffer.
* Run the replay finish check even when no records were fed
- A latched video-load divergence can exist with zero fed effect records (the recording journaled nothing; the replayed timeline loaded anyway), so the end-of-journal consistency check now runs unconditionally; hookless apps answer ReplayUnsupported, which is honestly nothing-to-check.
- Pinned: a zero-record recording replayed into a build that declares a video fails as ReplayEffectDivergence instead of reporting success.
* Refuse non-http(s) schemes at the videoLoadUrl seam
- The seam documents streaming-only and promises to reject bad arguments before the platform is asked, but forwarded any scheme - a direct caller could hand file:///... to a host whose media stack opens it (AVPlayer does); the scheme gate now holds at the seam for every caller, matching the engine's own load validation.
- Pinned: file:, ftp:, and unparseable URLs refuse as InvalidVideoOptions without reaching the platform; https passes.
* Retire the null platform's video player before the load probes refuse
- The macOS host stops the previous player before the file-existence probe, so a refused load leaves no playback behind; the null platform now models the same ordering in videoLoad and videoLoadUrl instead of returning early with the replaced player still emitting frames and events under its old token.
- Pinned: a VideoSourceNotFound load leaves nothing loaded, no pending acknowledgment, and no position ticks.
* Fail frame-less video loads and keep the seek mirror on platform truth
- An asset with no video geometry (an audio-only file loaded as video) now fails the load instead of acknowledging a playback that can never paint - sound over a permanently blank surface is the same broken promise as the conversion-buffer failure the attach path already refuses.
- seekVideo applies the platform seek before moving the mirror: a player the host already retired (a completed non-looping playback) refuses the call, and the snapshot and house slider must stay on the frame actually on the glass; pinned in the post-completion transport test.
* Refuse post-completion seeks on the fake video channel too
- A non-looping natural end retires the platform player (retire-before-emit), so a later seek refuses live and the mirror keeps the terminal position; the fake executor now latches the completion and refuses identically, so replayed chrome and snapshots land exactly where the recording left them instead of scrubbing a player that no longer exists.
- Pinned by a record/replay parity test: a scrub after the natural end keeps position at the duration on both sides, model and fingerprints identical.
* Pair replayed video deliveries from any stage position; stop cancels the TS stream
- The replay pairing in takeVideoMsg now scans the pending stage for the event's fed entry instead of checking only the head: a regenerated loop-side rejection staged earlier in the same dispatch keeps its own drain-time order and no longer reverses the recorded Msg order; pinned by a record/replay ordering test.
- VideoMsgFn may return null - the adapter tier's cancel gate - and the TS bridge uses it to honor Cmd.videoStop's wire contract (stop CLOSES the stream, no events for the key after this): a staged synchronous terminal from the very batch that stopped the stream drains through the engine but never reaches the app, and reopening the tag lifts the latch.
- The house videoMsg constructor never returns null, so Zig-native apps keep the one-terminal-per-load delivery whole; the bridge pin now asserts the swallow and the reopen.
* Cancel the key's staged video answers inside the engine at TS stop
- Cmd.videoStop's cancel now runs where the answers live: stopVideoCancel removes every staged-but-undrained entry for the key before the channel goes idle, so nothing for a stopped stream can reach the app - even the synchronous terminal of a load-fail-stop batch, and even when a later load reuses the same event tag before the drain (a bridge-side tag latch could not tell those generations apart).
- A cancelled answer never journals, so replay regenerates and cancels the same entries and the timelines stay identical with no extra machinery; the VideoMsgFn seam returns to plain Msg and the Zig-native stopVideo keeps its one-terminal-per-load delivery.
- The bridge pin now covers the same-tag reopen-before-drain shape.
* Mirror synchronous load refusals under replay; token-scope ownership and cancel
- The .video_load record's video_kind now carries the load's outcome: a refusal reset the live channel before loadVideo returned, so the replayed fake load resets at the same instant (parking its identity for the journaled terminal) and a snapshot an update reads inside the very dispatch matches the recording's; semantic epoch bumps to 3 (same bytes, the field gained meaning). Pinned by a record/replay probe test.
- Declarative video ownership now rides the load token the reconciler captured at its own load, never the derived key alone - the key is a pure function of the source string, and a manual load carrying it must survive declaration removal and flag deltas untouched; pinned.
- stopVideoCancel is token-scoped: the stopped stream is the latest accepted load, so a replaced predecessor sharing the arm's public key keeps its owed terminal - only stop cancels; pinned at the channel level.
* Refuse post-completion play on the fake video channel too
- A non-looping natural end retires the platform player, so play meets an absent player live: one .failed terminal and the channel resets before playVideo returns; the fake executor now models the same refusal (failVideoChannel on plain fake; under replay just the identity park and reset, because the journaled terminal delivers itself), so a snapshot an update reads right after its own play answers the same on every executor.
- Pinned by a record/replay probe test: the mid-dispatch snapshot reads inactive on both sides, the terminal delivers at its recorded wake, and models and fingerprints match.
* Break the video timer retain cycles and gate video-load outcomes at replay
- Both video timers (the 1/60 pixel clock and the 500ms readout) are now weak-host block timers that self-invalidate when the host is gone: a target-selector repeating timer retains its target through the run loop, so a host destroyed mid-playback without an orderly stop could never dealloc - leaving AVPlayer, the timers, and the conversion buffer permanently retained.
- Replay refuses a .video_load record whose video_kind is neither .loaded nor .failed as a damaged journal: the recorder writes exactly those two outcomes, and anything else would steer the replayed fake load into a state the recording never had; pinned by a hand-patched-journal test.
* Commit the video declaration at install; provenance-gate video rejections
- The main-canvas <video src> capture now stages during the build and commits only when the rebuild installs: a build that fails downstream never mounted, and the retained tree on the glass still shows the old declaration - a later reconcile acting on the unmounted capture would stop or replace a playback the presented tree still declares.
- Replay refuses a .video record whose kind and token are recorder-impossible: rejections stamp token 0 (a refused load never minted one) and every delivery carries its minted token, so a .rejected re-stamped onto a delivered record can no longer slip through the regeneration skip and silently omit the recorded Msg; pinned by a hand-patched-journal test.
* Fail the replay finish check on undelivered fed video results
- A fed record is one recorded delivery, and the event that consumed it live follows it in every honest journal - so a fed entry still staged when the journal ends means truncation or hand-editing, and finishReplay now refuses instead of reporting success; regenerated loop-side answers may honestly outlive the last drain on both timelines and stay exempt.
- Pinned: a fed-but-undelivered result fails the finish check and the same result delivered makes it silent again.
* Restart a finished playback from Play instead of failing it
- A non-looping natural end retires the player, so Play answered with one failed event and the seek was guaranteed to spring back - a broken response to a valid finished state. Effects.restartVideo is the resume path: a fresh load of the channel's own remembered source with autoplay, keeping key, surface, handler, and the loop and mute flags, journaling like any load.
- The house transport's toggle restarts when the completion latch is set and its scrub disables (Play is the live affordance); the snapshot and chrome state expose completed so custom players can do the same - the video-player example's Play now restarts its finished clip.
- Pinned: clicking the house toggle after the natural end issues a fresh platform load and the playback runs again.
* Validate replayed video payloads and source shapes; retire the example's dead seeks
- The replay pairing now requires the fed record's payload to equal what the platform event resolves to: the recorder journals every delivery verbatim from that event, so an altered kind or scalar around an intact identity refuses as divergence instead of handing the app a Msg the mirrors contradict; pinned by a hand-patched-journal test.
- A journaled cascade resolution must be one the load's request shape could select (.local needs a path, .stream needs a url); an impossible pairing latches divergence for the finish check, pinned at the channel level.
- The video-player example's seek-family controls (slider, back, forward) disable after a natural completion - a retired player refuses seeks and the thumb would spring back; Play stays live and restarts.
* Document the video output's vended-size contract at the frame tap
- The pixel-buffer width/height attributes are client requirements AVPlayerItemVideoOutput satisfies by scaling every vended frame, not hints: a 3840x2160 H.264 source tapped with fitted 1866x1050 attributes vends 1866x1050 buffers, so a 4K playback never hits the oversized-frame drop - that guard is defense in depth against a hypothetical non-conforming host, and the comment at the attributes site now says so.
* Keep declared ownership across restart; saturate past-window seeks
- The house transport's restart mints a fresh load identity, so the declarative reconciler re-captures its ownership token when the finished playback was its own - removal and same-source flag deltas keep working after a replay-from-end; pinned in the restart test.
- The TS bridge no longer rewinds a finite seek past the exact-integer window to zero: it saturates just below the window, where the engine's duration clamp lands it at the end - an oversized forward seek means the end, and only NaN and negatives (not millisecond offsets at all) seek to 0; pinned at the bridge level.
* Class non-finite seeks as invalid offsets, never oversized forward seeks
- Infinity reached the past-window saturation branch and sought an active video to its end, though the literal validation rejects non-finite offsets: the saturation now requires a FINITE value, so Infinity seeks to 0 like NaN and negatives - not millisecond offsets at all.
- Pinned at the bridge level beside the finite past-window seek.
* Keep the reserved video surface id inside the f64 exact window
- Surface ids ride the TS wire as f64, and a source-less <video> with custom controls is fed by the app's own Cmd.videoLoad naming exactly the reserved id - the old value sat near 2^63, so the bridge's exact-integer validation rejected it and the element could only ever show its placeholder from TypeScript.
- The id moves below 2^53 (mnemonic preserved, bit 63 clear, still a valid producer id reserved by convention); pinned: in the window, outside the derived-texture namespace, and round-trips the f64 wire unchanged.
* Deterministic seek mirror, journaled handler presence, and baked rotation
- The seek mirror now gates on the deterministic completion latch on every executor: a completed playback keeps its terminal position live and replayed alike (the platform is not even asked - the retired player could only refuse), and residual platform verdicts are fire-and-forget so an exotic host's answer can never diverge replay's mirrors from the recording's.
- Every .video record journals whether its delivery dispatched a Msg, and feedVideoRecord requires the replayed handler presence to agree - a record whose Msg silently vanished, or a Msg live never dispatched, refuses as divergence instead of a silent consume.
- Rotated media renders upright: a track with a non-identity preferredTransform gets a properties-of-asset video composition baked into the vended frames (verified against a portrait-flagged H.264 asset: the raw tap vends encoded orientation, the composition vends display orientation); identity-transform assets skip the render pass and track-less streams have no transform to bake.
* Repaint the chrome when the reconcile moves playback; seek on slider key steps
- A src change loading an autoplaying replacement reconciles after the build installs, so the just-installed chrome advertised the OLD transport state while its control acted on the new one - Play on the label, pause in effect. The rebuild now runs one guarded repass when the reconcile moved the mirrors the build rendered; the repass reconciles an unchanged declaration, so the mirrors are a fixed point. Pinned: the replacement's pause glyph shows in the same build.
- Keyboard and assistive steps on the house seek slider arrive as set_value intents with no widget change event behind them; they now map their fraction onto the duration and drive the channel like the pointer scrub instead of being consumed silently for the next tick to snap back. Pinned with a focused arrowright step.
* Repaint secondary-window chrome when its own reconcile moves playback
- The slot rebuild reconciles its captured declaration after the slot tree installs, so a first mount of an autoplaying <video controls> in a secondary window rendered its chrome from the still-inactive snapshot - disabled Play over a playing video - until some later platform event, seconds away on a slow stream. The slot path now runs the main rebuild's guarded repass when the reconcile moved the mirrors the build rendered; the repass reconciles an unchanged declaration, so the mirrors are a fixed point.
- Pinned: the slot's first installed build shows an enabled pause toggle.
* Latch missing video records; scope bridge verbs to their own stream
- A recorded video event arriving under replay with a handler bound and no fed record before it now latches divergence for the finish check: the recorder journals every handled delivery immediately before its event, so absence is truncation or hand-editing - never a Msg to drop silently; pinned at the channel level.
- Bridge video verbs now prove ownership with the load identity their own accepted load minted (Effects.videoMintedToken): a wire key whose stream was since replaced by a load the bridge never issued - a declarative element's - no-ops its transport verbs, and its stop cancels only its own stream's staged answers while the playback on the channel survives untouched; pinned at the bridge level.
* State the replace-is-not-stop rule at the videoLoad contract
- A replaced load still delivers the terminal it owes, routed to its own event arm - the never-silent promise applies to the replaced stream's failure exactly as to any other, and only Cmd.videoStop cancels undelivered answers. The wire contract now says so explicitly where the open-or-replace semantics are defined; the bridge's routing and the arm-separation pin already enforce it.
* Remember volume across a failed load; gate impossible terminal payloads
- Volume is a remembered preference the next load re-applies, so the bridge's ownership gate now lets videoSetVolume through when the channel is IDLE - a failed load's handler routinely sets it before retrying, and with nobody's playback on the channel there is nothing to protect; a foreign live playback still gates it. Pinned both ways.
- Replay refuses a .video record whose payload shape the recorder never writes for its kind: terminals deliver with playing and buffering false and no dimensions, and a completion pins position to the duration - a synchronously failed load's record has no platform event behind it to cross-check, so the gate is where a hand-set width refuses; pinned by a hand-patched-journal test.
|
||
|
|
e67d3bd9c5 |
External-source channels: journaled events from app threads, sockets, and watchers (#165)
* Add the external-source channel effect family to the engine
- fx.openChannel/closeChannel with a generation-stamped thread-safe ChannelHandle.post that stages into a per-channel non-lossy FIFO, wakes the host, and reports back-pressure through per-channel drop counters
- channels share the keyed families' key space (occupied from open until the .closed terminal delivers) and deliver every event through the ordinary drain, journaled at the boundary
- journal format v8: the .channel effect-record kind with inline post bytes; replay feeds recorded events verbatim (no source thread), regenerates admission rejections, and damage-gates impossible records
* Cover the channel family: lifecycle, back-pressure, key space, replay
- direct-channel tests: open/post/deliver/close order, thread posting, duplicate-key and full-table rejects, drop accounting, post-after-close/teardown safety, shared key space with fetch and spawn
- record/replay acceptance: a session recorded with a live posting thread (one honest drop aboard) replays fingerprint-identical offline with no source thread; the duplicate open's rejection regenerates
- damage gates: over-bound post bytes and byte-carrying terminals refuse replay as damaged records
* Give transpiled cores the channel family: Cmd.channelOpen/channelClose
- wire opcodes 0x15/0x16 (additive within cmd_format_version 3) with rt builders, SDK types (ChannelState/ChannelEventArm/ChannelEventKind), and emitter lowering with the image-id literal gates
- ts_core_host routes every event through a five-field by-name arm (key/state/bytes/droppedPending/droppedTotal) on a non-retiring bridge entry; duplicate live keys reject at the post-cycle boundary echoing the key
- posting stays native-side API (Effects.channelHandle) - transpiled cores open, close, and receive; the bridge test drives a real handle post through the drain
* Add the channel-monitor example: a worker-thread source, zero polling
- an app-owned std.Thread samples its own process (uptime, peak RSS) and posts each reading through the channel handle; the UI updates only when events arrive
- Stop closes the channel and the detached worker winds down on the handle's false answer - safe past close and past teardown by the handle's construction
- tests swap the worker for a handle-capturing stub: posted samples land, no fx timer is ever armed, close kills the handle, a refused open reports rejected
* State the channel family and its journal v8 bump in the changelog
- one fragment covering the Zig surface, back-pressure contract, the conscious v8 format break, the TS tier, and the channel-monitor example
* Answer channel posts with a four-way PostResult instead of a bool
- ChannelHandle.post now returns PostResult { accepted, dropped_full, dropped_oversized, closed }: a producer can tell transient back-pressure (skip and continue) from closure (exit the loop) instead of guessing at a false; both drop answers keep the exact drop-counter semantics, and oversized gets its own member because its remedy differs (no retry of the same bytes can ever land)
- channel-monitor's worker now reads the answer honestly: dropped_full skips the sample and keeps sampling (the status line reports the delivered drop counters), closed winds the thread down - a transient 32-entry stall no longer stops monitoring forever while the UI says "monitoring"
- unmerged API, clean break: every post site, the SDK doc comment, the handle-lifetime docs, and the example's tests/README move to the enum
* Park replayed channel opens so a re-run source thread cannot diverge the stream
- under armReplay, openChannel registers the occupancy exactly as live (duplicate opens keep rejecting symmetrically, the shared key space holds) but allocates no staging and returns an INERT handle: every post answers .closed immediately, so app code following the documented open-and-spawn pattern sees its re-spawned worker exit on its first post instead of interleaving live posts with the journal-fed events
- closeChannel tolerates the parked occupancy's missing posting header; the fed .closed terminal retires the parked slot at its recorded position, the same causal instant live delivery frees the key
- new acceptance regression records the open-and-spawn pattern with a real posting thread and replays it offline fingerprint-identical (pre-fix the re-run worker's accepted posts diverged the checkpoints), plus direct coverage that inert-handle posts stage nothing and count no drops and that duplicate opens under replay still reject
* Teach the evals Cmd decoder the channel opcodes
- decode channel_open (0x15: key f64 LE + event_tag u8, exactly the bytes rt.zig's cmdChannelOpen builds - no max_pending rides the wire) and channel_close (0x16: key f64 LE) into new Op arms
- batch test pins both records' lengths so a trailing record still decodes; the unknown-op panic already names the offending byte and offset, so the next opcode gap stays a one-line diagnosis
* Never wake the host for a refused channel post
- A producer continuing through .dropped_full (the documented contract) enqueued one host wake per refusal, growing the main-loop queue without bound while the stage itself stayed bounded.
- The post site now documents the invariant: a wake is issued only when a post makes new work drainable - a full stage's entries already carry their accepted posts' wakes and the drop counters ride the next delivered event, an oversized post stages nothing, and a closed post stays a pure no-op.
- New regression test pins the pending-wake count flat across a 65-refusal storm and pins the accepted path's exactly-one-wake behavior before and after the stage refills.
* Document the channel family across the TS and skill surfaces
- The TypeScript-cores page gains Cmd.channelOpen/channelClose in the command table, a five-field event-arm example with the exact ChannelState union, and the channel stream in the Sub-vs-stream teaching.
- Both shipped skills now teach the family: native-ui gets the fx.openChannel section (verbs, event arm, back-pressure and replay contracts, test surface) and ts-core adds the channel ops to its streaming-ops set.
- A stale OpenChannelOptions comment still said overflow posts return false; it now states the .dropped_full PostResult, and the changelog names the refused-post-never-wakes contract.
* Never hold the channel staging mutex across the host wake
- Split the channel post's host wake out of ChannelShared.mutex into a ChannelWake half behind its own mutex, mirroring the media-surface producer's data/wake split: posts stage under the staging mutex, release, then wake, so drain, close, and teardown never contend with a slow or blocking platform wake hook.
- Arm the binding at openChannel and disarm it under the wake mutex on every close path (closeChannel, terminal retire, teardown) — the abandon fence: after a disarm returns, no post is inside the host call and none can start one.
- Document the lock-order invariant at ChannelShared.mutex (the staging mutex is never held across a host callback; the two locks never nest) and pin it with an injected wake hook that probes the poster's lock state.
* Coalesce channel post wakes behind one latched host wake
- Latch an atomic wake-pending flag in the channel's wake half on the first accepted post and ride it for the rest of the burst, following MediaSurfaceWake exactly: a fill/drain/refill producer now costs one host-queue entry per drain instead of a standing backlog of redundant wakes.
- Clear the latch at the drain pass boundary BEFORE snapshotting the post order (adoptMediaSurfaceFrames' clear-before-sample placement), so a post racing the drain either lands inside the pass's snapshot or observes the cleared latch and wakes afresh — nothing staged is ever left wakeless; the flag is per-channel, riding the generation-fenced binding the lock split introduced, and checked lock-free so a reentrant wake hook coalesces instead of deadlocking.
- Gate the contract in tests: a full-stage burst latches exactly one wake, a post after the pass boundary wakes afresh and delivers next pass, refused posts still never wake, and a wake hook posting back into its own channel completes instead of deadlocking.
* Surface a failed sampler start in the channel-monitor example
- Claim "monitoring" only after the source thread actually starts: the spawn reports failure through the start seam instead of silently returning with a live channel and no producer.
- On failure, close the just-opened channel (the .closed terminal frees the key for a retry) and render "sampler failed to start" in the status line.
- Exercise the failure branch through the injected-source seam the tests already use — std.Thread.spawn cannot be made to fail deterministically — including full recovery on a retry with a healthy source.
* Reserve channel-table capacity for an alloc-failed open until its rejection drains
- Live table admission now counts staged executor-truth channel rejections as occupied capacity, matching the slot replay parks for the same open until the journaled terminal feeds; regenerating refusals (occupied key, full table) deliberately never reserve, the same line the key window draws
- Route channel storage creation through a swappable allocator seam so tests can fail one open's start surgically
- Cover the reservation window, the table-limit record/replay boundary, regenerating no-reserve accounting, and teardown with a reservation pending
* Dispatch bridge-refused rejections in command-stream order across families
- Merge the TS core host's per-family rejection staging (spawn, image, channel) into one kind-tagged stage drained in wire order, so a mixed Cmd.batch's refusals reach update under the performed-in-order contract; a future family joins with one enum member and one switch arm
- Spawn rejections now share the spill discipline: a batch carrying more refused spawns than the effect table holds slots yields one rejection each instead of a panic
- Pin the order at both tiers: bridge tests drive mixed, reversed, and three-family refused batches, and the transpiled e2e fixture records a mixed-rejection session that replays with identical cross-family order
* Preserve mixed-provenance channel rejection order under replay
- A replay-parked open now reserves its pending-order slot at dispatch: the park consumes the pending_seq stamp a live executor-truth refusal would have staged at, and the fed park-retiring .rejected delivers through the pending stage at that stamp (ordered insert, slot retired at delivery) instead of trailing every younger regenerating refusal through the completion queue.
- Accepted opens stamp too - refusal-vs-accepted is only knowable at the first feed - but any non-.rejected feed vacates the stamp unused, so their fed streams keep riding the queue unchanged.
- Cover the mixed sessions end to end: alloc-fail-then-table-full replays in live order, the regenerating-first shape keeps its lead, and a rejection handler that opens a third channel sees the same table state on both sides.
* Publish the channel wake's services binding and sweep staged work at bind
- Posting threads now read the services binding through an atomically published mirror (release store at bind, acquire load under the wake mutex) instead of racing the loop thread's plain-field write; the release/acquire pair is the publication contract that makes the host state fully visible to the first cross-thread wake.
- bindServices sweeps: a post accepted before the binding could neither wake nor latch, so the bind issues one catch-up host wake when anything is staged - without it a one-shot producer that posted early stranded forever.
- Cover both shapes: the pre-bind post delivers off the bind sweep's wake with no further post, and an idle bind wakes nobody.
* Give channel occupancies a channel-owned u64 generation
- Channel handles now stamp from a dedicated monotonic u64 counter instead of the shared wrapping u32 effect counter: the permanent-closed guarantee is absolute, and a u32 wrap after 2^32 occupancies would let a long-lived stale handle match a reused slot and post into another producer's channel (the media-surface producer handle's width, mirrored).
- Fed channel entries carry the u64 generation on their own queue field; the slot families keep the shared u32 counter for their loop-internal gates.
- Pin the width, the counter's independence, and the wrap case itself: a seeded counter reproduces the exact u32 truncation collision and the stale handle still answers closed.
* Add ChannelHandle.live() and state the replay re-run honestly
- live() is the producer-launch check: false for parked replay handles, refused opens, closed or reused occupancies, and torn-down runtimes - advisory only, the post's own answer stays authoritative. The replayed open parks, but the opening update re-executes, so a producer launched unconditionally really starts (connects and blocking setup before its first post included) and is stopped only at that first post; gating the launch on live() keeps replay fully offline.
- The channel-monitor example consults live() before spawning the sampler - the Msg stream and model are identical either way because nothing model-visible branches on it - and its tests pin that a replay-armed start never invokes the source seam.
- Say it honestly everywhere it was claimed: the changelog fragment, the openChannel doc comment, the test-suite header, and both skill surfaces now state that replay never NEEDS the source rather than that no source thread runs, and one journal replays identically under both producer disciplines (acceptance-tested).
* Repair the bind/post wake handshake with a seq_cst store-buffer pairing
- bindServices' services publish, hasPending's mirror loads, the post's pending increment, and requestHostWake's services load all carry seq_cst: release/acquire never orders a store before the same thread's later load of another location, so both sides could read stale values and strand an accepted post with no wake
- Document the total-order argument at each of the four operations: whichever store lands later in the seq_cst order, that side's subsequent load sees the other's store, so the poster wakes or the binder sweeps
- Add a bounded concurrent regression (300 iterations, one post racing one bind) asserting a wake is always observed and the post drains, documented as probabilistic-but-real for this race class
* Deliver bridge refusals through the engine's one seq-ordered rejection stream
- A batch mixing an engine-refused record (cross-family occupied key, staged into the pending FIFOs for the next drain) with a bridge-refused one (host-side stage, dispatched at the cycle boundary) delivered the second rejection before the first, breaking Cmd.batch's performed-in-order contract across layers
- Add Effects.stageLoopMsg: a non-lossy caller-staged Msg stage sharing the pending seq stamp, never journaled and regenerating by contract; the TS bridge now stages its spawn/image/channel refusals there at refusal time and its finishCycle rejection stage retires, so one seq-ordered drain delivers every rejection in command order
- Pin the engine-then-bridge and bridge-then-engine compositions, the three-family mix, the journal provenance (bridge refusals journal nothing, engine refusals journal marked regenerable), and record/replay identity for mixed-authority rejection order
* Run the channel host wake with the wake mutex free
- An embedder wake_fn has no enqueue-only contract: holding wake.mutex through the call deadlocked against drainBoundary (and post's never-blocks contract) whenever a wake synchronously marshaled to the loop thread.
- Posts now mark an in-flight count under the mutex, release it for the host call, and re-acquire to clear; disarm clears the binding under the mutex and waits for in-flight to reach zero, so a returned disarm still means no producer is inside the host call.
- Gate the invariant with a wake hook that takes the drain's own pass boundary (deadlocked before, completes now) and asserts the wake mutex is free during the call.
* Refuse terminal feeds into a live channel occupancy
- feedChannelEvent is the replay/test seam and replay parks its opens with inert handles; feeding a terminal into a LIVE occupancy (open posting header, staged backlog, or armed close marker) raced the producer: the terminal's delivery destroyed the staging FIFO and stranded channel_pending_count, leaving hasPending() true forever.
- Feed answers error.ChannelLiveFeed for that shape instead; live occupancies end through closeChannel, and teardown already reconciles the count to zero when it discards staged entries.
- Tests pin the loud refusal (open and .closing shapes), the reconciled count after teardown-with-staged-entries, and the parked replay feeds staying green.
* State the bridge refusal timing break and align the TS replay claim
- The changelog names the rejection-timing change deliberately: bridge-produced refusals (duplicate-spawn keys, image validation, channel admission) moved from the command cycle's own boundary to the next host drain so every rejection arrives in one seq-ordered stream, and a frame may now render between the cycle and the rejection Msg.
- Cmd.channelOpen's doc drops the last 'no source thread at all' overclaim: replay never NEEDS the source, an unconditional producer is stopped at its first post, and a live()-gated one keeps replay fully offline.
* Reset the channel-monitor drop total on restart
- The start path reset samples and visible rows but not dropped_total, so a fresh run showed the previous run's drops until the first data event.
- The restart test now ends a run with a counted drop and pins a zeroed readout immediately after the fresh start.
* Split channel wake disarm into a non-blocking revoke and a teardown quiesce
- closeChannel and retireChannelSlot now revoke the wake binding without waiting out in-flight host calls: a supported wake hook that synchronously marshals to the loop deadlocked against the old spin-wait when the marshaled dispatch's handler closed the channel
- teardown keeps the blocking quiesce but bounds it (injectable deadline, abandoned-call counter): deinit runs as the loop stops servicing dispatches, so it cannot guarantee an in-flight marshal ever returns, and everything the abandoned call still touches lives in the process-lifetime header
- gate tests: the synchronous-marshal close completes with one .closed terminal, a stale in-flight call outliving close and reopen cannot unlatch the fresh occupancy, and teardown both waits out a slow hook and abandons a stuck one safely
* Release the wake coalescer latch when a takeMsg sweep observes an empty stage
- only drainBoundary cleared the latch, so a caller on the public bare-takeMsg drain stranded the next accepted post: it saw the stale latch and never woke the host
- the sweep clears the latch exactly when a channel's staged queue is observed empty, then re-checks the queue: a post racing the clear either re-latches a fresh wake or its entry is visible to the re-check and delivers — one redundant wake is acceptable, a stranded event is not
- gate tests: a bare-takeMsg drain to empty is followed by a fresh wake on the next accepted post, and an event-driven consumer that only drains on wake movement never strands a concurrent producer's accepted posts
* Condition the channel post never-blocks guarantee on an enqueue-only wake contract
- Document PlatformServices.wake_fn as bounded, non-blocking, and enqueue-only — the shape every first-party host already implements (macOS dispatch_async, GTK g_idle_add, Win32 PostMessageW) and the same contract the media-surface frame request documents — and condition ChannelHandle.post's never-blocks promise on it: the runtime holds no channel lock across the call, so a violating wake hangs only its own posting thread, never the runtime's lock graph.
- Reframe the loop-marshaling wake tests as violator-containment pins rather than supported-usage examples: the pinned behavior (no lock-cycle deadlock through drain, close, or reopen) stays, and the revoke/quiesce rationale now names the marshal shape as out of contract.
- Sweep the never-blocks wording in the TS SDK channel docs, the native-ui skill surface, and the changelog fragment's back-pressure bullet to carry the same conditioning.
* Leak the platform when teardown abandons a stuck channel wake call
- An abandoned in-flight wake_fn call still holds PlatformServices.context past Effects teardown, and the process-lived channel header only protects runtime bookkeeping after the call returns — so the quiesce-abandon path now reports the abandon to the platform through a new note_channel_wake_abandoned_fn services seam, synchronously, while the platform is still alive.
- Every first-party platform latches the report and gates its destruction on it: MacPlatform/LinuxPlatform/WindowsPlatform deinit (the app runner's and the generated runner's one destroy path), the null platform's reference-model deinit, and the mobile ui host's destroy all skip destruction and deliberately leak the host, process-lived, with one loud log — the abandoned-worker idiom, applied to the platform itself. A conforming enqueue-only wake never meets the teardown deadline, so the gate is violator containment only.
- Tests: the quiesce-abandon test now binds a real null platform behind the stuck hook and asserts the destroy path is suppressed (latch set, destruction skipped), and a new non-regression twin pins the healthy half — a conforming wake quiesces fast, nothing is abandoned, and the platform destroys normally.
* Make an abandoned channel wake call's whole dereference chain process-lived
- bindServices publishes an immutable process-allocator snapshot of the services table, so a poster suspended before its services.wake() dereference never reads Runtime-owned memory; a rebind swaps in a fresh snapshot and never writes the old one
- runners heap-allocate the desktop platform wrappers (createWithOptions/destroy) and gate the free on the abandon latch, extending the deinit gate to the wrapper storage the wake context actually points at
- new coverage: a stuck wake whose first context dereference happens only after teardown completes and the owning scopes die (poisoned), plus rebind-generation snapshot coverage
* Resolve a replay park's pending-order stamp exactly once
- A .rejected record targeting a park already vacated no longer reuses the stamp and appends a duplicate terminal to the one-entry-per-open pending ring; it refuses as journal damage (error.ReplayDamagedRecord) with a teaching that names the key and the one-terminal-per-open rule.
- Pins the refusal and the single delivered terminal in effects_channel_tests.
* Gate channel record provenance against recorder truth at replay
- A .data or .closed channel record stamped with .rejected provenance no longer slips into the regeneration skip and silently drops from the Msg stream; the damage gate refuses it, naming both fields.
- Channel records can only carry .exited or .rejected exit reasons (the two journal sites), so any other decoded value refuses as damage too; .rejected kind with .exited provenance stays the executor-truth feed path.
- Pins both mismatch directions, the out-of-range stamp, and the executor-truth positive path in effects_channel_tests.
* Materialize the services snapshot lazily and free it on clean teardown
- bindServices records the loop-thread services pointer only; the process-lived snapshot allocates at whichever comes last of the bind and the first live openChannel, so apps that never open a channel never allocate one, and the seq_cst publish-then-sweep Dekker pair holds at both sites.
- A clean teardown (every wake header quiesced, zero abandons this deinit) frees the snapshot through the channel-storage seam; it stays deliberately process-lived only past an abandon, and the ownership rule is documented at the new wake_snapshot field.
- Re-points the abandon and rebind coverage (an abandoned generation's snapshot stays intact for stale calls; a rebind's fresh snapshot never writes the old one) and adds laziness and clean-free pins in effects_channel_tests.
* Refuse any channel record fed past the open's terminal at replay
- A fed terminal now marks the park .terminated until its delivery retires the slot, so a damaged journal's post-terminal .data/.closed/.rejected records refuse as ReplayDamagedRecord instead of enqueueing events the retire would silently discard from an unverified replay's stream.
- A .rejected fed after the stream proved the open accepted live stays damage and leaves the park intact, so the real terminal still lands.
- Pins data-after-rejection, data-after-closed, and rejection-after-data in effects_channel_tests.
* Refuse an open that cannot publish the services snapshot
- openChannel pre-flights the snapshot before committing the occupancy and rejects as executor truth on allocation failure, so no live channel can exist whose accepted posts strand with producer wakes disarmed.
- The one remaining live-through-failure shape, a channel opened before bindServices whose bind-time publication failed, heals at every drain pass boundary: the retried publication's Dekker sweep un-strands posts accepted in the disarmed window.
- Pins the refusing open (with recovery on the next open) and the boundary heal in effects_channel_tests.
* Resolve a fed park only after its event actually enqueues
- A .closed refused by completion-queue back-pressure no longer marks the park .terminated first, so the replay pump's drain-and-feed-again retry of a valid journal lands instead of refusing as damage.
- The .data vacate moves behind the same commit point; the .rejected path stages through the pending ring, which never back-pressures, and keeps its transition.
- Pins the back-pressured terminal's clean retry in effects_channel_tests.
* Close open channels when the bind cannot publish the services snapshot
- A failed bind-site publication no longer leaves pre-bind channels accepting posts that may never deliver (the pass-boundary retry needed a loop event that idle apps never produce); the bind closes them, flushing backlogs and delivering .closed terminals through its own loop-side wake.
- Together with the open-site refusal this pins one invariant: no live channel ever runs with producer wakes disarmed while services are bound - so the now-unreachable drainBoundary heal is removed.
- Re-points the bind-failure test: pre-bind backlog flushes ahead of the terminal, the producer's next post answers .closed, and a later open republishes and wakes normally.
|
||
|
|
e59091060f |
Fix the Linux Debug startup crash at the GTK create-view seam (#153)
* Fix the Linux Debug startup crash at the GTK create-view seam - Force the LLVM backend for the app executable on x86_64 (and in the ejected template's build): zig 0.16.0's self-hosted backend shifts native_sdk_gtk_create_view's stack-passed arguments, so Debug `native dev` runs crashed reading a garbage role pointer. - Cap the GTK host's view string lengths at the platform limits with a teaching refusal, so a corrupted C-ABI boundary fails loudly instead of faulting in strndup. Co-authored-by: codehz <13158903+codehz@users.noreply.github.com> * Add a Linux Debug-scaffold runtime smoke to CI - linux-dev-smoke scaffolds the default ts-core template with the CLI, builds it -Doptimize=Debug with automation, and drives it under Xvfb to ready + first presented pixels. - Pins the Debug half of the Linux runtime story: release-shaped lanes always use the LLVM backend and can never see a Debug-only x86_64 codegen fault. Co-authored-by: codehz <13158903+codehz@users.noreply.github.com> * Apply the x86_64 LLVM-backend workaround to the checked-in example builds - The web-frontend examples own expanded build.zigs that created their exe without use_llvm, leaving Debug x86_64 runs exposed to the same self-hosted-backend C-ABI miscompile the app graph now guards against. Co-authored-by: codehz <13158903+codehz@users.noreply.github.com> * Restore the windows-canvas-smoke job key the dev-smoke insertion ate - Duplicate name/runs-on/steps keys collapsed both jobs into one: the Wine smoke ran mislabeled and the new Debug scaffold smoke never executed. Co-authored-by: codehz <13158903+codehz@users.noreply.github.com> --------- Co-authored-by: codehz <13158903+codehz@users.noreply.github.com> |
||
|
|
26df3687f4 |
Make the Windows embedded WebView layer real (#86)
* Make the Windows embedded WebView layer real - Vendor the WebView2 SDK header and loader under third_party/webview2 (BSD-3-Clause, license preserved) so repo state alone compiles the embedded layer; a missing header is now a hard error instead of a silent stub - Fix the conformance blockers behind the guard: a local WRL callback factory for mingw, the uncaptured bridge-handler variable, an EventToken shim, and STA COM initialization on the host thread - Stage the arch-matched loader beside built, run, packaged, and scaffolded apps, mirror it into the npm payload, and pin the wiring with vendor and loader-layout tests * Carry the SDK root through package shortcuts and generated builds - packageShortcut and package-ios now pass the environ map into createPackage like the package verb, so NATIVE_SDK_PATH resolves the framework root for loader staging from standalone binaries - Generated frontend builds export NATIVE_SDK_PATH on the package command and stage the loader dir on the dev command's PATH, mirroring the SDK-dependency graph |
||
|
|
b47111069c |
Native SDK: the complete toolkit for building native desktop apps (#67)
zero-native becomes the Native SDK. Apps are authored as native markup plus Zig on a deterministic runtime and rendered by the toolkit's own engine into real OS windows — no browser, no WebView, no interpreter in the binary. - Desktop is complete on macOS, Windows, and Linux: native rendering with per-platform titlebar fidelity, audio playback with streaming, a verified track cache, and real spectrum analysis, native context menus, packaging with sealed code signing, and a deterministic automation and record-replay story. - Experimental iOS and Android host tiers ship behind the same app manifest: simulator and emulator dev loops, archive-ready packaging, real platform tab bars and push navigation, with embedding over the C ABI underneath. - The docs site, component catalog, theme packs, showcase apps, and CHANGELOG carry the full account. |
||
|
|
2591284640 | Fix generated manifest window options | ||
|
|
9af28f75d5 | Fix Windows main WebView bridge and accessibility | ||
|
|
230fe3ff00 | Fix chromium backend gating | ||
|
|
d2ac9d000a |
Add Linux credential store support
- Wire Linux system WebView credential service hooks - Store credentials through optional Secret Service/libsecret - Update Linux credential support docs and templates |
||
|
|
7ec674ea26 |
Add Windows native control views
- Adds Win32 native view creation/update/focus/close support for the Windows system WebView host. - Routes button-style native controls through the shared native command event. - Links common controls for Windows examples and updates native control support docs. |
||
|
|
1aa2267b69 |
Advance native-first app model
- Add generic native view/shell APIs with command routing, automation snapshots, and a native-shell example. - Add native OS capability coverage for notifications, external links, recent documents, and credentials with guarded bridge APIs. - Update manifests, TypeScript declarations, product docs, and platform hosts for the new native-first surface. |
||
|
|
670e8a488d |
Add keyboard shortcut support
- Add shortcut API, app.zon parsing, and runtime dispatch to Zig and JS listeners. - Register app.zon shortcuts automatically from generated and example runners, with Zig overrides for dynamic lists. - Document shortcut configuration and update package JavaScript typings. |
||
|
|
e6fcacbd9c | set macOS 11 baseline | ||
|
|
fe3b73cb83 | add linux and windows desktop support | ||
|
|
8dfe7c7a52 | fix Chromium example runtime staging | ||
|
|
aba572866d | init |