@milaboratories/pl-tree 1.14.4 → 1.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/delta_sync.cjs +114 -0
- package/dist/delta_sync.cjs.map +1 -0
- package/dist/delta_sync.js +114 -0
- package/dist/delta_sync.js.map +1 -0
- package/dist/index.cjs +3 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/state.cjs +55 -9
- package/dist/state.cjs.map +1 -1
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +55 -9
- package/dist/state.js.map +1 -1
- package/dist/sync.cjs +47 -13
- package/dist/sync.cjs.map +1 -1
- package/dist/sync.d.ts +42 -7
- package/dist/sync.d.ts.map +1 -1
- package/dist/sync.js +45 -14
- package/dist/sync.js.map +1 -1
- package/dist/synchronized_tree.cjs +46 -3
- package/dist/synchronized_tree.cjs.map +1 -1
- package/dist/synchronized_tree.d.ts +28 -1
- package/dist/synchronized_tree.d.ts.map +1 -1
- package/dist/synchronized_tree.js +47 -4
- package/dist/synchronized_tree.js.map +1 -1
- package/package.json +4 -4
- package/src/algorithm_equivalence.test.ts +197 -0
- package/src/delta_benchmark.test.ts +260 -0
- package/src/delta_sync.test.ts +490 -0
- package/src/delta_sync.ts +221 -0
- package/src/delta_sync_live.test.ts +259 -0
- package/src/state.test.ts +157 -0
- package/src/state.ts +98 -16
- package/src/sync.test.ts +105 -1
- package/src/sync.ts +110 -29
- package/src/synchronized_tree.ts +88 -6
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const require_state = require("./state.cjs");
|
|
2
|
+
const require_sync = require("./sync.cjs");
|
|
3
|
+
let _milaboratories_pl_client = require("@milaboratories/pl-client");
|
|
4
|
+
//#region src/delta_sync.ts
|
|
5
|
+
/** Emit everything at or below this depth from a resolution seed, whatever its change token
|
|
6
|
+
* says. It bounds descent as well as emission: under a token the walk otherwise ends at the
|
|
7
|
+
* first unchanged resource (api.proto, changed_since_token), so without this a round returns
|
|
8
|
+
* the seeds alone and a newly attached subtree costs one sequential round trip per level.
|
|
9
|
+
*
|
|
10
|
+
* Deep enough to clear a subtree in one round, which is the trade: real server-side walk and
|
|
11
|
+
* downlink for those levels, against a round trip each at ~1.4s on a slow link. Not free -
|
|
12
|
+
* lower it if resolution rounds ever dominate a poll. */
|
|
13
|
+
const RESOLUTION_DEPTH = 32;
|
|
14
|
+
/** Every id a body points at. Exactly what `updateFromResourceData` refcounts, and it throws
|
|
15
|
+
* `orphan resource` for any that resolves to nothing. */
|
|
16
|
+
function referencesOf(resource) {
|
|
17
|
+
const refs = [];
|
|
18
|
+
if ((0, _milaboratories_pl_client.isNotNullSignedResourceId)(resource.error)) refs.push(resource.error);
|
|
19
|
+
for (const field of resource.fields) {
|
|
20
|
+
if ((0, _milaboratories_pl_client.isNotNullSignedResourceId)(field.value)) refs.push(field.value);
|
|
21
|
+
if ((0, _milaboratories_pl_client.isNotNullSignedResourceId)(field.error)) refs.push(field.error);
|
|
22
|
+
}
|
|
23
|
+
return refs;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One delta poll: hand the backend this transaction's change token, take only what is newer.
|
|
27
|
+
*
|
|
28
|
+
* The caller commits the token, and only after the whole batch applies - advancing it past a
|
|
29
|
+
* partial apply loses the dropped resources for good.
|
|
30
|
+
*
|
|
31
|
+
* Removals arrive as a rewrite of the referring resource, never as absence, and the refcount
|
|
32
|
+
* GC does the rest. There is deliberately no periodic full reconcile; the blind spot is a
|
|
33
|
+
* removal under a resource the mirror marked final, which streaming and BFS share.
|
|
34
|
+
*/
|
|
35
|
+
async function loadDeltaTreeState(tx, loadingRequest, stats, logger) {
|
|
36
|
+
const { seedResources, finalResources, roots, knownResources, pruningFunction, fieldFilter, traverseStopRules, changedSinceToken } = loadingRequest;
|
|
37
|
+
if (traverseStopRules !== void 0 && changedSinceToken === void 0) logger?.warn("delta poll: traverseStopRules supplied but not sent, and this poll carries no token, so the backend walk is unpruned where the streaming path would have pruned it");
|
|
38
|
+
const seeds = seedResources.length > 0 ? seedResources : [...roots];
|
|
39
|
+
if (seeds.length === 0) return [];
|
|
40
|
+
const collected = /* @__PURE__ */ new Map();
|
|
41
|
+
const missing = /* @__PURE__ */ new Set();
|
|
42
|
+
const pending = [];
|
|
43
|
+
const collect = (resource) => {
|
|
44
|
+
collected.set(resource.id, resource);
|
|
45
|
+
missing.delete(resource.id);
|
|
46
|
+
for (const ref of referencesOf(resource)) if (!collected.has(ref) && !knownResources.has(ref) && !missing.has(ref)) {
|
|
47
|
+
missing.add(ref);
|
|
48
|
+
pending.push(ref);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
let warnedStopMarker = false;
|
|
52
|
+
const consume = async (walkSeeds, unconditionalDepth) => {
|
|
53
|
+
if (stats) {
|
|
54
|
+
stats.roundTrips++;
|
|
55
|
+
stats.streamRounds++;
|
|
56
|
+
stats.deltaSeedsSent += walkSeeds.length;
|
|
57
|
+
}
|
|
58
|
+
for await (const frame of tx.resourceTree(walkSeeds, {
|
|
59
|
+
includeKv: true,
|
|
60
|
+
fieldFilter,
|
|
61
|
+
changedSinceToken,
|
|
62
|
+
unconditionalDepth
|
|
63
|
+
})) {
|
|
64
|
+
if (frame.frameKind === "stopMarker") {
|
|
65
|
+
if (!warnedStopMarker) {
|
|
66
|
+
warnedStopMarker = true;
|
|
67
|
+
logger?.warn(`delta poll: ignoring a body-less frame for ${frame.id}; this poll sent no traverse stop rules, so the backend is using body-less frames for something this client does not interpret`);
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (stats) stats.resourceFrames++;
|
|
72
|
+
if (finalResources.has(frame.id)) {
|
|
73
|
+
if (stats) stats.finalResourcesSkipped++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const resource = {
|
|
77
|
+
id: frame.id,
|
|
78
|
+
type: frame.type,
|
|
79
|
+
kind: frame.kind,
|
|
80
|
+
data: frame.data,
|
|
81
|
+
resourceReady: frame.resourceReady,
|
|
82
|
+
error: frame.error,
|
|
83
|
+
originalResourceId: frame.originalResourceId,
|
|
84
|
+
final: frame.final,
|
|
85
|
+
inputsLocked: frame.inputsLocked,
|
|
86
|
+
outputsLocked: frame.outputsLocked,
|
|
87
|
+
fields: pruningFunction !== void 0 ? pruningFunction(frame) : frame.fields,
|
|
88
|
+
kv: frame.kv
|
|
89
|
+
};
|
|
90
|
+
if (stats) stats.prunedFields += frame.fields.length - resource.fields.length;
|
|
91
|
+
collect(resource);
|
|
92
|
+
require_sync.collectStatsForResource(resource, stats);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
await consume(seeds);
|
|
96
|
+
const collectedFromPoll = collected.size;
|
|
97
|
+
const fetched = /* @__PURE__ */ new Set();
|
|
98
|
+
while (missing.size > 0) {
|
|
99
|
+
const round = pending.splice(0).filter((id) => !fetched.has(id) && missing.has(id));
|
|
100
|
+
if (round.length === 0) throw new require_state.TreeStateUpdateError(`delta poll: ${missing.size} referenced resource(s) could not be resolved, first ${missing.values().next().value}`);
|
|
101
|
+
for (const id of round) fetched.add(id);
|
|
102
|
+
if (stats) stats.deltaResolutionRounds++;
|
|
103
|
+
await consume(round, RESOLUTION_DEPTH);
|
|
104
|
+
}
|
|
105
|
+
if (changedSinceToken !== void 0 && knownResources.size > 0 && collectedFromPoll >= knownResources.size) {
|
|
106
|
+
if (stats) stats.deltaSuspectedFullAnswers++;
|
|
107
|
+
logger?.warn(`delta poll: sent a token but received ${collectedFromPoll} resources against a mirror of ${knownResources.size}; the backend may have refused the token (instance change or rewound numbering), in which case this poll cost a full tree read`);
|
|
108
|
+
}
|
|
109
|
+
return [...collected.values()];
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
exports.loadDeltaTreeState = loadDeltaTreeState;
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=delta_sync.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delta_sync.cjs","names":["TreeStateUpdateError"],"sources":["../src/delta_sync.ts"],"sourcesContent":["import type { PlTransaction, SignedResourceId } from \"@milaboratories/pl-client\";\nimport { isNotNullSignedResourceId } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData } from \"./state\";\nimport { TreeStateUpdateError } from \"./state\";\nimport type { TreeLoadingRequest, TreeLoadingStat } from \"./sync\";\nimport { collectStatsForResource } from \"./sync\";\n\n/** Emit everything at or below this depth from a resolution seed, whatever its change token\n * says. It bounds descent as well as emission: under a token the walk otherwise ends at the\n * first unchanged resource (api.proto, changed_since_token), so without this a round returns\n * the seeds alone and a newly attached subtree costs one sequential round trip per level.\n *\n * Deep enough to clear a subtree in one round, which is the trade: real server-side walk and\n * downlink for those levels, against a round trip each at ~1.4s on a slow link. Not free -\n * lower it if resolution rounds ever dominate a poll. */\nconst RESOLUTION_DEPTH = 32;\n\n/** Every id a body points at. Exactly what `updateFromResourceData` refcounts, and it throws\n * `orphan resource` for any that resolves to nothing. */\nfunction referencesOf(resource: ExtendedResourceData): SignedResourceId[] {\n const refs: SignedResourceId[] = [];\n if (isNotNullSignedResourceId(resource.error)) refs.push(resource.error);\n for (const field of resource.fields) {\n if (isNotNullSignedResourceId(field.value)) refs.push(field.value);\n if (isNotNullSignedResourceId(field.error)) refs.push(field.error);\n }\n return refs;\n}\n\n/**\n * One delta poll: hand the backend this transaction's change token, take only what is newer.\n *\n * The caller commits the token, and only after the whole batch applies - advancing it past a\n * partial apply loses the dropped resources for good.\n *\n * Removals arrive as a rewrite of the referring resource, never as absence, and the refcount\n * GC does the rest. There is deliberately no periodic full reconcile; the blind spot is a\n * removal under a resource the mirror marked final, which streaming and BFS share.\n */\nexport async function loadDeltaTreeState(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n logger?: { warn: (msg: string) => void },\n): Promise<ExtendedResourceData[]> {\n const {\n seedResources,\n finalResources,\n roots,\n knownResources,\n pruningFunction,\n fieldFilter,\n traverseStopRules,\n changedSinceToken,\n } = loadingRequest;\n\n // Stop rules are never sent. The backend ignores them under a usable token anyway, but a\n // token-less poll then walks unpruned where streaming would have pruned. Sending them only\n // when the token is absent would change the request shape between polls, which a token\n // holder must not do - so the cost is warned about instead.\n if (traverseStopRules !== undefined && changedSinceToken === undefined) {\n logger?.warn(\n \"delta poll: traverseStopRules supplied but not sent, and this poll carries no token,\" +\n \" so the backend walk is unpruned where the streaming path would have pruned it\",\n );\n }\n\n // Seed at every non-final resource, not the roots. Required, not an optimisation: the\n // backend ends a walk at the first unchanged resource, so a root-seeded poll never reaches a\n // change under a quiet parent - measured at 0 of 3 changes delivered. algorithm_equivalence\n // fails on its KV step if this is ever narrowed back to the roots.\n //\n // The seed set changes every poll and the token is deliberately NOT discarded for it, though\n // api.proto counts it as a shape input. Safe because every seed is a resource the mirror\n // already holds, so a change can only widen the result, and widening hides nothing.\n // Discarding per poll would make every poll a full read and defeat the mechanism.\n //\n // The roots stand in when the frontier is empty: resourceTree needs at least one seed.\n const seeds = seedResources.length > 0 ? seedResources : [...roots];\n if (seeds.length === 0) return [];\n\n const collected = new Map<SignedResourceId, ExtendedResourceData>();\n // References neither the mirror nor this batch can satisfy; applying with one outstanding\n // invalidates the whole tree. Maintained incrementally as frames arrive: rescanning\n // everything collected once per round would be quadratic in a poll that needs several.\n const missing = new Set<SignedResourceId>();\n // Same ids, in arrival order, so a round consumes only what is new rather than walking the\n // whole set. An id enters both exactly once: `collected` is checked before the add and never\n // cleared, so nothing re-enters after it resolves.\n const pending: SignedResourceId[] = [];\n const collect = (resource: ExtendedResourceData) => {\n collected.set(resource.id, resource);\n missing.delete(resource.id);\n for (const ref of referencesOf(resource))\n if (!collected.has(ref) && !knownResources.has(ref) && !missing.has(ref)) {\n missing.add(ref);\n pending.push(ref);\n }\n };\n\n let warnedStopMarker = false;\n const consume = async (walkSeeds: SignedResourceId[], unconditionalDepth?: number) => {\n if (stats) {\n stats.roundTrips++;\n stats.streamRounds++;\n stats.deltaSeedsSent += walkSeeds.length;\n }\n\n for await (const frame of tx.resourceTree(walkSeeds, {\n includeKv: true,\n fieldFilter,\n changedSinceToken,\n unconditionalDepth,\n })) {\n // A body-less frame. The client reads that as a stop marker, and a stop marker can only\n // follow traverse_stop_rules, which this path never sends. Nothing here needs it, so it\n // is skipped - but not silently: reaching this means the backend put a meaning on a\n // body-less frame that this client does not know, and a quiet skip would leave no trace\n // of it. Warn once per poll rather than per frame, since a contract change would emit\n // one of these for every resource visited.\n if (frame.frameKind === \"stopMarker\") {\n if (!warnedStopMarker) {\n warnedStopMarker = true;\n logger?.warn(\n `delta poll: ignoring a body-less frame for ${frame.id}; this poll sent no ` +\n `traverse stop rules, so the backend is using body-less frames for something ` +\n `this client does not interpret`,\n );\n }\n continue;\n }\n\n if (stats) stats.resourceFrames++;\n\n // updateFromResourceData throws on any body for a held final resource and invalidates\n // the tree. A token-less poll walks unpruned and emits exactly those, which is what the\n // first poll after a restored snapshot does. Both sibling algorithms skip the same way.\n // Safe for references: finalResources is a subset of knownResources.\n if (finalResources.has(frame.id)) {\n if (stats) stats.finalResourcesSkipped++;\n continue;\n }\n\n // An explicit literal rather than two rest-spreads of the frame. Measured ~15% off the\n // per-frame cost: a spread copies every property twice and gives the object a hidden\n // class of its own, where this is the shape the streaming path already produces.\n const resource: ExtendedResourceData = {\n id: frame.id,\n type: frame.type,\n kind: frame.kind,\n data: frame.data,\n resourceReady: frame.resourceReady,\n error: frame.error,\n originalResourceId: frame.originalResourceId,\n final: frame.final,\n inputsLocked: frame.inputsLocked,\n outputsLocked: frame.outputsLocked,\n fields:\n pruningFunction !== undefined\n ? pruningFunction(frame as unknown as ExtendedResourceData)\n : frame.fields,\n kv: frame.kv,\n };\n if (stats) stats.prunedFields += frame.fields.length - resource.fields.length;\n\n collect(resource);\n collectStatsForResource(resource, stats);\n }\n };\n\n await consume(seeds);\n\n // Captured before the resolution rounds. They fetch unconditionally by design, so counting\n // afterwards lets a modest unheld subtree inflate the total past a small mirror and report a\n // refused token that was never refused.\n const collectedFromPoll = collected.size;\n\n // A body may reference a resource the response did not carry - a field repointed at one we\n // never held. Ids already asked for are never re-requested, so the loop terminates: the tree\n // is finite and `fetched` only grows.\n //\n // Deliberately unbounded on rounds. A round-count cap would turn a legal tree shape into a\n // desktop error, since how deep the references chain is a property of what the backend\n // allowed rather than of anything wrong here, and the streaming path's stop-marker follow-up\n // loop has always run on the same assumption.\n const fetched = new Set<SignedResourceId>();\n while (missing.size > 0) {\n const round = pending.splice(0).filter((id) => !fetched.has(id) && missing.has(id));\n if (round.length === 0) {\n // Asked for all of these and none arrived; a soft-deleted referent gets here, since\n // those frames are dropped client-side. TreeStateUpdateError specifically: the poll loop\n // rebuilds and discards the token for that class, where a plain Error is only logged and\n // the identical request fails forever.\n throw new TreeStateUpdateError(\n `delta poll: ${missing.size} referenced resource(s) could not be resolved, first ${missing.values().next().value}`,\n );\n }\n for (const id of round) fetched.add(id);\n\n if (stats) stats.deltaResolutionRounds++;\n await consume(round, RESOLUTION_DEPTH);\n }\n\n // A refused token is answered with the full tree and no error, so this is the only tell.\n // Reported, not acted on: the response is a correct superset, and the token stored after\n // this poll comes from the current instance, so it self-heals next poll.\n if (\n changedSinceToken !== undefined &&\n knownResources.size > 0 &&\n collectedFromPoll >= knownResources.size\n ) {\n if (stats) stats.deltaSuspectedFullAnswers++;\n logger?.warn(\n `delta poll: sent a token but received ${collectedFromPoll} resources against a mirror of ` +\n `${knownResources.size}; the backend may have refused the token (instance change or ` +\n `rewound numbering), in which case this poll cost a full tree read`,\n );\n }\n\n return [...collected.values()];\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { TreeStateUpdateError } from "./state.js";
|
|
2
|
+
import { collectStatsForResource } from "./sync.js";
|
|
3
|
+
import { isNotNullSignedResourceId } from "@milaboratories/pl-client";
|
|
4
|
+
//#region src/delta_sync.ts
|
|
5
|
+
/** Emit everything at or below this depth from a resolution seed, whatever its change token
|
|
6
|
+
* says. It bounds descent as well as emission: under a token the walk otherwise ends at the
|
|
7
|
+
* first unchanged resource (api.proto, changed_since_token), so without this a round returns
|
|
8
|
+
* the seeds alone and a newly attached subtree costs one sequential round trip per level.
|
|
9
|
+
*
|
|
10
|
+
* Deep enough to clear a subtree in one round, which is the trade: real server-side walk and
|
|
11
|
+
* downlink for those levels, against a round trip each at ~1.4s on a slow link. Not free -
|
|
12
|
+
* lower it if resolution rounds ever dominate a poll. */
|
|
13
|
+
const RESOLUTION_DEPTH = 32;
|
|
14
|
+
/** Every id a body points at. Exactly what `updateFromResourceData` refcounts, and it throws
|
|
15
|
+
* `orphan resource` for any that resolves to nothing. */
|
|
16
|
+
function referencesOf(resource) {
|
|
17
|
+
const refs = [];
|
|
18
|
+
if (isNotNullSignedResourceId(resource.error)) refs.push(resource.error);
|
|
19
|
+
for (const field of resource.fields) {
|
|
20
|
+
if (isNotNullSignedResourceId(field.value)) refs.push(field.value);
|
|
21
|
+
if (isNotNullSignedResourceId(field.error)) refs.push(field.error);
|
|
22
|
+
}
|
|
23
|
+
return refs;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* One delta poll: hand the backend this transaction's change token, take only what is newer.
|
|
27
|
+
*
|
|
28
|
+
* The caller commits the token, and only after the whole batch applies - advancing it past a
|
|
29
|
+
* partial apply loses the dropped resources for good.
|
|
30
|
+
*
|
|
31
|
+
* Removals arrive as a rewrite of the referring resource, never as absence, and the refcount
|
|
32
|
+
* GC does the rest. There is deliberately no periodic full reconcile; the blind spot is a
|
|
33
|
+
* removal under a resource the mirror marked final, which streaming and BFS share.
|
|
34
|
+
*/
|
|
35
|
+
async function loadDeltaTreeState(tx, loadingRequest, stats, logger) {
|
|
36
|
+
const { seedResources, finalResources, roots, knownResources, pruningFunction, fieldFilter, traverseStopRules, changedSinceToken } = loadingRequest;
|
|
37
|
+
if (traverseStopRules !== void 0 && changedSinceToken === void 0) logger?.warn("delta poll: traverseStopRules supplied but not sent, and this poll carries no token, so the backend walk is unpruned where the streaming path would have pruned it");
|
|
38
|
+
const seeds = seedResources.length > 0 ? seedResources : [...roots];
|
|
39
|
+
if (seeds.length === 0) return [];
|
|
40
|
+
const collected = /* @__PURE__ */ new Map();
|
|
41
|
+
const missing = /* @__PURE__ */ new Set();
|
|
42
|
+
const pending = [];
|
|
43
|
+
const collect = (resource) => {
|
|
44
|
+
collected.set(resource.id, resource);
|
|
45
|
+
missing.delete(resource.id);
|
|
46
|
+
for (const ref of referencesOf(resource)) if (!collected.has(ref) && !knownResources.has(ref) && !missing.has(ref)) {
|
|
47
|
+
missing.add(ref);
|
|
48
|
+
pending.push(ref);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
let warnedStopMarker = false;
|
|
52
|
+
const consume = async (walkSeeds, unconditionalDepth) => {
|
|
53
|
+
if (stats) {
|
|
54
|
+
stats.roundTrips++;
|
|
55
|
+
stats.streamRounds++;
|
|
56
|
+
stats.deltaSeedsSent += walkSeeds.length;
|
|
57
|
+
}
|
|
58
|
+
for await (const frame of tx.resourceTree(walkSeeds, {
|
|
59
|
+
includeKv: true,
|
|
60
|
+
fieldFilter,
|
|
61
|
+
changedSinceToken,
|
|
62
|
+
unconditionalDepth
|
|
63
|
+
})) {
|
|
64
|
+
if (frame.frameKind === "stopMarker") {
|
|
65
|
+
if (!warnedStopMarker) {
|
|
66
|
+
warnedStopMarker = true;
|
|
67
|
+
logger?.warn(`delta poll: ignoring a body-less frame for ${frame.id}; this poll sent no traverse stop rules, so the backend is using body-less frames for something this client does not interpret`);
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (stats) stats.resourceFrames++;
|
|
72
|
+
if (finalResources.has(frame.id)) {
|
|
73
|
+
if (stats) stats.finalResourcesSkipped++;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const resource = {
|
|
77
|
+
id: frame.id,
|
|
78
|
+
type: frame.type,
|
|
79
|
+
kind: frame.kind,
|
|
80
|
+
data: frame.data,
|
|
81
|
+
resourceReady: frame.resourceReady,
|
|
82
|
+
error: frame.error,
|
|
83
|
+
originalResourceId: frame.originalResourceId,
|
|
84
|
+
final: frame.final,
|
|
85
|
+
inputsLocked: frame.inputsLocked,
|
|
86
|
+
outputsLocked: frame.outputsLocked,
|
|
87
|
+
fields: pruningFunction !== void 0 ? pruningFunction(frame) : frame.fields,
|
|
88
|
+
kv: frame.kv
|
|
89
|
+
};
|
|
90
|
+
if (stats) stats.prunedFields += frame.fields.length - resource.fields.length;
|
|
91
|
+
collect(resource);
|
|
92
|
+
collectStatsForResource(resource, stats);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
await consume(seeds);
|
|
96
|
+
const collectedFromPoll = collected.size;
|
|
97
|
+
const fetched = /* @__PURE__ */ new Set();
|
|
98
|
+
while (missing.size > 0) {
|
|
99
|
+
const round = pending.splice(0).filter((id) => !fetched.has(id) && missing.has(id));
|
|
100
|
+
if (round.length === 0) throw new TreeStateUpdateError(`delta poll: ${missing.size} referenced resource(s) could not be resolved, first ${missing.values().next().value}`);
|
|
101
|
+
for (const id of round) fetched.add(id);
|
|
102
|
+
if (stats) stats.deltaResolutionRounds++;
|
|
103
|
+
await consume(round, RESOLUTION_DEPTH);
|
|
104
|
+
}
|
|
105
|
+
if (changedSinceToken !== void 0 && knownResources.size > 0 && collectedFromPoll >= knownResources.size) {
|
|
106
|
+
if (stats) stats.deltaSuspectedFullAnswers++;
|
|
107
|
+
logger?.warn(`delta poll: sent a token but received ${collectedFromPoll} resources against a mirror of ${knownResources.size}; the backend may have refused the token (instance change or rewound numbering), in which case this poll cost a full tree read`);
|
|
108
|
+
}
|
|
109
|
+
return [...collected.values()];
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { loadDeltaTreeState };
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=delta_sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delta_sync.js","names":[],"sources":["../src/delta_sync.ts"],"sourcesContent":["import type { PlTransaction, SignedResourceId } from \"@milaboratories/pl-client\";\nimport { isNotNullSignedResourceId } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData } from \"./state\";\nimport { TreeStateUpdateError } from \"./state\";\nimport type { TreeLoadingRequest, TreeLoadingStat } from \"./sync\";\nimport { collectStatsForResource } from \"./sync\";\n\n/** Emit everything at or below this depth from a resolution seed, whatever its change token\n * says. It bounds descent as well as emission: under a token the walk otherwise ends at the\n * first unchanged resource (api.proto, changed_since_token), so without this a round returns\n * the seeds alone and a newly attached subtree costs one sequential round trip per level.\n *\n * Deep enough to clear a subtree in one round, which is the trade: real server-side walk and\n * downlink for those levels, against a round trip each at ~1.4s on a slow link. Not free -\n * lower it if resolution rounds ever dominate a poll. */\nconst RESOLUTION_DEPTH = 32;\n\n/** Every id a body points at. Exactly what `updateFromResourceData` refcounts, and it throws\n * `orphan resource` for any that resolves to nothing. */\nfunction referencesOf(resource: ExtendedResourceData): SignedResourceId[] {\n const refs: SignedResourceId[] = [];\n if (isNotNullSignedResourceId(resource.error)) refs.push(resource.error);\n for (const field of resource.fields) {\n if (isNotNullSignedResourceId(field.value)) refs.push(field.value);\n if (isNotNullSignedResourceId(field.error)) refs.push(field.error);\n }\n return refs;\n}\n\n/**\n * One delta poll: hand the backend this transaction's change token, take only what is newer.\n *\n * The caller commits the token, and only after the whole batch applies - advancing it past a\n * partial apply loses the dropped resources for good.\n *\n * Removals arrive as a rewrite of the referring resource, never as absence, and the refcount\n * GC does the rest. There is deliberately no periodic full reconcile; the blind spot is a\n * removal under a resource the mirror marked final, which streaming and BFS share.\n */\nexport async function loadDeltaTreeState(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n logger?: { warn: (msg: string) => void },\n): Promise<ExtendedResourceData[]> {\n const {\n seedResources,\n finalResources,\n roots,\n knownResources,\n pruningFunction,\n fieldFilter,\n traverseStopRules,\n changedSinceToken,\n } = loadingRequest;\n\n // Stop rules are never sent. The backend ignores them under a usable token anyway, but a\n // token-less poll then walks unpruned where streaming would have pruned. Sending them only\n // when the token is absent would change the request shape between polls, which a token\n // holder must not do - so the cost is warned about instead.\n if (traverseStopRules !== undefined && changedSinceToken === undefined) {\n logger?.warn(\n \"delta poll: traverseStopRules supplied but not sent, and this poll carries no token,\" +\n \" so the backend walk is unpruned where the streaming path would have pruned it\",\n );\n }\n\n // Seed at every non-final resource, not the roots. Required, not an optimisation: the\n // backend ends a walk at the first unchanged resource, so a root-seeded poll never reaches a\n // change under a quiet parent - measured at 0 of 3 changes delivered. algorithm_equivalence\n // fails on its KV step if this is ever narrowed back to the roots.\n //\n // The seed set changes every poll and the token is deliberately NOT discarded for it, though\n // api.proto counts it as a shape input. Safe because every seed is a resource the mirror\n // already holds, so a change can only widen the result, and widening hides nothing.\n // Discarding per poll would make every poll a full read and defeat the mechanism.\n //\n // The roots stand in when the frontier is empty: resourceTree needs at least one seed.\n const seeds = seedResources.length > 0 ? seedResources : [...roots];\n if (seeds.length === 0) return [];\n\n const collected = new Map<SignedResourceId, ExtendedResourceData>();\n // References neither the mirror nor this batch can satisfy; applying with one outstanding\n // invalidates the whole tree. Maintained incrementally as frames arrive: rescanning\n // everything collected once per round would be quadratic in a poll that needs several.\n const missing = new Set<SignedResourceId>();\n // Same ids, in arrival order, so a round consumes only what is new rather than walking the\n // whole set. An id enters both exactly once: `collected` is checked before the add and never\n // cleared, so nothing re-enters after it resolves.\n const pending: SignedResourceId[] = [];\n const collect = (resource: ExtendedResourceData) => {\n collected.set(resource.id, resource);\n missing.delete(resource.id);\n for (const ref of referencesOf(resource))\n if (!collected.has(ref) && !knownResources.has(ref) && !missing.has(ref)) {\n missing.add(ref);\n pending.push(ref);\n }\n };\n\n let warnedStopMarker = false;\n const consume = async (walkSeeds: SignedResourceId[], unconditionalDepth?: number) => {\n if (stats) {\n stats.roundTrips++;\n stats.streamRounds++;\n stats.deltaSeedsSent += walkSeeds.length;\n }\n\n for await (const frame of tx.resourceTree(walkSeeds, {\n includeKv: true,\n fieldFilter,\n changedSinceToken,\n unconditionalDepth,\n })) {\n // A body-less frame. The client reads that as a stop marker, and a stop marker can only\n // follow traverse_stop_rules, which this path never sends. Nothing here needs it, so it\n // is skipped - but not silently: reaching this means the backend put a meaning on a\n // body-less frame that this client does not know, and a quiet skip would leave no trace\n // of it. Warn once per poll rather than per frame, since a contract change would emit\n // one of these for every resource visited.\n if (frame.frameKind === \"stopMarker\") {\n if (!warnedStopMarker) {\n warnedStopMarker = true;\n logger?.warn(\n `delta poll: ignoring a body-less frame for ${frame.id}; this poll sent no ` +\n `traverse stop rules, so the backend is using body-less frames for something ` +\n `this client does not interpret`,\n );\n }\n continue;\n }\n\n if (stats) stats.resourceFrames++;\n\n // updateFromResourceData throws on any body for a held final resource and invalidates\n // the tree. A token-less poll walks unpruned and emits exactly those, which is what the\n // first poll after a restored snapshot does. Both sibling algorithms skip the same way.\n // Safe for references: finalResources is a subset of knownResources.\n if (finalResources.has(frame.id)) {\n if (stats) stats.finalResourcesSkipped++;\n continue;\n }\n\n // An explicit literal rather than two rest-spreads of the frame. Measured ~15% off the\n // per-frame cost: a spread copies every property twice and gives the object a hidden\n // class of its own, where this is the shape the streaming path already produces.\n const resource: ExtendedResourceData = {\n id: frame.id,\n type: frame.type,\n kind: frame.kind,\n data: frame.data,\n resourceReady: frame.resourceReady,\n error: frame.error,\n originalResourceId: frame.originalResourceId,\n final: frame.final,\n inputsLocked: frame.inputsLocked,\n outputsLocked: frame.outputsLocked,\n fields:\n pruningFunction !== undefined\n ? pruningFunction(frame as unknown as ExtendedResourceData)\n : frame.fields,\n kv: frame.kv,\n };\n if (stats) stats.prunedFields += frame.fields.length - resource.fields.length;\n\n collect(resource);\n collectStatsForResource(resource, stats);\n }\n };\n\n await consume(seeds);\n\n // Captured before the resolution rounds. They fetch unconditionally by design, so counting\n // afterwards lets a modest unheld subtree inflate the total past a small mirror and report a\n // refused token that was never refused.\n const collectedFromPoll = collected.size;\n\n // A body may reference a resource the response did not carry - a field repointed at one we\n // never held. Ids already asked for are never re-requested, so the loop terminates: the tree\n // is finite and `fetched` only grows.\n //\n // Deliberately unbounded on rounds. A round-count cap would turn a legal tree shape into a\n // desktop error, since how deep the references chain is a property of what the backend\n // allowed rather than of anything wrong here, and the streaming path's stop-marker follow-up\n // loop has always run on the same assumption.\n const fetched = new Set<SignedResourceId>();\n while (missing.size > 0) {\n const round = pending.splice(0).filter((id) => !fetched.has(id) && missing.has(id));\n if (round.length === 0) {\n // Asked for all of these and none arrived; a soft-deleted referent gets here, since\n // those frames are dropped client-side. TreeStateUpdateError specifically: the poll loop\n // rebuilds and discards the token for that class, where a plain Error is only logged and\n // the identical request fails forever.\n throw new TreeStateUpdateError(\n `delta poll: ${missing.size} referenced resource(s) could not be resolved, first ${missing.values().next().value}`,\n );\n }\n for (const id of round) fetched.add(id);\n\n if (stats) stats.deltaResolutionRounds++;\n await consume(round, RESOLUTION_DEPTH);\n }\n\n // A refused token is answered with the full tree and no error, so this is the only tell.\n // Reported, not acted on: the response is a correct superset, and the token stored after\n // this poll comes from the current instance, so it self-heals next poll.\n if (\n changedSinceToken !== undefined &&\n knownResources.size > 0 &&\n collectedFromPoll >= knownResources.size\n ) {\n if (stats) stats.deltaSuspectedFullAnswers++;\n logger?.warn(\n `delta poll: sent a token but received ${collectedFromPoll} resources against a mirror of ` +\n `${knownResources.size}; the backend may have refused the token (instance change or ` +\n `rewound numbering), in which case this poll cost a full tree read`,\n );\n }\n\n return [...collected.values()];\n}\n"],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -19,6 +19,7 @@ exports.PlTreeState = require_state.PlTreeState;
|
|
|
19
19
|
exports.SynchronizedTreeState = require_synchronized_tree.SynchronizedTreeState;
|
|
20
20
|
exports.TreeStateUpdateError = require_state.TreeStateUpdateError;
|
|
21
21
|
exports.captureTreeState = require_persisted_tree.captureTreeState;
|
|
22
|
+
exports.collectStatsForResource = require_sync.collectStatsForResource;
|
|
22
23
|
exports.constructTreeLoadingRequest = require_sync.constructTreeLoadingRequest;
|
|
23
24
|
exports.decodePersistedTree = require_persisted_tree.decodePersistedTree;
|
|
24
25
|
exports.derivePollingInterval = require_synchronized_tree.derivePollingInterval;
|
|
@@ -35,8 +36,10 @@ exports.makeResourceSnapshot = require_snapshot.makeResourceSnapshot;
|
|
|
35
36
|
exports.mapValueAndError = require_value_and_error.mapValueAndError;
|
|
36
37
|
exports.mapValueAndErrorIfDefined = require_value_and_error.mapValueAndErrorIfDefined;
|
|
37
38
|
exports.readPersistedTreeHeader = require_persisted_tree.readPersistedTreeHeader;
|
|
39
|
+
exports.resolveTreeLoadingAlgorithm = require_sync.resolveTreeLoadingAlgorithm;
|
|
38
40
|
exports.restoreTreeState = require_persisted_tree.restoreTreeState;
|
|
39
41
|
exports.rsSchema = require_snapshot.rsSchema;
|
|
42
|
+
exports.supportsResourceTreeTraversal = require_sync.supportsResourceTreeTraversal;
|
|
40
43
|
exports.treeDumpStats = require_dump.treeDumpStats;
|
|
41
44
|
exports.treeEntryToResourceInfo = require_accessors.treeEntryToResourceInfo;
|
|
42
45
|
exports.treeEntryToResourceWithData = require_snapshot.treeEntryToResourceWithData;
|
package/dist/index.d.ts
CHANGED
|
@@ -3,9 +3,9 @@ import { ValueAndError, mapValueAndError, mapValueAndErrorIfDefined } from "./va
|
|
|
3
3
|
import { ValueOrError } from "./value_or_error.js";
|
|
4
4
|
import { PlError, PlTreeEntry, PlTreeEntryAccessor, PlTreeNodeAccessor, PlTreeRootsAccessor, PlTreeRootsEntry, ResourceInfo, TreeAccessorData, TreeAccessorInstanceData, isPlTreeEntry, isPlTreeEntryAccessor, isPlTreeNodeAccessor, isPlTreeRootsAccessor, isPlTreeRootsEntry, treeEntryToResourceInfo } from "./accessors.js";
|
|
5
5
|
import { ExtendedResourceData, PlTreeResource, PlTreeState, ResourceDataWithFinalState, ResourceUpdateStat, TreeStateUpdateError } from "./state.js";
|
|
6
|
-
import { PruningFunction, TraversalMode, TreeLoadingRequest, TreeLoadingStat, constructTreeLoadingRequest, formatTreeLoadingStat, initialTreeLoadingStat, loadTreeState } from "./sync.js";
|
|
6
|
+
import { PruningFunction, TraversalMode, TreeLoadingAlgorithmName, TreeLoadingRequest, TreeLoadingStat, collectStatsForResource, constructTreeLoadingRequest, formatTreeLoadingStat, initialTreeLoadingStat, loadTreeState, resolveTreeLoadingAlgorithm, supportsResourceTreeTraversal } from "./sync.js";
|
|
7
7
|
import { InferSnapshot, ResourceSnapshot, ResourceSnapshotSchema, ResourceWithData, ResourceWithMetadata, makeResourceSnapshot, rsSchema, treeEntryToResourceWithData, treeEntryToResourceWithMetadata } from "./snapshot.js";
|
|
8
8
|
import { DecodePersistedTreeOps, EncodePersistedTreeOps, PERSISTED_TREE_SCHEMA_VERSION, PersistedTree, PersistedTreeHeader, PersistedTreeReadFailure, PersistedTreeReadResult, captureTreeState, decodePersistedTree, encodePersistedTree, readPersistedTreeHeader, restoreTreeState } from "./persisted_tree.js";
|
|
9
9
|
import { ExplicitRootSeed, SharedTypeSeed, SynchronizedTreeOps, SynchronizedTreeState, TreeSeed, derivePollingInterval } from "./synchronized_tree.js";
|
|
10
10
|
import { ResourceStats, TreeDumpStats, treeDumpStats } from "./dump.js";
|
|
11
|
-
export { CommonFieldTraverseOps, CommonTraversalOps, DecodePersistedTreeOps, EncodePersistedTreeOps, ExplicitRootSeed, ExtendedResourceData, FieldTraversalStep, GetFieldStep, InferSnapshot, PERSISTED_TREE_SCHEMA_VERSION, PersistedTree, PersistedTreeHeader, PersistedTreeReadFailure, PersistedTreeReadResult, PlError, PlTreeEntry, PlTreeEntryAccessor, PlTreeNodeAccessor, PlTreeResource, PlTreeRootsAccessor, PlTreeRootsEntry, PlTreeState, PruningFunction, ResourceDataWithFinalState, ResourceInfo, ResourceSnapshot, ResourceSnapshotSchema, ResourceStats, ResourceTraversalOps, ResourceUpdateStat, ResourceWithData, ResourceWithMetadata, SharedTypeSeed, SynchronizedTreeOps, SynchronizedTreeState, TraversalMode, TreeAccessorData, TreeAccessorInstanceData, TreeDumpStats, TreeLoadingRequest, TreeLoadingStat, TreeSeed, TreeStateUpdateError, ValueAndError, ValueOrError, captureTreeState, constructTreeLoadingRequest, decodePersistedTree, derivePollingInterval, encodePersistedTree, formatTreeLoadingStat, initialTreeLoadingStat, isPlTreeEntry, isPlTreeEntryAccessor, isPlTreeNodeAccessor, isPlTreeRootsAccessor, isPlTreeRootsEntry, loadTreeState, makeResourceSnapshot, mapValueAndError, mapValueAndErrorIfDefined, readPersistedTreeHeader, restoreTreeState, rsSchema, treeDumpStats, treeEntryToResourceInfo, treeEntryToResourceWithData, treeEntryToResourceWithMetadata };
|
|
11
|
+
export { CommonFieldTraverseOps, CommonTraversalOps, DecodePersistedTreeOps, EncodePersistedTreeOps, ExplicitRootSeed, ExtendedResourceData, FieldTraversalStep, GetFieldStep, InferSnapshot, PERSISTED_TREE_SCHEMA_VERSION, PersistedTree, PersistedTreeHeader, PersistedTreeReadFailure, PersistedTreeReadResult, PlError, PlTreeEntry, PlTreeEntryAccessor, PlTreeNodeAccessor, PlTreeResource, PlTreeRootsAccessor, PlTreeRootsEntry, PlTreeState, PruningFunction, ResourceDataWithFinalState, ResourceInfo, ResourceSnapshot, ResourceSnapshotSchema, ResourceStats, ResourceTraversalOps, ResourceUpdateStat, ResourceWithData, ResourceWithMetadata, SharedTypeSeed, SynchronizedTreeOps, SynchronizedTreeState, TraversalMode, TreeAccessorData, TreeAccessorInstanceData, TreeDumpStats, TreeLoadingAlgorithmName, TreeLoadingRequest, TreeLoadingStat, TreeSeed, TreeStateUpdateError, ValueAndError, ValueOrError, captureTreeState, collectStatsForResource, constructTreeLoadingRequest, decodePersistedTree, derivePollingInterval, encodePersistedTree, formatTreeLoadingStat, initialTreeLoadingStat, isPlTreeEntry, isPlTreeEntryAccessor, isPlTreeNodeAccessor, isPlTreeRootsAccessor, isPlTreeRootsEntry, loadTreeState, makeResourceSnapshot, mapValueAndError, mapValueAndErrorIfDefined, readPersistedTreeHeader, resolveTreeLoadingAlgorithm, restoreTreeState, rsSchema, supportsResourceTreeTraversal, treeDumpStats, treeEntryToResourceInfo, treeEntryToResourceWithData, treeEntryToResourceWithMetadata };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { mapValueAndError, mapValueAndErrorIfDefined } from "./value_and_error.js";
|
|
2
2
|
import { PlError, PlTreeEntry, PlTreeEntryAccessor, PlTreeNodeAccessor, PlTreeRootsAccessor, PlTreeRootsEntry, isPlTreeEntry, isPlTreeEntryAccessor, isPlTreeNodeAccessor, isPlTreeRootsAccessor, isPlTreeRootsEntry, treeEntryToResourceInfo } from "./accessors.js";
|
|
3
3
|
import { PlTreeResource, PlTreeState, TreeStateUpdateError } from "./state.js";
|
|
4
|
-
import { constructTreeLoadingRequest, formatTreeLoadingStat, initialTreeLoadingStat, loadTreeState } from "./sync.js";
|
|
4
|
+
import { collectStatsForResource, constructTreeLoadingRequest, formatTreeLoadingStat, initialTreeLoadingStat, loadTreeState, resolveTreeLoadingAlgorithm, supportsResourceTreeTraversal } from "./sync.js";
|
|
5
5
|
import { makeResourceSnapshot, rsSchema, treeEntryToResourceWithData, treeEntryToResourceWithMetadata } from "./snapshot.js";
|
|
6
6
|
import { PERSISTED_TREE_SCHEMA_VERSION, captureTreeState, decodePersistedTree, encodePersistedTree, readPersistedTreeHeader, restoreTreeState } from "./persisted_tree.js";
|
|
7
7
|
import { SynchronizedTreeState, derivePollingInterval } from "./synchronized_tree.js";
|
|
8
8
|
import { treeDumpStats } from "./dump.js";
|
|
9
|
-
export { PERSISTED_TREE_SCHEMA_VERSION, PlError, PlTreeEntry, PlTreeEntryAccessor, PlTreeNodeAccessor, PlTreeResource, PlTreeRootsAccessor, PlTreeRootsEntry, PlTreeState, SynchronizedTreeState, TreeStateUpdateError, captureTreeState, constructTreeLoadingRequest, decodePersistedTree, derivePollingInterval, encodePersistedTree, formatTreeLoadingStat, initialTreeLoadingStat, isPlTreeEntry, isPlTreeEntryAccessor, isPlTreeNodeAccessor, isPlTreeRootsAccessor, isPlTreeRootsEntry, loadTreeState, makeResourceSnapshot, mapValueAndError, mapValueAndErrorIfDefined, readPersistedTreeHeader, restoreTreeState, rsSchema, treeDumpStats, treeEntryToResourceInfo, treeEntryToResourceWithData, treeEntryToResourceWithMetadata };
|
|
9
|
+
export { PERSISTED_TREE_SCHEMA_VERSION, PlError, PlTreeEntry, PlTreeEntryAccessor, PlTreeNodeAccessor, PlTreeResource, PlTreeRootsAccessor, PlTreeRootsEntry, PlTreeState, SynchronizedTreeState, TreeStateUpdateError, captureTreeState, collectStatsForResource, constructTreeLoadingRequest, decodePersistedTree, derivePollingInterval, encodePersistedTree, formatTreeLoadingStat, initialTreeLoadingStat, isPlTreeEntry, isPlTreeEntryAccessor, isPlTreeNodeAccessor, isPlTreeRootsAccessor, isPlTreeRootsEntry, loadTreeState, makeResourceSnapshot, mapValueAndError, mapValueAndErrorIfDefined, readPersistedTreeHeader, resolveTreeLoadingAlgorithm, restoreTreeState, rsSchema, supportsResourceTreeTraversal, treeDumpStats, treeEntryToResourceInfo, treeEntryToResourceWithData, treeEntryToResourceWithMetadata };
|
package/dist/state.cjs
CHANGED
|
@@ -311,18 +311,47 @@ var PlTreeState = class {
|
|
|
311
311
|
this.checkValid();
|
|
312
312
|
const incrementRefs = [];
|
|
313
313
|
const decrementRefs = [];
|
|
314
|
+
let addedCount = 0;
|
|
315
|
+
let firstAdded;
|
|
316
|
+
let errRd;
|
|
317
|
+
let errRes;
|
|
318
|
+
let errOriginalResourceId = _milaboratories_pl_client.NullSignedResourceId;
|
|
319
|
+
let errError = _milaboratories_pl_client.NullSignedResourceId;
|
|
320
|
+
let errInputsLocked = false;
|
|
321
|
+
let errOutputsLocked = false;
|
|
322
|
+
let errResourceReady = false;
|
|
323
|
+
let errFinal = false;
|
|
324
|
+
const unexpectedTransitionError = (reason) => {
|
|
325
|
+
const { fields, ...rdWithoutFields } = errRd;
|
|
326
|
+
const statBeforeMutation = errRes === void 0 ? void 0 : {
|
|
327
|
+
id: errRes.id,
|
|
328
|
+
kind: errRes.kind,
|
|
329
|
+
type: errRes.type,
|
|
330
|
+
data: errRes.data,
|
|
331
|
+
resourceReady: errResourceReady,
|
|
332
|
+
inputsLocked: errInputsLocked,
|
|
333
|
+
outputsLocked: errOutputsLocked,
|
|
334
|
+
error: errError,
|
|
335
|
+
originalResourceId: errOriginalResourceId,
|
|
336
|
+
final: errFinal
|
|
337
|
+
};
|
|
338
|
+
this.invalidateTree();
|
|
339
|
+
throw new TreeStateUpdateError(`Unexpected resource state transition (${reason}): ${(0, _milaboratories_pl_client.stringifyWithResourceId)(rdWithoutFields)} -> ${(0, _milaboratories_pl_client.stringifyWithResourceId)(statBeforeMutation)}`);
|
|
340
|
+
};
|
|
314
341
|
for (const rd of resourceData) {
|
|
315
342
|
let resource = this.resources.get(rd.id);
|
|
316
343
|
const held = resource !== void 0;
|
|
317
344
|
let changed = false;
|
|
318
345
|
let metadataChanged = false;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
const { fields, ...rdWithoutFields } = rd;
|
|
322
|
-
this.invalidateTree();
|
|
323
|
-
throw new TreeStateUpdateError(`Unexpected resource state transition (${reason}): ${(0, _milaboratories_pl_client.stringifyWithResourceId)(rdWithoutFields)} -> ${(0, _milaboratories_pl_client.stringifyWithResourceId)(statBeforeMutation)}`);
|
|
324
|
-
};
|
|
346
|
+
errRd = rd;
|
|
347
|
+
errRes = resource;
|
|
325
348
|
if (resource !== void 0) {
|
|
349
|
+
errOriginalResourceId = resource.originalResourceId;
|
|
350
|
+
errError = resource.error;
|
|
351
|
+
errInputsLocked = resource.inputsLocked;
|
|
352
|
+
errOutputsLocked = resource.outputsLocked;
|
|
353
|
+
errResourceReady = resource.resourceReady;
|
|
354
|
+
errFinal = resource.finalFlag;
|
|
326
355
|
if (resource.finalState) unexpectedTransitionError("resource state can be updated after it is marked as final");
|
|
327
356
|
resource.version += 1;
|
|
328
357
|
if (resource.originalResourceId !== rd.originalResourceId) {
|
|
@@ -340,8 +369,15 @@ var PlTreeState = class {
|
|
|
340
369
|
changed = true;
|
|
341
370
|
if (stat) stat.errorsAttached++;
|
|
342
371
|
}
|
|
372
|
+
let walk = rd.fields.length > 0 ? resource.fieldsMap.values() : void 0;
|
|
343
373
|
for (const fd of rd.fields) {
|
|
344
|
-
let field
|
|
374
|
+
let field;
|
|
375
|
+
if (walk !== void 0) {
|
|
376
|
+
const next = walk.next();
|
|
377
|
+
if (next.done === true || next.value.name !== fd.name) walk = void 0;
|
|
378
|
+
else field = next.value;
|
|
379
|
+
}
|
|
380
|
+
if (field === void 0) field = resource.fieldsMap.get(fd.name);
|
|
345
381
|
if (!field) {
|
|
346
382
|
field = new PlTreeField(fd.name, fd.type, fd.value, fd.error, fd.status, fd.valueIsFinal, resource.version);
|
|
347
383
|
if ((0, _milaboratories_pl_client.isNotNullSignedResourceId)(fd.value)) incrementRefs.push(fd.value);
|
|
@@ -436,10 +472,18 @@ var PlTreeState = class {
|
|
|
436
472
|
changed = true;
|
|
437
473
|
if (stat) stat.readyFlips++;
|
|
438
474
|
}
|
|
475
|
+
let kvWalk = rd.kv.length > 0 ? resource.kv.entries() : void 0;
|
|
439
476
|
for (const kv of rd.kv) {
|
|
440
|
-
|
|
477
|
+
let current;
|
|
478
|
+
if (kvWalk !== void 0) {
|
|
479
|
+
const next = kvWalk.next();
|
|
480
|
+
if (next.done === true || next.value[0] !== kv.key) kvWalk = void 0;
|
|
481
|
+
else current = next.value[1];
|
|
482
|
+
}
|
|
483
|
+
if (current === void 0) current = resource.kv.get(kv.key);
|
|
441
484
|
if (current === void 0) {
|
|
442
485
|
resource.kv.set(kv.key, kv.value);
|
|
486
|
+
kvWalk = void 0;
|
|
443
487
|
(0, _milaboratories_ts_helpers.notEmpty)(resource.kvChangedPerKey).markChanged(kv.key, `kv added for ${(0, _milaboratories_pl_client.resourceIdToString)(resource.id)}: ${kv.key}`);
|
|
444
488
|
changed = true;
|
|
445
489
|
if (stat) stat.kvChanged++;
|
|
@@ -481,7 +525,8 @@ var PlTreeState = class {
|
|
|
481
525
|
for (const kv of rd.kv) resource.kv.set(kv.key, kv.value);
|
|
482
526
|
if (this.isFinalPredicate(resource)) resource.markFinal();
|
|
483
527
|
this.resources.set(resource.id, resource);
|
|
484
|
-
|
|
528
|
+
if (addedCount === 0) firstAdded = resource.id;
|
|
529
|
+
addedCount++;
|
|
485
530
|
}
|
|
486
531
|
if (stat) if (!held) stat.resourcesNew++;
|
|
487
532
|
else if (changed) {
|
|
@@ -494,6 +539,7 @@ var PlTreeState = class {
|
|
|
494
539
|
if (!stat.usedStreaming) stat.bfsRequestsWasted++;
|
|
495
540
|
}
|
|
496
541
|
}
|
|
542
|
+
if (firstAdded !== void 0) this.resourcesAdded.markChanged(addedCount === 1 ? `new resource ${(0, _milaboratories_pl_client.resourceIdToString)(firstAdded)} added` : `${addedCount} new resources added, first ${(0, _milaboratories_pl_client.resourceIdToString)(firstAdded)}`);
|
|
497
543
|
for (const rid of incrementRefs) {
|
|
498
544
|
const res = this.resources.get(rid);
|
|
499
545
|
if (!res) {
|