Back to Outbreak: Soycoon City
SOURCE / PINNED RELEASE

Made of little things.

Outbreak: Soycoon City

Release
db7750b1d009…
Author-recorded commit
3ee3f5fd27a6…
License
LICENSE
Author’s source reference
nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b87c04571a9

Archive hash verified: dbebdc772b142a1c…. The source-to-build association is the author’s claim; it has not been independently rebuilt.

src/sharing-host.ts
import { identity, outbox, resource, upload, type UploadResult } from '@napplet/sdk';
import { appDataCollection, type AppDataHost } from '../docs/examples/app-data.js';
import { fileReference, LEVEL_SCHEMA, sha256, validateLevelRecord, type FileReference } from './level-format.js';
import { runtimeHasDomain } from './domain-availability.js';
import type { LevelFileIO } from './level-files.js';
export function levelCollection() {
  if (!runtimeHasDomain('identity') || !runtimeHasDomain('outbox')) throw new Error('Level sharing is unavailable in this host. Keep using the editor and JSON export.');
  // The SDK exports no shell initialization wrapper. Observe the existing
  // injected onReady policy via the documented helper; all other calls are SDK-first.
  const shell = (window as unknown as { napplet?: AppDataHost }).napplet?.shell;
  return appDataCollection({ collection:'levels', schema:LEVEL_SCHEMA, version:1, validate:validateLevelRecord, host:{ shell:shell!, identity, outbox:outbox as unknown as AppDataHost['outbox'] } });
}
export type LevelCollection = Awaited<ReturnType<typeof levelCollection>>;
export function hostLevelFiles(): LevelFileIO & { dispose(): void } {
  const jobs = new Map<string, UploadResult>(), cancels = new Set<()=>void>();
  const wait = (id: string): Promise<UploadResult> => new Promise((resolve,reject) => {
    let done=false, subscription: {close():void}|undefined;
    let deadline:ReturnType<typeof setTimeout>, timer:ReturnType<typeof setTimeout>;
    const finish=(result?:UploadResult,error?:unknown)=>{if(done)return;done=true;clearTimeout(deadline);clearTimeout(timer);subscription?.close();cancels.delete(cancel);if(error)reject(error);else resolve(result!);};
    const cancel=()=>finish(undefined,new Error('Level upload cancelled when this view closed.'));cancels.add(cancel);
    const check=(r:UploadResult)=>{if(r.uploadId!==id)return;if(r.status==='complete')finish(r);else if(r.status==='failed'||r.status==='cancelled')finish(undefined,new Error(r.error??'Upload was denied or failed. Retry keeps your level draft.'));};
    const poll=()=>{void (async()=>{try{check(await upload.status(id));if(!done)timer=setTimeout(poll,1500);}catch(e){finish(undefined,e);}})();};
    try { subscription=upload.onStatus(check); } catch { /* Older hosts may provide polling only. */ }
    if(done){subscription?.close();return;}
    deadline=setTimeout(()=>finish(undefined,new Error('Upload is still pending. Complete host approval, then retry.')),300_000);poll();
  });
  return {
    async read(ref:FileReference) {
      if(!runtimeHasDomain('resource'))throw new Error('This host cannot load linked level files. Resource support is needed.');
      return resource.bytes(ref.url);
    },
    async upload(blob:Blob,name:string) {
      if(!runtimeHasDomain('upload'))throw new Error('This host has no Blossom upload support. Export your level JSON instead.');
      const hash=await sha256(blob);let result=jobs.get(hash);
      if(!result || result.status==='failed'||result.status==='cancelled') {
        result=await upload.upload({data:blob,filename:name,mimeType:blob.type,caption:'Soyindent Evil player level asset',noTransform:true});jobs.set(hash,result);
      }
      if(result.status!=='complete') { try {result=await wait(result.uploadId);jobs.set(hash,result);}catch(error){try{jobs.set(hash,await upload.status(result.uploadId));}catch{}throw error;} }
      if(!result.ok)throw new Error(result.error??'The host did not complete this upload.');
      return fileReference({url:result.url,sha256:result.sha256,mime:result.mimeType,size:result.size},blob.type,blob.size);
    },
    dispose(){for(const cancel of [...cancels])cancel();jobs.clear();}
  };
}