SOURCE / PINNED RELEASE
Made of little things.
Napcraft
- Release
- ee47ac6a25df…
- Author-recorded commit
- 410dea87e109…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-52f9e22f5ce
Archive hash verified: 9e6e0cabb18d0d46…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import { BackendCallError } from '../docs/examples/backend-client';
import type { BackendIntent, BackendResult, BackendTarget } from '../docs/examples/backend-client';
import { cell, top } from './world';
import type { World } from './world';
export type Edit = { x: number; y: number; z: number; expected: number; block: number };
export type Delta = { from: number; edits: number; locked: boolean; reset: boolean; changes: { n: number; x: number; y: number; z: number; block: number }[] };
export type EditJournal = { actor?: string; target: BackendTarget; queue: Edit[]; active?: { intent: BackendIntent; count: number } };
type Api = {
intent(target: BackendTarget, operation: string, input: unknown): BackendIntent;
invoke<T>(intent: BackendIntent): Promise<BackendResult<T>>;
call<T>(name: string, args: unknown): Promise<T>;
};
export type EditorView = {
world: World; revision: number; confirmedEdits: number; pending: number;
cells: Set<number>; state: 'saved' | 'saving' | 'retry' | 'refreshing'; message: string;
};
const copy = (world: World): World => ({ ...world, blocks: [...world.blocks] });
export function validEdit(world: World, e: Edit) {
return [e.x, e.y, e.z, e.expected, e.block].every(Number.isInteger) &&
e.x >= 0 && e.x < 18 && e.z >= 0 && e.z < 18 && e.y >= 2 && e.y < 12 &&
e.block >= 0 && e.block <= 6 && e.expected >= 0 && e.expected <= 6 &&
!world.locked && world.blocks[cell(e.x,e.y,e.z)] === e.expected &&
e.block !== e.expected && (e.block === 0 ? top(world,e.x,e.z) === e.y :
e.expected === 0 && world.blocks[cell(e.x,e.y-1,e.z)] !== 0);
}
function apply(world: World, e: Edit) { world.blocks[cell(e.x,e.y,e.z)] = e.block; world.edits++; }
export function validJournal(value: unknown, target: BackendTarget): value is EditJournal {
const j = value as EditJournal;
if (!j || JSON.stringify(j.target) !== JSON.stringify(target) || !Array.isArray(j.queue) || j.queue.length > 24) return false;
if (!j.queue.every(e => e && [e.x,e.y,e.z,e.expected,e.block].every(Number.isInteger) &&
e.x >= 0 && e.x < 18 && e.z >= 0 && e.z < 18 && e.y >= 2 && e.y < 12 && e.expected >= 0 && e.expected <= 6 && e.block >= 0 && e.block <= 6)) return false;
if (!j.active) return true;
const {intent, count} = j.active;
if (!intent || JSON.stringify(intent.target) !== JSON.stringify(target) ||
typeof intent.requestId !== 'string' || !Number.isSafeInteger(intent.expiresAt) ||
!Number.isInteger(count) || count < 1 || count > 8 || count > j.queue.length) return false;
const input = intent.input as {after?:number; edits?:Edit[]};
return intent.operation === 'editBlock' ? count === 1 && JSON.stringify(input) === JSON.stringify(j.queue[0]) :
intent.operation === 'editBlocks' && Number.isSafeInteger(input.after) && Number(input.after) >= 0 &&
JSON.stringify(input.edits) === JSON.stringify(j.queue.slice(0,count));
}
/** Owns confirmed state, bounded predictions, exact retry receipts and one CVM lane. */
export class SharedEditor {
static readonly capacity = 24;
private world: World;
private revision: number;
private queue: Edit[] = [];
private active?: EditJournal['active'];
private running = false;
private stopped = false;
private retryTimer?: ReturnType<typeof setTimeout>;
private retryDelay = 1000;
private conflicts = 0;
private waiting = false;
private resuming = false;
private recovery = false;
private refreshRequested = false;
private force = false;
private lastHint = -Infinity;
private hintTimer?: ReturnType<typeof setTimeout>;
private message = '';
private saveChain: Promise<void> = Promise.resolve();
constructor(readonly target: BackendTarget, world: World, revision: number, private compact: boolean,
private api: Api, private effects: {
change(view: EditorView): void;
persist(journal: EditJournal | null): Promise<void>;
saved(revision: number): void;
notice(message: string): void;
}, journal: EditJournal | undefined, private actor: string) {
this.world = copy(world); this.revision = revision;
if (journal && validJournal(journal,target)) {
if (journal.actor !== actor) {
// Provider receipts are principal-scoped. A new guest transport must NOT
// replay an old intent: the same cell may have changed back (ABA).
this.recovery = true;
this.effects.notice('Your player session changed. Pending work was cancelled; review the saved world.');
} else {
this.queue = structuredClone(journal.queue); this.active = structuredClone(journal.active);
this.resuming = Boolean(this.active);
}
}
}
get hasWork() { return this.queue.length > 0 || Boolean(this.active) || this.recovery; }
get uncertain() { return this.waiting; }
start() { this.render(); void this.pump(); }
dispose() { this.stopped = true; clearTimeout(this.retryTimer); clearTimeout(this.hintTimer); }
private journal(): EditJournal | null {
return this.queue.length ? { actor: this.actor, target: this.target, queue: structuredClone(this.queue), ...(this.active ? {active:structuredClone(this.active)} : {}) } : null;
}
private persist() {
const snapshot = this.journal();
// Serialize captured values so a slow earlier write cannot restore an old queue.
this.saveChain = this.saveChain.catch(() => {}).then(() => this.stopped ? undefined : this.effects.persist(snapshot));
return this.saveChain;
}
private predicted() {
const result = copy(this.world);
if (!this.resuming) for (const e of this.queue) if (validEdit(result,e)) apply(result,e);
return result;
}
private render() {
if (this.stopped) return;
this.effects.change({world:this.predicted(),revision:this.revision,confirmedEdits:this.world.edits,
pending:this.queue.length,cells:new Set(this.queue.map(e => e.x+e.z*18)),
state:this.waiting?'retry':this.recovery||this.resuming?'refreshing':this.queue.length?'saving':'saved',message:this.message});
}
enqueue(edit: Edit): boolean {
if (this.stopped || this.recovery || this.resuming) { this.effects.notice('Refreshing saved blocks. Try again in a moment.'); return false; }
if (this.queue.length >= SharedEditor.capacity) { this.effects.notice('Save queue full (24). Wait for a save or retry the connection.'); return false; }
if (!validEdit(this.predicted(),edit)) { this.effects.notice('That block changed. Aim again before building.'); return false; }
if (!this.queue.length) this.conflicts = 0;
this.queue.push({...edit});
this.render();
void this.persist().catch(() => { this.message = 'Could not keep recovery data. Retry save.'; this.render(); });
void this.pump();
return true;
}
retry() { clearTimeout(this.retryTimer); this.waiting = false; void this.pump(); }
sync(hint = false) {
if (this.stopped) return;
if (hint) {
// Untrusted peers can request a read, never supply saved blocks or revisions.
const remaining = 500 - (Date.now()-this.lastHint);
if (remaining > 0) {
// Coalesce a burst's FINAL notification too; dropping it would restore
// polling latency for the last block after a fast batch of saves.
if (!this.hintTimer) this.hintTimer = setTimeout(() => {
this.hintTimer = undefined; this.sync(true);
},remaining+1);
return;
}
clearTimeout(this.hintTimer); this.hintTimer = undefined;
this.lastHint = Date.now(); this.force = true;
}
this.refreshRequested = true;
void this.pump();
}
private acceptSnapshot(world: World, revision: number) {
if (revision < this.revision) return;
if (!world || world.blocks?.length !== 3888) throw new Error('Invalid saved world.');
this.world = copy(world); this.revision = revision;
}
private acceptDelta(delta: Delta, revision: number): boolean {
if (revision < this.revision) return true;
if (delta.reset || delta.from > this.world.edits || delta.edits < this.world.edits) return false;
const next = copy(this.world);
for (const change of delta.changes) {
if (change.n <= next.edits) continue;
if (change.n !== next.edits + 1) return false;
next.blocks[cell(change.x,change.y,change.z)] = change.block; next.edits = change.n;
}
if (next.edits !== delta.edits) return false;
next.locked = delta.locked; this.world = next; this.revision = revision; return true;
}
private async read(full = false) {
const operation = this.compact && !full ? 'readUpdates' : 'readWorld';
const r = await this.api.invoke<World | Delta>(this.api.intent(this.target,operation,operation === 'readWorld' ? {} : {after:this.world.edits}));
if (this.stopped) return;
if (operation === 'readWorld') this.acceptSnapshot(r.result as World,r.revision);
else if (!this.acceptDelta(r.result as Delta,r.revision)) await this.read(true);
}
private reconcileQueue() {
const next = copy(this.world);
for (const e of this.queue) {
if (!validEdit(next,e)) {
this.queue = [];
this.effects.notice('Saved blocks changed. Unsent edits were cancelled; aim again.');
break;
}
apply(next,e);
}
}
private backoff(message: string) {
if (this.stopped) return;
this.waiting = true; this.message = message;
clearTimeout(this.retryTimer);
this.retryTimer = setTimeout(() => this.retry(),this.retryDelay);
this.retryDelay = Math.min(8000,this.retryDelay*2);
}
private async pump() {
if (this.running || this.stopped || this.waiting) return;
this.running = true;
try {
while (!this.stopped) {
if (this.recovery) {
try {
await this.read(true);
if (this.stopped) return;
this.recovery = false; this.resuming = false; this.message = '';
this.reconcileQueue();
await this.persist(); this.render();
} catch { this.backoff('Could not refresh saved blocks. Retrying…'); break; }
}
if (this.queue.length) {
if (!this.active) {
this.reconcileQueue();
if (!this.queue.length) { await this.persist(); this.render(); continue; }
const count = this.compact ? Math.min(8,this.queue.length) : 1;
const input = this.compact ? {after:this.world.edits,edits:this.queue.slice(0,count).map(e=>({...e}))} : {...this.queue[0]};
this.active = {intent:this.api.intent(this.target,this.compact?'editBlocks':'editBlock',input),count};
}
if (this.active.intent.expiresAt <= Date.now()/1000) {
// An expired receipt is not permission to replay an edit with a new ID.
this.queue = []; this.active = undefined; this.recovery = true;
this.effects.notice('Save retry expired. Refreshing; review your blocks before editing again.');
continue;
}
try {
await this.persist();
if (this.stopped) return;
const active = this.active;
const r = await this.api.invoke<World | Delta>(active.intent);
if (this.stopped) return;
this.queue.splice(0,active.count); this.active = undefined; this.resuming = false;
if (active.intent.operation === 'editBlock') this.acceptSnapshot(r.result as World,r.revision);
else if (!this.acceptDelta(r.result as Delta,r.revision)) { this.recovery = true; }
// A receipt restored after reload can be older than the join snapshot.
this.reconcileQueue();
await this.persist(); this.retryDelay = 1000; this.conflicts = 0; this.message = '';
this.effects.saved(r.revision); this.render();
} catch (error) {
if (this.stopped) return;
if (error instanceof BackendCallError && error.definitive) {
this.active = undefined; this.resuming = false; this.recovery = true;
if (error.code === 'CONFLICT' && ++this.conflicts <= 2) {
// Definitively rejected, so a NEW intent is safe only AFTER reading
// and revalidating every original cell/support/lock precondition.
this.message = 'Checking changes from friends…';
} else {
this.queue = [];
this.effects.notice(error.code === 'CONFLICT' ? 'World is busy. Pending edits cancelled; try again after it refreshes.' : error.message);
}
await this.persist(); this.render(); continue;
}
this.backoff(this.active ? 'Save unconfirmed · Retrying safely…' : 'Recovery data needs a retry.');
break;
}
continue;
}
if (this.refreshRequested) {
const forced = this.force; this.force = false; this.refreshRequested = false;
try {
if (forced) await this.read();
else {
const changes = await this.api.call<{revision:number}>('soy_backend_changes',{target:this.target,after:this.revision});
if (this.stopped) return;
if (changes.revision > this.revision) await this.read();
}
if (this.stopped) return;
this.reconcileQueue(); await this.persist(); this.message = ''; this.render();
} catch (e) {
this.refreshRequested = true;
if (e instanceof BackendCallError && e.code === 'RESYNC_REQUIRED') this.recovery = true;
this.backoff('Connection interrupted · Saved blocks will refresh.'); break;
}
continue;
}
break;
}
} catch { this.backoff('Recovery data needs a retry.'); }
finally { this.running = false; this.render(); }
}
}
