SOURCE / PINNED RELEASE
Made of little things.
Limb Race
- Release
- 6a8ae4fc626e…
- Author-recorded commit
- 008c01ee6454…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-c2869f12ad8
Archive hash verified: 7be8492d1f89b891…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import {identity, outbox, storage} from '@napplet/sdk';
import {appDataCollection, type AppDataHost, type AppRecord} from '../docs/examples/app-data.js';
import {runtimeHasDomain} from './domain-availability.js';
import project from '../napplet.json';
import {starterLevel, validateLevel, type LevelData, type LevelSnapshot} from './level-model.js';
export type LevelRecord = AppRecord<LevelData>;
export type LevelDraft = {id:string; title:string; version:number; data:LevelData; base:LevelRecord|null};
const KEY='limb-race-levels-v1';
const freshId=()=>globalThis.crypto?.randomUUID?.()??`level-${Date.now()}-${Math.random().toString(36).slice(2)}`;
export function createDraft(data=starterLevel(),title='My new track'):LevelDraft {return {id:freshId(),title,version:1,data:validateLevel(data),base:null};}
export function draftSnapshot(draft:LevelDraft):LevelSnapshot {return {key:`draft:${draft.id}:${draft.version}`,title:draft.title.trim()||'Untitled track',data:validateLevel(draft.data)};}
export function recordSnapshot(record:LevelRecord):LevelSnapshot {
if(record.deleted||!record.data)throw new Error('This level was unpublished.');
return {key:`custom:${record.revision}`,title:record.title,data:validateLevel(record.data)};
}
export async function viewerKey():Promise<string>{try{return runtimeHasDomain('identity')?await identity.getPublicKey():'';}catch{return '';}}
export class LevelStore {
drafts:LevelDraft[]=[];
private saves:Promise<unknown>=Promise.resolve();
async load():Promise<void>{
if(!runtimeHasDomain('storage'))return;
const raw=await storage.getItem(KEY);if(!raw)return;
const values=JSON.parse(raw);if(!Array.isArray(values))throw new Error('Saved levels could not be read.');
this.drafts=values.slice(0,20).flatMap((value:LevelDraft)=>{
try {
if(!value||typeof value.id!=='string'||!/^[a-zA-Z0-9_-]{1,64}$/.test(value.id)||typeof value.title!=='string'||value.title.length>60||!Number.isSafeInteger(value.version)||value.version<1)return [];
const data=validateLevel(value.data);
const base=value.base&&typeof value.base.author==='string'&&/^[a-f0-9]{64}$/.test(value.base.revision)?value.base:null;
return [{id:value.id,title:value.title,version:value.version,data,base}];
}catch{return [];}
});
}
async save(draft:LevelDraft):Promise<boolean>{
const existing=this.drafts.findIndex(d=>d.id===draft.id);
if(existing<0&&this.drafts.length>=20)throw new Error('You have 20 drafts. Remove one before creating another.');
const copy=structuredClone(draft);
if(existing<0)this.drafts.unshift(copy);else this.drafts[existing]=copy;
return this.persist();
}
async remove(id:string):Promise<boolean>{this.drafts=this.drafts.filter(d=>d.id!==id);return this.persist();}
private async persist():Promise<boolean>{
if(!runtimeHasDomain('storage'))return false;
const data=JSON.stringify(this.drafts);
const next=this.saves.catch(()=>{}).then(()=>storage.setItem(KEY,data));this.saves=next;await next;return true;
}
}
let collectionPromise:ReturnType<typeof appDataCollection<LevelData>>|undefined;
export function levelCollection(){
if(!collectionPromise){
// The supplied helper observes soyLI's documented policy hint. Operational
// identity/outbox calls still use SDK wrappers; no app-owned handshake.
const injected=(globalThis as unknown as {napplet?:AppDataHost}).napplet;
// Keep the public library on the creation's declared storage relay. soyLI's
// preview prepends its ephemeral CVM transport, which cannot store NIP-78.
// This is only a request: the host still requires a configured destination.
collectionPromise=appDataCollection({collection:'levels',schema:'limb-race.level',version:1,validate:validateLevel,relays:project.relays.length?project.relays.slice(0,1):undefined,host:{...injected,identity,outbox} as AppDataHost}).catch(error=>{collectionPromise=undefined;throw error;});
}
return collectionPromise;
}
export async function latestLevel(record:LevelRecord):Promise<LevelRecord>{
const library=await levelCollection(),result=await library.get(record.author,record.id,record.scope);
if(result.incomplete)throw new Error('Some relays did not respond. Retry before editing this level.');
if(!result.record)throw new Error('This level is unavailable. Your draft is kept.');
return result.record;
}
export class LevelPublisher {
private pending:{fingerprint:string;change:Awaited<ReturnType<Awaited<ReturnType<typeof levelCollection>>['prepare']>>}|null=null;
clear():void{this.pending=null;}
async publish(draft:LevelDraft,deleted=false):Promise<LevelRecord>{
const fingerprint=JSON.stringify([draft.id,draft.title,draft.data,draft.base?.revision,deleted]);
if(this.pending?.fingerprint!==fingerprint){
const library=await levelCollection();
if(draft.base){
const latest=await latestLevel(draft.base);
if(latest.revision!==draft.base.revision)throw new Error('This level changed elsewhere. Open the latest version in Community, or make a copy of your draft.');
}
const change=await library.prepare({id:draft.id,title:draft.title.trim(),data:draft.data,base:draft.base,deleted});
this.pending={fingerprint,change};
}
const record=await this.pending.change.publish();this.pending=null;return record;
}
}
export function levelError(error:unknown):string{
const message=error instanceof Error?error.message:'Could not reach the level library. Retry.';
if(/not-signed-in|identity-required/.test(message))return 'Connect your Nostr identity in the host to publish. Your draft is kept.';
if(/app-data-unavailable/.test(message))return 'This host needs the updated public-level sharing support. Your draft is kept locally; try an updated host.';
if(/relay-not-configured/.test(message))return `Add ${project.relays[0]||'a level storage relay'} in the host Network settings, then retry. Your draft is kept.`;
if(/identity-changed|owner-mismatch/.test(message))return 'The selected identity changed. Use the original identity, or make a copy to publish as yourself.';
if(/stale|app-data-conflict/.test(message))return 'This level changed elsewhere. Open the latest version in Community, or make a copy of your draft.';
if(/denied|reject|cancel/.test(message))return 'Publishing was not approved. Your draft is kept; you can retry.';
return message.slice(0,240);
}
