@native-sdk/core 0.5.3 → 0.6.0
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 +706 -5
- package/sdk/events.ts +17 -8
package/package.json
CHANGED
package/sdk/core.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// return path (NS1017) — they never live in the Model, in a Msg, in a local,
|
|
11
11
|
// or in a helper.
|
|
12
12
|
//
|
|
13
|
-
// The
|
|
13
|
+
// The v3 command set:
|
|
14
14
|
//
|
|
15
15
|
// Cmd.none no effects (what a bare `return model` means)
|
|
16
16
|
// Cmd.persist() ask the host to persist the committed model
|
|
@@ -103,6 +103,128 @@
|
|
|
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.
|
|
123
|
+
// Cmd.imageLoad(id, { path?, url?, cachePath?, expectedBytes? }, { event })
|
|
124
|
+
// load an image at runtime by its model-owned
|
|
125
|
+
// numeric ImageId (the id markup binds:
|
|
126
|
+
// <image image="{id}"/>, avatar likewise): the
|
|
127
|
+
// host resolves the source cascade (local path
|
|
128
|
+
// first, then a verified cache entry, then the
|
|
129
|
+
// network), decodes through the platform codec,
|
|
130
|
+
// registers the pixels under the id, and
|
|
131
|
+
// dispatches exactly ONE `event` arm (the
|
|
132
|
+
// five-field record below, the requested id
|
|
133
|
+
// echoed) — state "loaded" with the decoded
|
|
134
|
+
// width/height, or one failure class. One load
|
|
135
|
+
// per id at a time: a duplicate live id
|
|
136
|
+
// dispatches state "rejected".
|
|
137
|
+
// Cmd.imageCancel(id) end the in-flight load under the id, if any
|
|
138
|
+
// — loud, the spawn discipline: the load's
|
|
139
|
+
// event arm delivers state "cancelled" and the
|
|
140
|
+
// id frees for a fresh load once it lands. An
|
|
141
|
+
// id with no live load no-ops.
|
|
142
|
+
// Cmd.imageUnregister(id) free the registry slot under the id: the
|
|
143
|
+
// pixels are released, views referencing the
|
|
144
|
+
// id draw their fallback, and the slot is
|
|
145
|
+
// open for another load. Synchronous registry
|
|
146
|
+
// surgery like registration itself — not an
|
|
147
|
+
// effect, no Msg follows; an id with no
|
|
148
|
+
// registration no-ops. A load IN FLIGHT under
|
|
149
|
+
// the id is untouched: its terminal still
|
|
150
|
+
// registers — cancel the load first to keep
|
|
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.
|
|
213
|
+
//
|
|
214
|
+
// The window verbs (fire-and-forget, no result Msg — the window's own
|
|
215
|
+
// frame event carries the state):
|
|
216
|
+
//
|
|
217
|
+
// Cmd.showWindow(label) un-hide + activate the window with the
|
|
218
|
+
// declared label — the counterpart to a
|
|
219
|
+
// `close_policy = "hide"` hide and the tray
|
|
220
|
+
// "Open" consequence; also restores a
|
|
221
|
+
// minimized window. An unknown label is a
|
|
222
|
+
// no-op.
|
|
223
|
+
// Cmd.quitApp() graceful terminate, the tray "Quit"
|
|
224
|
+
// consequence: the host quits through the
|
|
225
|
+
// SAME shutdown path a last-window close
|
|
226
|
+
// takes, so the stop hook runs exactly once
|
|
227
|
+
// and a recording session seals its journal.
|
|
106
228
|
//
|
|
107
229
|
// The keyed-effect discipline is ONE rule: a keyed effect REPLACES its live
|
|
108
230
|
// predecessor (the superseded effect's result is dropped — no message), and
|
|
@@ -119,8 +241,8 @@
|
|
|
119
241
|
// only in that function's return path (NS1025). Sub and the streaming Cmds
|
|
120
242
|
// are different animals on purpose: a Sub is DECLARED from the model (the
|
|
121
243
|
// host starts/stops it by reconciliation; the app never opens one), while
|
|
122
|
-
// spawn/audioPlay streams are Cmd-INITIATED — imperative opens
|
|
123
|
-
// 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.
|
|
124
246
|
//
|
|
125
247
|
// The factories return plain frozen-shape objects so the same core runs
|
|
126
248
|
// under node: a dev harness can interpret the `op` tags directly.
|
|
@@ -248,17 +370,264 @@ export type AudioEventArm = {
|
|
|
248
370
|
};
|
|
249
371
|
|
|
250
372
|
/// The Msg arms an audio event stream may target: arms whose payload is
|
|
251
|
-
/// exactly the six AudioEventArm fields.
|
|
373
|
+
/// exactly the six AudioEventArm fields. The `state` check runs BOTH
|
|
374
|
+
/// directions: the `&` constraint holds the arm's states to AudioState,
|
|
375
|
+
/// and the tuple-wrapped reverse check holds AudioState to the arm's
|
|
376
|
+
/// states — a narrower union would silently drop event states the host
|
|
377
|
+
/// emits, so it is refused here, not discovered at runtime.
|
|
252
378
|
export type AudioEventKind<M extends Msgish> = M extends Msgish
|
|
253
379
|
? [Exclude<keyof M, "kind">] extends [keyof AudioEventArm]
|
|
254
380
|
? [keyof AudioEventArm] extends [Exclude<keyof M, "kind">]
|
|
255
381
|
? M extends Msgish & AudioEventArm
|
|
256
|
-
? M["
|
|
382
|
+
? [AudioState] extends [M["state"]]
|
|
383
|
+
? M["kind"]
|
|
384
|
+
: never
|
|
385
|
+
: never
|
|
386
|
+
: never
|
|
387
|
+
: never
|
|
388
|
+
: never;
|
|
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
|
+
|
|
438
|
+
/// The image load result states, mirroring the engine's outcome vocabulary:
|
|
439
|
+
/// "loaded" means the pixels are registered under the requested id (width and
|
|
440
|
+
/// height carry what the codec decoded); every other state is the failure
|
|
441
|
+
/// class — "rejected" (a refused command: invalid id, no source, a duplicate
|
|
442
|
+
/// live id), "not_found" (missing local file, no url), "io_failed" (a local
|
|
443
|
+
/// read failure), "connect_failed"/"tls_failed"/"protocol_failed"/"timed_out"
|
|
444
|
+
/// (the fetch taxonomy), "http_status" (a non-2xx answer; `status` carries
|
|
445
|
+
/// it), "cancelled", "too_large" (source or decoded pixels over budget),
|
|
446
|
+
/// "unsupported" (no platform codec), "decode_failed", "registry_full", and
|
|
447
|
+
/// "alloc_failed" (the host refused the memory the registration needed —
|
|
448
|
+
/// resource exhaustion, not corrupt bytes: the same source may load once
|
|
449
|
+
/// memory frees).
|
|
450
|
+
export type ImageState =
|
|
451
|
+
| "loaded"
|
|
452
|
+
| "rejected"
|
|
453
|
+
| "not_found"
|
|
454
|
+
| "io_failed"
|
|
455
|
+
| "connect_failed"
|
|
456
|
+
| "tls_failed"
|
|
457
|
+
| "protocol_failed"
|
|
458
|
+
| "timed_out"
|
|
459
|
+
| "http_status"
|
|
460
|
+
| "cancelled"
|
|
461
|
+
| "too_large"
|
|
462
|
+
| "unsupported"
|
|
463
|
+
| "decode_failed"
|
|
464
|
+
| "registry_full"
|
|
465
|
+
| "alloc_failed";
|
|
466
|
+
|
|
467
|
+
/// The payload shape of an image result arm — five fields, matched by NAME
|
|
468
|
+
/// (the AudioEventArm convention). `id` is the requested ImageId echoed
|
|
469
|
+
/// verbatim, so two concurrent loads sharing one event arm stay
|
|
470
|
+
/// distinguishable in `update` (an id the wire cannot carry exactly — 0,
|
|
471
|
+
/// negatives, fractions, 2^53 and past — echoes 0, the no-image sentinel:
|
|
472
|
+
/// there is no honest integer to echo); `state` must be a named
|
|
473
|
+
/// string-literal-union alias carrying exactly the fifteen ImageState
|
|
474
|
+
/// members (any declaration order — the host matches members by name);
|
|
475
|
+
/// `width`/`height` are the decoded pixel dimensions ("loaded" only, 0
|
|
476
|
+
/// otherwise); `status` is the HTTP status for url loads that performed an
|
|
477
|
+
/// exchange, 0 when none occurred (local paths, cache hits) — 0 is signal,
|
|
478
|
+
/// not a missing value: a cache hit is a real "loaded" with no exchange
|
|
479
|
+
/// behind it, so apps can distinguish a network load from a cached one.
|
|
480
|
+
export type ImageEventArm = {
|
|
481
|
+
readonly id: number;
|
|
482
|
+
readonly state: ImageState;
|
|
483
|
+
readonly width: number;
|
|
484
|
+
readonly height: number;
|
|
485
|
+
readonly status: number;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
/// The Msg arms an image load result may target: arms whose payload is
|
|
489
|
+
/// exactly the five ImageEventArm fields. The `state` check runs BOTH
|
|
490
|
+
/// directions (the AudioEventKind convention): the `&` constraint holds
|
|
491
|
+
/// the arm's states to ImageState, and the tuple-wrapped reverse check
|
|
492
|
+
/// holds ImageState to the arm's states — a narrower union would
|
|
493
|
+
/// silently drop result states the host emits, so it is refused here,
|
|
494
|
+
/// not discovered at runtime.
|
|
495
|
+
export type ImageEventKind<M extends Msgish> = M extends Msgish
|
|
496
|
+
? [Exclude<keyof M, "kind">] extends [keyof ImageEventArm]
|
|
497
|
+
? [keyof ImageEventArm] extends [Exclude<keyof M, "kind">]
|
|
498
|
+
? M extends Msgish & ImageEventArm
|
|
499
|
+
? [ImageState] extends [M["state"]]
|
|
500
|
+
? M["kind"]
|
|
501
|
+
: never
|
|
502
|
+
: never
|
|
503
|
+
: never
|
|
504
|
+
: never
|
|
505
|
+
: never;
|
|
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
|
|
257
548
|
: never
|
|
258
549
|
: never
|
|
259
550
|
: never
|
|
260
551
|
: never;
|
|
261
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
|
+
|
|
262
631
|
/// One field of a host record payload; see hostRecordBytes for the encoding.
|
|
263
632
|
export type HostScalar = number | boolean | Uint8Array;
|
|
264
633
|
|
|
@@ -335,6 +704,50 @@ export interface AudioRoute<M extends Msgish> {
|
|
|
335
704
|
readonly event: AudioEventKind<M>;
|
|
336
705
|
}
|
|
337
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
|
+
|
|
731
|
+
/// A `Cmd.imageLoad` source: the audio cascade's shape exactly. The local
|
|
732
|
+
/// `path` is tried first; a missing file falls through to `url` (fetched
|
|
733
|
+
/// whole, installed at `cachePath` when given and verified against
|
|
734
|
+
/// `expectedBytes` — 0/omitted means unknown size, existence alone qualifies
|
|
735
|
+
/// a cache entry; omit `cachePath` and the host derives the conventional
|
|
736
|
+
/// content-addressed path when a caches directory is configured). At least
|
|
737
|
+
/// one of path/url must be present.
|
|
738
|
+
export interface ImageSource {
|
|
739
|
+
readonly path?: Uint8Array;
|
|
740
|
+
readonly url?: Uint8Array;
|
|
741
|
+
readonly cachePath?: Uint8Array;
|
|
742
|
+
readonly expectedBytes?: number;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/// `Cmd.imageLoad` routing: the ONE terminal result dispatches the `event`
|
|
746
|
+
/// arm (the five-field ImageEventArm record, matched by field name).
|
|
747
|
+
export interface ImageRoute<M extends Msgish> {
|
|
748
|
+
readonly event: ImageEventKind<M>;
|
|
749
|
+
}
|
|
750
|
+
|
|
338
751
|
/// The closed HTTP verb set of `Cmd.fetch` (wire value = declaration order).
|
|
339
752
|
export type FetchMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD";
|
|
340
753
|
|
|
@@ -431,6 +844,53 @@ export type Cmd<M extends Msgish> =
|
|
|
431
844
|
/// Seek position (ms) / volume (0..1); 0 for the value-less verbs.
|
|
432
845
|
readonly value: number;
|
|
433
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
|
+
}
|
|
866
|
+
| { readonly op: "window_show"; readonly label: string }
|
|
867
|
+
| { readonly op: "quit_app" }
|
|
868
|
+
| {
|
|
869
|
+
readonly op: "image_load";
|
|
870
|
+
readonly id: number;
|
|
871
|
+
readonly eventKind: string;
|
|
872
|
+
readonly path: Uint8Array;
|
|
873
|
+
readonly url: Uint8Array;
|
|
874
|
+
readonly cachePath: Uint8Array;
|
|
875
|
+
readonly expectedBytes: number;
|
|
876
|
+
}
|
|
877
|
+
| { readonly op: "image_cancel"; readonly id: number }
|
|
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 }
|
|
434
894
|
| { readonly op: "batch"; readonly cmds: readonly Cmd<M>[] };
|
|
435
895
|
|
|
436
896
|
/// The wire encoding of a host record payload, byte-identical to what the
|
|
@@ -659,6 +1119,247 @@ export const Cmd = {
|
|
|
659
1119
|
return { op: "audio_ctl", key, verb: "volume", value: volume };
|
|
660
1120
|
},
|
|
661
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
|
+
|
|
1197
|
+
/// Show the window with the declared `label`: un-hide + activate — the
|
|
1198
|
+
/// counterpart to a `close_policy = "hide"` hide and the tray "Open"
|
|
1199
|
+
/// consequence; also restores a minimized window. Fire-and-forget: no
|
|
1200
|
+
/// result Msg (the window's own frame event carries the state), and an
|
|
1201
|
+
/// unknown label is a no-op. The label is a string literal — window
|
|
1202
|
+
/// labels are declarations.
|
|
1203
|
+
showWindow(label: string): Cmd<never> {
|
|
1204
|
+
return { op: "window_show", label };
|
|
1205
|
+
},
|
|
1206
|
+
|
|
1207
|
+
/// Quit the app for real — the graceful terminate, and the tray "Quit"
|
|
1208
|
+
/// consequence. The host quits through the SAME shutdown path a
|
|
1209
|
+
/// last-window close takes, so the stop hook runs exactly once and a
|
|
1210
|
+
/// recording session seals its journal. Fire-and-forget.
|
|
1211
|
+
quitApp(): Cmd<never> {
|
|
1212
|
+
return { op: "quit_app" };
|
|
1213
|
+
},
|
|
1214
|
+
|
|
1215
|
+
/// Load an image at runtime under the model-owned numeric ImageId your
|
|
1216
|
+
/// markup binds (`<image image="{id}"/>`, `<avatar image="{id}"/>`):
|
|
1217
|
+
/// resolve the source cascade (local path first, then a verified cache
|
|
1218
|
+
/// entry, then the network), decode through the platform codec, register
|
|
1219
|
+
/// the pixels under `id`, and dispatch exactly ONE `event` arm — state
|
|
1220
|
+
/// "loaded" with the decoded width/height, or one failure class, always
|
|
1221
|
+
/// echoing the requested id so concurrent loads sharing the arm stay
|
|
1222
|
+
/// distinguishable. Failure is never silent, and views referencing the
|
|
1223
|
+
/// id repaint on the next
|
|
1224
|
+
/// frame. One load per id at a time: a duplicate live id dispatches state
|
|
1225
|
+
/// "rejected" (finish or re-key instead — ids are model data). Ids are
|
|
1226
|
+
/// positive integers below 2^53 outside the reserved bit-63 namespace;
|
|
1227
|
+
/// 0 is the no-image sentinel and dispatches "rejected".
|
|
1228
|
+
imageLoad<M extends Msgish>(id: number, source: ImageSource, route: ImageRoute<M>): Cmd<M> {
|
|
1229
|
+
return {
|
|
1230
|
+
op: "image_load",
|
|
1231
|
+
id,
|
|
1232
|
+
eventKind: route.event,
|
|
1233
|
+
path: source.path ?? new Uint8Array(0),
|
|
1234
|
+
url: source.url ?? new Uint8Array(0),
|
|
1235
|
+
cachePath: source.cachePath ?? new Uint8Array(0),
|
|
1236
|
+
expectedBytes: source.expectedBytes ?? 0,
|
|
1237
|
+
};
|
|
1238
|
+
},
|
|
1239
|
+
|
|
1240
|
+
/// End the in-flight image load under `id`, if any — LOUDLY: the load's
|
|
1241
|
+
/// one terminal arrives as its own `event` arm with state "cancelled"
|
|
1242
|
+
/// (ending an in-flight load is an observable event, the spawn cancel
|
|
1243
|
+
/// discipline), and the id is free for a fresh load once that terminal
|
|
1244
|
+
/// lands. Aimed at an id with no live load it no-ops (the load it aimed
|
|
1245
|
+
/// at already delivered its terminal). Image loads are keyed by their
|
|
1246
|
+
/// numeric id, so the string-keyed `Cmd.cancel` never touches them —
|
|
1247
|
+
/// this is their cancel, the way `Cmd.audioStop` is audio's.
|
|
1248
|
+
imageCancel(id: number): Cmd<never> {
|
|
1249
|
+
return { op: "image_cancel", id };
|
|
1250
|
+
},
|
|
1251
|
+
|
|
1252
|
+
/// Free the registry slot under `id`: the pixels are released, views
|
|
1253
|
+
/// referencing the id draw their fallback (avatar initials) on the next
|
|
1254
|
+
/// frame, and the slot — one of the registry's 16 — is open for another
|
|
1255
|
+
/// load (the gallery eviction move: unregister the evictee, load the
|
|
1256
|
+
/// newcomer under a fresh id). Like registration itself this is
|
|
1257
|
+
/// synchronous registry surgery, not an effect: no Msg follows, and an
|
|
1258
|
+
/// id with no registration no-ops (whatever it aimed at is already
|
|
1259
|
+
/// gone, `Cmd.imageCancel`'s idle rule). It frees only the CURRENT
|
|
1260
|
+
/// registration — a load IN FLIGHT under the id is untouched and its
|
|
1261
|
+
/// terminal still registers the pixels; to keep the slot free, end the
|
|
1262
|
+
/// load with `Cmd.imageCancel(id)` first.
|
|
1263
|
+
imageUnregister(id: number): Cmd<never> {
|
|
1264
|
+
return { op: "image_unregister", id };
|
|
1265
|
+
},
|
|
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
|
+
|
|
662
1363
|
/// Several commands from one dispatch, performed in order.
|
|
663
1364
|
batch<M extends Msgish>(cmds: readonly Cmd<M>[]): Cmd<M> {
|
|
664
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
|