Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions examples/ai-chat-ts/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,20 +98,28 @@ function composerApply(d: ComposerDraft, event: TextInputEvent): ComposerDraft {
if (clamped === null) return d;
const nextClamped = applyTextInputEvent(state, clamped, MAX_DRAFT);
if (nextClamped === null) return d;
// Composition bounds land in i64-classed slots: bind them, guard
// the range (an ordered comparison excludes NaN), and state
// wholeness with Math.trunc at the write; -1 stays the no-composition
// sentinel.
const clampedStart = nextClamped.composition !== null ? nextClamped.composition.start : -1;
const clampedEnd = nextClamped.composition !== null ? nextClamped.composition.end : -1;
return {
bytes: nextClamped.text,
anchor: nextClamped.selection.anchor,
focus: nextClamped.selection.focus,
compStart: nextClamped.composition !== null ? nextClamped.composition.start : -1,
compEnd: nextClamped.composition !== null ? nextClamped.composition.end : -1,
compStart: clampedStart >= -1 && clampedStart <= 9007199254740991 ? Math.trunc(clampedStart) : -1,
compEnd: clampedEnd >= -1 && clampedEnd <= 9007199254740991 ? Math.trunc(clampedEnd) : -1,
};
}
const nextStart = next.composition !== null ? next.composition.start : -1;
const nextEnd = next.composition !== null ? next.composition.end : -1;
return {
bytes: next.text,
anchor: next.selection.anchor,
focus: next.selection.focus,
compStart: next.composition !== null ? next.composition.start : -1,
compEnd: next.composition !== null ? next.composition.end : -1,
compStart: nextStart >= -1 && nextStart <= 9007199254740991 ? Math.trunc(nextStart) : -1,
compEnd: nextEnd >= -1 && nextEnd <= 9007199254740991 ? Math.trunc(nextEnd) : -1,
};
}

Expand Down Expand Up @@ -297,7 +305,7 @@ export function update(model: Model, msg: Msg): [Model, Cmd<Msg>] {
{
...model,
turns: turns,
nextId: model.nextId + 1,
nextId: model.nextId < 9007199254740991 ? model.nextId + 1 : 9007199254740991,
phase: "sending",
failReason: new Uint8Array(0),
draft: composerInit(),
Expand Down Expand Up @@ -367,7 +375,7 @@ export function update(model: Model, msg: Msg): [Model, Cmd<Msg>] {
return [{
...model,
turns: [...model.turns, { id: model.nextId, role: "assistant", text: content }],
nextId: model.nextId + 1,
nextId: model.nextId < 9007199254740991 ? model.nextId + 1 : 9007199254740991,
phase: "idle",
chatScrollTop: SCROLL_BOTTOM,
}, Cmd.none];
Expand Down
6 changes: 3 additions & 3 deletions examples/soundboard-ts/dev-script.ndjson
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

# Play track 2 (Harvest Lot) - the transcript shows the audio_play command
# with the prepared path, the hosted URL, and the manifest byte size.
{"kind":"play_track","id":2}
{"kind":"play_track","playTrackId":2}

# The engine acknowledges the load; the platform's duration report (47s -
# an estimate) must NOT replace the manifest total of 46944ms.
Expand All @@ -24,7 +24,7 @@
# Queue Velvet Jackpot (track 16) behind the current track, then let the
# current track complete: the queue wins the advance and a fresh
# audio_play for track 16 appears in the transcript.
{"kind":"queue_track","id":16}
{"kind":"queue_track","queueTrackId":16}
{"kind":"audio_event","state":"completed","positionMs":46944,"durationMs":47000,"playing":false,"buffering":false,"bands":{"$bytes":""}}
{"kind":"audio_event","state":"loaded","positionMs":0,"durationMs":131000,"playing":true,"buffering":false,"bands":{"$bytes":""}}

Expand All @@ -33,7 +33,7 @@
{"kind":"toggle_play"}

# Copy Title puts the track title on the clipboard (fire-and-forget).
{"kind":"copy_title","id":16}
{"kind":"copy_title","copyTitleId":16}

# Search narrows both libraries; the committed model shows the new draft.
{"kind":"search_edit","edit":{"kind":"insert_text","text":{"$bytes":"night"}}}
Expand Down
119 changes: 81 additions & 38 deletions examples/soundboard-ts/src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,20 +147,28 @@ function searchApply(d: SearchDraft, event: TextInputEvent): SearchDraft {
if (clamped === null) return d;
const nextClamped = applyTextInputEvent(state, clamped, MAX_SEARCH);
if (nextClamped === null) return d;
// Composition bounds land in i64-classed slots: bind them, guard
// the range (an ordered comparison excludes NaN), and state
// wholeness with Math.trunc at the write; -1 stays the
// no-composition sentinel.
const clampedStart = nextClamped.composition !== null ? nextClamped.composition.start : -1;
const clampedEnd = nextClamped.composition !== null ? nextClamped.composition.end : -1;
return {
bytes: nextClamped.text,
anchor: nextClamped.selection.anchor,
focus: nextClamped.selection.focus,
compStart: nextClamped.composition !== null ? nextClamped.composition.start : -1,
compEnd: nextClamped.composition !== null ? nextClamped.composition.end : -1,
compStart: clampedStart >= -1 && clampedStart <= 9007199254740991 ? Math.trunc(clampedStart) : -1,
compEnd: clampedEnd >= -1 && clampedEnd <= 9007199254740991 ? Math.trunc(clampedEnd) : -1,
};
}
const nextStart = next.composition !== null ? next.composition.start : -1;
const nextEnd = next.composition !== null ? next.composition.end : -1;
return {
bytes: next.text,
anchor: next.selection.anchor,
focus: next.selection.focus,
compStart: next.composition !== null ? next.composition.start : -1,
compEnd: next.composition !== null ? next.composition.end : -1,
compStart: nextStart >= -1 && nextStart <= 9007199254740991 ? Math.trunc(nextStart) : -1,
compEnd: nextEnd >= -1 && nextEnd <= 9007199254740991 ? Math.trunc(nextEnd) : -1,
};
}

Expand Down Expand Up @@ -266,17 +274,20 @@ export function initialModel(): Model {
export type Msg =
| { readonly kind: "show_albums" }
| { readonly kind: "show_songs" }
| { readonly kind: "open_album"; readonly id: number }
// Distinct source field names keep these i64 proof obligations from
// collapsing into QueueEntry.id or one another. Each wire payload
// remains the arm's single scalar.
| { readonly kind: "open_album"; readonly openAlbumId: number }
| { readonly kind: "close_album" }
| { readonly kind: "play_album"; readonly id: number }
| { readonly kind: "play_album"; readonly playAlbumId: number }
/// The play gesture on a track row: a different track starts fresh, the
/// loaded one toggles play/pause in place.
| { readonly kind: "play_track"; readonly id: number }
| { readonly kind: "play_track"; readonly playTrackId: number }
| { readonly kind: "toggle_play" }
| { readonly kind: "next_track" }
| { readonly kind: "prev_track" }
| { readonly kind: "queue_track"; readonly id: number }
| { readonly kind: "copy_title"; readonly id: number }
| { readonly kind: "queue_track"; readonly queueTrackId: number }
| { readonly kind: "copy_title"; readonly copyTitleId: number }
| { readonly kind: "search_edit"; readonly edit: TextInputEvent }
/// Every audio playback report: the load acknowledgment, position
/// ticks, the one completion, failures, spectrum frames.
Expand Down Expand Up @@ -400,14 +411,20 @@ export function visibleAlbums(model: Model): readonly AlbumCell[] {
const out: AlbumCell[] = [];
for (const album of ALBUMS) {
if (!albumMatches(query, album)) continue;
out.push({
id: album.id,
title: album.title,
artist: album.artist,
initials: album.initials,
cover: album.id,
playing: albumIsPlaying(model, album.id),
});
// Ids land in i64-classed slots: bind, range-guard (an ordered
// comparison excludes NaN), and state wholeness with Math.trunc at
// the write.
const albumId = album.id;
if (albumId >= 0 && albumId <= 9007199254740991) {
out.push({
id: Math.trunc(albumId),
title: album.title,
artist: album.artist,
initials: album.initials,
cover: Math.trunc(albumId),
playing: albumIsPlaying(model, album.id),
});
}
}
return out;
}
Expand Down Expand Up @@ -682,18 +699,23 @@ export function update(model: Model, msg: Msg): [Model, Cmd<Msg>] {
return [{ ...model, tab: "songs", libraryScrollTop: 0 }, Cmd.none];
case "open_album": {
// Resolve the payload id against the table and store the TABLE's
// id: markup payloads arrive as wire f64s, and keeping every id in
// the model integer-classed starts here.
const album = ALBUMS.find((a) => a.id === msg.id);
// id: the compiled adapter proves this arm's i64 class before the
// update, and resolving against the table keeps model ids classed.
const album = ALBUMS.find((a) => a.id === msg.openAlbumId);
if (album === undefined) return [model, Cmd.none];
// Every page change resets the controlled scroll: the offset is
// model state, so the fresh page opens at its top.
return [{ ...model, openAlbum: album.id, tab: "albums", libraryScrollTop: 0 }, Cmd.none];
// model state, so the fresh page opens at its top. The id lands in
// an i64-classed slot: bind, range-guard, Math.trunc at the write.
const albumId = album.id;
if (albumId >= 0 && albumId <= 9007199254740991) {
return [{ ...model, openAlbum: Math.trunc(albumId), tab: "albums", libraryScrollTop: 0 }, Cmd.none];
}
return [model, Cmd.none];
}
case "close_album":
return [{ ...model, openAlbum: null, libraryScrollTop: 0 }, Cmd.none];
case "play_album": {
const album = ALBUMS.find((a) => a.id === msg.id);
const album = ALBUMS.find((a) => a.id === msg.playAlbumId);
if (album === undefined) return [model, Cmd.none];
const albumId = album.id;
const first = TRACKS.find((t) => t.album === albumId && t.number === 1);
Expand All @@ -710,11 +732,11 @@ export function update(model: Model, msg: Msg): [Model, Cmd<Msg>] {
case "play_track": {
// The loaded track toggles play/pause in place; a different track
// starts fresh.
if (model.now === msg.id) {
if (model.now === msg.playTrackId) {
if (model.playing) return [{ ...model, playing: false }, Cmd.audioPause("player")];
return [{ ...model, playing: true }, Cmd.audioResume("player")];
}
const track = TRACKS.find((t) => t.id === msg.id);
const track = TRACKS.find((t) => t.id === msg.playTrackId);
if (track === undefined) return [model, Cmd.none];
return [
startedModel(model, track.id, track.durationMs),
Expand Down Expand Up @@ -775,19 +797,25 @@ export function update(model: Model, msg: Msg): [Model, Cmd<Msg>] {
];
}
case "queue_track": {
const track = TRACKS.find((t) => t.id === msg.id);
const track = TRACKS.find((t) => t.id === msg.queueTrackId);
if (track === undefined) return [model, Cmd.none];
if (model.queue.some((q) => q.id === track.id)) return [model, Cmd.none];
if (model.queue.length >= MAX_QUEUE) {
return [{ ...model, queueDropped: model.queueDropped + 1 }, Cmd.none];
return [{ ...model, queueDropped: model.queueDropped < 9007199254740991 ? model.queueDropped + 1 : 9007199254740991 }, Cmd.none];
}
// The id lands in an i64-classed slot: bind, range-guard,
// Math.trunc at the write.
const trackId = track.id;
if (trackId >= 0 && trackId <= 9007199254740991) {
return [{ ...model, queue: [...model.queue, { id: Math.trunc(trackId) }] }, Cmd.none];
}
return [{ ...model, queue: [...model.queue, { id: track.id }] }, Cmd.none];
return [model, Cmd.none];
}
case "copy_title": {
const track = TRACKS.find((t) => t.id === msg.id);
const track = TRACKS.find((t) => t.id === msg.copyTitleId);
if (track === undefined) return [model, Cmd.none];
return [
{ ...model, copiesRequested: model.copiesRequested + 1 },
{ ...model, copiesRequested: model.copiesRequested < 9007199254740991 ? model.copiesRequested + 1 : 9007199254740991 },
Cmd.clipboardWrite(track.title),
];
}
Expand All @@ -797,10 +825,14 @@ export function update(model: Model, msg: Msg): [Model, Cmd<Msg>] {
switch (msg.state) {
case "loaded": {
const next = adoptPlatformDuration(model, msg.durationMs);
// The position lands in an i64-classed slot: bind, range-guard
// (an ordered comparison excludes NaN), and state wholeness
// with Math.trunc at the write.
const loadedPos = msg.positionMs;
return [{
...next,
loadPending: false,
elapsedMs: msg.positionMs,
elapsedMs: loadedPos >= 0 && loadedPos <= 9007199254740991 ? Math.trunc(loadedPos) : 0,
playing: msg.playing,
buffering: msg.buffering,
}, Cmd.none];
Expand All @@ -810,20 +842,24 @@ export function update(model: Model, msg: Msg): [Model, Cmd<Msg>] {
// land before the new track's `loaded` — drop it by the guard.
if (model.loadPending || model.now === null) return [model, Cmd.none];
const next = adoptPlatformDuration(model, msg.durationMs);
// The tick lands in an i64-classed slot: bind, range-guard,
// Math.trunc at the write.
const rawPos = msg.positionMs;
const tick = rawPos >= 0 && rawPos <= 9007199254740991 ? Math.trunc(rawPos) : 0;
// The coarse tick corrects the rendered clock. In motion,
// forward corrections apply and small backward ones hold flat
// (the bar never visibly rewinds); a past-slack disagreement
// snaps. With no motion the tick is simply the truth.
if (next.playing && !msg.buffering) {
if (
msg.positionMs > next.elapsedMs ||
next.elapsedMs - msg.positionMs > POSITION_SNAP_SLACK_MS
tick > next.elapsedMs ||
next.elapsedMs - tick > POSITION_SNAP_SLACK_MS
) {
return [{ ...next, elapsedMs: msg.positionMs, buffering: msg.buffering }, Cmd.none];
return [{ ...next, elapsedMs: tick, buffering: msg.buffering }, Cmd.none];
}
return [{ ...next, buffering: msg.buffering }, Cmd.none];
}
return [{ ...next, elapsedMs: msg.positionMs, buffering: msg.buffering }, Cmd.none];
return [{ ...next, elapsedMs: tick, buffering: msg.buffering }, Cmd.none];
}
// Band-magnitude analysis frames are consciously ignored — the
// soundboard's identity is the clean catalog (parity with the
Expand Down Expand Up @@ -890,7 +926,11 @@ export function update(model: Model, msg: Msg): [Model, Cmd<Msg>] {
rest -= 1000;
thousandth += 1;
}
const target = thousandth * permille;
// The product lands in an i64-classed slot: bind, range-guard (an
// ordered comparison excludes NaN), and state wholeness with
// Math.trunc at the write.
const scaled = thousandth * permille;
const target = scaled >= 0 && scaled <= 9007199254740991 ? Math.trunc(scaled) : 0;
// The rendered clock jumps to the scrub target directly (a seek is
// the one sanctioned rewind) and the engine seeks the real player.
return [{ ...model, elapsedMs: target }, Cmd.audioSeek("player", target)];
Expand Down Expand Up @@ -923,9 +963,12 @@ export function update(model: Model, msg: Msg): [Model, Cmd<Msg>] {
// cadence, so each tick steps exactly one interval — a burst of
// queued fires is bounded by construction — and the position
// events correct any drift. The clock never passes the total.
// The advance saturates at the i64 class's provable ceiling, and
// the cap comparison replaces Math.min so the proof stays in view.
const advanced = model.elapsedMs + CLOCK_TICK_MS;
const cap = model.nowDurationMs > 0 ? model.nowDurationMs : advanced;
return [{ ...model, elapsedMs: Math.min(advanced, cap) }, Cmd.none];
const stepped = advanced >= 0 ? (advanced <= 9007199254740991 ? Math.trunc(advanced) : 9007199254740991) : 0;
const cap = model.nowDurationMs > 0 ? model.nowDurationMs : stepped;
return [{ ...model, elapsedMs: stepped <= cap ? stepped : cap }, Cmd.none];
}
}
}
Expand Down
9 changes: 7 additions & 2 deletions examples/soundboard-ts/src/library.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,9 +259,14 @@ export function trackRow(model: Model, track: TrackInfo, withAlbum: boolean): Tr
subtitle = dashJoin(album.artist, album.title);
}
}
// Ids and numbers land in i64-classed slots: bind, range-guard (an
// ordered comparison excludes NaN), and state wholeness with
// Math.trunc at the write.
const trackId = track.id;
const trackNumber = track.number;
return {
id: track.id,
number: track.number,
id: trackId >= 0 && trackId <= 9007199254740991 ? Math.trunc(trackId) : 0,
number: trackNumber >= 0 && trackNumber <= 9007199254740991 ? Math.trunc(trackNumber) : 0,
title: track.title,
subtitle: subtitle,
duration: formatMs(track.durationMs),
Expand Down
21 changes: 15 additions & 6 deletions examples/soundboard-ts/src/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ export function nowTrack(model: Model): TrackInfo | undefined {
/// rule), a fresh attempt clears the degraded notices, and the stale-event
/// guard goes up until the new playback's `loaded` lands.
export function startedModel(model: Model, trackId: number, durationMs: number): Model {
// The id and duration land in i64-classed slots: range-guard each (an
// ordered comparison excludes NaN) and state wholeness with Math.trunc
// at the write.
return {
...model,
now: trackId,
now: trackId >= 0 && trackId <= 9007199254740991 ? Math.trunc(trackId) : 0,
playing: true,
elapsedMs: 0,
nowDurationMs: durationMs,
nowDurationMs: durationMs >= 0 && durationMs <= 9007199254740991 ? Math.trunc(durationMs) : 0,
platformDurationMs: 0,
loadPending: true,
buffering: false,
Expand Down Expand Up @@ -93,9 +96,15 @@ export function localOnly(model: Model): boolean {
/// Mirror the platform's duration report, and adopt it as the displayed
/// total ONLY when the manifest gave none (the duration rule).
export function adoptPlatformDuration(model: Model, reportedMs: number): Model {
if (reportedMs === 0) return model;
if (model.nowDurationMs === 0) {
return { ...model, platformDurationMs: reportedMs, nowDurationMs: reportedMs };
// The report lands in i64-classed slots: range-guard it (an ordered
// comparison excludes NaN) and state wholeness with Math.trunc at the
// write; a zero or unprovable report changes nothing.
if (reportedMs > 0 && reportedMs <= 9007199254740991) {
const reported = Math.trunc(reportedMs);
if (model.nowDurationMs === 0) {
return { ...model, platformDurationMs: reported, nowDurationMs: reported };
}
return { ...model, platformDurationMs: reported };
}
return { ...model, platformDurationMs: reportedMs };
return model;
}
Loading
Loading