Separate looted currency and preserve growing realm state in atomic chunks
Core tests / core (push) Successful in 49s

This commit is contained in:
Codex
2026-09-24 03:16:52 +12:00
parent e7b9df9e08
commit 8e01cdee22
13 changed files with 54 additions and 17 deletions
+4 -2
View File
@@ -1,12 +1,12 @@
import type {State} from './types';
import {activityReason,type Evidence} from './realm-insights';
export const counterKeys=['quests','abandoned','lootEvents','lootItems','partyKills','rewarded'] as const;
export const counterKeys=['quests','abandoned','lootEvents','lootItems','partyKills','rewarded','lootedCopper'] as const;
export type NativeOutcome={guid:number;name:string}&Record<typeof counterKeys[number],number>;
export type OutcomeBaseline={at:number;startedAt:number;rows:NativeOutcome[]};
export function outcomeChanges(rows:NativeOutcome[],previous:OutcomeBaseline|undefined,now:number){
const events:Evidence[]=[];
for(const r of rows){const old=previous?.rows.find(b=>b.guid===r.guid);if(!old)continue;
for(const key of counterKeys){const amount=r[key]-old[key];if(amount>0)events.push({id:`outcome:${r.guid}:${key}:${now}:${r[key]}`,at:now,kind:key,name:r.name,amount,detail:({quests:'Quest objectives completed',abandoned:'Quests abandoned',lootEvents:'Loot collections',lootItems:'Items collected',partyKills:'Kills with a nearby bot attacking the same target',rewarded:'New rewarded quest records'})[key]});}
for(const key of counterKeys){const amount=r[key]-old[key];if(amount>0)events.push({id:`outcome:${r.guid}:${key}:${now}:${r[key]}`,at:now,kind:key,name:r.name,amount,detail:({lootedCopper:'Currency looted',quests:'Quest objectives completed',abandoned:'Quests abandoned',lootEvents:'Loot collections',lootItems:'Items collected',partyKills:'Kills with a nearby bot attacking the same target',rewarded:'New rewarded quest records'})[key]});}
}
return {baseline:{at:now,startedAt:previous?.startedAt??now,rows},events};
}
@@ -35,3 +35,5 @@ export function attentionItems(s:State,now:number,collectedAt:number|null,outcom
for(const r of latest.values())if(r.status==='no_progress'&&now-r.at<3600&&online.some(b=>b.autonomy?.characterGuid===r.guid&&activityReason(b,now).label==='Possible stall'))add('recovery:'+r.id,'warning',r.name+' · recovery needs follow-up','No native movement or level progress was observed within three minutes.','Recovery Results',r.name);
return items.sort((a,b)=>Number(b.severity==='critical')-Number(a.severity==='critical'));
}
export function formatCopper(amount:number){const n=Math.max(0,Math.floor(Number.isFinite(amount)?amount:0));return `${Math.floor(n/10000)}g ${Math.floor(n%10000/100)}s ${n%100}c`;}
+2
View File
@@ -0,0 +1,2 @@
// Bound each SQL value to at most 512 KiB of UTF-8; preserve all resident history.
export function splitState(state:unknown){const text=JSON.stringify(state),chunks:string[]=[];for(let i=0;i<text.length;){let end=Math.min(i+120000,text.length);const c=text.charCodeAt(end-1);if(c>=0xd800&&c<=0xdbff)end--;chunks.push(text.slice(i,end));i=end;}return chunks;}
+15 -6
View File
@@ -1,4 +1,5 @@
import { env } from 'cloudflare:workers';
import { splitState } from './state-chunks';
import { seed } from './seed';
import type { State,StateStore,Bot,Recipe,Home } from './types';
@@ -68,13 +69,21 @@ export class D1Store implements StateStore {
if(!env.DB) throw new Error('Realm database unavailable');
const initial=seed();
await env.DB.prepare('INSERT OR IGNORE INTO realm_state (id,revision,payload) VALUES (?,?,?)').bind('realm',0,JSON.stringify(initial)).run();
const row=await env.DB.prepare('SELECT payload FROM realm_state WHERE id=?').bind('realm').first<{payload:string}>();
if(!row)throw new Error('Realm state unavailable');
return migrate(JSON.parse(row.payload));
await this.chunks();
const result=await env.DB.prepare("SELECT s.payload AS manifest,c.payload AS chunk FROM realm_state s LEFT JOIN realm_state_chunks c ON c.token=json_extract(s.payload,'$.chunkToken') WHERE s.id=? ORDER BY c.part").bind('realm').all<{manifest:string;chunk:string|null}>();
const rows=result.results;if(!rows.length)throw new Error('Realm state unavailable');
const manifest=JSON.parse(rows[0].manifest);
if(manifest.chunkToken){if(rows.length!==manifest.parts||rows.some(r=>r.chunk===null))throw new Error('Realm state chunks incomplete');return migrate(JSON.parse(rows.map(r=>r.chunk).join('')));}
return migrate(manifest);
}
private async chunks(){await env.DB!.prepare('CREATE TABLE IF NOT EXISTS realm_state_chunks (token TEXT,part INTEGER,payload TEXT,PRIMARY KEY(token,part))').run();}
async save(s:State,expected:number){
s.revision=expected+1;
const result=await env.DB!.prepare('UPDATE realm_state SET revision=?,payload=? WHERE id=? AND revision=?').bind(s.revision,JSON.stringify(s),'realm',expected).run();
if(result.meta.changes!==1)throw new Error('State changed in another request. Refresh and try again.');
await this.chunks();s.revision=expected+1;
const token=crypto.randomUUID(),parts=splitState(s),manifest=JSON.stringify({chunkToken:token,parts:parts.length});
const statements=[env.DB!.prepare('UPDATE realm_state SET revision=?,payload=? WHERE id=? AND revision=?').bind(s.revision,manifest,'realm',expected),
...parts.map((part,i)=>env.DB!.prepare('INSERT INTO realm_state_chunks(token,part,payload) SELECT ?,?,? WHERE EXISTS (SELECT 1 FROM realm_state WHERE id=? AND payload=?)').bind(token,i,part,'realm',manifest)),
env.DB!.prepare('DELETE FROM realm_state_chunks WHERE token<>? AND EXISTS (SELECT 1 FROM realm_state WHERE id=? AND payload=?)').bind(token,'realm',manifest)];
const result=await env.DB!.batch(statements);
if(result[0].meta.changes!==1)throw new Error('State changed in another request. Refresh and try again.');
}
}