@native-sdk/core 0.5.4 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/sdk/core.ts +481 -2
- package/sdk/events.ts +17 -8
package/package.json
CHANGED
package/sdk/core.ts
CHANGED
|
@@ -103,6 +103,23 @@
|
|
|
103
103
|
// forget control verbs whose consequences arrive
|
|
104
104
|
// on the event stream; aimed at a key with no
|
|
105
105
|
// open stream they no-op.
|
|
106
|
+
// Cmd.videoLoad(key, { surface, path?, url?, autoplay?, loop?, muted? }, { event })
|
|
107
|
+
// open (or replace — one player is the whole
|
|
108
|
+
// surface) the video event stream feeding the
|
|
109
|
+
// media-surface `surface` names (the same
|
|
110
|
+
// model-owned id the markup binds): every
|
|
111
|
+
// playback event dispatches the `event` arm (the
|
|
112
|
+
// seven-field record below) until Cmd.videoStop
|
|
113
|
+
// closes the stream. Pixels never ride the
|
|
114
|
+
// events — decoded frames flow platform-side
|
|
115
|
+
// into the bound surface.
|
|
116
|
+
// Cmd.videoPlay(key) / videoPause(key) / videoStop(key)
|
|
117
|
+
// Cmd.videoSeek(key, ms) / Cmd.videoSetVolume(key, volume)
|
|
118
|
+
// Cmd.videoSetMuted(key, muted) / Cmd.videoSetLoop(key, loop)
|
|
119
|
+
// drive the open playback in place — fire-and-
|
|
120
|
+
// forget control verbs whose consequences arrive
|
|
121
|
+
// on the event stream; aimed at a key with no
|
|
122
|
+
// open stream they no-op.
|
|
106
123
|
// Cmd.imageLoad(id, { path?, url?, cachePath?, expectedBytes? }, { event })
|
|
107
124
|
// load an image at runtime by its model-owned
|
|
108
125
|
// numeric ImageId (the id markup binds:
|
|
@@ -132,6 +149,67 @@
|
|
|
132
149
|
// the id is untouched: its terminal still
|
|
133
150
|
// registers — cancel the load first to keep
|
|
134
151
|
// the slot free.
|
|
152
|
+
// Cmd.channelOpen(key, { event })
|
|
153
|
+
// open an EXTERNAL-SOURCE CHANNEL under the
|
|
154
|
+
// app's numeric key: the host stages a
|
|
155
|
+
// long-lived, thread-safe posting seam its
|
|
156
|
+
// NATIVE side feeds — embedders and
|
|
157
|
+
// platform-services extensions post bytes
|
|
158
|
+
// from their own threads (sockets, watchers,
|
|
159
|
+
// workers), and each accepted post arrives as
|
|
160
|
+
// one "data" event through the `event` arm
|
|
161
|
+
// (the five-field record below) with the
|
|
162
|
+
// honest back-pressure counters aboard. The
|
|
163
|
+
// TS tier opens, closes, and receives;
|
|
164
|
+
// POSTING is not a TS verb — transpiled cores
|
|
165
|
+
// are single-threaded by design, so the
|
|
166
|
+
// posting handle lives on the native side
|
|
167
|
+
// (`Effects.channelHandle(key)`). One channel
|
|
168
|
+
// per key at a time: a duplicate live key
|
|
169
|
+
// dispatches state "rejected". No timer
|
|
170
|
+
// polling anywhere: the source wakes the
|
|
171
|
+
// loop itself.
|
|
172
|
+
// Cmd.channelClose(key) close the open channel under the key, if
|
|
173
|
+
// any: staged posts flush, exactly one
|
|
174
|
+
// "closed" event (final drop totals)
|
|
175
|
+
// dispatches the event arm, and the key
|
|
176
|
+
// frees. A key with no open channel no-ops.
|
|
177
|
+
// Cmd.ptySpawn(argv, { key?, cols?, rows?, term?, event })
|
|
178
|
+
// open a PSEUDO-TERMINAL SESSION — a spawn
|
|
179
|
+
// with a different transport: run argv on a
|
|
180
|
+
// fresh pty whose initial grid is cols x rows
|
|
181
|
+
// (80x24 by default) and whose TERM is `term`
|
|
182
|
+
// (omitted = the engine's default). Every
|
|
183
|
+
// session event dispatches the `event` arm
|
|
184
|
+
// (the six-field record below): "output"
|
|
185
|
+
// events carry coalesced batches of child
|
|
186
|
+
// output across dispatches — feed them to the
|
|
187
|
+
// terminal emulator — until the exactly-one
|
|
188
|
+
// "exit" terminal (a refused spawn is one
|
|
189
|
+
// "exit" with reason "rejected"; a transport
|
|
190
|
+
// that could not start, one with
|
|
191
|
+
// "spawn_failed"). One session per key at a
|
|
192
|
+
// time, never replaced implicitly — kill it
|
|
193
|
+
// first, the spawn discipline.
|
|
194
|
+
// Cmd.ptyWrite(key, bytes) write bytes toward the session's child —
|
|
195
|
+
// keystrokes and pastes, fire-and-forget: a
|
|
196
|
+
// key with no open session no-ops, and
|
|
197
|
+
// refused payloads count into the exit's
|
|
198
|
+
// droppedWrites, never silence.
|
|
199
|
+
// Cmd.ptyResize(key, cols, rows)
|
|
200
|
+
// push a new grid to the session so the child
|
|
201
|
+
// receives SIGWINCH — fire-and-forget like
|
|
202
|
+
// ptyWrite; a key with no open session
|
|
203
|
+
// no-ops.
|
|
204
|
+
// Cmd.ptyKill(key) terminate the session's child — LOUD, the
|
|
205
|
+
// spawn cancel discipline: the session's one
|
|
206
|
+
// "exit" terminal arrives through its own
|
|
207
|
+
// event arm with reason "cancelled" and the
|
|
208
|
+
// key frees once it lands. A key with no open
|
|
209
|
+
// session no-ops. Sessions are their own
|
|
210
|
+
// family's to end: the string-keyed
|
|
211
|
+
// Cmd.cancel never touches them, the way
|
|
212
|
+
// Cmd.audioStop is audio's close.
|
|
135
213
|
//
|
|
136
214
|
// The window verbs (fire-and-forget, no result Msg — the window's own
|
|
137
215
|
// frame event carries the state):
|
|
@@ -163,8 +241,8 @@
|
|
|
163
241
|
// only in that function's return path (NS1025). Sub and the streaming Cmds
|
|
164
242
|
// are different animals on purpose: a Sub is DECLARED from the model (the
|
|
165
243
|
// host starts/stops it by reconciliation; the app never opens one), while
|
|
166
|
-
// spawn/audioPlay streams are Cmd-INITIATED — imperative opens
|
|
167
|
-
// lifecycle the app drives and cancels.
|
|
244
|
+
// spawn/audioPlay/videoLoad streams are Cmd-INITIATED — imperative opens
|
|
245
|
+
// with a keyed lifecycle the app drives and cancels.
|
|
168
246
|
//
|
|
169
247
|
// The factories return plain frozen-shape objects so the same core runs
|
|
170
248
|
// under node: a dev harness can interpret the `op` tags directly.
|
|
@@ -309,6 +387,54 @@ export type AudioEventKind<M extends Msgish> = M extends Msgish
|
|
|
309
387
|
: never
|
|
310
388
|
: never;
|
|
311
389
|
|
|
390
|
+
/// The video event states, the audio vocabulary without spectrum: `loaded`
|
|
391
|
+
/// acknowledges a successful load with the player's duration estimate and
|
|
392
|
+
/// the stream's decoded pixel dimensions; `position` ticks at the
|
|
393
|
+
/// platform's honest cadence (~500ms) while playing; `completed` fires
|
|
394
|
+
/// exactly once when a NON-LOOPING playback reaches its natural end (a
|
|
395
|
+
/// looping playback wraps and never completes); `failed` reports a
|
|
396
|
+
/// load/decode/device failure; `rejected` a command the effects layer
|
|
397
|
+
/// refused (an empty or over-long source, a non-http(s) url, an invalid
|
|
398
|
+
/// surface id).
|
|
399
|
+
export type VideoState = "loaded" | "position" | "completed" | "failed" | "rejected";
|
|
400
|
+
|
|
401
|
+
/// The payload shape of a video event arm — seven fields, matched by NAME
|
|
402
|
+
/// (the AudioEventArm convention). `state` must be a named
|
|
403
|
+
/// string-literal-union alias carrying exactly the five VideoState members
|
|
404
|
+
/// (any declaration order — the host matches members by name).
|
|
405
|
+
/// `positionMs`/`durationMs` are milliseconds; `playing` is the player's
|
|
406
|
+
/// transport state; `buffering` is true while a streamed source is stalled
|
|
407
|
+
/// waiting for network bytes; `width`/`height` are the stream's decoded
|
|
408
|
+
/// pixel dimensions (delivered on "loaded", 0 elsewhere).
|
|
409
|
+
export type VideoEventArm = {
|
|
410
|
+
readonly state: VideoState;
|
|
411
|
+
readonly positionMs: number;
|
|
412
|
+
readonly durationMs: number;
|
|
413
|
+
readonly playing: boolean;
|
|
414
|
+
readonly buffering: boolean;
|
|
415
|
+
readonly width: number;
|
|
416
|
+
readonly height: number;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/// The Msg arms a video event stream may target: arms whose payload is
|
|
420
|
+
/// exactly the seven VideoEventArm fields. The `state` check runs BOTH
|
|
421
|
+
/// directions (the AudioEventKind convention): the `&` constraint holds
|
|
422
|
+
/// the arm's states to VideoState, and the tuple-wrapped reverse check
|
|
423
|
+
/// holds VideoState to the arm's states — a narrower union would
|
|
424
|
+
/// silently drop event states the host emits, so it is refused here,
|
|
425
|
+
/// not discovered at runtime.
|
|
426
|
+
export type VideoEventKind<M extends Msgish> = M extends Msgish
|
|
427
|
+
? [Exclude<keyof M, "kind">] extends [keyof VideoEventArm]
|
|
428
|
+
? [keyof VideoEventArm] extends [Exclude<keyof M, "kind">]
|
|
429
|
+
? M extends Msgish & VideoEventArm
|
|
430
|
+
? [VideoState] extends [M["state"]]
|
|
431
|
+
? M["kind"]
|
|
432
|
+
: never
|
|
433
|
+
: never
|
|
434
|
+
: never
|
|
435
|
+
: never
|
|
436
|
+
: never;
|
|
437
|
+
|
|
312
438
|
/// The image load result states, mirroring the engine's outcome vocabulary:
|
|
313
439
|
/// "loaded" means the pixels are registered under the requested id (width and
|
|
314
440
|
/// height carry what the codec decoded); every other state is the failure
|
|
@@ -378,6 +504,130 @@ export type ImageEventKind<M extends Msgish> = M extends Msgish
|
|
|
378
504
|
: never
|
|
379
505
|
: never;
|
|
380
506
|
|
|
507
|
+
/// The external-source channel event states, mirroring the engine's
|
|
508
|
+
/// vocabulary: "data" is one delivered post from the native side's
|
|
509
|
+
/// thread-safe handle; "closed" is the exactly-one terminal
|
|
510
|
+
/// `Cmd.channelClose` produces (final drop totals aboard); "rejected" is
|
|
511
|
+
/// the exactly-one terminal of a refused open (duplicate live key, a
|
|
512
|
+
/// full channel table, an occupied engine key).
|
|
513
|
+
export type ChannelState = "data" | "closed" | "rejected";
|
|
514
|
+
|
|
515
|
+
/// The payload shape of a channel event arm — five fields, matched by
|
|
516
|
+
/// NAME (the AudioEventArm convention). `key` is the channel key echoed
|
|
517
|
+
/// verbatim, so concurrent channels sharing one arm stay
|
|
518
|
+
/// distinguishable (a key the wire cannot carry exactly echoes 0);
|
|
519
|
+
/// `state` must be a named string-literal-union alias carrying exactly
|
|
520
|
+
/// the three ChannelState members (any declaration order — the host
|
|
521
|
+
/// matches members by name); `bytes` is the post's payload ("data"
|
|
522
|
+
/// events only, empty otherwise); `droppedPending`/`droppedTotal` are
|
|
523
|
+
/// the back-pressure counters — posts the native side's handle refused
|
|
524
|
+
/// since the previous delivered event, and over the channel's whole
|
|
525
|
+
/// life. Never silent drops: the counters ride every event.
|
|
526
|
+
export type ChannelEventArm = {
|
|
527
|
+
readonly key: number;
|
|
528
|
+
readonly state: ChannelState;
|
|
529
|
+
readonly bytes: Uint8Array;
|
|
530
|
+
readonly droppedPending: number;
|
|
531
|
+
readonly droppedTotal: number;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
/// The Msg arms a channel event stream may target: arms whose payload is
|
|
535
|
+
/// exactly the five ChannelEventArm fields. The `state` check runs BOTH
|
|
536
|
+
/// directions (the AudioEventKind convention): the `&` constraint holds
|
|
537
|
+
/// the arm's states to ChannelState, and the tuple-wrapped reverse check
|
|
538
|
+
/// holds ChannelState to the arm's states — a narrower union would
|
|
539
|
+
/// silently drop event states the host emits, so it is refused here,
|
|
540
|
+
/// not discovered at runtime.
|
|
541
|
+
export type ChannelEventKind<M extends Msgish> = M extends Msgish
|
|
542
|
+
? [Exclude<keyof M, "kind">] extends [keyof ChannelEventArm]
|
|
543
|
+
? [keyof ChannelEventArm] extends [Exclude<keyof M, "kind">]
|
|
544
|
+
? M extends Msgish & ChannelEventArm
|
|
545
|
+
? [ChannelState] extends [M["state"]]
|
|
546
|
+
? M["kind"]
|
|
547
|
+
: never
|
|
548
|
+
: never
|
|
549
|
+
: never
|
|
550
|
+
: never
|
|
551
|
+
: never;
|
|
552
|
+
|
|
553
|
+
/// `Cmd.channelOpen` routing: every channel event dispatches the `event`
|
|
554
|
+
/// arm (the five-field ChannelEventArm record, matched by field name).
|
|
555
|
+
export interface ChannelRoute<M extends Msgish> {
|
|
556
|
+
readonly event: ChannelEventKind<M>;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/// The pty session event states, mirroring the engine's vocabulary:
|
|
560
|
+
/// "output" is one coalesced batch of child output; "exit" is the
|
|
561
|
+
/// exactly-one terminal every session produces — a clean end, a signal,
|
|
562
|
+
/// a `Cmd.ptyKill`, a refused spawn, or a transport that could not
|
|
563
|
+
/// start (the `reason` field tells which).
|
|
564
|
+
export type PtyState = "output" | "exit";
|
|
565
|
+
|
|
566
|
+
/// How a pty session ended, the spawn exit vocabulary: "exited" with
|
|
567
|
+
/// the child's code, "signaled" with the signal, "cancelled" after
|
|
568
|
+
/// `Cmd.ptyKill`, "rejected" for a spawn refused before a child existed
|
|
569
|
+
/// (duplicate live key, full table, bad grid), "spawn_failed" when the
|
|
570
|
+
/// pty or exec could not start.
|
|
571
|
+
export type PtyExitReason = "exited" | "signaled" | "cancelled" | "rejected" | "spawn_failed";
|
|
572
|
+
|
|
573
|
+
/// The payload shape of a pty event arm — seven fields, matched by NAME
|
|
574
|
+
/// (the AudioEventArm convention). `key` is the app's own session key
|
|
575
|
+
/// (the `Cmd.ptySpawn` `key`, or "" when the spawn named none) — two
|
|
576
|
+
/// sessions routing one event arm are told apart by this field. `state`
|
|
577
|
+
/// must be a named string-literal-union alias carrying exactly the two
|
|
578
|
+
/// PtyState members and `reason` one carrying exactly the five
|
|
579
|
+
/// PtyExitReason members (any declaration order — the host matches
|
|
580
|
+
/// members by name); `bytes` is the coalesced output batch ("output"
|
|
581
|
+
/// events only, empty otherwise); `code` is the child's exit code on an
|
|
582
|
+
/// "exited" end, -1 otherwise; `signal` is the fatal signal after a
|
|
583
|
+
/// "signaled" end, else 0; `droppedWrites` counts `Cmd.ptyWrite`
|
|
584
|
+
/// payloads refused over the session's life — zero means every write
|
|
585
|
+
/// reached the child, never a silent drop.
|
|
586
|
+
export type PtyEventArm = {
|
|
587
|
+
readonly key: string;
|
|
588
|
+
readonly state: PtyState;
|
|
589
|
+
readonly bytes: Uint8Array;
|
|
590
|
+
readonly code: number;
|
|
591
|
+
readonly reason: PtyExitReason;
|
|
592
|
+
readonly signal: number;
|
|
593
|
+
readonly droppedWrites: number;
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
/// The Msg arms a pty session may target: arms whose payload is exactly
|
|
597
|
+
/// the six PtyEventArm fields. The `state` and `reason` checks run BOTH
|
|
598
|
+
/// directions (the AudioEventKind convention): the `&` constraint holds
|
|
599
|
+
/// the arm's unions to PtyState/PtyExitReason, and the tuple-wrapped
|
|
600
|
+
/// reverse checks hold them to the arm's — a narrower union would
|
|
601
|
+
/// silently drop events the host emits, so it is refused here, not
|
|
602
|
+
/// discovered at runtime.
|
|
603
|
+
export type PtyEventKind<M extends Msgish> = M extends Msgish
|
|
604
|
+
? [Exclude<keyof M, "kind">] extends [keyof PtyEventArm]
|
|
605
|
+
? [keyof PtyEventArm] extends [Exclude<keyof M, "kind">]
|
|
606
|
+
? M extends Msgish & PtyEventArm
|
|
607
|
+
? [PtyState] extends [M["state"]]
|
|
608
|
+
? [PtyExitReason] extends [M["reason"]]
|
|
609
|
+
? M["kind"]
|
|
610
|
+
: never
|
|
611
|
+
: never
|
|
612
|
+
: never
|
|
613
|
+
: never
|
|
614
|
+
: never
|
|
615
|
+
: never;
|
|
616
|
+
|
|
617
|
+
/// `Cmd.ptySpawn` routing: every session event dispatches the `event`
|
|
618
|
+
/// arm (the six-field PtyEventArm record, matched by field name).
|
|
619
|
+
/// `cols`/`rows` are the initial grid the child observes (80x24 when
|
|
620
|
+
/// omitted); `term` is the TERM the child starts with (omitted = the
|
|
621
|
+
/// engine's default). The optional `key` names the session for
|
|
622
|
+
/// ptyWrite/ptyResize/ptyKill.
|
|
623
|
+
export interface PtyRoute<M extends Msgish> {
|
|
624
|
+
readonly key?: string;
|
|
625
|
+
readonly cols?: number;
|
|
626
|
+
readonly rows?: number;
|
|
627
|
+
readonly term?: string;
|
|
628
|
+
readonly event: PtyEventKind<M>;
|
|
629
|
+
}
|
|
630
|
+
|
|
381
631
|
/// One field of a host record payload; see hostRecordBytes for the encoding.
|
|
382
632
|
export type HostScalar = number | boolean | Uint8Array;
|
|
383
633
|
|
|
@@ -454,6 +704,30 @@ export interface AudioRoute<M extends Msgish> {
|
|
|
454
704
|
readonly event: AudioEventKind<M>;
|
|
455
705
|
}
|
|
456
706
|
|
|
707
|
+
/// A `Cmd.videoLoad` source. `surface` is the model-owned media-surface id
|
|
708
|
+
/// the markup binds — the texture channel the decoded frames feed. The
|
|
709
|
+
/// local `path` is tried first; a missing file falls through to `url`
|
|
710
|
+
/// (streamed progressively, playable before the download finishes). At
|
|
711
|
+
/// least one of path/url must be present. `autoplay` (default true) starts
|
|
712
|
+
/// playback as soon as the load lands — false loads paused at position
|
|
713
|
+
/// zero, the poster-frame shape; `loop` wraps from the natural end back to
|
|
714
|
+
/// zero (a looping playback never delivers "completed"); `muted` starts
|
|
715
|
+
/// the audio track muted, independent of the remembered volume.
|
|
716
|
+
export interface VideoSource {
|
|
717
|
+
readonly surface: number;
|
|
718
|
+
readonly path?: Uint8Array;
|
|
719
|
+
readonly url?: Uint8Array;
|
|
720
|
+
readonly autoplay?: boolean;
|
|
721
|
+
readonly loop?: boolean;
|
|
722
|
+
readonly muted?: boolean;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/// `Cmd.videoLoad` routing: every playback event dispatches the `event`
|
|
726
|
+
/// arm (the seven-field VideoEventArm record, matched by field name).
|
|
727
|
+
export interface VideoRoute<M extends Msgish> {
|
|
728
|
+
readonly event: VideoEventKind<M>;
|
|
729
|
+
}
|
|
730
|
+
|
|
457
731
|
/// A `Cmd.imageLoad` source: the audio cascade's shape exactly. The local
|
|
458
732
|
/// `path` is tried first; a missing file falls through to `url` (fetched
|
|
459
733
|
/// whole, installed at `cachePath` when given and verified against
|
|
@@ -570,6 +844,25 @@ export type Cmd<M extends Msgish> =
|
|
|
570
844
|
/// Seek position (ms) / volume (0..1); 0 for the value-less verbs.
|
|
571
845
|
readonly value: number;
|
|
572
846
|
}
|
|
847
|
+
| {
|
|
848
|
+
readonly op: "video_load";
|
|
849
|
+
readonly key: string;
|
|
850
|
+
readonly eventKind: string;
|
|
851
|
+
readonly surface: number;
|
|
852
|
+
readonly path: Uint8Array;
|
|
853
|
+
readonly url: Uint8Array;
|
|
854
|
+
readonly autoplay: boolean;
|
|
855
|
+
readonly loop: boolean;
|
|
856
|
+
readonly muted: boolean;
|
|
857
|
+
}
|
|
858
|
+
| {
|
|
859
|
+
readonly op: "video_ctl";
|
|
860
|
+
readonly key: string;
|
|
861
|
+
readonly verb: "play" | "pause" | "stop" | "seek" | "volume" | "muted" | "loop";
|
|
862
|
+
/// Seek position (ms) / volume (0..1) / the muted-loop switch
|
|
863
|
+
/// (0 = off, 1 = on); 0 for the value-less verbs.
|
|
864
|
+
readonly value: number;
|
|
865
|
+
}
|
|
573
866
|
| { readonly op: "window_show"; readonly label: string }
|
|
574
867
|
| { readonly op: "quit_app" }
|
|
575
868
|
| {
|
|
@@ -583,6 +876,21 @@ export type Cmd<M extends Msgish> =
|
|
|
583
876
|
}
|
|
584
877
|
| { readonly op: "image_cancel"; readonly id: number }
|
|
585
878
|
| { readonly op: "image_unregister"; readonly id: number }
|
|
879
|
+
| { readonly op: "channel_open"; readonly key: number; readonly eventKind: string }
|
|
880
|
+
| { readonly op: "channel_close"; readonly key: number }
|
|
881
|
+
| {
|
|
882
|
+
readonly op: "pty_spawn";
|
|
883
|
+
readonly key: string;
|
|
884
|
+
readonly eventKind: string;
|
|
885
|
+
readonly cols: number;
|
|
886
|
+
readonly rows: number;
|
|
887
|
+
/// "" = the engine's default TERM (the wire never bakes it in).
|
|
888
|
+
readonly term: string;
|
|
889
|
+
readonly argv: readonly Uint8Array[];
|
|
890
|
+
}
|
|
891
|
+
| { readonly op: "pty_write"; readonly key: string; readonly bytes: Uint8Array }
|
|
892
|
+
| { readonly op: "pty_resize"; readonly key: string; readonly cols: number; readonly rows: number }
|
|
893
|
+
| { readonly op: "pty_kill"; readonly key: string }
|
|
586
894
|
| { readonly op: "batch"; readonly cmds: readonly Cmd<M>[] };
|
|
587
895
|
|
|
588
896
|
/// The wire encoding of a host record payload, byte-identical to what the
|
|
@@ -811,6 +1119,81 @@ export const Cmd = {
|
|
|
811
1119
|
return { op: "audio_ctl", key, verb: "volume", value: volume };
|
|
812
1120
|
},
|
|
813
1121
|
|
|
1122
|
+
/// Open (or replace — one player is the whole surface) the keyed video
|
|
1123
|
+
/// event stream: claim the media-surface the source names, resolve the
|
|
1124
|
+
/// source cascade (local path, then url) and start playback (autoplay,
|
|
1125
|
+
/// the default). Every playback event dispatches the `event` arm until
|
|
1126
|
+
/// `Cmd.videoStop(key)` closes the stream. Pixels never ride the
|
|
1127
|
+
/// events: decoded frames flow platform-side into the bound surface.
|
|
1128
|
+
/// Failure is never silent: an unplayable source arrives as a "failed"
|
|
1129
|
+
/// event, a refused command as "rejected". Replacing is not stopping:
|
|
1130
|
+
/// a replaced load still delivers the terminal it owes (its failure is
|
|
1131
|
+
/// never silent either), routed to ITS OWN event arm — only
|
|
1132
|
+
/// `Cmd.videoStop` cancels a stream's undelivered answers.
|
|
1133
|
+
videoLoad<M extends Msgish>(key: string, source: VideoSource, route: VideoRoute<M>): Cmd<M> {
|
|
1134
|
+
return {
|
|
1135
|
+
op: "video_load",
|
|
1136
|
+
key,
|
|
1137
|
+
eventKind: route.event,
|
|
1138
|
+
surface: source.surface,
|
|
1139
|
+
path: source.path ?? new Uint8Array(0),
|
|
1140
|
+
url: source.url ?? new Uint8Array(0),
|
|
1141
|
+
autoplay: source.autoplay ?? true,
|
|
1142
|
+
loop: source.loop ?? false,
|
|
1143
|
+
muted: source.muted ?? false,
|
|
1144
|
+
};
|
|
1145
|
+
},
|
|
1146
|
+
|
|
1147
|
+
/// Start or resume the loaded playback — the poster-frame counterpart
|
|
1148
|
+
/// of `autoplay: false`, and un-pause. A key with no open stream
|
|
1149
|
+
/// no-ops; a player that can no longer start reports one "failed"
|
|
1150
|
+
/// event on the stream instead of silence.
|
|
1151
|
+
videoPlay(key: string): Cmd<never> {
|
|
1152
|
+
return { op: "video_ctl", key, verb: "play", value: 0 };
|
|
1153
|
+
},
|
|
1154
|
+
|
|
1155
|
+
/// Pause the keyed playback in place; the surface keeps its last frame
|
|
1156
|
+
/// (no event echo — the caller commanded it). A key with no open
|
|
1157
|
+
/// stream no-ops.
|
|
1158
|
+
videoPause(key: string): Cmd<never> {
|
|
1159
|
+
return { op: "video_ctl", key, verb: "pause", value: 0 };
|
|
1160
|
+
},
|
|
1161
|
+
|
|
1162
|
+
/// Stop the keyed playback, release the surface claim, and CLOSE its
|
|
1163
|
+
/// event stream: no events for the key after this. Stop is the video
|
|
1164
|
+
/// stream's cancel, exactly `Cmd.audioStop`'s discipline.
|
|
1165
|
+
videoStop(key: string): Cmd<never> {
|
|
1166
|
+
return { op: "video_ctl", key, verb: "stop", value: 0 };
|
|
1167
|
+
},
|
|
1168
|
+
|
|
1169
|
+
/// Jump the keyed playback to `ms` (the platform clamps to the
|
|
1170
|
+
/// duration; a paused seek still pushes the sought frame, so scrubbing
|
|
1171
|
+
/// is visible). No event echo — the next position tick reports from
|
|
1172
|
+
/// there.
|
|
1173
|
+
videoSeek(key: string, ms: number): Cmd<never> {
|
|
1174
|
+
return { op: "video_ctl", key, verb: "seek", value: ms };
|
|
1175
|
+
},
|
|
1176
|
+
|
|
1177
|
+
/// Set playback volume, clamped to 0..1 and remembered across loads:
|
|
1178
|
+
/// the next videoLoad re-applies it. Independent of mute.
|
|
1179
|
+
videoSetVolume(key: string, volume: number): Cmd<never> {
|
|
1180
|
+
return { op: "video_ctl", key, verb: "volume", value: volume };
|
|
1181
|
+
},
|
|
1182
|
+
|
|
1183
|
+
/// Mute or unmute the playback's audio track without touching the
|
|
1184
|
+
/// remembered volume (a fresh load's `muted` option is the way to
|
|
1185
|
+
/// start muted).
|
|
1186
|
+
videoSetMuted(key: string, muted: boolean): Cmd<never> {
|
|
1187
|
+
return { op: "video_ctl", key, verb: "muted", value: muted ? 1 : 0 };
|
|
1188
|
+
},
|
|
1189
|
+
|
|
1190
|
+
/// Enable or disable looping on the open playback (a fresh load's
|
|
1191
|
+
/// `loop` option covers the start-looping case). A looping playback
|
|
1192
|
+
/// never delivers "completed".
|
|
1193
|
+
videoSetLoop(key: string, loop: boolean): Cmd<never> {
|
|
1194
|
+
return { op: "video_ctl", key, verb: "loop", value: loop ? 1 : 0 };
|
|
1195
|
+
},
|
|
1196
|
+
|
|
814
1197
|
/// Show the window with the declared `label`: un-hide + activate — the
|
|
815
1198
|
/// counterpart to a `close_policy = "hide"` hide and the tray "Open"
|
|
816
1199
|
/// consequence; also restores a minimized window. Fire-and-forget: no
|
|
@@ -881,6 +1264,102 @@ export const Cmd = {
|
|
|
881
1264
|
return { op: "image_unregister", id };
|
|
882
1265
|
},
|
|
883
1266
|
|
|
1267
|
+
/// Open an external-source channel under your numeric `key`: the
|
|
1268
|
+
/// host stages a long-lived, thread-safe posting seam its NATIVE
|
|
1269
|
+
/// side feeds — an embedder or platform-services extension resolves
|
|
1270
|
+
/// the handle (`Effects.channelHandle(key)`) and posts bytes from
|
|
1271
|
+
/// its own threads (a socket reader, a file watcher, a worker), and
|
|
1272
|
+
/// each accepted post arrives as one "data" event through the
|
|
1273
|
+
/// `event` arm, waking the loop itself — no timer polling anywhere.
|
|
1274
|
+
/// POSTING is deliberately not a TS verb: transpiled cores are
|
|
1275
|
+
/// single-threaded, so the TS tier opens, closes, and receives while
|
|
1276
|
+
/// the native side feeds. Back-pressure is part of the contract:
|
|
1277
|
+
/// the native `post` answers a four-way `ChannelHandle.PostResult`
|
|
1278
|
+
/// (accepted / dropped_full / dropped_oversized / closed), so a
|
|
1279
|
+
/// producer tells transient back-pressure from closure, and refused
|
|
1280
|
+
/// posts count into `droppedPending`/`droppedTotal` on the next
|
|
1281
|
+
/// delivered event, never silence. The native post never blocks its
|
|
1282
|
+
/// producer given a conforming host wake — the platform's `wake_fn`
|
|
1283
|
+
/// is contractually a bounded, enqueue-only nudge (see
|
|
1284
|
+
/// `PlatformServices.wake_fn`; every first-party host conforms), and
|
|
1285
|
+
/// the runtime holds no channel lock across it. One channel per key at a
|
|
1286
|
+
/// time — a duplicate live key dispatches state "rejected" — and the
|
|
1287
|
+
/// key shares the engine's effect-key space (a same-key fetch is
|
|
1288
|
+
/// blocked while the channel lives). Keys are positive integers
|
|
1289
|
+
/// below 2^53; the events are journaled at the effect boundary, so
|
|
1290
|
+
/// a recorded session replays the whole stream from the journal and
|
|
1291
|
+
/// never NEEDS the source — under replay the open parks and the
|
|
1292
|
+
/// native handle is inert (every post answers closed). The opening
|
|
1293
|
+
/// update still re-executes, so a native producer launched
|
|
1294
|
+
/// unconditionally really starts and is stopped at its first post;
|
|
1295
|
+
/// one that consults the handle's live() before launching never
|
|
1296
|
+
/// starts, keeping replay fully offline.
|
|
1297
|
+
channelOpen<M extends Msgish>(key: number, route: ChannelRoute<M>): Cmd<M> {
|
|
1298
|
+
return { op: "channel_open", key, eventKind: route.event };
|
|
1299
|
+
},
|
|
1300
|
+
|
|
1301
|
+
/// Close the open channel under `key`: posts stop landing, the
|
|
1302
|
+
/// staged backlog flushes, and exactly one "closed" event (final
|
|
1303
|
+
/// drop totals aboard) dispatches the event arm — then the key is
|
|
1304
|
+
/// free again. A key with no open channel no-ops. Channels are keyed
|
|
1305
|
+
/// by their numeric key, so the string-keyed `Cmd.cancel` never
|
|
1306
|
+
/// touches them — this is their close, the way `Cmd.audioStop` is
|
|
1307
|
+
/// audio's.
|
|
1308
|
+
channelClose(key: number): Cmd<never> {
|
|
1309
|
+
return { op: "channel_close", key };
|
|
1310
|
+
},
|
|
1311
|
+
|
|
1312
|
+
/// Open a pseudo-terminal session — a spawn with a different
|
|
1313
|
+
/// transport: run `argv` on a fresh pty (same argv budgets, same
|
|
1314
|
+
/// child environment policy) whose initial grid is `cols` x `rows`
|
|
1315
|
+
/// (80x24 by default) and whose TERM is `term` (omitted = the
|
|
1316
|
+
/// engine's default). Every session event dispatches the `event` arm:
|
|
1317
|
+
/// "output" events carry coalesced batches of child output across
|
|
1318
|
+
/// dispatches — feed them to the terminal emulator and move on —
|
|
1319
|
+
/// until the exactly-one "exit" terminal retires the session (a
|
|
1320
|
+
/// refused spawn is one "exit" with reason "rejected"; a transport
|
|
1321
|
+
/// that could not start, one with "spawn_failed" — failure is never
|
|
1322
|
+
/// silent). One session per key at a time, never replaced implicitly:
|
|
1323
|
+
/// a running terminal's child is a running subprocess — kill it
|
|
1324
|
+
/// first, the spawn discipline.
|
|
1325
|
+
ptySpawn<M extends Msgish>(argv: readonly Uint8Array[], route: PtyRoute<M>): Cmd<M> {
|
|
1326
|
+
return {
|
|
1327
|
+
op: "pty_spawn",
|
|
1328
|
+
key: route.key ?? "",
|
|
1329
|
+
eventKind: route.event,
|
|
1330
|
+
cols: route.cols ?? 80,
|
|
1331
|
+
rows: route.rows ?? 24,
|
|
1332
|
+
term: route.term ?? "",
|
|
1333
|
+
argv,
|
|
1334
|
+
};
|
|
1335
|
+
},
|
|
1336
|
+
|
|
1337
|
+
/// Write bytes toward the keyed session's child — keystrokes and
|
|
1338
|
+
/// pastes, fire-and-forget: a key with no open session no-ops (the
|
|
1339
|
+
/// exit was already on its way), and refused payloads (over the
|
|
1340
|
+
/// engine's per-write bound, or a child that stopped reading) count
|
|
1341
|
+
/// into the exit event's droppedWrites — never silence.
|
|
1342
|
+
ptyWrite(key: string, bytes: Uint8Array): Cmd<never> {
|
|
1343
|
+
return { op: "pty_write", key, bytes };
|
|
1344
|
+
},
|
|
1345
|
+
|
|
1346
|
+
/// Push a new grid to the keyed session so the child receives
|
|
1347
|
+
/// SIGWINCH — fire-and-forget like ptyWrite; a key with no open
|
|
1348
|
+
/// session no-ops.
|
|
1349
|
+
ptyResize(key: string, cols: number, rows: number): Cmd<never> {
|
|
1350
|
+
return { op: "pty_resize", key, cols, rows };
|
|
1351
|
+
},
|
|
1352
|
+
|
|
1353
|
+
/// Terminate the keyed session's child — LOUD, the spawn cancel
|
|
1354
|
+
/// discipline: the session's one "exit" terminal arrives through its
|
|
1355
|
+
/// own event arm with reason "cancelled" and the key frees once it
|
|
1356
|
+
/// lands. A key with no open session no-ops. Sessions are their own
|
|
1357
|
+
/// family's to end: the string-keyed `Cmd.cancel` never touches them
|
|
1358
|
+
/// — this is their kill, the way `Cmd.audioStop` is audio's close.
|
|
1359
|
+
ptyKill(key: string): Cmd<never> {
|
|
1360
|
+
return { op: "pty_kill", key };
|
|
1361
|
+
},
|
|
1362
|
+
|
|
884
1363
|
/// Several commands from one dispatch, performed in order.
|
|
885
1364
|
batch<M extends Msgish>(cmds: readonly Cmd<M>[]): Cmd<M> {
|
|
886
1365
|
return { op: "batch", cmds };
|
package/sdk/events.ts
CHANGED
|
@@ -40,15 +40,24 @@
|
|
|
40
40
|
export type { TextCaretDirection, TextCaretMove, TextSelection, TextInputEvent } from "./text.ts";
|
|
41
41
|
|
|
42
42
|
/// The scroll-state mirror markup's `on-scroll` matches structurally: a
|
|
43
|
-
/// record of exactly these
|
|
44
|
-
///
|
|
45
|
-
///
|
|
46
|
-
///
|
|
43
|
+
/// record of exactly these eight numeric fields — the TWO-AXIS shape, one
|
|
44
|
+
/// offset/velocity/viewport/content quartet per axis. Offsets and extents
|
|
45
|
+
/// are canvas points; velocities are points per second while a fling
|
|
46
|
+
/// decays. A vertical list carries live `...Y` fields and quiet `...X`
|
|
47
|
+
/// ones (offset 0, content pinned to the viewport width); a horizontal
|
|
48
|
+
/// shelf the reverse. Echo `offsetY` into the model field bound as the
|
|
49
|
+
/// scroll's `value` (and `offsetX` into `value-x` on horizontal-capable
|
|
50
|
+
/// regions) to keep the region model-driven — setting those fields in
|
|
51
|
+
/// update scrolls it.
|
|
47
52
|
export interface ScrollState {
|
|
48
|
-
readonly
|
|
49
|
-
readonly
|
|
50
|
-
readonly
|
|
51
|
-
readonly
|
|
53
|
+
readonly offsetX: number;
|
|
54
|
+
readonly offsetY: number;
|
|
55
|
+
readonly velocityX: number;
|
|
56
|
+
readonly velocityY: number;
|
|
57
|
+
readonly viewportExtentX: number;
|
|
58
|
+
readonly viewportExtentY: number;
|
|
59
|
+
readonly contentExtentX: number;
|
|
60
|
+
readonly contentExtentY: number;
|
|
52
61
|
}
|
|
53
62
|
|
|
54
63
|
/// The presented-frame channel's record (`frameMsg(model, frame)`): the
|