@native-sdk/core 0.5.3 → 0.5.4

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/sdk/core.ts +225 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@native-sdk/core",
3
- "version": "0.5.3",
3
+ "version": "0.5.4",
4
4
  "description": "The TypeScript authoring tier: the app-core subset, its dev-time transpiler to arena-backed Zig, and the SDK module cores import",
5
5
  "repository": {
6
6
  "type": "git",
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 v2 command set:
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,50 @@
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.imageLoad(id, { path?, url?, cachePath?, expectedBytes? }, { event })
107
+ // load an image at runtime by its model-owned
108
+ // numeric ImageId (the id markup binds:
109
+ // <image image="{id}"/>, avatar likewise): the
110
+ // host resolves the source cascade (local path
111
+ // first, then a verified cache entry, then the
112
+ // network), decodes through the platform codec,
113
+ // registers the pixels under the id, and
114
+ // dispatches exactly ONE `event` arm (the
115
+ // five-field record below, the requested id
116
+ // echoed) — state "loaded" with the decoded
117
+ // width/height, or one failure class. One load
118
+ // per id at a time: a duplicate live id
119
+ // dispatches state "rejected".
120
+ // Cmd.imageCancel(id) end the in-flight load under the id, if any
121
+ // — loud, the spawn discipline: the load's
122
+ // event arm delivers state "cancelled" and the
123
+ // id frees for a fresh load once it lands. An
124
+ // id with no live load no-ops.
125
+ // Cmd.imageUnregister(id) free the registry slot under the id: the
126
+ // pixels are released, views referencing the
127
+ // id draw their fallback, and the slot is
128
+ // open for another load. Synchronous registry
129
+ // surgery like registration itself — not an
130
+ // effect, no Msg follows; an id with no
131
+ // registration no-ops. A load IN FLIGHT under
132
+ // the id is untouched: its terminal still
133
+ // registers — cancel the load first to keep
134
+ // the slot free.
135
+ //
136
+ // The window verbs (fire-and-forget, no result Msg — the window's own
137
+ // frame event carries the state):
138
+ //
139
+ // Cmd.showWindow(label) un-hide + activate the window with the
140
+ // declared label — the counterpart to a
141
+ // `close_policy = "hide"` hide and the tray
142
+ // "Open" consequence; also restores a
143
+ // minimized window. An unknown label is a
144
+ // no-op.
145
+ // Cmd.quitApp() graceful terminate, the tray "Quit"
146
+ // consequence: the host quits through the
147
+ // SAME shutdown path a last-window close
148
+ // takes, so the stop hook runs exactly once
149
+ // and a recording session seals its journal.
106
150
  //
107
151
  // The keyed-effect discipline is ONE rule: a keyed effect REPLACES its live
108
152
  // predecessor (the superseded effect's result is dropped — no message), and
@@ -248,12 +292,87 @@ export type AudioEventArm = {
248
292
  };
249
293
 
250
294
  /// The Msg arms an audio event stream may target: arms whose payload is
251
- /// exactly the six AudioEventArm fields.
295
+ /// exactly the six AudioEventArm fields. The `state` check runs BOTH
296
+ /// directions: the `&` constraint holds the arm's states to AudioState,
297
+ /// and the tuple-wrapped reverse check holds AudioState to the arm's
298
+ /// states — a narrower union would silently drop event states the host
299
+ /// emits, so it is refused here, not discovered at runtime.
252
300
  export type AudioEventKind<M extends Msgish> = M extends Msgish
253
301
  ? [Exclude<keyof M, "kind">] extends [keyof AudioEventArm]
254
302
  ? [keyof AudioEventArm] extends [Exclude<keyof M, "kind">]
255
303
  ? M extends Msgish & AudioEventArm
256
- ? M["kind"]
304
+ ? [AudioState] extends [M["state"]]
305
+ ? M["kind"]
306
+ : never
307
+ : never
308
+ : never
309
+ : never
310
+ : never;
311
+
312
+ /// The image load result states, mirroring the engine's outcome vocabulary:
313
+ /// "loaded" means the pixels are registered under the requested id (width and
314
+ /// height carry what the codec decoded); every other state is the failure
315
+ /// class — "rejected" (a refused command: invalid id, no source, a duplicate
316
+ /// live id), "not_found" (missing local file, no url), "io_failed" (a local
317
+ /// read failure), "connect_failed"/"tls_failed"/"protocol_failed"/"timed_out"
318
+ /// (the fetch taxonomy), "http_status" (a non-2xx answer; `status` carries
319
+ /// it), "cancelled", "too_large" (source or decoded pixels over budget),
320
+ /// "unsupported" (no platform codec), "decode_failed", "registry_full", and
321
+ /// "alloc_failed" (the host refused the memory the registration needed —
322
+ /// resource exhaustion, not corrupt bytes: the same source may load once
323
+ /// memory frees).
324
+ export type ImageState =
325
+ | "loaded"
326
+ | "rejected"
327
+ | "not_found"
328
+ | "io_failed"
329
+ | "connect_failed"
330
+ | "tls_failed"
331
+ | "protocol_failed"
332
+ | "timed_out"
333
+ | "http_status"
334
+ | "cancelled"
335
+ | "too_large"
336
+ | "unsupported"
337
+ | "decode_failed"
338
+ | "registry_full"
339
+ | "alloc_failed";
340
+
341
+ /// The payload shape of an image result arm — five fields, matched by NAME
342
+ /// (the AudioEventArm convention). `id` is the requested ImageId echoed
343
+ /// verbatim, so two concurrent loads sharing one event arm stay
344
+ /// distinguishable in `update` (an id the wire cannot carry exactly — 0,
345
+ /// negatives, fractions, 2^53 and past — echoes 0, the no-image sentinel:
346
+ /// there is no honest integer to echo); `state` must be a named
347
+ /// string-literal-union alias carrying exactly the fifteen ImageState
348
+ /// members (any declaration order — the host matches members by name);
349
+ /// `width`/`height` are the decoded pixel dimensions ("loaded" only, 0
350
+ /// otherwise); `status` is the HTTP status for url loads that performed an
351
+ /// exchange, 0 when none occurred (local paths, cache hits) — 0 is signal,
352
+ /// not a missing value: a cache hit is a real "loaded" with no exchange
353
+ /// behind it, so apps can distinguish a network load from a cached one.
354
+ export type ImageEventArm = {
355
+ readonly id: number;
356
+ readonly state: ImageState;
357
+ readonly width: number;
358
+ readonly height: number;
359
+ readonly status: number;
360
+ };
361
+
362
+ /// The Msg arms an image load result may target: arms whose payload is
363
+ /// exactly the five ImageEventArm fields. The `state` check runs BOTH
364
+ /// directions (the AudioEventKind convention): the `&` constraint holds
365
+ /// the arm's states to ImageState, and the tuple-wrapped reverse check
366
+ /// holds ImageState to the arm's states — a narrower union would
367
+ /// silently drop result states the host emits, so it is refused here,
368
+ /// not discovered at runtime.
369
+ export type ImageEventKind<M extends Msgish> = M extends Msgish
370
+ ? [Exclude<keyof M, "kind">] extends [keyof ImageEventArm]
371
+ ? [keyof ImageEventArm] extends [Exclude<keyof M, "kind">]
372
+ ? M extends Msgish & ImageEventArm
373
+ ? [ImageState] extends [M["state"]]
374
+ ? M["kind"]
375
+ : never
257
376
  : never
258
377
  : never
259
378
  : never
@@ -335,6 +454,26 @@ export interface AudioRoute<M extends Msgish> {
335
454
  readonly event: AudioEventKind<M>;
336
455
  }
337
456
 
457
+ /// A `Cmd.imageLoad` source: the audio cascade's shape exactly. The local
458
+ /// `path` is tried first; a missing file falls through to `url` (fetched
459
+ /// whole, installed at `cachePath` when given and verified against
460
+ /// `expectedBytes` — 0/omitted means unknown size, existence alone qualifies
461
+ /// a cache entry; omit `cachePath` and the host derives the conventional
462
+ /// content-addressed path when a caches directory is configured). At least
463
+ /// one of path/url must be present.
464
+ export interface ImageSource {
465
+ readonly path?: Uint8Array;
466
+ readonly url?: Uint8Array;
467
+ readonly cachePath?: Uint8Array;
468
+ readonly expectedBytes?: number;
469
+ }
470
+
471
+ /// `Cmd.imageLoad` routing: the ONE terminal result dispatches the `event`
472
+ /// arm (the five-field ImageEventArm record, matched by field name).
473
+ export interface ImageRoute<M extends Msgish> {
474
+ readonly event: ImageEventKind<M>;
475
+ }
476
+
338
477
  /// The closed HTTP verb set of `Cmd.fetch` (wire value = declaration order).
339
478
  export type FetchMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD";
340
479
 
@@ -431,6 +570,19 @@ export type Cmd<M extends Msgish> =
431
570
  /// Seek position (ms) / volume (0..1); 0 for the value-less verbs.
432
571
  readonly value: number;
433
572
  }
573
+ | { readonly op: "window_show"; readonly label: string }
574
+ | { readonly op: "quit_app" }
575
+ | {
576
+ readonly op: "image_load";
577
+ readonly id: number;
578
+ readonly eventKind: string;
579
+ readonly path: Uint8Array;
580
+ readonly url: Uint8Array;
581
+ readonly cachePath: Uint8Array;
582
+ readonly expectedBytes: number;
583
+ }
584
+ | { readonly op: "image_cancel"; readonly id: number }
585
+ | { readonly op: "image_unregister"; readonly id: number }
434
586
  | { readonly op: "batch"; readonly cmds: readonly Cmd<M>[] };
435
587
 
436
588
  /// The wire encoding of a host record payload, byte-identical to what the
@@ -659,6 +811,76 @@ export const Cmd = {
659
811
  return { op: "audio_ctl", key, verb: "volume", value: volume };
660
812
  },
661
813
 
814
+ /// Show the window with the declared `label`: un-hide + activate — the
815
+ /// counterpart to a `close_policy = "hide"` hide and the tray "Open"
816
+ /// consequence; also restores a minimized window. Fire-and-forget: no
817
+ /// result Msg (the window's own frame event carries the state), and an
818
+ /// unknown label is a no-op. The label is a string literal — window
819
+ /// labels are declarations.
820
+ showWindow(label: string): Cmd<never> {
821
+ return { op: "window_show", label };
822
+ },
823
+
824
+ /// Quit the app for real — the graceful terminate, and the tray "Quit"
825
+ /// consequence. The host quits through the SAME shutdown path a
826
+ /// last-window close takes, so the stop hook runs exactly once and a
827
+ /// recording session seals its journal. Fire-and-forget.
828
+ quitApp(): Cmd<never> {
829
+ return { op: "quit_app" };
830
+ },
831
+
832
+ /// Load an image at runtime under the model-owned numeric ImageId your
833
+ /// markup binds (`<image image="{id}"/>`, `<avatar image="{id}"/>`):
834
+ /// resolve the source cascade (local path first, then a verified cache
835
+ /// entry, then the network), decode through the platform codec, register
836
+ /// the pixels under `id`, and dispatch exactly ONE `event` arm — state
837
+ /// "loaded" with the decoded width/height, or one failure class, always
838
+ /// echoing the requested id so concurrent loads sharing the arm stay
839
+ /// distinguishable. Failure is never silent, and views referencing the
840
+ /// id repaint on the next
841
+ /// frame. One load per id at a time: a duplicate live id dispatches state
842
+ /// "rejected" (finish or re-key instead — ids are model data). Ids are
843
+ /// positive integers below 2^53 outside the reserved bit-63 namespace;
844
+ /// 0 is the no-image sentinel and dispatches "rejected".
845
+ imageLoad<M extends Msgish>(id: number, source: ImageSource, route: ImageRoute<M>): Cmd<M> {
846
+ return {
847
+ op: "image_load",
848
+ id,
849
+ eventKind: route.event,
850
+ path: source.path ?? new Uint8Array(0),
851
+ url: source.url ?? new Uint8Array(0),
852
+ cachePath: source.cachePath ?? new Uint8Array(0),
853
+ expectedBytes: source.expectedBytes ?? 0,
854
+ };
855
+ },
856
+
857
+ /// End the in-flight image load under `id`, if any — LOUDLY: the load's
858
+ /// one terminal arrives as its own `event` arm with state "cancelled"
859
+ /// (ending an in-flight load is an observable event, the spawn cancel
860
+ /// discipline), and the id is free for a fresh load once that terminal
861
+ /// lands. Aimed at an id with no live load it no-ops (the load it aimed
862
+ /// at already delivered its terminal). Image loads are keyed by their
863
+ /// numeric id, so the string-keyed `Cmd.cancel` never touches them —
864
+ /// this is their cancel, the way `Cmd.audioStop` is audio's.
865
+ imageCancel(id: number): Cmd<never> {
866
+ return { op: "image_cancel", id };
867
+ },
868
+
869
+ /// Free the registry slot under `id`: the pixels are released, views
870
+ /// referencing the id draw their fallback (avatar initials) on the next
871
+ /// frame, and the slot — one of the registry's 16 — is open for another
872
+ /// load (the gallery eviction move: unregister the evictee, load the
873
+ /// newcomer under a fresh id). Like registration itself this is
874
+ /// synchronous registry surgery, not an effect: no Msg follows, and an
875
+ /// id with no registration no-ops (whatever it aimed at is already
876
+ /// gone, `Cmd.imageCancel`'s idle rule). It frees only the CURRENT
877
+ /// registration — a load IN FLIGHT under the id is untouched and its
878
+ /// terminal still registers the pixels; to keep the slot free, end the
879
+ /// load with `Cmd.imageCancel(id)` first.
880
+ imageUnregister(id: number): Cmd<never> {
881
+ return { op: "image_unregister", id };
882
+ },
883
+
662
884
  /// Several commands from one dispatch, performed in order.
663
885
  batch<M extends Msgish>(cmds: readonly Cmd<M>[]): Cmd<M> {
664
886
  return { op: "batch", cmds };