@milaboratories/pl-tree 1.14.4 → 1.15.0
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/sync.cjs +45 -11
- 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 +43 -12
- 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 +3 -3
- 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/sync.test.ts +74 -1
- package/src/sync.ts +107 -27
- 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/sync.cjs
CHANGED
|
@@ -1,9 +1,31 @@
|
|
|
1
1
|
const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
|
|
2
|
+
const require_delta_sync = require("./delta_sync.cjs");
|
|
2
3
|
let _milaboratories_pl_client = require("@milaboratories/pl-client");
|
|
3
4
|
let _milaboratories_ts_helpers = require("@milaboratories/ts-helpers");
|
|
4
5
|
let denque = require("denque");
|
|
5
6
|
denque = require_runtime.__toESM(denque, 1);
|
|
6
7
|
//#region src/sync.ts
|
|
8
|
+
/** Resolves a traversal mode into the algorithm a tree will run. A tree calls this once, when
|
|
9
|
+
* it is made, and keeps the answer for its whole life, so the choice (and the fallback warning
|
|
10
|
+
* for a preference the backend cannot serve) happens once rather than on every poll. */
|
|
11
|
+
function resolveTreeLoadingAlgorithm(mode, capabilities = [], logger) {
|
|
12
|
+
const streaming = supportsResourceTreeTraversal(capabilities);
|
|
13
|
+
const delta = supportsTreeDelta(capabilities);
|
|
14
|
+
switch (mode) {
|
|
15
|
+
case "client-bfs": return "client-bfs";
|
|
16
|
+
case "backend-delta":
|
|
17
|
+
if (delta) return "backend-delta";
|
|
18
|
+
(logger ?? console).warn("traversalMode=backend-delta but backend lacks treeChangedSince:v1 capability; falling back to " + (streaming ? "backend-streaming" : "client-bfs"));
|
|
19
|
+
return streaming ? "backend-streaming" : "client-bfs";
|
|
20
|
+
case "backend-streaming":
|
|
21
|
+
if (streaming) return "backend-streaming";
|
|
22
|
+
(logger ?? console).warn("traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS");
|
|
23
|
+
return "client-bfs";
|
|
24
|
+
case "auto":
|
|
25
|
+
if (delta) return "backend-delta";
|
|
26
|
+
return streaming ? "backend-streaming" : "client-bfs";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
7
29
|
/** Given the current tree state, build the request object to pass to
|
|
8
30
|
* {@link loadTreeState} to load updated state. */
|
|
9
31
|
function constructTreeLoadingRequest(tree, options = {}) {
|
|
@@ -19,9 +41,12 @@ function constructTreeLoadingRequest(tree, options = {}) {
|
|
|
19
41
|
return {
|
|
20
42
|
seedResources,
|
|
21
43
|
finalResources,
|
|
44
|
+
roots: [...tree.roots],
|
|
45
|
+
knownResources: materialized,
|
|
22
46
|
pruningFunction: options.pruningFunction,
|
|
23
47
|
fieldFilter: options.fieldFilter,
|
|
24
|
-
traverseStopRules: options.traverseStopRules
|
|
48
|
+
traverseStopRules: options.traverseStopRules,
|
|
49
|
+
changedSinceToken: options.changedSinceToken
|
|
25
50
|
};
|
|
26
51
|
}
|
|
27
52
|
function initialTreeLoadingStat() {
|
|
@@ -45,6 +70,9 @@ function initialTreeLoadingStat() {
|
|
|
45
70
|
traverseWasStoppedCount: 0,
|
|
46
71
|
bfsResourcesRequested: 0,
|
|
47
72
|
bfsResourcesNotFound: 0,
|
|
73
|
+
deltaSeedsSent: 0,
|
|
74
|
+
deltaResolutionRounds: 0,
|
|
75
|
+
deltaSuspectedFullAnswers: 0,
|
|
48
76
|
resourcesNew: 0,
|
|
49
77
|
resourcesChanged: 0,
|
|
50
78
|
resourcesUnchanged: 0,
|
|
@@ -83,12 +111,16 @@ Unchanged bytes (wasted downlink): ${stat.bytesUnchanged}
|
|
|
83
111
|
Changed with stable metadata: ${stat.metadataStableChanged}
|
|
84
112
|
BFS fetches wasted on unchanged: ${stat.bfsRequestsWasted}
|
|
85
113
|
Used streaming: ${stat.usedStreaming}
|
|
86
|
-
[
|
|
87
|
-
[bfs] resources requested: ${stat.bfsResourcesRequested}, not found: ${stat.bfsResourcesNotFound}
|
|
114
|
+
[backend] rounds: ${stat.streamRounds}, resource frames: ${stat.resourceFrames}, stop-marker frames: ${stat.stopMarkerFrames}, stop->follow-up: ${stat.stopMarkersFollowUp}, traverse-stopped: ${stat.traverseWasStoppedCount}
|
|
115
|
+
[bfs] resources requested: ${stat.bfsResourcesRequested}, not found: ${stat.bfsResourcesNotFound}
|
|
116
|
+
[delta] seeds sent: ${stat.deltaSeedsSent}, resolution rounds: ${stat.deltaResolutionRounds}, suspected full answers: ${stat.deltaSuspectedFullAnswers}`;
|
|
88
117
|
}
|
|
89
118
|
function supportsResourceTreeTraversal(capabilities = []) {
|
|
90
119
|
return (0, _milaboratories_pl_client.hasCapability)(capabilities, "treeFilter:v2");
|
|
91
120
|
}
|
|
121
|
+
function supportsTreeDelta(capabilities = []) {
|
|
122
|
+
return (0, _milaboratories_pl_client.hasCapability)(capabilities, "treeChangedSince:v1");
|
|
123
|
+
}
|
|
92
124
|
function collectStatsForResource(resource, stats) {
|
|
93
125
|
if (!stats) return;
|
|
94
126
|
stats.retrievedResources++;
|
|
@@ -252,23 +284,25 @@ async function loadTreeState(tx, loadingRequest, stats, capabilities = [], mode
|
|
|
252
284
|
const startTimestamp = Date.now();
|
|
253
285
|
if (stats) stats.requests++;
|
|
254
286
|
try {
|
|
255
|
-
const
|
|
256
|
-
if (stats) stats.usedStreaming =
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
287
|
+
const algorithm = resolveTreeLoadingAlgorithm(mode, capabilities, logger);
|
|
288
|
+
if (stats) stats.usedStreaming = algorithm !== "client-bfs";
|
|
289
|
+
switch (algorithm) {
|
|
290
|
+
case "backend-delta": return await require_delta_sync.loadDeltaTreeState(tx, loadingRequest, stats, logger);
|
|
291
|
+
case "backend-streaming": return await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger);
|
|
292
|
+
case "client-bfs": return await loadTreeStateViaBfs(tx, loadingRequest, stats);
|
|
293
|
+
default: throw new Error(`unknown tree loading algorithm: ${algorithm}`);
|
|
262
294
|
}
|
|
263
|
-
return wantsStreaming ? await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger) : await loadTreeStateViaBfs(tx, loadingRequest, stats);
|
|
264
295
|
} finally {
|
|
265
296
|
if (stats) stats.millisSpent += Date.now() - startTimestamp;
|
|
266
297
|
}
|
|
267
298
|
}
|
|
268
299
|
//#endregion
|
|
300
|
+
exports.collectStatsForResource = collectStatsForResource;
|
|
269
301
|
exports.constructTreeLoadingRequest = constructTreeLoadingRequest;
|
|
270
302
|
exports.formatTreeLoadingStat = formatTreeLoadingStat;
|
|
271
303
|
exports.initialTreeLoadingStat = initialTreeLoadingStat;
|
|
272
304
|
exports.loadTreeState = loadTreeState;
|
|
305
|
+
exports.resolveTreeLoadingAlgorithm = resolveTreeLoadingAlgorithm;
|
|
306
|
+
exports.supportsResourceTreeTraversal = supportsResourceTreeTraversal;
|
|
273
307
|
|
|
274
308
|
//# sourceMappingURL=sync.cjs.map
|
package/dist/sync.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.cjs","names":["ConcurrencyLimitingExecutor","Denque"],"sources":["../src/sync.ts"],"sourcesContent":["import type {\n FieldData,\n Filter,\n OptionalSignedResourceId,\n PlTransaction,\n ResourceTreeFrame,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport Denque from \"denque\";\nimport { hasCapability, isNullSignedResourceId } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData, PlTreeState, ResourceUpdateStat } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\n\n/** Applied to list of fields in resource data. */\nexport type PruningFunction = (resource: ExtendedResourceData) => FieldData[];\n\nexport interface TreeLoadingRequest {\n /** Resource to prime the traversal algorithm. It is ok, if some of them\n * doesn't exist anymore. Should not contain elements from final resource\n * set. */\n readonly seedResources: SignedResourceId[];\n\n /** Resource ids for which state is already known and not expected to change.\n * Algorithm will not continue traversal over those ids, and states will not\n * be retrieved for them. */\n readonly finalResources: Set<SignedResourceId>;\n\n /** Applied to each resource field list in fallback BFS mode and to streamed results. */\n readonly pruningFunction?: PruningFunction;\n\n /** ResourceTree field filter passed to the backend when supported. */\n readonly fieldFilter?: Filter;\n\n /** ResourceTree traversal stop rules passed to the backend when supported. */\n readonly traverseStopRules?: Filter;\n}\n\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use backend streaming when the backend advertises `treeFilter:v2`,\n * fall back to client-side BFS otherwise.\n * - `\"client-bfs\"`: always use client-side BFS, even on capable backends.\n * - `\"backend-streaming\"`: always prefer backend streaming; if the capability is absent,\n * logs a warning and falls back to BFS (never throws).\n */\nexport type TraversalMode = \"auto\" | \"client-bfs\" | \"backend-streaming\";\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n options: Pick<TreeLoadingRequest, \"pruningFunction\" | \"fieldFilter\" | \"traverseStopRules\"> = {},\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n const materialized = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n materialized.add(res.id);\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n for (const root of tree.roots) if (!materialized.has(root)) seedResources.push(root);\n\n return {\n seedResources,\n finalResources,\n pruningFunction: options.pruningFunction,\n fieldFilter: options.fieldFilter,\n traverseStopRules: options.traverseStopRules,\n };\n}\n\nexport type TreeLoadingStat = ResourceUpdateStat & {\n requests: number;\n roundTrips: number;\n retrievedResources: number;\n retrievedFields: number;\n retrievedKeyValues: number;\n retrievedResourceDataBytes: number;\n retrievedKeyValueBytes: number;\n prunedFields: number;\n finalResourcesSkipped: number;\n millisSpent: number;\n /** Stop-marker frames whose id was already final locally and were skipped. */\n stopMarkersSkipped: number;\n /** Number of follow-up resourceTree() calls issued to resolve unknown stop markers. */\n stopMarkerFollowUpRoundTrips: number;\n /** Streaming (resourceTree) path: resourceTree() streams consumed (1, or 2 with a follow-up). */\n streamRounds: number;\n /** Streaming path: resource frames received. */\n resourceFrames: number;\n /** Streaming path: stopMarker frames received (both skipped and follow-up). */\n stopMarkerFrames: number;\n /** Streaming path: stop markers that were not final locally and triggered a follow-up fetch. */\n stopMarkersFollowUp: number;\n /** Streaming path: frames where the backend stopped traversal (final or traverseWasStopped). */\n traverseWasStoppedCount: number;\n /** BFS path: resource states actually requested from the backend this cycle (after intra-cycle dedup). */\n bfsResourcesRequested: number;\n /** BFS path: requested resources that no longer exist (undefined reply). */\n bfsResourcesNotFound: number;\n};\n\nexport function initialTreeLoadingStat(): TreeLoadingStat {\n return {\n requests: 0,\n roundTrips: 0,\n retrievedResources: 0,\n retrievedFields: 0,\n retrievedKeyValues: 0,\n retrievedResourceDataBytes: 0,\n retrievedKeyValueBytes: 0,\n prunedFields: 0,\n finalResourcesSkipped: 0,\n millisSpent: 0,\n stopMarkersSkipped: 0,\n stopMarkerFollowUpRoundTrips: 0,\n streamRounds: 0,\n resourceFrames: 0,\n stopMarkerFrames: 0,\n stopMarkersFollowUp: 0,\n traverseWasStoppedCount: 0,\n bfsResourcesRequested: 0,\n bfsResourcesNotFound: 0,\n resourcesNew: 0,\n resourcesChanged: 0,\n resourcesUnchanged: 0,\n bytesUnchanged: 0,\n metadataStableChanged: 0,\n bfsRequestsWasted: 0,\n usedStreaming: false,\n fieldsAdded: 0,\n fieldsRemoved: 0,\n fieldsChanged: 0,\n kvChanged: 0,\n readyFlips: 0,\n locksChanged: 0,\n errorsAttached: 0,\n duplicatesResolved: 0,\n resourcesMarkedFinal: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n return `Requests: ${stat.requests}\nTotal time: ${msToHumanReadable(stat.millisSpent)}\nRound-trips: ${stat.roundTrips}\nResources: ${stat.retrievedResources}\nFields: ${stat.retrievedFields}\nKV: ${stat.retrievedKeyValues}\nData Bytes: ${stat.retrievedResourceDataBytes}\nKV Bytes: ${stat.retrievedKeyValueBytes}\nPruned fields: ${stat.prunedFields}\nFinal resources skipped: ${stat.finalResourcesSkipped}\nStop markers skipped: ${stat.stopMarkersSkipped}\nStop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}\nNew resources: ${stat.resourcesNew}\nChanged resources: ${stat.resourcesChanged}\nUnchanged (duplicate re-fetch) resources: ${stat.resourcesUnchanged}\nUnchanged bytes (wasted downlink): ${stat.bytesUnchanged}\nChanged with stable metadata: ${stat.metadataStableChanged}\nBFS fetches wasted on unchanged: ${stat.bfsRequestsWasted}\nUsed streaming: ${stat.usedStreaming}\n[streaming] rounds: ${stat.streamRounds}, resource frames: ${stat.resourceFrames}, stop-marker frames: ${stat.stopMarkerFrames}, stop->follow-up: ${stat.stopMarkersFollowUp}, traverse-stopped: ${stat.traverseWasStoppedCount}\n[bfs] resources requested: ${stat.bfsResourcesRequested}, not found: ${stat.bfsResourcesNotFound}`;\n}\n\nfunction supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeFilter:v2\");\n}\n\nfunction collectStatsForResource(resource: ExtendedResourceData, stats?: TreeLoadingStat) {\n if (!stats) return;\n stats.retrievedResources++;\n stats.retrievedFields += resource.fields.length;\n stats.retrievedKeyValues += resource.kv.length;\n stats.retrievedResourceDataBytes += resource.data?.length ?? 0;\n for (const kv of resource.kv) stats.retrievedKeyValueBytes += kv.value.length;\n}\n\nasync function loadTreeStateViaBfs(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction } = loadingRequest;\n\n // Limits the number of concurrent gRPC fetches to bound peak memory\n // from in-flight request/response buffers.\n const limiter = new ConcurrencyLimitingExecutor(100);\n\n // Promises of resource states, in the order they were requested.\n const pending = new Denque<Promise<ExtendedResourceData | undefined>>();\n\n // vars to calculate number of roundtrips for stats\n let roundTripToggle: boolean = true;\n let numberOfRoundTrips = 0;\n\n // tracking resources we already requested or queued\n const requested = new Set<SignedResourceId>();\n\n /** Mark a resource for fetching. Deduplicates and respects final-resource set. */\n const requestState = (rid: OptionalSignedResourceId) => {\n if (isNullSignedResourceId(rid) || requested.has(rid)) return;\n\n if (finalResources.has(rid)) {\n if (stats) stats.finalResourcesSkipped++;\n return;\n }\n\n requested.add(rid);\n if (stats) stats.bfsResourcesRequested++;\n\n pending.push(\n limiter.run(async () => {\n const resourceData = tx.getResourceDataIfExists(rid, true);\n const kvData = tx.listKeyValuesIfResourceExists(rid);\n\n // counting round-trip (begin)\n const addRT = roundTripToggle;\n if (roundTripToggle) roundTripToggle = false;\n\n const [resource, kv] = await Promise.all([resourceData, kvData]);\n\n // counting round-trip, actually incrementing counter and returning toggle back,\n // so the next request can acquire it\n if (addRT) {\n numberOfRoundTrips++;\n roundTripToggle = true;\n }\n\n if (resource === undefined) {\n if (stats) stats.bfsResourcesNotFound++;\n return undefined;\n }\n if (kv === undefined) throw new Error(\"Inconsistent replies\");\n\n return { ...resource, kv };\n }),\n );\n };\n\n // sending seed requests\n seedResources.forEach((rid) => requestState(rid));\n\n const result: ExtendedResourceData[] = [];\n let nextPromise: Promise<ExtendedResourceData | undefined> | undefined;\n while ((nextPromise = pending.shift()) !== undefined) {\n // at this point we pause and wait for the next requested resource state to arrive\n let nextResource = await nextPromise;\n if (nextResource === undefined)\n // ignoring resources that were not found (this may happen for seed resource ids)\n continue;\n\n if (pruningFunction !== undefined) {\n // apply field pruning, if requested\n const fieldsAfterPruning = pruningFunction(nextResource);\n // collecting stats\n if (stats) stats.prunedFields += nextResource.fields.length - fieldsAfterPruning.length;\n nextResource = { ...nextResource, fields: fieldsAfterPruning };\n }\n\n // continue traversal over the referenced resources\n requestState(nextResource.error);\n for (const field of nextResource.fields) {\n requestState(field.value);\n requestState(field.error);\n }\n\n // collecting stats\n collectStatsForResource(nextResource, stats);\n\n // aggregating the state\n result.push(nextResource);\n }\n\n if (stats) stats.roundTrips += numberOfRoundTrips;\n\n return result;\n}\n\nasync function processResourceTreeStream(\n treeItems: AsyncIterable<ResourceTreeFrame>,\n finalResources: Set<SignedResourceId>,\n pruningFunction: PruningFunction | undefined,\n stats: TreeLoadingStat | undefined,\n): Promise<{ result: ExtendedResourceData[]; followUpSeeds: SignedResourceId[] }> {\n const result: ExtendedResourceData[] = [];\n const followUpSeeds: SignedResourceId[] = [];\n\n // backend returns two types of frames:\n // - 'resource' frames contain the resource state and are processed normally\n // - 'stopMarker' frames indicate resources that are ignored due stop rules fired\n //\n // Usually stop rules indicates the resources with final state. In that case middle layer\n // should make a decision: has it already loaded the resource or should it be requested for get the latest state?\n for await (const frame of treeItems) {\n if (frame.frameKind === \"stopMarker\") {\n if (stats) stats.stopMarkerFrames++;\n if (finalResources.has(frame.id)) {\n if (stats) stats.stopMarkersSkipped++;\n continue;\n }\n if (stats) stats.stopMarkersFollowUp++;\n followUpSeeds.push(frame.id);\n continue;\n }\n\n // Normal resource frame.\n if (stats) {\n stats.resourceFrames++;\n if (frame.traverseWasStopped) stats.traverseWasStoppedCount++;\n }\n if (finalResources.has(frame.id)) {\n if (stats) stats.finalResourcesSkipped++;\n continue;\n }\n\n let nextResource: 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 // traverseWasStopped: backend matched traverse stop rules — children were not streamed.\n // Mark as terminal; fields are resolved below.\n final: frame.final || frame.traverseWasStopped,\n inputsLocked: frame.inputsLocked,\n outputsLocked: frame.outputsLocked,\n fields: frame.fields,\n kv: frame.kv,\n };\n\n // Apply field rules: traverseWasStopped drops all fields to keep the refCount\n // invariant; pruning function further filters the remaining fields.\n const rawFields = frame.traverseWasStopped ? [] : nextResource.fields;\n const resolvedFields =\n pruningFunction !== undefined\n ? pruningFunction({ ...nextResource, fields: rawFields })\n : rawFields;\n if (stats) stats.prunedFields += nextResource.fields.length - resolvedFields.length;\n nextResource = { ...nextResource, fields: resolvedFields };\n\n collectStatsForResource(nextResource, stats);\n result.push(nextResource);\n }\n\n return { result, followUpSeeds };\n}\n\nasync function loadTreeStateViaResourceTree(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction, fieldFilter, traverseStopRules } =\n loadingRequest;\n\n // Round 0: initial tree traversal.\n const treeItems = tx.resourceTree(seedResources, {\n includeKv: true,\n fieldFilter,\n traverseStopRules,\n });\n\n const { result, followUpSeeds } = await processResourceTreeStream(\n treeItems,\n finalResources,\n pruningFunction,\n stats,\n );\n if (stats) {\n stats.roundTrips++;\n stats.streamRounds++;\n }\n\n // Resolve stop-marker seeds by fetching them (see the note below on why the stop rule is not\n // reapplied). Loop in case a fetch still yields stop markers; dedup fetched ids so shared refs\n // or diamonds cannot loop forever (the id set is finite, so the loop terminates). A referenced\n // resource left unloaded would make updateFromResourceData throw \"orphan resource\".\n let pendingSeeds = followUpSeeds;\n const fetchedSeeds = new Set<SignedResourceId>();\n while (pendingSeeds.length > 0) {\n const roundSeeds = pendingSeeds.filter((id) => !fetchedSeeds.has(id));\n if (roundSeeds.length === 0) break;\n for (const id of roundSeeds) fetchedSeeds.add(id);\n\n // No traverseStopRules here on purpose: the seeds are the stop-marked resources, and the\n // stop rule (finality-based) would re-flag them as stop markers instead of loading their\n // state, leaving resources that reference them as orphans. The retry must fetch them plainly.\n const followUpItems = tx.resourceTree(roundSeeds, {\n includeKv: true,\n fieldFilter,\n });\n const { result: followUpResult, followUpSeeds: nextSeeds } = await processResourceTreeStream(\n followUpItems,\n finalResources,\n pruningFunction,\n stats,\n );\n result.push(...followUpResult);\n if (stats) {\n logger?.info?.(\n `loadTreeStateViaResourceTree: follow-up request for ${roundSeeds.length} stop-marker seeds: ${JSON.stringify(roundSeeds)}`,\n );\n stats.roundTrips++;\n stats.streamRounds++;\n stats.stopMarkerFollowUpRoundTrips++;\n }\n pendingSeeds = nextSeeds;\n }\n\n return result;\n}\n\n/** Given the transaction (preferably read-only) and loading request, executes\n * the tree traversal algorithm, and collects fresh states of resources\n * to update the tree state. */\nexport async function loadTreeState(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n capabilities: readonly string[] = [],\n mode: TraversalMode = \"auto\",\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const startTimestamp = Date.now();\n if (stats) stats.requests++;\n\n try {\n const wantsStreaming =\n mode === \"backend-streaming\" ||\n (mode === \"auto\" && supportsResourceTreeTraversal(capabilities));\n\n if (stats) stats.usedStreaming = wantsStreaming && supportsResourceTreeTraversal(capabilities);\n\n if (wantsStreaming && !supportsResourceTreeTraversal(capabilities)) {\n const msg =\n \"traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS\";\n if (logger) logger.warn(msg);\n else console.warn(msg);\n return await loadTreeStateViaBfs(tx, loadingRequest, stats);\n }\n\n return wantsStreaming\n ? await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger)\n : await loadTreeStateViaBfs(tx, loadingRequest, stats);\n } finally {\n if (stats) stats.millisSpent += Date.now() - startTimestamp;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sync.cjs","names":["ConcurrencyLimitingExecutor","Denque","loadDeltaTreeState"],"sources":["../src/sync.ts"],"sourcesContent":["import type {\n FieldData,\n Filter,\n OptionalSignedResourceId,\n PlTransaction,\n ResourceTreeFrame,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport Denque from \"denque\";\nimport { hasCapability, isNullSignedResourceId } from \"@milaboratories/pl-client\";\nimport type { ExtendedResourceData, PlTreeState, ResourceUpdateStat } from \"./state\";\nimport { ConcurrencyLimitingExecutor, msToHumanReadable } from \"@milaboratories/ts-helpers\";\nimport { loadDeltaTreeState } from \"./delta_sync\";\n\n/** Applied to list of fields in resource data. */\nexport type PruningFunction = (resource: ExtendedResourceData) => FieldData[];\n\nexport interface TreeLoadingRequest {\n /** Resource to prime the traversal algorithm. It is ok, if some of them\n * doesn't exist anymore. Should not contain elements from final resource\n * set. */\n readonly seedResources: SignedResourceId[];\n\n /** Resource ids for which state is already known and not expected to change.\n * Algorithm will not continue traversal over those ids, and states will not\n * be retrieved for them. */\n readonly finalResources: Set<SignedResourceId>;\n\n /** Applied to each resource field list in fallback BFS mode and to streamed results. */\n readonly pruningFunction?: PruningFunction;\n\n /** ResourceTree field filter passed to the backend when supported. */\n readonly fieldFilter?: Filter;\n\n /** ResourceTree traversal stop rules passed to the backend when supported.\n * @deprecated prune with {@link changedSinceToken} instead; ignored by the backend under one. */\n readonly traverseStopRules?: Filter;\n\n /** The tree's roots. Delta seeds at these only when the non-final frontier is empty;\n * resourceTree requires at least one seed. */\n readonly roots: readonly SignedResourceId[];\n\n /** Every id the mirror currently holds, final or not. Delta uses it to tell a reference it\n * must resolve from one already satisfied locally. */\n readonly knownResources: ReadonlySet<SignedResourceId>;\n\n /** Change token from the transaction this request will run in, for a delta walk. Absent\n * means \"send the full tree\", which is also what a token the server cannot use gets. */\n readonly changedSinceToken?: Uint8Array;\n}\n\n/** Controls which tree-loading path is used.\n * - `\"auto\"` (default): use delta polling when the backend advertises `treeChangedSince:v1`,\n * else backend streaming when it advertises `treeFilter:v2`, else client-side BFS.\n * - `\"client-bfs\"`: always use client-side BFS, even on capable backends.\n * - `\"backend-streaming\"`: always prefer backend streaming; if the capability is absent,\n * logs a warning and falls back to BFS (never throws).\n * - `\"backend-delta\"`: always prefer delta polling, which hands the backend the transaction's\n * change token and takes only what changed since it; if `treeChangedSince:v1` is absent,\n * logs a warning and falls back to the best available path (never throws).\n */\nexport type TraversalMode = \"auto\" | \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** A concrete loading algorithm: a {@link TraversalMode} with `\"auto\"` and any unsupported\n * preference already resolved against the server's capabilities. */\nexport type TreeLoadingAlgorithmName = \"client-bfs\" | \"backend-streaming\" | \"backend-delta\";\n\n/** Resolves a traversal mode into the algorithm a tree will run. A tree calls this once, when\n * it is made, and keeps the answer for its whole life, so the choice (and the fallback warning\n * for a preference the backend cannot serve) happens once rather than on every poll. */\nexport function resolveTreeLoadingAlgorithm(\n mode: TraversalMode,\n capabilities: readonly string[] = [],\n logger?: { warn: (msg: string) => void },\n): TreeLoadingAlgorithmName {\n const streaming = supportsResourceTreeTraversal(capabilities);\n const delta = supportsTreeDelta(capabilities);\n switch (mode) {\n case \"client-bfs\":\n return \"client-bfs\";\n case \"backend-delta\":\n if (delta) return \"backend-delta\";\n (logger ?? console).warn(\n \"traversalMode=backend-delta but backend lacks treeChangedSince:v1 capability; falling back to \" +\n (streaming ? \"backend-streaming\" : \"client-bfs\"),\n );\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n case \"backend-streaming\":\n if (streaming) return \"backend-streaming\";\n (logger ?? console).warn(\n \"traversalMode=backend-streaming but backend lacks treeFilter:v2 capability; falling back to BFS\",\n );\n return \"client-bfs\";\n case \"auto\":\n // Delta first: its cost tracks what changed rather than what the tree holds. Streaming\n // is the fallback for a backend that can shape a walk but not date one.\n if (delta) return \"backend-delta\";\n return streaming ? \"backend-streaming\" : \"client-bfs\";\n }\n}\n\n/** Given the current tree state, build the request object to pass to\n * {@link loadTreeState} to load updated state. */\nexport function constructTreeLoadingRequest(\n tree: PlTreeState,\n options: Pick<\n TreeLoadingRequest,\n \"pruningFunction\" | \"fieldFilter\" | \"traverseStopRules\" | \"changedSinceToken\"\n > = {},\n): TreeLoadingRequest {\n const seedResources: SignedResourceId[] = [];\n const finalResources = new Set<SignedResourceId>();\n const materialized = new Set<SignedResourceId>();\n tree.forEachResource((res) => {\n materialized.add(res.id);\n if (res.finalState) finalResources.add(res.id);\n else seedResources.push(res.id);\n });\n\n for (const root of tree.roots) if (!materialized.has(root)) seedResources.push(root);\n\n return {\n seedResources,\n finalResources,\n roots: [...tree.roots],\n knownResources: materialized,\n pruningFunction: options.pruningFunction,\n fieldFilter: options.fieldFilter,\n traverseStopRules: options.traverseStopRules,\n changedSinceToken: options.changedSinceToken,\n };\n}\n\nexport type TreeLoadingStat = ResourceUpdateStat & {\n requests: number;\n roundTrips: number;\n retrievedResources: number;\n retrievedFields: number;\n retrievedKeyValues: number;\n retrievedResourceDataBytes: number;\n retrievedKeyValueBytes: number;\n prunedFields: number;\n finalResourcesSkipped: number;\n millisSpent: number;\n /** Stop-marker frames whose id was already final locally and were skipped. */\n stopMarkersSkipped: number;\n /** Number of follow-up resourceTree() calls issued to resolve unknown stop markers. */\n stopMarkerFollowUpRoundTrips: number;\n /** Backend paths: resourceTree() streams consumed. Streaming spends 1, or 2 with a\n * follow-up; delta spends 1 plus one per resolution round. */\n streamRounds: number;\n /** Backend paths: resource frames received. */\n resourceFrames: number;\n /** Streaming path: stopMarker frames received (both skipped and follow-up). */\n stopMarkerFrames: number;\n /** Streaming path: stop markers that were not final locally and triggered a follow-up fetch. */\n stopMarkersFollowUp: number;\n /** Streaming path: frames where the backend stopped traversal (final or traverseWasStopped). */\n traverseWasStoppedCount: number;\n /** BFS path: resource states actually requested from the backend this cycle (after intra-cycle dedup). */\n bfsResourcesRequested: number;\n /** BFS path: requested resources that no longer exist (undefined reply). */\n bfsResourcesNotFound: number;\n /** Delta path: seed ids handed to the backend, summed over every round of the poll.\n * This is what seeding the whole frontier costs on the uplink. */\n deltaSeedsSent: number;\n /** Delta path: extra rounds spent resolving references a delta body pointed at but the\n * response did not carry. */\n deltaResolutionRounds: number;\n /** Delta path: polls that sent a token and got back a response the size of the whole\n * mirror, which is what a refused token looks like from here - rejection is silent, so this\n * is the only tell. Heuristic: a genuinely large change set trips it too. */\n deltaSuspectedFullAnswers: number;\n};\n\nexport function initialTreeLoadingStat(): TreeLoadingStat {\n return {\n requests: 0,\n roundTrips: 0,\n retrievedResources: 0,\n retrievedFields: 0,\n retrievedKeyValues: 0,\n retrievedResourceDataBytes: 0,\n retrievedKeyValueBytes: 0,\n prunedFields: 0,\n finalResourcesSkipped: 0,\n millisSpent: 0,\n stopMarkersSkipped: 0,\n stopMarkerFollowUpRoundTrips: 0,\n streamRounds: 0,\n resourceFrames: 0,\n stopMarkerFrames: 0,\n stopMarkersFollowUp: 0,\n traverseWasStoppedCount: 0,\n bfsResourcesRequested: 0,\n bfsResourcesNotFound: 0,\n deltaSeedsSent: 0,\n deltaResolutionRounds: 0,\n deltaSuspectedFullAnswers: 0,\n resourcesNew: 0,\n resourcesChanged: 0,\n resourcesUnchanged: 0,\n bytesUnchanged: 0,\n metadataStableChanged: 0,\n bfsRequestsWasted: 0,\n usedStreaming: false,\n fieldsAdded: 0,\n fieldsRemoved: 0,\n fieldsChanged: 0,\n kvChanged: 0,\n readyFlips: 0,\n locksChanged: 0,\n errorsAttached: 0,\n duplicatesResolved: 0,\n resourcesMarkedFinal: 0,\n };\n}\n\nexport function formatTreeLoadingStat(stat: TreeLoadingStat): string {\n return `Requests: ${stat.requests}\nTotal time: ${msToHumanReadable(stat.millisSpent)}\nRound-trips: ${stat.roundTrips}\nResources: ${stat.retrievedResources}\nFields: ${stat.retrievedFields}\nKV: ${stat.retrievedKeyValues}\nData Bytes: ${stat.retrievedResourceDataBytes}\nKV Bytes: ${stat.retrievedKeyValueBytes}\nPruned fields: ${stat.prunedFields}\nFinal resources skipped: ${stat.finalResourcesSkipped}\nStop markers skipped: ${stat.stopMarkersSkipped}\nStop marker follow-up round-trips: ${stat.stopMarkerFollowUpRoundTrips}\nNew resources: ${stat.resourcesNew}\nChanged resources: ${stat.resourcesChanged}\nUnchanged (duplicate re-fetch) resources: ${stat.resourcesUnchanged}\nUnchanged bytes (wasted downlink): ${stat.bytesUnchanged}\nChanged with stable metadata: ${stat.metadataStableChanged}\nBFS fetches wasted on unchanged: ${stat.bfsRequestsWasted}\nUsed streaming: ${stat.usedStreaming}\n[backend] rounds: ${stat.streamRounds}, resource frames: ${stat.resourceFrames}, stop-marker frames: ${stat.stopMarkerFrames}, stop->follow-up: ${stat.stopMarkersFollowUp}, traverse-stopped: ${stat.traverseWasStoppedCount}\n[bfs] resources requested: ${stat.bfsResourcesRequested}, not found: ${stat.bfsResourcesNotFound}\n[delta] seeds sent: ${stat.deltaSeedsSent}, resolution rounds: ${stat.deltaResolutionRounds}, suspected full answers: ${stat.deltaSuspectedFullAnswers}`;\n}\n\nexport function supportsResourceTreeTraversal(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeFilter:v2\");\n}\n\nfunction supportsTreeDelta(capabilities: readonly string[] = []): boolean {\n return hasCapability(capabilities, \"treeChangedSince:v1\");\n}\n\nexport function collectStatsForResource(resource: ExtendedResourceData, stats?: TreeLoadingStat) {\n if (!stats) return;\n stats.retrievedResources++;\n stats.retrievedFields += resource.fields.length;\n stats.retrievedKeyValues += resource.kv.length;\n stats.retrievedResourceDataBytes += resource.data?.length ?? 0;\n for (const kv of resource.kv) stats.retrievedKeyValueBytes += kv.value.length;\n}\n\nasync function loadTreeStateViaBfs(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction } = loadingRequest;\n\n // Limits the number of concurrent gRPC fetches to bound peak memory\n // from in-flight request/response buffers.\n const limiter = new ConcurrencyLimitingExecutor(100);\n\n // Promises of resource states, in the order they were requested.\n const pending = new Denque<Promise<ExtendedResourceData | undefined>>();\n\n // vars to calculate number of roundtrips for stats\n let roundTripToggle: boolean = true;\n let numberOfRoundTrips = 0;\n\n // tracking resources we already requested or queued\n const requested = new Set<SignedResourceId>();\n\n /** Mark a resource for fetching. Deduplicates and respects final-resource set. */\n const requestState = (rid: OptionalSignedResourceId) => {\n if (isNullSignedResourceId(rid) || requested.has(rid)) return;\n\n if (finalResources.has(rid)) {\n if (stats) stats.finalResourcesSkipped++;\n return;\n }\n\n requested.add(rid);\n if (stats) stats.bfsResourcesRequested++;\n\n pending.push(\n limiter.run(async () => {\n const resourceData = tx.getResourceDataIfExists(rid, true);\n const kvData = tx.listKeyValuesIfResourceExists(rid);\n\n // counting round-trip (begin)\n const addRT = roundTripToggle;\n if (roundTripToggle) roundTripToggle = false;\n\n const [resource, kv] = await Promise.all([resourceData, kvData]);\n\n // counting round-trip, actually incrementing counter and returning toggle back,\n // so the next request can acquire it\n if (addRT) {\n numberOfRoundTrips++;\n roundTripToggle = true;\n }\n\n if (resource === undefined) {\n if (stats) stats.bfsResourcesNotFound++;\n return undefined;\n }\n if (kv === undefined) throw new Error(\"Inconsistent replies\");\n\n return { ...resource, kv };\n }),\n );\n };\n\n // sending seed requests\n seedResources.forEach((rid) => requestState(rid));\n\n const result: ExtendedResourceData[] = [];\n let nextPromise: Promise<ExtendedResourceData | undefined> | undefined;\n while ((nextPromise = pending.shift()) !== undefined) {\n // at this point we pause and wait for the next requested resource state to arrive\n let nextResource = await nextPromise;\n if (nextResource === undefined)\n // ignoring resources that were not found (this may happen for seed resource ids)\n continue;\n\n if (pruningFunction !== undefined) {\n // apply field pruning, if requested\n const fieldsAfterPruning = pruningFunction(nextResource);\n // collecting stats\n if (stats) stats.prunedFields += nextResource.fields.length - fieldsAfterPruning.length;\n nextResource = { ...nextResource, fields: fieldsAfterPruning };\n }\n\n // continue traversal over the referenced resources\n requestState(nextResource.error);\n for (const field of nextResource.fields) {\n requestState(field.value);\n requestState(field.error);\n }\n\n // collecting stats\n collectStatsForResource(nextResource, stats);\n\n // aggregating the state\n result.push(nextResource);\n }\n\n if (stats) stats.roundTrips += numberOfRoundTrips;\n\n return result;\n}\n\nasync function processResourceTreeStream(\n treeItems: AsyncIterable<ResourceTreeFrame>,\n finalResources: Set<SignedResourceId>,\n pruningFunction: PruningFunction | undefined,\n stats: TreeLoadingStat | undefined,\n): Promise<{ result: ExtendedResourceData[]; followUpSeeds: SignedResourceId[] }> {\n const result: ExtendedResourceData[] = [];\n const followUpSeeds: SignedResourceId[] = [];\n\n // backend returns two types of frames:\n // - 'resource' frames contain the resource state and are processed normally\n // - 'stopMarker' frames indicate resources that are ignored due stop rules fired\n //\n // Usually stop rules indicates the resources with final state. In that case middle layer\n // should make a decision: has it already loaded the resource or should it be requested for get the latest state?\n for await (const frame of treeItems) {\n if (frame.frameKind === \"stopMarker\") {\n if (stats) stats.stopMarkerFrames++;\n if (finalResources.has(frame.id)) {\n if (stats) stats.stopMarkersSkipped++;\n continue;\n }\n if (stats) stats.stopMarkersFollowUp++;\n followUpSeeds.push(frame.id);\n continue;\n }\n\n // Normal resource frame.\n if (stats) {\n stats.resourceFrames++;\n if (frame.traverseWasStopped) stats.traverseWasStoppedCount++;\n }\n if (finalResources.has(frame.id)) {\n if (stats) stats.finalResourcesSkipped++;\n continue;\n }\n\n let nextResource: 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 // traverseWasStopped: backend matched traverse stop rules — children were not streamed.\n // Mark as terminal; fields are resolved below.\n final: frame.final || frame.traverseWasStopped,\n inputsLocked: frame.inputsLocked,\n outputsLocked: frame.outputsLocked,\n fields: frame.fields,\n kv: frame.kv,\n };\n\n // Apply field rules: traverseWasStopped drops all fields to keep the refCount\n // invariant; pruning function further filters the remaining fields.\n const rawFields = frame.traverseWasStopped ? [] : nextResource.fields;\n const resolvedFields =\n pruningFunction !== undefined\n ? pruningFunction({ ...nextResource, fields: rawFields })\n : rawFields;\n if (stats) stats.prunedFields += nextResource.fields.length - resolvedFields.length;\n nextResource = { ...nextResource, fields: resolvedFields };\n\n collectStatsForResource(nextResource, stats);\n result.push(nextResource);\n }\n\n return { result, followUpSeeds };\n}\n\nasync function loadTreeStateViaResourceTree(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const { seedResources, finalResources, pruningFunction, fieldFilter, traverseStopRules } =\n loadingRequest;\n\n // Round 0: initial tree traversal.\n const treeItems = tx.resourceTree(seedResources, {\n includeKv: true,\n fieldFilter,\n traverseStopRules,\n });\n\n const { result, followUpSeeds } = await processResourceTreeStream(\n treeItems,\n finalResources,\n pruningFunction,\n stats,\n );\n if (stats) {\n stats.roundTrips++;\n stats.streamRounds++;\n }\n\n // Resolve stop-marker seeds by fetching them (see the note below on why the stop rule is not\n // reapplied). Loop in case a fetch still yields stop markers; dedup fetched ids so shared refs\n // or diamonds cannot loop forever (the id set is finite, so the loop terminates). A referenced\n // resource left unloaded would make updateFromResourceData throw \"orphan resource\".\n let pendingSeeds = followUpSeeds;\n const fetchedSeeds = new Set<SignedResourceId>();\n while (pendingSeeds.length > 0) {\n const roundSeeds = pendingSeeds.filter((id) => !fetchedSeeds.has(id));\n if (roundSeeds.length === 0) break;\n for (const id of roundSeeds) fetchedSeeds.add(id);\n\n // No traverseStopRules here on purpose: the seeds are the stop-marked resources, and the\n // stop rule (finality-based) would re-flag them as stop markers instead of loading their\n // state, leaving resources that reference them as orphans. The retry must fetch them plainly.\n const followUpItems = tx.resourceTree(roundSeeds, {\n includeKv: true,\n fieldFilter,\n });\n const { result: followUpResult, followUpSeeds: nextSeeds } = await processResourceTreeStream(\n followUpItems,\n finalResources,\n pruningFunction,\n stats,\n );\n result.push(...followUpResult);\n if (stats) {\n logger?.info?.(\n `loadTreeStateViaResourceTree: follow-up request for ${roundSeeds.length} stop-marker seeds: ${JSON.stringify(roundSeeds)}`,\n );\n stats.roundTrips++;\n stats.streamRounds++;\n stats.stopMarkerFollowUpRoundTrips++;\n }\n pendingSeeds = nextSeeds;\n }\n\n return result;\n}\n\n/** Given the transaction (preferably read-only) and loading request, executes\n * the tree traversal algorithm, and collects fresh states of resources\n * to update the tree state. */\nexport async function loadTreeState(\n tx: PlTransaction,\n loadingRequest: TreeLoadingRequest,\n stats?: TreeLoadingStat,\n capabilities: readonly string[] = [],\n mode: TraversalMode = \"auto\",\n logger?: { warn: (msg: string) => void; info?: (msg: unknown) => void },\n): Promise<ExtendedResourceData[]> {\n const startTimestamp = Date.now();\n if (stats) stats.requests++;\n\n try {\n // A tree passes its pinned algorithm here, which resolves to itself. The mode form is\n // kept for callers that run a single load.\n const algorithm = resolveTreeLoadingAlgorithm(mode, capabilities, logger);\n // Both backend paths, not just streaming: state.ts reads this to attribute a BFS-only\n // wasted-fetch counter, and leaving it false for delta reports phantom waste.\n if (stats) stats.usedStreaming = algorithm !== \"client-bfs\";\n\n switch (algorithm) {\n case \"backend-delta\":\n return await loadDeltaTreeState(tx, loadingRequest, stats, logger);\n case \"backend-streaming\":\n return await loadTreeStateViaResourceTree(tx, loadingRequest, stats, logger);\n case \"client-bfs\":\n return await loadTreeStateViaBfs(tx, loadingRequest, stats);\n default:\n throw new Error(`unknown tree loading algorithm: ${algorithm as string}`);\n }\n } finally {\n if (stats) stats.millisSpent += Date.now() - startTimestamp;\n }\n}\n"],"mappings":"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"}
|