2026-10-23 20:54:13 +08:00
import { jsonTextCodeUnits } from "../jsonSerialization.js" ;
2026-10-21 23:15:35 -07:00
import type { JsonValue } from "../jsonValue.js" ;
import {
2026-10-26 18:27:22 +08:00
ownMember ,
2026-10-22 06:52:01 -07:00
collectionPathOf ,
2026-10-21 23:15:35 -07:00
isObject ,
2026-10-22 06:52:01 -07:00
isParam ,
normalizedPath ,
2026-10-24 14:31:46 -07:00
same ,
2026-10-21 23:15:35 -07:00
scalarText ,
2026-10-22 06:52:01 -07:00
segmentsOf ,
selectedCollection ,
2026-10-21 23:15:35 -07:00
type JsonObject ,
type StateEntity ,
2026-10-22 06:52:01 -07:00
type SuppliedSegment ,
2026-10-21 23:15:35 -07:00
} from "./environmentStateRules.js" ;
2026-10-24 05:52:03 -07:00
import {
createdFields ,
recordedGeneratedValue ,
} from "./environmentStateWriteApply.js" ;
import {
createdToken ,
2026-10-27 16:51:03 -07:00
heldObjects ,
2026-10-24 05:52:03 -07:00
isOpaqueToken ,
2026-10-27 16:51:03 -07:00
nextMadeCount ,
2026-10-24 05:52:03 -07:00
restampedFields ,
2026-10-27 16:51:03 -07:00
stampedBy ,
2026-10-24 05:52:03 -07:00
} from "./environmentStateWriteStamps.js" ;
2026-10-24 00:58:32 -07:00
import {
2026-10-27 16:51:03 -07:00
anyLinkedSource ,
2026-10-24 00:58:32 -07:00
recordedPathValues ,
respondedIds ,
sameLinkedSource ,
} from "./environmentStateWriteLinks.js" ;
2026-10-21 23:15:35 -07:00
import {
collectionTarget ,
entityTarget ,
} from "./environmentStateWriteChecks.js" ;
import {
built ,
callOf ,
2026-10-24 14:06:21 -07:00
clockEvidence ,
2026-10-21 23:15:35 -07:00
fail ,
2026-10-24 14:31:46 -07:00
referencedIn ,
2026-10-24 05:52:03 -07:00
itemReads ,
2026-10-21 23:15:35 -07:00
refuse ,
2026-10-24 05:52:03 -07:00
requestBodyPlan ,
2026-10-21 23:15:35 -07:00
retainFixed ,
2026-10-27 08:15:52 +08:00
retainCreateExemplar ,
2026-10-21 23:15:35 -07:00
searchFrom ,
startOf ,
UNKNOWN ,
type EffectContext ,
type EffectResult ,
type WriteDraft ,
type WriteStep ,
} from "./environmentStateWriteEffects.js" ;
import {
2026-10-24 14:06:21 -07:00
createdInteger ,
2026-10-21 23:15:35 -07:00
explainCreate ,
2026-10-27 08:15:52 +08:00
type CreateEvidence ,
2026-10-21 23:15:35 -07:00
insertCandidates ,
insertIndex ,
2026-10-24 05:52:03 -07:00
randomIdentities ,
2026-10-21 23:15:35 -07:00
nextInteger ,
responseShapeOf ,
} from "./environmentStateWriteExplain.js" ;
import {
laterObservation ,
newEntityIn ,
2026-10-27 14:05:21 -07:00
loadedLater ,
unloadedPointers ,
2026-10-25 00:06:27 -07:00
withRead ,
2026-10-21 23:15:35 -07:00
type Observed ,
} from "./environmentStateWriteObserve.js" ;
import {
2026-10-24 14:31:46 -07:00
keptOrders ,
2026-10-21 23:15:35 -07:00
pathValuesOf ,
2026-10-24 14:06:21 -07:00
siblingTrimmedClocks ,
2026-10-21 23:15:35 -07:00
type FieldRule ,
type WriteCall ,
} from "./environmentStateWriteRules.js" ;
import {
2026-10-24 14:06:21 -07:00
fittedClockFormat ,
noteFixedWidths ,
} from "./environmentStateWriteClockFormats.js" ;
2026-10-27 08:15:52 +08:00
import {
mergedCreatePlan ,
noteAmpersands ,
noteHeldAmpersands ,
} from "./environmentStateWriteCreateCalls.js" ;
2026-10-24 14:06:21 -07:00
import {
containsDelimited ,
generatedIntegerDigits ,
2026-10-21 23:15:35 -07:00
isUuid ,
2026-10-24 14:06:21 -07:00
likeGeneratedInteger ,
2026-10-21 23:15:35 -07:00
nextSequence ,
2026-10-27 16:51:03 -07:00
startedSequence ,
2026-10-22 08:46:52 -07:00
sequenceNumber ,
spelledLike ,
2026-10-21 23:15:35 -07:00
} from "./environmentStateWriteValues.js" ;
/** The id a response names alone: a scalar body, or an object's id field or single scalar member. */
const respondedId = (
body : JsonValue | undefined ,
idField : string ,
) : string | undefined => {
if ( body === undefined ) return undefined ;
if ( ! isObject ( body )) return scalarText ( body );
if ( Object . hasOwn ( body , idField )) return scalarText ( body [ idField ]);
const members = Object . values ( body );
return members . length === 1 ? scalarText ( members [ 0 ]) : undefined ;
};
2026-10-25 00:06:27 -07:00
/**
* A create's answer with the first read of its entity before the next write over it, when that read shows fields the
* answer lacks or spells its clocks to another precision: the state holds what reads show. An item read that agrees
* with the answer otherwise lends it its fields and then observes rather than checks; one that disagrees still checks
* the create. A list read, which may show fewer keys of an object, only adds fields the answer lacks and still checks.
*/
const withReadBack = (
context : EffectContext ,
step : WriteStep ,
target : {
readonly type : string ;
readonly collection : string ;
readonly idField : string ;
},
own : Observed ,
) : Observed => {
const later = laterObservation ( searchFrom ( context , step ), {
type : target . type ,
collection : target.collection ,
idField : target.idField ,
id : own.id ,
held : () => false ,
isWrite : context.isWrite ,
});
if ( later === undefined ) return own ;
const view = context . viewOf ( later . ordinal );
if (
view ? . shape === "item" &&
( view . fields . some (
( field ) =>
Object . hasOwn ( own . object , field ) && ! Object . hasOwn ( later . object , field ),
) ||
Object . entries ( later . object ). some (
([ field , value ]) =>
Object . hasOwn ( own . object , field ) &&
2026-10-27 14:05:21 -07:00
! loadedLater ( own . object [ field ], value ),
2026-10-25 00:06:27 -07:00
))
)
return own ;
const shape = view ? . shape === "item" ? "item" : "list" ;
const object = withRead ( own . object , later . object , shape );
if ( same ( object , own . object )) return own ;
if ( shape === "item" ) context . observed ( later . ordinal );
2026-10-27 14:05:21 -07:00
// Answers keep the values the response had not loaded, which the state holds as the item read shows them.
const unloaded =
shape === "item" ? unloadedPointers ( own . object , later . object ) : [];
2026-10-25 00:06:27 -07:00
return {
... own ,
object ,
2026-10-27 14:05:21 -07:00
read : {
ordinal : later.ordinal ,
pointer : later.pointer ,
shape ,
...( unloaded . length === 0 ? {} : { unloaded }),
},
2026-10-25 00:06:27 -07:00
};
};
2026-10-24 00:58:32 -07:00
/**
* The written entity a create's response or a later read shows. A linked create's read must name an
* id its response returned, as the read that linked it did, envelopes included.
*/
2026-10-21 23:15:35 -07:00
const createdObservation = (
context : EffectContext ,
step : WriteStep ,
target : {
readonly type : string ;
readonly collection : string ;
readonly idField : string ;
2026-10-24 00:58:32 -07:00
readonly linked : boolean ;
2026-10-21 23:15:35 -07:00
},
) : Observed | undefined => {
const held = ( id : string ) =>
context . state . entity ( target . type , target . collection , id ) !== undefined ;
const own = context . io . withBody ( step . record . ordinal , ( body ) =>
newEntityIn ( step . record . ordinal , body , target . idField , held ),
);
2026-10-25 00:06:27 -07:00
if ( own !== undefined ) return withReadBack ( context , step , target , own );
2026-10-24 00:58:32 -07:00
const { id , ids } = context . io . withBody ( step . record . ordinal , ( body ) => {
const returned = target . linked ? respondedIds ( body ) : [];
// A response that names the created id alone ties the later read to that id.
const named =
respondedId ( body , target . idField ) ??
( returned . length === 1 ? returned [ 0 ] : undefined );
return { id : named , ids : returned };
});
2026-10-21 23:15:35 -07:00
const later = laterObservation ( searchFrom ( context , step ), {
2026-10-24 00:58:32 -07:00
type : target . type ,
collection : target.collection ,
idField : target.idField ,
2026-10-21 23:15:35 -07:00
...( id === undefined ? {} : { id }),
held ,
2026-10-24 00:58:32 -07:00
isWrite : context.isWrite ,
2026-10-21 23:15:35 -07:00
});
2026-10-24 00:58:32 -07:00
// A read named by the id is tied already; otherwise a linked create's read must name a returned id.
if (
later === undefined ||
( target . linked && id === undefined && ! ids . includes ( later . id ))
)
return undefined ;
context . observed ( later . ordinal );
2026-10-21 23:15:35 -07:00
return later ;
};
const fieldsOf = ( entities : readonly StateEntity []) : JsonObject [] =>
entities . map (( entity ) => entity . fields );
2026-10-24 14:06:21 -07:00
/** Whether a created field holds a generated token or integer that another field holds too, which no rule rebuilds. */
const linkedGenerated = (
field : string ,
entity : JsonObject ,
type : readonly JsonObject [],
) : boolean =>
createdToken ( field , entity , type ) === "linked" ||
createdInteger ( field , entity , type ) === "linked" ;
/** Whether a field's rule rebuilds it from `source`, as a template or a composition does. */
const rebuiltFrom = ( rule : FieldRule | undefined , source : string ) : boolean =>
rule ? . rule === "template"
? rule . from === source
: rule?.rule === "composed" &&
rule . parts . some (( part ) => "from" in part && part . from === source );
/** Whether a unique integer's digits show in another string whose rule does not rebuild them from it, which seeding would leave behind. */
const strandedInteger = (
rules : readonly FieldRule [],
entity : JsonObject ,
) : boolean =>
rules . some (( rule ) => {
const digits =
rule . rule === "unique"
2026-10-26 18:27:22 +08:00
? generatedIntegerDigits ( ownMember ( entity , rule . field ))
2026-10-24 14:06:21 -07:00
: undefined ;
return (
digits !== undefined &&
Object . entries ( entity ). some (
([ other , kept ]) =>
other !== rule . field &&
typeof kept === "string" &&
containsDelimited ( kept , digits ) &&
! rebuiltFrom (
rules . find (( candidate ) => candidate . field === other ),
rule . field ,
),
)
);
});
2026-10-24 14:31:46 -07:00
/** Whether a created field no rule explains holds a generated value another field links to, or a unique integer another string strands, which serving cannot keep consistent. */
const linkedUnexplained = (
rules : readonly FieldRule [],
entity : JsonObject ,
held : readonly JsonObject [],
) : boolean => {
const explained = new Set ( rules . map (( rule ) => rule . field ));
return (
Object . keys ( entity ). some (
( field ) => ! explained . has ( field ) && linkedGenerated ( field , entity , held ),
) || strandedInteger ( rules , entity )
);
};
/** Widen a create plan by a later call, when that call explained on its own agrees with it; whether it did. */
const refineCreate = (
2026-10-21 23:15:35 -07:00
context : EffectContext ,
draft : WriteDraft ,
step : WriteStep ,
2026-10-24 14:31:46 -07:00
evidence : CreateCallEvidence ,
2026-10-24 05:52:03 -07:00
) : boolean => {
2026-10-24 14:31:46 -07:00
const { explained , held } = explainedCall ( context , draft , step , evidence );
for ( const member of keptOrders ( evidence . call . body , evidence . observed . object ))
draft . kept . add ( member );
if (
( explained . tied && draft . plan . collection === null ) ||
linkedUnexplained ( explained . rules , evidence . observed . object , held )
)
return false ;
2026-10-27 08:15:52 +08:00
const later = laterOf ( evidence , explained );
const widened =
mergedCreatePlan (
{ rules : draft.plan.rules , body : draft.plan.body ! },
draft . observed ! ,
later ,
2026-10-29 01:14:05 -07:00
earlierOf ( draft , evidence . collection ),
) ?? reexplained ( draft , explained . rules , later , evidence . collection );
2026-10-27 08:15:52 +08:00
if ( widened === undefined ) return false ;
retainFixed ( context , widened . body . fixed . slice ( draft . plan . body ! . fixed . length ));
draft . plan . rules = widened . rules ;
draft . plan . body = widened . body ;
draft . exemplarInput = undefined ;
noteAmpersands ( draft . ampersands , explained . rules , evidence . observed . object );
return true ;
};
/** What the plan's earlier calls showed, for merging a later one. */
2026-10-29 01:14:05 -07:00
const earlierOf = ( draft : WriteDraft , collection : string ) => ({
2026-10-27 08:15:52 +08:00
kept : draft.kept ,
clockOf : ( field : string ) => clockEvidence ( draft , field ),
2026-10-27 16:51:03 -07:00
startOf : ( field : string ) => draft . starts . get ( field ),
createCollections : draft.createCollections ,
2026-10-29 01:14:05 -07:00
sentLargest : ( field : string , scope : "collection" | "type" ) =>
draft . sentLargest . get (
scope === "collection" ? ` ${ field } ${ collection } ` : field ,
),
2026-10-27 08:15:52 +08:00
ampersands : draft.ampersands ,
});
/** A later create as the plan merge reads it. */
const laterOf = (
evidence : CreateCallEvidence ,
explained : ReturnType < typeof explainCreate >,
) => ({
entity : evidence.observed.object ,
body : evidence.call.body ,
rules : explained.rules ,
contract : {
... explained . body ,
...( evidence . envelope === undefined ? {} : { envelope : evidence.envelope }),
},
});
/**
* The plan its exemplar gives again with the composed rules a later call learned for fields the exemplar left
* unexplained, such as an address spelled from an id that equals the title's slug, merged with that call. Undefined
* once a later call widened the plan, or unless the rules spell the exemplar's own values.
*/
const reexplained = (
draft : WriteDraft ,
learned : readonly FieldRule [],
later : ReturnType < typeof laterOf >,
2026-10-29 01:14:05 -07:00
collection : string ,
2026-10-27 08:15:52 +08:00
) => {
const input = draft . exemplarInput ;
if ( input === undefined ) return undefined ;
const hints = learned . filter (
( rule ) =>
rule . rule === "composed" &&
! draft . plan . rules . some (( held ) => held . field === rule . field ),
);
if ( hints . length === 0 ) return undefined ;
const again = explainCreate ({ ... input , hints });
if ( ! hints . every (( hint ) => again . rules . includes ( hint ))) return undefined ;
const envelope = draft . plan . body ! . envelope ;
return mergedCreatePlan (
2026-10-24 14:31:46 -07:00
{
2026-10-27 08:15:52 +08:00
rules : again.rules ,
body : {
... again . body ,
...( envelope === undefined ? {} : { envelope }),
2026-10-24 14:31:46 -07:00
},
},
2026-10-27 08:15:52 +08:00
draft . observed ! ,
later ,
2026-10-29 01:14:05 -07:00
earlierOf ( draft , collection ),
2026-10-24 14:31:46 -07:00
);
};
type CreateCallEvidence = {
readonly observed : Observed ;
readonly call : WriteCall & { readonly body : JsonObject };
readonly collection : string ;
readonly envelope : string | undefined ;
};
/** Explain one recorded create on its own, against the entities its collection and type held before it. */
const explainedCall = (
context : EffectContext ,
draft : WriteDraft ,
step : WriteStep ,
evidence : CreateCallEvidence ,
) => {
2026-10-21 23:15:35 -07:00
const type = draft . plan . target ! ;
const members = fieldsOf ( context . state . members ( type , evidence . collection ));
2026-10-24 05:52:03 -07:00
const held = fieldsOf ( context . state . ofType ( type ));
2026-10-27 08:15:52 +08:00
const input : CreateEvidence = {
2026-10-21 23:15:35 -07:00
entity : evidence.observed.object ,
call : evidence.call ,
2026-10-24 14:31:46 -07:00
idField : context.idField ( type ),
2026-10-21 23:15:35 -07:00
startMs : startOf ( context , step ),
collection : members ,
2026-10-24 05:52:03 -07:00
type : held ,
named : ( id ) => context . state . named ( id ),
2026-10-27 08:15:52 +08:00
};
return { explained : explainCreate ( input ), members , held , input };
2026-10-24 14:31:46 -07:00
};
/** Build a create plan from its exemplar's observation. */
const buildCreate = (
context : EffectContext ,
draft : WriteDraft ,
step : WriteStep ,
evidence : CreateCallEvidence ,
) : boolean => {
const type = draft . plan . target ! ;
const idField = context . idField ( type );
2026-10-27 08:15:52 +08:00
const { explained , members , held , input } = explainedCall (
2026-10-24 14:31:46 -07:00
context ,
draft ,
step ,
evidence ,
);
if ( linkedUnexplained ( explained . rules , evidence . observed . object , held )) {
2026-10-24 05:52:03 -07:00
refuse ( draft , step , "unexplained-change" );
return false ;
}
2026-10-21 23:15:35 -07:00
const clocks = explained . rules . flatMap (( rule ) =>
rule . rule === "clock" ? [ rule . field ] : [],
);
// Identity and clock orders first, then any other scalar field the collection may be sorted by.
const scalars = Object . keys ( members [ 0 ] ?? {}). filter (( field ) =>
members . every (( member ) =>
2026-10-26 18:27:22 +08:00
[ "string" , "number" ]. includes ( typeof ownMember ( member , field )),
2026-10-21 23:15:35 -07:00
),
);
2026-10-24 05:52:03 -07:00
const generated = explained . rules . some (
( rule ) =>
rule . rule === "identity" &&
rule . field === idField &&
rule . generator !== "sequence" ,
);
draft . candidates = insertCandidates (
members ,
[... new Set ([ idField , ... clocks , ... scalars ])],
{
...( generated && randomIdentities ( members , idField , clocks )
? { random : idField }
: {}),
created : evidence.observed.object ,
},
);
2026-10-21 23:15:35 -07:00
retainFixed ( context , explained . body . fixed );
2026-10-24 14:31:46 -07:00
for ( const member of keptOrders ( evidence . call . body , evidence . observed . object ))
draft . kept . add ( member );
2026-10-21 23:15:35 -07:00
Object . assign ( draft . plan , {
exemplar_ordinal : step.record.ordinal ,
observation : {
ordinal : evidence.observed.ordinal ,
pointer : evidence.observed.pointer ,
2026-10-25 00:06:27 -07:00
...( evidence . observed . read === undefined
? {}
: { read : evidence.observed.read }),
2026-10-21 23:15:35 -07:00
},
response : context.io.withBody ( step . record . ordinal , ( body ) =>
responseShapeOf (
step . record . ordinal ,
body ,
context . io . emptyResponse ( step . record . ordinal ),
{ idField , id : evidence.observed.id },
),
),
2026-10-24 05:52:03 -07:00
body : {
... explained . body ,
2026-10-24 08:57:30 -07:00
...( evidence . envelope === undefined
? {}
: { envelope : evidence.envelope }),
2026-10-24 05:52:03 -07:00
},
2026-10-21 23:15:35 -07:00
rules : explained.rules ,
2026-10-24 00:58:32 -07:00
collection :
explained.tied || draft . linked !== undefined ? evidence.collection : null ,
2026-10-21 23:15:35 -07:00
insert : draft.candidates [ 0 ] ! ,
});
draft . observed = evidence . observed . object ;
2026-10-27 08:15:52 +08:00
draft . exemplarInput = retainCreateExemplar ( context , input , explained . rules );
noteHeldAmpersands ( draft . ampersands , explained . rules , held );
noteAmpersands ( draft . ampersands , explained . rules , evidence . observed . object );
2026-10-24 14:06:21 -07:00
for ( const entity of held ) noteFixedWidths ( draft . fixedWidth , entity );
2026-10-27 16:51:03 -07:00
const ruled = new Set ( explained . rules . map (( rule ) => rule . field ));
for ( const [ field , value ] of Object . entries ( evidence . observed . object )) {
const integer = sequenceNumber ( value );
if ( ruled . has ( field ) || integer === undefined ) continue ;
const next = nextInteger ( held , field , value );
if ( next === undefined || integer >= next ) draft . starts . set ( field , integer );
}
2026-10-24 05:52:03 -07:00
return true ;
2026-10-21 23:15:35 -07:00
};
type SequenceRule = Extract < FieldRule , { rule : "sequence" }>;
/** The sequences a create's plan generates, each with the key serving counts it under. */
const sequencesOf = (
draft : WriteDraft ,
collection : string ,
) : { readonly rule : SequenceRule ; readonly key : string }[] =>
draft . plan . rules . flatMap (( rule ) =>
rule . rule === "sequence"
? [
{
rule ,
key :
rule.scope === "collection"
? ` ${ rule . field } ${ collection } `
: rule . field ,
},
]
: [],
);
2026-10-29 01:14:05 -07:00
/** Whether a later call's sequences hold what serving generates: one past the largest value their scope holds or started with and past the plan's last, spelled as the exemplar spells it; or, for one that takes a sent value, the value the call sent. */
2026-10-21 23:15:35 -07:00
const followsSequences = (
context : EffectContext ,
draft : WriteDraft ,
2026-10-29 01:14:05 -07:00
created : Pick < StateEntity , "type" | "collection" | "fields" > & {
readonly sent : JsonObject ;
readonly observed : JsonObject ;
},
2026-10-21 23:15:35 -07:00
) : boolean =>
sequencesOf ( draft , created . collection ). every (({ rule , key }) => {
2026-10-29 01:14:05 -07:00
if ( rule . sent === true && Object . hasOwn ( created . sent , rule . field ))
return (
! Object . hasOwn ( created . observed , rule . field ) ||
same ( ownMember ( created . observed , rule . field ), created . sent [ rule . field ])
);
2026-10-21 23:15:35 -07:00
const scope = [
... context . state . ofType ( created . type ),
... context . startedWith ( created . type ),
]. filter (
( entity ) =>
rule . scope === "type" || entity . collection === created . collection ,
);
const next = nextSequence (
2026-10-27 16:51:03 -07:00
startedSequence (
nextInteger (
fieldsOf ( scope ),
rule . field ,
ownMember ( draft . observed , rule . field ),
),
rule . start ,
2026-10-26 18:27:22 +08:00
),
2026-10-21 23:15:35 -07:00
draft . sequences . get ( key ),
);
2026-10-22 08:46:52 -07:00
return (
2026-10-26 18:27:22 +08:00
ownMember ( created . fields , rule . field ) ===
spelledLike ( ownMember ( draft . observed , rule . field ) ?? "" , next )
2026-10-22 08:46:52 -07:00
);
2026-10-21 23:15:35 -07:00
});
2026-10-24 14:31:46 -07:00
/** Whether a later call's rules reproduce its observed fields and its omitted members agree with the learned defaults. */
const agreesLater = (
context : EffectContext ,
draft : WriteDraft ,
created : Pick < StateEntity , "type" | "collection" | "fields" >,
2026-10-27 16:51:03 -07:00
call : {
readonly body : JsonObject ;
readonly observed : JsonObject ;
readonly startMs : number | undefined ;
},
2026-10-24 14:31:46 -07:00
) : boolean =>
2026-10-27 16:51:03 -07:00
followsCreateRules ( context , draft , {
... created ,
observed : call.observed ,
startMs : call.startMs ,
2026-10-29 01:14:05 -07:00
sent : call.body ,
2026-10-27 16:51:03 -07:00
}) &&
2026-10-27 14:05:21 -07:00
! contradictsDefault ( draft . plan . rules , call , created . fields ) &&
! unruledDiffers ( draft . plan . rules , call . observed , {
written : created.fields ,
volatile : context.volatileOf ( created . type ),
});
/** Whether the call stored a field no rule writes and no read leaves unchecked otherwise than the plan keeps it, as the exemplar stored it. */
const unruledDiffers = (
rules : readonly FieldRule [],
observed : JsonObject ,
plan : {
readonly written : JsonObject ;
readonly volatile : ReadonlySet < string >;
},
) : boolean => {
const { written , volatile } = plan ;
const ruled = new Set ( rules . map (( rule ) => rule . field ));
return Object . keys ( observed ). some (
( field ) =>
! ruled . has ( field ) &&
! volatile . has ( field ) &&
Object . hasOwn ( written , field ) &&
! same ( observed [ field ], ownMember ( written , field )),
);
};
2026-10-24 14:31:46 -07:00
/** Whether an echoed member the call left out was stored otherwise than its learned default. */
const contradictsDefault = (
rules : readonly FieldRule [],
call : { readonly body : JsonObject ; readonly observed : JsonObject },
written : JsonObject ,
) : boolean =>
rules . some (
( rule ) =>
rule . rule === "echo" &&
! Object . hasOwn ( call . body , rule . field ) &&
Object . hasOwn ( call . observed , rule . field ) &&
2026-10-26 18:27:22 +08:00
! same ( call . observed [ rule . field ], ownMember ( written , rule . field )),
2026-10-24 14:31:46 -07:00
);
2026-10-24 14:06:21 -07:00
/** Whether a later create preserves generation shapes and its derived fields reproduce the captured observation. */
const followsCreateRules = (
2026-10-21 23:15:35 -07:00
context : EffectContext ,
draft : WriteDraft ,
2026-10-24 14:06:21 -07:00
created : Pick < StateEntity , "type" | "collection" | "fields" > & {
readonly observed : JsonObject ;
2026-10-27 16:51:03 -07:00
readonly startMs : number | undefined ;
2026-10-29 01:14:05 -07:00
readonly sent : JsonObject ;
2026-10-24 14:06:21 -07:00
},
2026-10-21 23:15:35 -07:00
) : boolean =>
draft . plan . rules . every (( rule ) => {
2026-10-26 18:27:22 +08:00
const value = ownMember ( created . fields , rule . field );
const exemplar = ownMember ( draft . observed , rule . field );
2026-10-27 08:15:52 +08:00
// Serving always rebuilds a composed or templated field, so a response that leaves it out contradicts the plan.
if ( rule . rule === "composed" || rule . rule === "template" )
return value === ownMember ( created . observed , rule . field );
if ( rule . rule === "reference" )
2026-10-24 14:06:21 -07:00
return (
! Object . hasOwn ( created . observed , rule . field ) ||
2026-10-27 08:15:52 +08:00
same ( value , ownMember ( created . observed , rule . field ))
2026-10-24 14:06:21 -07:00
);
2026-10-21 23:15:35 -07:00
if ( rule . rule === "clock" )
return (
2026-10-24 14:06:21 -07:00
fittedClockFormat (
rule . format ,
value ,
clockEvidence ( draft , rule . field ),
) !== undefined
2026-10-21 23:15:35 -07:00
);
if ( rule . rule === "unique" )
return (
2026-10-24 14:06:21 -07:00
( isUuid ( exemplar )
? isUuid ( value )
: generatedIntegerDigits ( exemplar ) !== undefined
? likeGeneratedInteger ( value , exemplar )
: isOpaqueToken ( value , exemplar )) &&
2026-10-21 23:15:35 -07:00
! context . state
. ofType ( created . type )
2026-10-26 18:27:22 +08:00
. some (( held ) => ownMember ( held . fields , rule . field ) === value )
2026-10-21 23:15:35 -07:00
);
return (
rule . rule !== "identity" || rule . generator !== "uuid" || isUuid ( value )
);
2026-10-27 16:51:03 -07:00
}) &&
followsSequences ( context , draft , created ) &&
followsMadeObjects ( context , draft , created );
/** The key a made object's count is taken under in a plan's sequences. */
const madeKey = ( field : string , member : string ) : string => ` ${ field } / ${ member } ` ;
/** Whether a later call's recorded made objects hold what serving makes: the exemplar's members, the entity's identity, clocks, and each count one past what held objects show or started with, past the plan's last. */
const followsMadeObjects = (
context : EffectContext ,
draft : WriteDraft ,
created : Pick < StateEntity , "type" | "collection" | "fields" > & {
readonly observed : JsonObject ;
readonly startMs : number | undefined ;
},
) : boolean =>
draft . plan . rules . every (( rule ) => {
if ( rule . rule !== "made-object" ) return true ;
const made = ownMember ( created . observed , rule . field );
const exemplar = ownMember ( draft . observed , rule . field );
if ( ! isObject ( made ) || ! isObject ( exemplar )) return false ;
const id = scalarText (
ownMember ( created . fields , context . idField ( created . type )),
);
const held = heldObjects (
[
... context . state . ofType ( created . type ),
... context . startedWith ( created . type ),
]. map (( entity ) => entity . fields ),
rule . field ,
);
return [... new Set ([... Object . keys ( exemplar ), ... Object . keys ( made )])]. every (
( member ) => {
const value = ownMember ( made , member );
if ( rule . clocks . includes ( member ))
return stampedBy ( value , rule . format , created . startMs );
if ( rule . own . includes ( member )) return scalarText ( value ) === id ;
const count = rule . counts . find (( entry ) => entry . member === member );
if ( count === undefined )
return same ( value , ownMember ( exemplar , member ));
return (
sequenceNumber ( value ) ===
nextSequence (
nextMadeCount ( held , count , ownMember ( exemplar , member )),
draft . sequences . get ( madeKey ( rule . field , member )),
)
);
},
);
});
2026-10-21 23:15:35 -07:00
2026-10-24 14:06:21 -07:00
/** Widen the plan's clock formats to the trimmed spelling a later call showed, and its other clocks with them unless a value the plan saw shows them at a fixed width. */
const fitClocks = ( draft : WriteDraft , fields : JsonObject ) : void => {
const fitted = draft . plan . rules . map (( rule ) => {
if ( rule . rule !== "clock" ) return rule ;
const format = fittedClockFormat (
rule . format ,
2026-10-26 18:27:22 +08:00
ownMember ( fields , rule . field ),
2026-10-24 14:06:21 -07:00
clockEvidence ( draft , rule . field ),
);
return format === undefined || format === rule . format
? rule
: { ... rule , format };
});
draft . plan . rules = siblingTrimmedClocks (
fitted ,
draft . observed ,
fields ,
Object . fromEntries ( draft . fixedWidth ),
);
noteFixedWidths ( draft . fixedWidth , fields );
};
2026-10-29 01:14:05 -07:00
/** Record the values a create's sequences took, which the next call follows; as serving does, the largest stays, so a lower sent value does not lower it. */
2026-10-21 23:15:35 -07:00
const takeSequences = (
draft : WriteDraft ,
created : Pick < StateEntity , "collection" | "fields" >,
) : void => {
2026-10-29 01:14:05 -07:00
const take = ( key : string , value : number | undefined ) => {
if ( value === undefined ) return ;
const taken = draft . sequences . get ( key );
draft . sequences . set (
key ,
taken === undefined ? value : Math.max ( taken , value ),
);
};
for ( const { rule , key } of sequencesOf ( draft , created . collection ))
take (
key ,
sequenceNumber (
ownMember ( created . fields , rule . field ),
ownMember ( draft . observed , rule . field ),
),
2026-10-22 08:46:52 -07:00
);
2026-10-27 16:51:03 -07:00
for ( const rule of draft . plan . rules ) {
if ( rule . rule !== "made-object" ) continue ;
const made = ownMember ( created . fields , rule . field );
for ( const { member } of rule . counts ) {
2026-10-29 01:14:05 -07:00
take (
madeKey ( rule . field , member ),
isObject ( made ) ? sequenceNumber ( ownMember ( made , member )) : undefined ,
);
}
}
for ( const rule of draft . plan . rules ) {
if ( rule . rule !== "echo" ) continue ;
const value = sequenceNumber ( ownMember ( created . fields , rule . field ));
// By type and by collection, as a sequence adopted later may count either.
for ( const key of [ rule . field , ` ${ rule . field } ${ created . collection } ` ]) {
const held = draft . sentLargest . get ( key );
if ( value !== undefined && ( held === undefined || value > held ))
draft . sentLargest . set ( key , value );
2026-10-27 16:51:03 -07:00
}
}
2026-10-21 23:15:35 -07:00
};
2026-10-22 06:52:01 -07:00
/** Segments of a recorded path as a create call supplies them: template literals, and each parameter's recorded spelling. */
const recordedSegments = (
template : string ,
path : string ,
) : SuppliedSegment [] | undefined => {
const pattern = segmentsOf ( template );
const concrete = segmentsOf ( path );
if ( pattern . length !== concrete . length ) return undefined ;
const segments : SuppliedSegment [] = [];
for ( const [ index , part ] of pattern . entries ()) {
const value = concrete [ index ] ! ;
if ( ! isParam ( part )) {
if ( part !== value ) return undefined ;
segments . push ({ text : part , supplied : false });
continue ;
}
if ( value . length === 0 ) return undefined ;
segments . push ({ text : value , supplied : true });
}
return segments ;
};
/** Collections a create may join, in serving's order: held ones, then emptied ones as first known, then checked list lines. */
const createCandidates = ( context : EffectContext , type : string ) : string [] => {
const candidates : string [] = [];
const seen = new Set < string >();
const add = ( collection : string ) : void => {
if ( seen . has ( collection )) return ;
seen . add ( collection );
candidates . push ( collection );
};
for ( const entity of context . state . ofType ( type )) add ( entity . collection );
for ( const collection of context . state . knownCollections ( type ))
add ( collection );
for ( const record of context . records ) {
const view = context . viewOf ( record . ordinal );
if (
view ? . shape === "list" &&
view . type === type &&
view . checked_record_ordinals . includes ( record . ordinal )
)
add ( collectionPathOf ( record , false ));
}
return candidates ;
};
/** The collection serving would write this recorded create into, when the path instantiates the tool. */
const servedCollection = (
context : EffectContext ,
step : WriteStep ,
type : string ,
) : string | undefined => {
const segments = recordedSegments ( step . tool . path_template , step . record . path );
return segments === undefined
? undefined
: selectedCollection ( segments , createCandidates ( context , type ));
};
2026-10-24 00:58:32 -07:00
/** Whether a linked create's call sends the source path values its exemplar did, the only ones its link holds for. */
const fromLinkedSource = (
context : EffectContext ,
draft : WriteDraft ,
step : WriteStep ,
) : boolean =>
2026-10-27 16:51:03 -07:00
anyLinkedSource ( draft . plan , step . tool . path_template ) ||
2026-10-24 00:58:32 -07:00
sameLinkedSource (
step . tool . path_template ,
context . records . find (
( record ) => record . ordinal === draft . plan . exemplar_ordinal ,
) ? . path ,
recordedPathValues ( step . tool . path_template , step . record . path ),
);
2026-10-21 23:15:35 -07:00
/** Add a created entity to the state at the plan's insert position, or last before the plan has one. */
const insertCreated = (
context : EffectContext ,
draft : WriteDraft ,
entity : StateEntity ,
) : string => {
const members = fieldsOf (
context . state . members ( entity . type , entity . collection ),
);
2026-10-23 20:54:13 +08:00
context . io . retain ( 64 + jsonTextCodeUnits ( entity . fields ) * 2 );
2026-10-21 23:15:35 -07:00
context . state . put (
entity ,
draft . plan . insert === null
? members.length
: insertIndex ( draft . plan . insert , members , entity . fields ),
);
return entityTarget ( entity . type , entity . collection , entity . id );
};
/** Apply a recorded create, with the values it recorded for generated fields; an unexplained create adds what it recorded. */
export const applyCreate = (
context : EffectContext ,
draft : WriteDraft ,
step : WriteStep ,
) : EffectResult => {
const type = draft . plan . target ! ;
const idField = context . idField ( type );
2026-10-22 06:52:01 -07:00
const spelled = normalizedPath ( step . record . path );
// Serving writes a create into the collection it chooses for the path, and a tied plan refuses any other, so a create there is not counted.
2026-10-24 00:58:32 -07:00
const collection =
draft . linked ?? servedCollection ( context , step , type ) ?? spelled ;
// A linked plan's collection holds only for its exemplar's source path, so serving refuses any other.
2026-10-22 06:52:01 -07:00
if (
built ( draft ) &&
2026-10-24 00:58:32 -07:00
(( draft . plan . collection !== null && draft . plan . collection !== collection ) ||
( draft . linked !== undefined && ! fromLinkedSource ( context , draft , step )))
2026-10-22 06:52:01 -07:00
)
return UNKNOWN ;
2026-10-21 23:15:35 -07:00
const observed = createdObservation ( context , step , {
type ,
collection ,
idField ,
2026-10-24 00:58:32 -07:00
linked : draft.linked !== undefined ,
2026-10-21 23:15:35 -07:00
});
2026-10-24 08:57:30 -07:00
const request = requestBodyPlan ( context , step , draft , observed ? . object );
const body = request ? . object ;
2026-10-21 23:15:35 -07:00
if ( observed === undefined ) {
fail ( draft , step , body === undefined ? "body-unusable" : "no-observation" );
return UNKNOWN ;
}
const entity = ( fields : JsonObject , id : string ) : StateEntity => ({
type ,
collection ,
id ,
fields ,
view_fields : [],
record_ordinals : [],
});
const recorded = () : EffectResult => {
const inserted = insertCreated (
context ,
draft ,
entity ( observed . object , observed . id ),
);
return {
kind : "observed" ,
targets : [ inserted , collectionTarget ( type , collection )],
entity : { type , collection , id : observed.id },
};
};
if ( step . observeOnly || body === undefined ) {
fail ( draft , step , "body-unusable" );
return recorded ();
}
const call = { ... callOf ( step , body ), body };
// The exemplar defines the plan's sequences; each later call must follow them.
const later = built ( draft );
2026-10-24 05:52:03 -07:00
if (
later &&
2026-10-24 14:06:21 -07:00
( draft . plan . rules . some (
2026-10-24 05:52:03 -07:00
( rule ) =>
rule . rule === "unique" &&
2026-10-24 14:06:21 -07:00
linkedGenerated (
2026-10-24 05:52:03 -07:00
rule . field ,
observed . object ,
fieldsOf ( context . state . ofType ( type )),
2026-10-24 14:06:21 -07:00
),
) ||
strandedInteger ( draft . plan . rules , observed . object ))
2026-10-24 05:52:03 -07:00
) {
refuse ( draft , step , "unexplained-change" );
return recorded ();
}
if (
! later &&
2026-10-24 08:57:30 -07:00
! buildCreate ( context , draft , step , {
observed ,
call ,
collection ,
envelope : request?.envelope ,
})
2026-10-24 05:52:03 -07:00
)
return recorded ();
2026-10-21 23:15:35 -07:00
// Rules learn from the recorded path; serving fills path fields from the collection it selects.
const created = {
... call ,
pathValues :
pathValuesOf ( step . tool . path_template , collection ) ?? call . pathValues ,
};
2026-10-24 14:31:46 -07:00
const fieldsOfCall = () =>
createdFields (
draft . plan ,
draft . observed ! ,
created ,
( rule ) =>
recordedGeneratedValue (
rule ,
2026-10-26 18:27:22 +08:00
ownMember ( draft . observed ! , rule . field ),
ownMember ( observed . object , rule . field ) ?? null ,
2026-10-24 14:31:46 -07:00
),
referencedIn ( context ),
2026-10-27 16:51:03 -07:00
idField ,
2026-10-24 14:31:46 -07:00
);
// A later call that agrees with the plan widens it, as a later update does: to members it alone sent or left out, an
// array it showed sorted, or a clock spelling. One that disagrees leaves the plan as it was.
if ( later )
refineCreate ( context , draft , step , {
observed ,
call ,
collection ,
envelope : request?.envelope ,
});
const fields = fieldsOfCall ();
2026-10-26 18:27:22 +08:00
const id = fields . ok
? scalarText ( ownMember ( fields . value , idField ))
: undefined ;
2026-10-21 23:15:35 -07:00
if (
! fields . ok ||
id === undefined ||
context . state . entity ( type , collection , id ) !== undefined ||
( later &&
2026-10-24 14:31:46 -07:00
! agreesLater (
context ,
draft ,
{ type , collection , fields : fields.value },
2026-10-27 16:51:03 -07:00
{
body : call.body ,
observed : observed.object ,
startMs : startOf ( context , step ),
},
2026-10-24 14:31:46 -07:00
))
2026-10-21 23:15:35 -07:00
) {
refuse ( draft , step , "unexplained-change" );
return recorded ();
}
takeSequences ( draft , { collection , fields : fields.value });
2026-10-24 14:06:21 -07:00
fitClocks ( draft , fields . value );
2026-10-24 05:52:03 -07:00
// The call's own values follow the forms serving generates; the state holds a nested clock as the read-back restamped it, as an update's.
const stored = restampedFields (
searchFrom ( context , step ),
itemReads ( context , { type , idField , collection , id }),
fields . value ,
draft . plan . rules ,
);
const inserted = insertCreated ( context , draft , entity ( stored , id ));
2026-10-27 16:51:03 -07:00
context . io . retain ( collection . length * 2 + 16 );
draft . createCollections . push ( collection );
2026-10-21 23:15:35 -07:00
return {
kind : "applied" ,
targets : [ inserted , collectionTarget ( type , collection )],
entity : { type , collection , id },
inserted ,
};
};