// @native-sdk/core — the SDK module an app core imports. The subset program // maps the "@native-sdk/core" specifier onto this file, so stock tsc types // it; the transpiler lowers references to it onto the rt kernel and never // emits this module's own code. // // `Cmd` is the typed-effects surface (spec section 2): `update` (and, since // v2, `initialModel`) may return `[model, cmd]`, where the cmd is INERT DATA // describing effects for the runtime to perform after the returned model // commits. Commands are built only from these factories, and only in that // return path (NS1017) — they never live in the Model, in a Msg, in a local, // or in a helper. // // The v2 command set: // // Cmd.none no effects (what a bare `return model` means) // Cmd.persist() ask the host to persist the committed model // Cmd.now("tick") request a timestamp; the runtime dispatches // the named Msg arm with the time (ms) as its // single number payload field // Cmd.host(name, ...args) a host command by name with scalar args — // the host interprets the name // Cmd.host(name, payload) the same, carrying one bytes payload — a // Uint8Array, or a flat record of number / // boolean / Uint8Array fields that lowers to // bytes (hostRecordBytes below) // Cmd.request(name, payload, a routed host command: the host performs it // { key?, ok, err }) and dispatches the `ok` Msg arm with the // result bytes, or the `err` arm with the error // bytes — each arm carries exactly one // Uint8Array payload field, checked by tsc. The // optional `key` names the in-flight effect: // re-issuing a live key replaces it, and // Cmd.cancel(key) drops it. // Cmd.cancel(key) drop the in-flight keyed effect — request, // readFile/writeFile/fetch/clipboardRead, or // delay — SILENTLY (no terminal arm dispatch). // Aimed at a live spawn it stays LOUD: the // child dies and the err arm runs with // "cancelled" — killing a process is an // observable event // Cmd.batch([a, b, ...]) several commands from one dispatch // // The named engine ops (each maps onto the host's effect engine directly; // routing follows the request rules — string-literal arm names, tsc-checked // arm shapes): // // Cmd.readFile(path, { key?, ok, err }) // whole-file read; ok arm carries the bytes // (one Uint8Array field), err arm the reason // bytes ("not_found", "io_failed", "truncated", // "rejected") // Cmd.writeFile(path, bytes, { key?, ok, err }) // whole-file write (parents created, replaced // whole); ok arm carries NOTHING (an arm with // no payload fields), err arm the reason bytes // Cmd.fetch({ url, method?, headers?, body?, timeoutMs? }, { key?, ok, err }) // buffered HTTP(S) exchange; ok arm carries a // two-field record — one number field (the real // HTTP status, non-2xx included) and one // Uint8Array field (the whole body) — err arm // the reason bytes ("connect_failed", // "tls_failed", "protocol_failed", "timed_out", // "rejected", "truncated") // Cmd.clipboardWrite(bytes) fire-and-forget clipboard write // Cmd.clipboardRead({ key?, ok, err }) // clipboard read; ok arm carries the text bytes, // err arm the reason bytes ("failed", // "rejected") // Cmd.delay(key, ms, "fired") a keyed ONE-SHOT timer: dispatches the named // arm once after `ms`, with the fire time (ms) // as its single number payload; re-issuing a // live key re-arms from now (debounce), and // Cmd.cancel(key) drops it silently // // The streaming ops (one issue, MANY result Msgs across dispatches, a keyed // lifecycle the app drives): // // Cmd.spawn(argv, { key?, stdin?, line?, exit, err }) // Cmd.spawn(argv, { key?, stdin?, collect: true, exit, err }) // run a subprocess. Line mode streams each // stdout line to the `line` arm (one Uint8Array // field; omit `line` to drop lines); collect // mode buffers whole stdout instead. Exactly one // terminal follows: a clean exit dispatches // `exit` — line mode: one number field (the exit // code); collect mode: a two-field record, one // number (the code) and one Uint8Array (the // collected stdout), matched by type — and every // other end dispatches `err` with the reason // bytes ("signaled", "cancelled", "rejected", // "spawn_failed", "truncated"). Cmd.cancel(key) // ends the child mid-stream (err arm // "cancelled" — never silent). // Cmd.audioPlay(key, { path?, url?, cachePath?, expectedBytes? }, { event }) // open (or replace — one player is the whole // surface) the audio event stream: every // playback event dispatches the `event` arm (the // six-field record below) until Cmd.audioStop // closes the stream. // Cmd.audioPause(key) / audioResume(key) / audioStop(key) // Cmd.audioSeek(key, ms) / Cmd.audioSetVolume(key, volume) // drive the open stream in place — fire-and- // forget control verbs whose consequences arrive // on the event stream; aimed at a key with no // open stream they no-op. // // The keyed-effect discipline is ONE rule: a keyed effect REPLACES its live // predecessor (the superseded effect's result is dropped — no message), and // Cmd.cancel drops it silently. That holds for request, readFile, writeFile, // fetch, clipboardRead, and delay alike. The ONE exception is a live spawn // key: a duplicate REJECTS the new spawn (err arm "rejected") — a running // subprocess is never killed implicitly; cancel it first. And spawn's cancel // stays loud (err arm "cancelled"): killing a process is an observable event. // // `Sub` is the recurring-effects surface: an app may export // `subscriptions(model): Sub` returning declarative descriptors the // host reconciles after every commit (Sub.timer fires the named arm with the // current time on each interval). Like Cmd, Sub values are inert data, legal // only in that function's return path (NS1025). Sub and the streaming Cmds // are different animals on purpose: a Sub is DECLARED from the model (the // host starts/stops it by reconciliation; the app never opens one), while // spawn/audioPlay streams are Cmd-INITIATED — imperative opens with a keyed // lifecycle the app drives and cancels. // // The factories return plain frozen-shape objects so the same core runs // under node: a dev harness can interpret the `op` tags directly. // The byte-text method surface (s.toUpperCase(), s.split(sep), ... on // Uint8Array): the transpiler adds the ambient file to every core's program // itself; this reference carries the same surface into EDITORS of apps that // import "@native-sdk/core", so tsc-in-the-editor and `native check` agree. /// /// The text intrinsic: turn a string literal or template into bytes. The /// transpiler recognizes calls BY IDENTITY (this import, renames honored) /// and folds them at compile time — a literal argument becomes rodata, a /// template becomes frame-arena bytes via bufPrint — so no string ever /// exists at native runtime. Under node this body runs as-is, byte for /// byte the same result. Arguments must be literals or templates; dynamic /// text lives in the model as Uint8Array from the start. export function asciiBytes(s: string): Uint8Array { const out = new Uint8Array(s.length); for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i); return out; } /// Every app Msg is a discriminated union on a string `kind` tag. export type Msgish = { readonly kind: string }; /// The Msg arms `Cmd.now` may target: arms whose payload is exactly one /// number-typed field (the runtime dispatches the arm with the timestamp in /// that field). Anything else is unrepresentable — the runtime has only a /// number to give back. export type TimestampKind = M extends Msgish ? { [K in Exclude]-?: M[K] extends number ? [Exclude] extends [K] ? M["kind"] : never : never; }[Exclude] : never; /// The Msg arms a routed host result may target: arms whose payload is /// exactly one Uint8Array-typed field (the runtime dispatches the arm with /// the result/error bytes in that field). export type BytesKind = M extends Msgish ? { [K in Exclude]-?: M[K] extends Uint8Array ? [Exclude] extends [K] ? M["kind"] : never : never; }[Exclude] : never; /// The Msg arms a payload-less routed result may target: arms with no /// payload fields at all (`Cmd.writeFile`'s ok route — a successful write /// has nothing to report beyond success). export type EmptyKind = M extends Msgish ? [Exclude] extends [never] ? M["kind"] : never : never; /// The Msg arms a buffered fetch response may target: arms whose payload is /// exactly two fields — one number (the HTTP status) and one Uint8Array (the /// body). The runtime matches the fields by TYPE, so their names are yours. export type FetchedKind = M extends Msgish ? { [K in Exclude]-?: M[K] extends Uint8Array ? Exclude extends infer O ? O extends keyof M ? M[O] extends number ? [Exclude] extends [never] ? M["kind"] : never : never : never : never : never; }[Exclude] : never; // ------------------------------------------------- wiring channel shapes // The generated wiring's opt-in host-event channels: export the channel // and it is wired (`commandMsg(name: string): Msg | null` is the same // family — menus, shortcuts, chrome tabs — and predates these). Event // RECORD shapes (`frameMsg`'s FrameEvent, `keyMsg`'s KeyEvent, the // appearanceMsg/chromeMsg arm payloads) are DECLARED IN YOUR CORE and // matched by field name, the TextInputEvent rule — they must emit as your // module's own records, so an SDK interface cannot stand in for them. /// One `envMsgs` entry: `export const envMsgs = [{ env: "NAME", msg: /// "" }] as const` — each named environment variable present at launch /// dispatches its value through the arm (exactly one `Uint8Array` field) as /// an ordinary journaled Msg right after the boot command. The core itself /// never reads the environment (NS1005); replay carries the recorded values. export interface EnvMsg { readonly env: string; readonly msg: BytesKind; } /// The audio event states, mirroring the engine's event vocabulary: `loaded` /// acknowledges a successful load with the player's duration estimate; /// `position` ticks at the platform's honest cadence (~500ms) while playing; /// `completed` fires exactly once at the natural end; `failed` reports a /// load/decode/device failure; `rejected` a command the effects layer refused /// (an empty or over-long source); `spectrum` carries a band-magnitude /// analysis frame from hosts that analyze their playback. export type AudioState = "loaded" | "position" | "completed" | "failed" | "rejected" | "spectrum"; /// The payload shape of an audio event arm — six fields, matched by NAME (the /// one SDK-fixed record shape, so the host can build it from your union). /// `state` must be a named string-literal-union alias carrying exactly the /// six AudioState members (any declaration order — the host matches members /// by name). `positionMs`/`durationMs` are milliseconds; `playing` is the /// player's transport state; `buffering` is true while a streamed source is /// stalled waiting for network bytes; `bands` is the 32 spectrum band /// magnitudes (0..255 each, all zeros outside "spectrum" events). export type AudioEventArm = { readonly state: AudioState; readonly positionMs: number; readonly durationMs: number; readonly playing: boolean; readonly buffering: boolean; readonly bands: Uint8Array; }; /// The Msg arms an audio event stream may target: arms whose payload is /// exactly the six AudioEventArm fields. export type AudioEventKind = M extends Msgish ? [Exclude] extends [keyof AudioEventArm] ? [keyof AudioEventArm] extends [Exclude] ? M extends Msgish & AudioEventArm ? M["kind"] : never : never : never : never; /// One field of a host record payload; see hostRecordBytes for the encoding. export type HostScalar = number | boolean | Uint8Array; /// A structured host payload: a flat record of scalar/bytes fields, lowered /// to one bytes payload at build time (natively) and by hostRecordBytes /// (under node) — the same bytes either way. export type HostRecord = { readonly [field: string]: HostScalar }; /// How a `Cmd.request` result comes back: the host dispatches the `ok` arm /// with the result bytes on success, or the `err` arm with the error bytes. /// Arm names are string literals — the routing is data, never a callback. /// `key` (optional) names the in-flight effect for replace/cancel semantics. export interface RequestRoute { readonly key?: string; readonly ok: BytesKind; readonly err: BytesKind; } /// `Cmd.writeFile` routing: the ok arm carries no payload (success has /// nothing to report); the err arm carries the reason bytes. export interface WriteRoute { readonly key?: string; readonly ok: EmptyKind; readonly err: BytesKind; } /// `Cmd.fetch` routing: the ok arm carries `{ status, body }` (one number /// field, one bytes field — matched by type); the err arm the reason bytes. export interface FetchRoute { readonly key?: string; readonly ok: FetchedKind; readonly err: BytesKind; } /// `Cmd.spawn` routing, line mode: each stdout line dispatches the optional /// `line` arm (one bytes field; omitted = lines dropped), a clean exit the /// `exit` arm (one number field — the exit code), every other end the `err` /// arm with the reason bytes. export interface SpawnRoute { readonly key?: string; readonly stdin?: Uint8Array; readonly line?: BytesKind; readonly exit: TimestampKind; readonly err: BytesKind; } /// `Cmd.spawn` routing, collect mode: whole stdout buffers until the exit, /// which dispatches the `exit` arm as a two-field record — one number field /// (the exit code) and one bytes field (the collected stdout), matched by /// type so the names are yours. No line arm: there is no line framing. export interface SpawnCollectRoute { readonly key?: string; readonly stdin?: Uint8Array; readonly collect: true; readonly exit: FetchedKind; readonly err: BytesKind; } /// A `Cmd.audioPlay` source: the resolution cascade of the engine underneath. /// The local `path` is tried first; a missing file falls through to `url` /// (streamed progressively, cached at `cachePath` when given and verified /// against `expectedBytes` — 0/omitted means unknown size, existence alone /// qualifies a cache entry). At least one of path/url must be present. export interface AudioSource { readonly path?: Uint8Array; readonly url?: Uint8Array; readonly cachePath?: Uint8Array; readonly expectedBytes?: number; } /// `Cmd.audioPlay` routing: every playback event dispatches the `event` arm /// (the six-field AudioEventArm record, matched by field name). export interface AudioRoute { readonly event: AudioEventKind; } /// The closed HTTP verb set of `Cmd.fetch` (wire value = declaration order). export type FetchMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" | "HEAD"; /// A `Cmd.fetch` request. `url` is bytes (asciiBytes for literals); headers /// are a flat record whose NAMES are compile-time text and whose VALUES are /// string literals or runtime bytes (`Uint8Array` — how a launch-supplied /// credential rides an `Authorization` header); `timeoutMs` omitted means /// the host engine's default. export interface FetchSpec { readonly url: Uint8Array; readonly method?: FetchMethod; readonly headers?: { readonly [name: string]: string | Uint8Array }; readonly body?: Uint8Array; readonly timeoutMs?: number; } /// An inert command value, parameterized by the app's Msg union so the /// factories can validate message targets. Opaque to app code: build with /// the `Cmd.*` factories, return from `update`/`initialModel`, never inspect /// or store. export type Cmd = | { readonly op: "none" } | { readonly op: "persist" } | { readonly op: "now"; readonly msgKind: string } | { readonly op: "host"; readonly name: string; readonly args: readonly number[] } | { readonly op: "host_bytes"; readonly name: string; readonly payload: Uint8Array } | { readonly op: "request"; readonly name: string; readonly key: string; readonly okKind: string; readonly errKind: string; readonly payload: Uint8Array; } | { readonly op: "cancel"; readonly key: string } | { readonly op: "read_file"; readonly key: string; readonly okKind: string; readonly errKind: string; readonly path: Uint8Array; } | { readonly op: "write_file"; readonly key: string; readonly okKind: string; readonly errKind: string; readonly path: Uint8Array; readonly bytes: Uint8Array; } | { readonly op: "fetch"; readonly key: string; readonly okKind: string; readonly errKind: string; readonly method: FetchMethod; readonly timeoutMs: number; readonly url: Uint8Array; /// Header pairs, already in TS-field-name (code-unit) sort order. /// A string value is compile-time text; a Uint8Array value is /// runtime bytes (both encode as the record's length-prefixed /// value field). readonly headers: readonly { readonly name: string; readonly value: string | Uint8Array }[]; readonly body: Uint8Array; } | { readonly op: "clip_write"; readonly bytes: Uint8Array } | { readonly op: "clip_read"; readonly key: string; readonly okKind: string; readonly errKind: string } | { readonly op: "delay"; readonly key: string; readonly afterMs: number; readonly msgKind: string } | { readonly op: "spawn"; readonly key: string; /// "" = no line routing (collect mode, or a line spawn that only /// cares about the exit). readonly lineKind: string; readonly exitKind: string; readonly errKind: string; readonly collect: boolean; readonly argv: readonly Uint8Array[]; readonly stdin: Uint8Array; } | { readonly op: "audio_play"; readonly key: string; readonly eventKind: string; readonly path: Uint8Array; readonly url: Uint8Array; readonly cachePath: Uint8Array; readonly expectedBytes: number; } | { readonly op: "audio_ctl"; readonly key: string; readonly verb: "pause" | "resume" | "stop" | "seek" | "volume"; /// Seek position (ms) / volume (0..1); 0 for the value-less verbs. readonly value: number; } | { readonly op: "batch"; readonly cmds: readonly Cmd[] }; /// The wire encoding of a host record payload, byte-identical to what the /// transpiler derives from the record's TS shape at build time: fields /// sorted by name (code-unit order), concatenated with no field headers — /// number -> f64 little-endian (8 bytes), boolean -> one 0/1 byte, /// Uint8Array -> u32 little-endian length + bytes. export function hostRecordBytes(payload: HostRecord): Uint8Array { const names = Object.keys(payload).sort(); let len = 0; for (const n of names) { const v = payload[n]; if (typeof v === "number") len += 8; else if (typeof v === "boolean") len += 1; else len += 4 + v.length; } const out = new Uint8Array(len); const dv = new DataView(out.buffer); let off = 0; for (const n of names) { const v = payload[n]; if (typeof v === "number") { dv.setFloat64(off, v, true); off += 8; } else if (typeof v === "boolean") { out[off] = v ? 1 : 0; off += 1; } else { dv.setUint32(off, v.length, true); off += 4; out.set(v, off); off += v.length; } } return out; } function lowerHostPayload(payload: Uint8Array | HostRecord): Uint8Array { return payload instanceof Uint8Array ? payload : hostRecordBytes(payload); } /// A host command by name; the host decides what the name means. The name is /// a string literal. Args are scalar numbers, or exactly one bytes payload /// (a Uint8Array, or a flat record that lowers to bytes). function hostCmd(name: string, payload: Uint8Array | HostRecord): Cmd; function hostCmd(name: string, ...args: readonly number[]): Cmd; function hostCmd(name: string, ...rest: readonly (number | Uint8Array | HostRecord)[]): Cmd { const first = rest[0]; if (rest.length === 1 && typeof first === "object" && first !== null) { return { op: "host_bytes", name, payload: lowerHostPayload(first) }; } return { op: "host", name, args: rest as readonly number[] }; } export const Cmd = { /// No effects. `return model` is sugar for `return [model, Cmd.none]`. none: { op: "none" } as Cmd, /// Ask the host to persist the committed model. persist(): Cmd { return { op: "persist" }; }, /// Request the current time. The runtime dispatches the named Msg arm with /// the timestamp (milliseconds, a plain number) as its single payload field. now(msgKind: TimestampKind): Cmd { return { op: "now", msgKind }; }, host: hostCmd, /// A routed host command: the host performs `name` with the payload and /// dispatches exactly one result Msg back — the `ok` arm with the result /// bytes, or the `err` arm with the error bytes. Both arms must carry /// exactly one Uint8Array payload field (tsc checks that). An optional /// `key` names the in-flight effect: re-issuing a live key replaces it, /// and Cmd.cancel(key) drops it. request( name: string, payload: Uint8Array | HostRecord, route: RequestRoute, ): Cmd { return { op: "request", name, key: route.key ?? "", okKind: route.ok, errKind: route.err, payload: lowerHostPayload(payload), }; }, /// Drop the in-flight keyed effect — request, named engine op, or delay — /// with this key, if any, SILENTLY (neither routing arm is dispatched for /// it). The exception is a live spawn: cancel ends the child and its err /// arm runs with "cancelled" — killing a process is an observable event. cancel(key: string): Cmd { return { op: "cancel", key }; }, /// Read a whole file. Exactly one terminal Msg: the `ok` arm with the /// content bytes (one Uint8Array field), or the `err` arm with the reason /// bytes ("not_found", "io_failed", "truncated", "rejected"). readFile(path: Uint8Array, route: RequestRoute): Cmd { return { op: "read_file", key: route.key ?? "", okKind: route.ok, errKind: route.err, path }; }, /// Write a whole file (parent directories created, an existing file /// replaced whole). Exactly one terminal Msg: the `ok` arm — which carries /// no payload — or the `err` arm with the reason bytes. writeFile(path: Uint8Array, bytes: Uint8Array, route: WriteRoute): Cmd { return { op: "write_file", key: route.key ?? "", okKind: route.ok, errKind: route.err, path, bytes }; }, /// A buffered HTTP(S) exchange. Exactly one terminal Msg: the `ok` arm /// with `{ status, body }` (one number field, one bytes field — a non-2xx /// status is still ok: an HTTP-level error is a delivered response), or /// the `err` arm with the reason bytes. fetch(spec: FetchSpec, route: FetchRoute): Cmd { const names = Object.keys(spec.headers ?? {}).sort(); return { op: "fetch", key: route.key ?? "", okKind: route.ok, errKind: route.err, method: spec.method ?? "GET", timeoutMs: spec.timeoutMs ?? 0, url: spec.url, headers: names.map((n) => ({ name: n, value: spec.headers![n] })), body: spec.body ?? new Uint8Array(0), }; }, /// Put bytes on the system clipboard, fire-and-forget (an over-bound or /// refused write is dropped — there is no route to report on). clipboardWrite(bytes: Uint8Array): Cmd { return { op: "clip_write", bytes }; }, /// Read the system clipboard. Exactly one terminal Msg: the `ok` arm with /// the text bytes, or the `err` arm with the reason bytes ("failed", /// "rejected"). clipboardRead(route: RequestRoute): Cmd { return { op: "clip_read", key: route.key ?? "", okKind: route.ok, errKind: route.err }; }, /// A keyed one-shot delay: dispatch the named Msg arm once, `ms` from now, /// with the fire time (milliseconds) as its single number payload field. /// Re-issuing a live key re-arms it from now (the debounce discipline); /// `Cmd.cancel(key)` drops it silently. delay(key: string, ms: number, msgKind: TimestampKind): Cmd { return { op: "delay", key, afterMs: ms, msgKind }; }, /// Run a subprocess as a STREAM: each stdout line dispatches the `line` /// arm as it arrives (line mode), or whole stdout buffers to the exit /// (`collect: true`); exactly one terminal follows — the `exit` arm on a /// clean exit, the `err` arm with the reason bytes on every other end. /// The key stays live for the whole stream: `Cmd.cancel(key)` ends the /// child mid-stream (err arm "cancelled" — loud on purpose: killing a /// process is an observable event), and a spawn whose key is already /// streaming is rejected, never replaced — a running subprocess is never /// killed implicitly; cancel it first. spawn( argv: readonly Uint8Array[], route: SpawnRoute | SpawnCollectRoute, ): Cmd { const collect = "collect" in route && route.collect === true; return { op: "spawn", key: route.key ?? "", lineKind: collect ? "" : ((route as SpawnRoute).line ?? ""), exitKind: route.exit, errKind: route.err, collect, argv, stdin: route.stdin ?? new Uint8Array(0), }; }, /// Open (or replace — one player is the whole surface) the keyed audio /// event stream: resolve the source cascade (local path, then url, cached /// and integrity-gated) and start playback. Every playback event /// dispatches the `event` arm until `Cmd.audioStop(key)` closes the /// stream. Failure is never silent: an unplayable source arrives as a /// "failed" event, a refused command as "rejected". audioPlay(key: string, source: AudioSource, route: AudioRoute): Cmd { return { op: "audio_play", key, eventKind: route.event, path: source.path ?? new Uint8Array(0), url: source.url ?? new Uint8Array(0), cachePath: source.cachePath ?? new Uint8Array(0), expectedBytes: source.expectedBytes ?? 0, }; }, /// Pause the keyed playback in place (no event echo — the caller /// commanded it). A key with no open stream no-ops. audioPause(key: string): Cmd { return { op: "audio_ctl", key, verb: "pause", value: 0 }; }, /// Resume the keyed playback. A player that can no longer resume reports /// one "failed" event on the stream instead of silence. audioResume(key: string): Cmd { return { op: "audio_ctl", key, verb: "resume", value: 0 }; }, /// Stop the keyed playback and CLOSE its event stream: no events for the /// key after this. Stop is the audio stream's cancel. audioStop(key: string): Cmd { return { op: "audio_ctl", key, verb: "stop", value: 0 }; }, /// Jump the keyed playback to `ms` (the platform clamps to the duration). /// No event echo — the next position tick reports from there. audioSeek(key: string, ms: number): Cmd { return { op: "audio_ctl", key, verb: "seek", value: ms }; }, /// Set playback volume, clamped to 0..1 and remembered across tracks: the /// next audioPlay re-applies it. audioSetVolume(key: string, volume: number): Cmd { return { op: "audio_ctl", key, verb: "volume", value: volume }; }, /// Several commands from one dispatch, performed in order. batch(cmds: readonly Cmd[]): Cmd { return { op: "batch", cmds }; }, }; /// An inert subscription descriptor: recurring effects declared FROM the /// model. An app that needs them exports `subscriptions(model): Sub`; /// after every commit the host reconciles the returned set against its /// active timers by key (new key or changed interval re-arms; a missing key /// cancels). Like Cmd, Sub values are data, legal only in that function's /// return path (NS1025). export type Sub = | { readonly op: "none" } | { readonly op: "timer"; readonly key: string; readonly everyMs: number; readonly msgKind: string } | { readonly op: "batch"; readonly subs: readonly Sub[] }; export const Sub = { /// No subscriptions (e.g. everything paused). none: { op: "none" } as Sub, /// A repeating timer named by `key`, firing every `everyMs` milliseconds. /// Each fire dispatches the named Msg arm with the current time (ms) as /// its single number payload field. timer(key: string, everyMs: number, msgKind: TimestampKind): Sub { return { op: "timer", key, everyMs, msgKind }; }, /// Several subscriptions at once. batch(subs: readonly Sub[]): Sub { return { op: "batch", subs }; }, };