Stage transaction creations without copying full tables per command

This commit is contained in:
2026-11-10 01:30:47 +08:00
parent 198c7f9863
commit fb997f23a8
4 changed files with 66 additions and 1 deletions
+30
View File
@@ -0,0 +1,30 @@
import type {CanvasRecord,Change,Document,ElementRecord,Id} from './types.ts';
import {applyChanges} from './document.ts';
import {clone,fault,freeze,readMap,validJson} from './values.ts';
/** Transaction-local append views. Each retained view has fixed membership;
* at most 64 additions are copied before folding into a new read-only base. */
const overlays=new WeakMap<object,{base:ReadonlyMap<Id,unknown>;added:ReadonlyMap<Id,unknown>}>(),drafts=new WeakSet<Document>();
function append<T>(input:ReadonlyMap<Id,T>,id:Id,value:T):ReadonlyMap<Id,T>{
if(input.has(id))throw fault('duplicate_identity',[id]);
let previous=overlays.get(input) as {base:ReadonlyMap<Id,T>;added:ReadonlyMap<Id,T>}|undefined;
if(previous&&previous.added.size>=64)previous=undefined;
const base=previous?.base??(overlays.has(input)?readMap(input):input),added=new Map(previous?.added);
added.set(id,value);
const entries=function*():Generator<[Id,T],undefined,unknown>{yield* base;yield* added;return undefined;};
const view:ReadonlyMap<Id,T>=Object.freeze({size:base.size+added.size,get:(key:Id)=>added.has(key)?added.get(key):base.get(key),has:(key:Id)=>added.has(key)||base.has(key),entries,
*keys():Generator<Id,undefined,unknown>{for(const [key]of entries())yield key;return undefined;},*values():Generator<T,undefined,unknown>{for(const [,item]of entries())yield item;return undefined;},[Symbol.iterator]:entries,
forEach(callback:(value:T,key:Id,map:ReadonlyMap<Id,T>)=>void,thisArg?:unknown){for(const[key,item]of entries())callback.call(thisArg,item,key,view);},
});
overlays.set(view,{base,added});return view;
}
/** Internal editor path: input tables already have fixed membership. */
export function stageCreation(document:Document,change:Extract<Change,{kind:'canvas'|'element'}>):Document{
if(change.before!==null||!change.after)throw fault('invalid_creation_draft');
if(!validJson(change.after))throw fault(change.kind==='canvas'?'invalid_canvas':'invalid_element',[change.id]);
if(change.after.id!==change.id)throw fault('identity_mismatch',[change.id]);
const value=freeze(clone(change.after));
const next=Object.freeze({...document,...(change.kind==='canvas'?{canvases:append(document.canvases,change.id,value as CanvasRecord)}:{elements:append(document.elements,change.id,value as ElementRecord)})});drafts.add(next);return next;
}
/** Only published documents use the core's owned table envelopes. */
export const materializeCreations=(document:Document)=>drafts.has(document)?applyChanges(document,[]):document;
+3 -1
View File
@@ -1,3 +1,4 @@
import {stageCreation,materializeCreations} from './creation-draft.ts';
import type { Applied, Availability, Capability, Change, Command, Commit, CoreSnapshot, Document, ElementRecord, Geometry, HistoryEntry, HistoryState, Id, OperationIntent, Origin, Registry, Result, Snapshot } from './types.ts';
import { applyChanges, createDocument, invert, targetElement, validateDocument } from './document.ts';
import { clone, equal, fault, freeze, readMap, reject, validJson } from './values.ts';
@@ -117,7 +118,7 @@ export function createEditor<Runtime = undefined>(options: EditorOptions<Runtime
changes = output;
}
}
const next = applyChanges(document, changes);
const next = command.type==='element.create'||command.type==='canvas.create'?stageCreation(document,changes[0] as Extract<Change,{kind:'canvas'|'element'}>):applyChanges(document, changes);
return { document: next, changes: freeze(clone(changes)) };
}
@@ -221,6 +222,7 @@ export function createEditor<Runtime = undefined>(options: EditorOptions<Runtime
active = false;
if (isThenable(result)) { Promise.resolve(result).catch(() => {}); throw fault('async_transaction'); }
if (failed) throw failed;
document=materializeCreations(document);
validateDocument(document, options.registry);
if (!changes.length || documentsEqual(document, state.document)) return Object.freeze({ status: 'applied', revision: state.revision, operationIds: Object.freeze([]) });
let history = state.history, historyEntryId: Id | undefined;