export type ToolcraftPersistenceSliceCodec = Readonly<{ fields: readonly (keyof Decoded & string)[]; read: (context: Context, data: Record) => Partial | undefined; write: (state: State, context: Context) => Partial; }>; /** Slice selection and field ownership are independent of module and payload names. */ export function createToolcraftPersistenceCodec( slices: Readonly>>, ) { const entries = (Object.entries(slices) as [Slice, ToolcraftPersistenceSliceCodec][]) .map(([slice, codec]) => [slice, Object.freeze({ fields: Object.freeze([...codec.fields]), read: codec.read, write: codec.write, })] as const); const owners = new Map(); for (const [slice, codec] of entries) { for (const field of codec.fields) { if (owners.has(field)) throw new Error(`Persistence field "${field}" has multiple owners: ${owners.get(field)}, ${slice}.`); owners.set(field, slice); } } function combine(included: ReadonlySet, select: (codec: ToolcraftPersistenceSliceCodec) => Partial | undefined) { const result: Partial = {}; for (const [slice, codec] of entries) { if (!included.has(slice)) continue; const payload = select(codec); for (const field of Object.keys(payload ?? {})) { if (owners.get(field) !== slice) throw new Error(`Persistence slice "${slice}" wrote undeclared field "${field}".`); } Object.assign(result, payload); } return result; } return Object.freeze({ read: (context: Context, data: Record, included: ReadonlySet) => { const result = combine(included, codec => codec.read(context, data)); return Object.keys(result).length ? result : undefined; }, write: (state: State, context: Context, included: ReadonlySet) => combine(included, codec => codec.write(state, context)), }); }