@hops-ops/distributed 0.0.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/README.md +454 -0
- package/dist/auth-headers.d.ts +8 -0
- package/dist/auth-headers.js +35 -0
- package/dist/diagnostics.d.ts +2 -0
- package/dist/diagnostics.js +1 -0
- package/dist/document.d.ts +8 -0
- package/dist/document.js +6 -0
- package/dist/identity.d.ts +23 -0
- package/dist/identity.js +73 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/internal/cache-engine/create.d.ts +5 -0
- package/dist/internal/cache-engine/create.js +7 -0
- package/dist/internal/cache-engine/engine.d.ts +32 -0
- package/dist/internal/cache-engine/engine.js +1287 -0
- package/dist/internal/cache-engine/errors.d.ts +5 -0
- package/dist/internal/cache-engine/errors.js +10 -0
- package/dist/internal/cache-engine/helpers.d.ts +55 -0
- package/dist/internal/cache-engine/helpers.js +545 -0
- package/dist/internal/cache-engine/index.d.ts +3 -0
- package/dist/internal/cache-engine/index.js +2 -0
- package/dist/internal/cache-engine/types.d.ts +300 -0
- package/dist/internal/cache-engine/types.js +1 -0
- package/dist/internal/cache-engine.d.ts +3 -0
- package/dist/internal/cache-engine.js +2 -0
- package/dist/lib/assert-name.d.ts +2 -0
- package/dist/lib/assert-name.js +6 -0
- package/dist/lib/compare-code-units.d.ts +2 -0
- package/dist/lib/compare-code-units.js +4 -0
- package/dist/lib/deep-equal.d.ts +2 -0
- package/dist/lib/deep-equal.js +24 -0
- package/dist/lib/freeze-record.d.ts +2 -0
- package/dist/lib/freeze-record.js +13 -0
- package/dist/lib/index.d.ts +6 -0
- package/dist/lib/index.js +6 -0
- package/dist/lib/is-plain-record.d.ts +2 -0
- package/dist/lib/is-plain-record.js +8 -0
- package/dist/lib/report.d.ts +7 -0
- package/dist/lib/report.js +26 -0
- package/dist/protocol.d.ts +179 -0
- package/dist/protocol.js +558 -0
- package/dist/react/context.d.ts +20 -0
- package/dist/react/context.js +24 -0
- package/dist/react/index.d.ts +2 -0
- package/dist/react/index.js +3 -0
- package/dist/react/query.d.ts +20 -0
- package/dist/react/query.js +99 -0
- package/dist/replica/command-id.d.ts +2 -0
- package/dist/replica/command-id.js +19 -0
- package/dist/replica/command-runtime/constants.d.ts +5 -0
- package/dist/replica/command-runtime/constants.js +5 -0
- package/dist/replica/command-runtime/create.d.ts +2 -0
- package/dist/replica/command-runtime/create.js +644 -0
- package/dist/replica/command-runtime/errors.d.ts +16 -0
- package/dist/replica/command-runtime/errors.js +41 -0
- package/dist/replica/command-runtime/index.d.ts +5 -0
- package/dist/replica/command-runtime/index.js +4 -0
- package/dist/replica/command-runtime/lib/binding.d.ts +4 -0
- package/dist/replica/command-runtime/lib/binding.js +64 -0
- package/dist/replica/command-runtime/lib/effects.d.ts +11 -0
- package/dist/replica/command-runtime/lib/effects.js +89 -0
- package/dist/replica/command-runtime/lib/index.d.ts +9 -0
- package/dist/replica/command-runtime/lib/index.js +9 -0
- package/dist/replica/command-runtime/lib/inventory.d.ts +10 -0
- package/dist/replica/command-runtime/lib/inventory.js +141 -0
- package/dist/replica/command-runtime/lib/output.d.ts +13 -0
- package/dist/replica/command-runtime/lib/output.js +190 -0
- package/dist/replica/command-runtime/lib/projection.d.ts +20 -0
- package/dist/replica/command-runtime/lib/projection.js +255 -0
- package/dist/replica/command-runtime/lib/status.d.ts +7 -0
- package/dist/replica/command-runtime/lib/status.js +127 -0
- package/dist/replica/command-runtime/lib/transport.d.ts +16 -0
- package/dist/replica/command-runtime/lib/transport.js +112 -0
- package/dist/replica/command-runtime/lib/util.d.ts +16 -0
- package/dist/replica/command-runtime/lib/util.js +127 -0
- package/dist/replica/command-runtime/lifecycle.d.ts +11 -0
- package/dist/replica/command-runtime/lifecycle.js +13 -0
- package/dist/replica/command-runtime/symbols.d.ts +30 -0
- package/dist/replica/command-runtime/symbols.js +30 -0
- package/dist/replica/command-runtime/types.d.ts +222 -0
- package/dist/replica/command-runtime/types.js +1 -0
- package/dist/replica/command-runtime.d.ts +3 -0
- package/dist/replica/command-runtime.js +2 -0
- package/dist/replica/commands/clone.d.ts +12 -0
- package/dist/replica/commands/clone.js +187 -0
- package/dist/replica/commands/constants.d.ts +6 -0
- package/dist/replica/commands/constants.js +6 -0
- package/dist/replica/commands/errors.d.ts +7 -0
- package/dist/replica/commands/errors.js +22 -0
- package/dist/replica/commands/implementation.d.ts +8 -0
- package/dist/replica/commands/implementation.js +8 -0
- package/dist/replica/commands/index.d.ts +5 -0
- package/dist/replica/commands/index.js +4 -0
- package/dist/replica/commands/prepare.d.ts +27 -0
- package/dist/replica/commands/prepare.js +77 -0
- package/dist/replica/commands/presets.d.ts +12 -0
- package/dist/replica/commands/presets.js +84 -0
- package/dist/replica/commands/receipt.d.ts +3 -0
- package/dist/replica/commands/receipt.js +36 -0
- package/dist/replica/commands/resolve.d.ts +11 -0
- package/dist/replica/commands/resolve.js +165 -0
- package/dist/replica/commands/types.d.ts +278 -0
- package/dist/replica/commands/types.js +2 -0
- package/dist/replica/commands/util.d.ts +21 -0
- package/dist/replica/commands/util.js +96 -0
- package/dist/replica/commands/validate.d.ts +18 -0
- package/dist/replica/commands/validate.js +477 -0
- package/dist/replica/commands.d.ts +3 -0
- package/dist/replica/commands.js +2 -0
- package/dist/replica/diagnostics/event-log.d.ts +4 -0
- package/dist/replica/diagnostics/event-log.js +463 -0
- package/dist/replica/diagnostics/implementation.d.ts +4 -0
- package/dist/replica/diagnostics/implementation.js +4 -0
- package/dist/replica/diagnostics/index.d.ts +2 -0
- package/dist/replica/diagnostics/index.js +1 -0
- package/dist/replica/diagnostics/inspect.d.ts +6 -0
- package/dist/replica/diagnostics/inspect.js +170 -0
- package/dist/replica/diagnostics/types.d.ts +295 -0
- package/dist/replica/diagnostics/types.js +2 -0
- package/dist/replica/diagnostics.d.ts +3 -0
- package/dist/replica/diagnostics.js +2 -0
- package/dist/replica/distributed-replica/clocks.d.ts +23 -0
- package/dist/replica/distributed-replica/clocks.js +156 -0
- package/dist/replica/distributed-replica/constants.d.ts +12 -0
- package/dist/replica/distributed-replica/constants.js +10 -0
- package/dist/replica/distributed-replica/helpers.d.ts +48 -0
- package/dist/replica/distributed-replica/helpers.js +445 -0
- package/dist/replica/distributed-replica/hydration.d.ts +19 -0
- package/dist/replica/distributed-replica/hydration.js +380 -0
- package/dist/replica/distributed-replica/impl-diagnostics.d.ts +22 -0
- package/dist/replica/distributed-replica/impl-diagnostics.js +189 -0
- package/dist/replica/distributed-replica/impl-fetch-live.d.ts +39 -0
- package/dist/replica/distributed-replica/impl-fetch-live.js +395 -0
- package/dist/replica/distributed-replica/impl-hydration-orchestrate.d.ts +56 -0
- package/dist/replica/distributed-replica/impl-hydration-orchestrate.js +299 -0
- package/dist/replica/distributed-replica/impl-optimistic.d.ts +31 -0
- package/dist/replica/distributed-replica/impl-optimistic.js +172 -0
- package/dist/replica/distributed-replica/impl-protocol.d.ts +53 -0
- package/dist/replica/distributed-replica/impl-protocol.js +108 -0
- package/dist/replica/distributed-replica/impl.d.ts +44 -0
- package/dist/replica/distributed-replica/impl.js +1776 -0
- package/dist/replica/distributed-replica/index.d.ts +2 -0
- package/dist/replica/distributed-replica/index.js +4 -0
- package/dist/replica/distributed-replica/optimistic.d.ts +30 -0
- package/dist/replica/distributed-replica/optimistic.js +221 -0
- package/dist/replica/distributed-replica/types.d.ts +147 -0
- package/dist/replica/distributed-replica/types.js +1 -0
- package/dist/replica/distributed-replica/watch.d.ts +19 -0
- package/dist/replica/distributed-replica/watch.js +95 -0
- package/dist/replica/distributed-replica.d.ts +2 -0
- package/dist/replica/distributed-replica.js +2 -0
- package/dist/replica/graphql-transport.d.ts +24 -0
- package/dist/replica/graphql-transport.js +122 -0
- package/dist/replica/identity/clone.d.ts +6 -0
- package/dist/replica/identity/clone.js +54 -0
- package/dist/replica/identity/codec.d.ts +48 -0
- package/dist/replica/identity/codec.js +789 -0
- package/dist/replica/identity/constants.d.ts +3 -0
- package/dist/replica/identity/constants.js +18 -0
- package/dist/replica/identity/implementation.d.ts +5 -0
- package/dist/replica/identity/implementation.js +5 -0
- package/dist/replica/identity/index.d.ts +3 -0
- package/dist/replica/identity/index.js +3 -0
- package/dist/replica/identity/keys.d.ts +29 -0
- package/dist/replica/identity/keys.js +173 -0
- package/dist/replica/identity.d.ts +2 -0
- package/dist/replica/identity.js +2 -0
- package/dist/replica/index-maintenance/engine.d.ts +84 -0
- package/dist/replica/index-maintenance/engine.js +677 -0
- package/dist/replica/index-maintenance/implementation.d.ts +4 -0
- package/dist/replica/index-maintenance/implementation.js +4 -0
- package/dist/replica/index-maintenance/index.d.ts +2 -0
- package/dist/replica/index-maintenance/index.js +1 -0
- package/dist/replica/index-maintenance/registry.d.ts +2 -0
- package/dist/replica/index-maintenance/registry.js +49 -0
- package/dist/replica/index-maintenance/types.d.ts +58 -0
- package/dist/replica/index-maintenance/types.js +1 -0
- package/dist/replica/index-maintenance.d.ts +3 -0
- package/dist/replica/index-maintenance.js +2 -0
- package/dist/replica/index.d.ts +17 -0
- package/dist/replica/index.js +9 -0
- package/dist/replica/materialize.d.ts +10 -0
- package/dist/replica/materialize.js +163 -0
- package/dist/replica/normalize.d.ts +27 -0
- package/dist/replica/normalize.js +298 -0
- package/dist/replica/operation-binding.d.ts +14 -0
- package/dist/replica/operation-binding.js +76 -0
- package/dist/replica/persistence/idb.d.ts +20 -0
- package/dist/replica/persistence/idb.js +193 -0
- package/dist/replica/persistence/implementation.d.ts +4 -0
- package/dist/replica/persistence/implementation.js +4 -0
- package/dist/replica/persistence/index.d.ts +2 -0
- package/dist/replica/persistence/index.js +1 -0
- package/dist/replica/persistence/state.d.ts +104 -0
- package/dist/replica/persistence/state.js +734 -0
- package/dist/replica/persistence/types.d.ts +54 -0
- package/dist/replica/persistence/types.js +1 -0
- package/dist/replica/persistence.d.ts +3 -0
- package/dist/replica/persistence.js +2 -0
- package/dist/replica/query-plan/constants.d.ts +13 -0
- package/dist/replica/query-plan/constants.js +7 -0
- package/dist/replica/query-plan/filter.d.ts +39 -0
- package/dist/replica/query-plan/filter.js +544 -0
- package/dist/replica/query-plan/index.d.ts +4 -0
- package/dist/replica/query-plan/index.js +3 -0
- package/dist/replica/query-plan/order.d.ts +11 -0
- package/dist/replica/query-plan/order.js +158 -0
- package/dist/replica/query-plan/pagination.d.ts +6 -0
- package/dist/replica/query-plan/pagination.js +90 -0
- package/dist/replica/query-plan/resolve.d.ts +14 -0
- package/dist/replica/query-plan/resolve.js +163 -0
- package/dist/replica/query-plan/types.d.ts +89 -0
- package/dist/replica/query-plan/types.js +1 -0
- package/dist/replica/query-plan/util.d.ts +13 -0
- package/dist/replica/query-plan/util.js +62 -0
- package/dist/replica/query-plan.d.ts +3 -0
- package/dist/replica/query-plan.js +2 -0
- package/dist/replica/revalidation.d.ts +9 -0
- package/dist/replica/revalidation.js +86 -0
- package/dist/replica/selection.d.ts +20 -0
- package/dist/replica/selection.js +109 -0
- package/dist/replica/types.d.ts +657 -0
- package/dist/replica/types.js +1 -0
- package/dist/request.d.ts +17 -0
- package/dist/request.js +123 -0
- package/dist/sveltekit/auth.d.ts +18 -0
- package/dist/sveltekit/auth.js +9 -0
- package/dist/sveltekit/context.d.ts +28 -0
- package/dist/sveltekit/context.js +58 -0
- package/dist/sveltekit/index.d.ts +4 -0
- package/dist/sveltekit/index.js +4 -0
- package/dist/sveltekit/replica.d.ts +145 -0
- package/dist/sveltekit/replica.js +491 -0
- package/dist/sveltekit/server-replica.d.ts +52 -0
- package/dist/sveltekit/server-replica.js +176 -0
- package/dist/sveltekit/vite.d.ts +103 -0
- package/dist/sveltekit/vite.js +878 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.js +1 -0
- package/dist/websocket.d.ts +41 -0
- package/dist/websocket.js +187 -0
- package/package.json +95 -0
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
import { replicaIndexKey, resolveArguments } from '../identity.js';
|
|
2
|
+
import { compareReplicaOrder, decideReplicaPaginationMaintenance, evaluateReplicaFilter } from '../query-plan.js';
|
|
3
|
+
import { compareCodeUnits } from '../../lib/compare-code-units.js';
|
|
4
|
+
export function formatReplicaIndexStaleReason(reasonValue) {
|
|
5
|
+
const path = reasonValue.path.length === 0
|
|
6
|
+
? ''
|
|
7
|
+
: `:${reasonValue.path.map(String).join('.')}`;
|
|
8
|
+
return `query-plan:${reasonValue.code}${path}`;
|
|
9
|
+
}
|
|
10
|
+
export function compilePlans(registration, artifact, variables) {
|
|
11
|
+
const plans = [];
|
|
12
|
+
const trustedPresets = artifact.protocol.trustedPresets;
|
|
13
|
+
for (const [index, root] of artifact.roots.entries()) {
|
|
14
|
+
const path = ['roots', index, root.field];
|
|
15
|
+
const targetModel = selectionModel(root.selection);
|
|
16
|
+
const kind = targetModel === undefined ? 'snapshot' : 'entity';
|
|
17
|
+
const argumentsValue = resolveArguments(root.arguments, variables, root.coverage);
|
|
18
|
+
plans.push(plan({
|
|
19
|
+
registration,
|
|
20
|
+
kind,
|
|
21
|
+
exactIndexKey: replicaIndexKey({
|
|
22
|
+
field: root.field,
|
|
23
|
+
arguments: argumentsValue
|
|
24
|
+
}),
|
|
25
|
+
targetModel,
|
|
26
|
+
field: root.field,
|
|
27
|
+
arguments: argumentsValue,
|
|
28
|
+
dependencies: root.dependencies,
|
|
29
|
+
cardinality: root.cardinality,
|
|
30
|
+
filter: root.filter,
|
|
31
|
+
order: root.order,
|
|
32
|
+
pagination: root.pagination,
|
|
33
|
+
coverage: root.coverage,
|
|
34
|
+
variables,
|
|
35
|
+
trustedPresets,
|
|
36
|
+
path
|
|
37
|
+
}));
|
|
38
|
+
compileSelectionPlans(plans, registration, root.selection, targetModel, variables, trustedPresets, path);
|
|
39
|
+
}
|
|
40
|
+
return Object.freeze(plans);
|
|
41
|
+
}
|
|
42
|
+
export function compileSelectionPlans(output, registration, selection, parentModel, variables, trustedPresets, path) {
|
|
43
|
+
for (const [index, member] of selection.members.entries()) {
|
|
44
|
+
if (member.kind !== 'branch')
|
|
45
|
+
continue;
|
|
46
|
+
compileBranchPlan(output, registration, member, parentModel, variables, trustedPresets, [...path, 'members', index, member.field]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
export function compileBranchPlan(output, registration, branch, parentModel, variables, trustedPresets, path) {
|
|
50
|
+
const argumentsValue = resolveArguments(branch.arguments, variables, branch.coverage);
|
|
51
|
+
const targetModel = selectionModel(branch.selection);
|
|
52
|
+
output.push(plan({
|
|
53
|
+
registration,
|
|
54
|
+
kind: branch.semantic === 'relationship' ? 'relationship' : 'snapshot',
|
|
55
|
+
parentModel,
|
|
56
|
+
targetModel,
|
|
57
|
+
field: branch.field,
|
|
58
|
+
arguments: argumentsValue,
|
|
59
|
+
dependencies: branch.dependencies,
|
|
60
|
+
cardinality: branch.cardinality,
|
|
61
|
+
filter: branch.filter,
|
|
62
|
+
order: branch.order,
|
|
63
|
+
pagination: branch.pagination,
|
|
64
|
+
coverage: branch.coverage,
|
|
65
|
+
relationship: branch.relationship,
|
|
66
|
+
variables,
|
|
67
|
+
trustedPresets,
|
|
68
|
+
path
|
|
69
|
+
}));
|
|
70
|
+
compileSelectionPlans(output, registration, branch.selection, targetModel ?? parentModel, variables, trustedPresets, path);
|
|
71
|
+
}
|
|
72
|
+
export function plan(value) {
|
|
73
|
+
const signature = canonicalValue({
|
|
74
|
+
kind: value.kind,
|
|
75
|
+
parentModel: value.parentModel ?? null,
|
|
76
|
+
targetModel: value.targetModel ?? null,
|
|
77
|
+
field: value.field,
|
|
78
|
+
arguments: value.arguments,
|
|
79
|
+
dependencies: [...value.dependencies].sort(compareCodeUnits),
|
|
80
|
+
cardinality: value.cardinality,
|
|
81
|
+
filter: value.filter ?? null,
|
|
82
|
+
order: value.order ?? null,
|
|
83
|
+
pagination: value.pagination ?? null,
|
|
84
|
+
coverage: value.coverage ?? null,
|
|
85
|
+
relationship: value.relationship ?? null,
|
|
86
|
+
trustedPresets: value.trustedPresets
|
|
87
|
+
});
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
...value,
|
|
90
|
+
arguments: cloneRecord(value.arguments),
|
|
91
|
+
dependencies: Object.freeze([...new Set(value.dependencies)].sort(compareCodeUnits)),
|
|
92
|
+
trustedPresets: Object.freeze(value.trustedPresets.map((descriptor) => Object.freeze({ name: descriptor.name, codec: descriptor.codec }))),
|
|
93
|
+
path: Object.freeze([...value.path]),
|
|
94
|
+
signature
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
export function selectionModel(selection) {
|
|
98
|
+
return selection.storage.kind === 'normalized'
|
|
99
|
+
? selection.storage.model
|
|
100
|
+
: undefined;
|
|
101
|
+
}
|
|
102
|
+
export function instantiatePlans(plans, indexes) {
|
|
103
|
+
const result = new Map();
|
|
104
|
+
for (const value of plans) {
|
|
105
|
+
if (value.exactIndexKey !== undefined) {
|
|
106
|
+
if (indexes.has(value.exactIndexKey)) {
|
|
107
|
+
appendPlan(result, value.exactIndexKey, value);
|
|
108
|
+
}
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
for (const index of indexes.values()) {
|
|
112
|
+
const parent = index.metadata.parent;
|
|
113
|
+
if (parent === undefined ||
|
|
114
|
+
index.metadata.field !== value.field ||
|
|
115
|
+
replicaIndexKey({
|
|
116
|
+
parent,
|
|
117
|
+
field: value.field,
|
|
118
|
+
arguments: value.arguments
|
|
119
|
+
}) !== index.key ||
|
|
120
|
+
(value.parentModel !== undefined &&
|
|
121
|
+
!recordKeyMatchesModel(parent, value.parentModel))) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
appendPlan(result, index.key, value);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
export function appendPlan(output, key, value) {
|
|
130
|
+
const current = output.get(key) ?? [];
|
|
131
|
+
if (current.some((candidate) => candidate.registration === value.registration &&
|
|
132
|
+
candidate.signature === value.signature)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
current.push(value);
|
|
136
|
+
output.set(key, current);
|
|
137
|
+
}
|
|
138
|
+
export function applyLayers(base, layers) {
|
|
139
|
+
const records = new Map();
|
|
140
|
+
for (const input of base) {
|
|
141
|
+
validateName(input.key, 'record key');
|
|
142
|
+
validateName(input.model, 'record model');
|
|
143
|
+
if (records.has(input.key)) {
|
|
144
|
+
throw new TypeError(`duplicate maintenance record: ${input.key}`);
|
|
145
|
+
}
|
|
146
|
+
records.set(input.key, {
|
|
147
|
+
key: input.key,
|
|
148
|
+
model: input.model,
|
|
149
|
+
fields: { ...input.fields }
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const baseRecords = new Map([...records].map(([key, record]) => [
|
|
153
|
+
key,
|
|
154
|
+
{
|
|
155
|
+
key: record.key,
|
|
156
|
+
model: record.model,
|
|
157
|
+
fields: { ...record.fields }
|
|
158
|
+
}
|
|
159
|
+
]));
|
|
160
|
+
const recordChanges = [];
|
|
161
|
+
const relationshipChanges = [];
|
|
162
|
+
const invalidatedDependencies = new Set();
|
|
163
|
+
const allDependencies = new Set();
|
|
164
|
+
const layerIds = new Set();
|
|
165
|
+
for (const layer of layers) {
|
|
166
|
+
validateName(layer.id, 'semantic layer id');
|
|
167
|
+
if (layerIds.has(layer.id)) {
|
|
168
|
+
throw new TypeError(`duplicate semantic layer: ${layer.id}`);
|
|
169
|
+
}
|
|
170
|
+
layerIds.add(layer.id);
|
|
171
|
+
for (const change of layer.changes) {
|
|
172
|
+
if (change.kind === 'invalidate') {
|
|
173
|
+
for (const dependency of validateDependencies(change.dependencies)) {
|
|
174
|
+
invalidatedDependencies.add(dependency);
|
|
175
|
+
allDependencies.add(dependency);
|
|
176
|
+
}
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (isRelationshipChange(change)) {
|
|
180
|
+
validateName(change.sourceModel, 'relationship source model');
|
|
181
|
+
validateName(change.field, 'relationship field');
|
|
182
|
+
validateName(change.targetModel, 'relationship target model');
|
|
183
|
+
validateName(change.sourceKey, 'relationship source key');
|
|
184
|
+
validateName(change.targetKey, 'relationship target key');
|
|
185
|
+
for (const dependency of validateDependencies(change.dependencies)) {
|
|
186
|
+
allDependencies.add(dependency);
|
|
187
|
+
}
|
|
188
|
+
relationshipChanges.push(change);
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
validateName(change.model, 'record change model');
|
|
192
|
+
validateName(change.key, 'record change key');
|
|
193
|
+
for (const dependency of validateDependencies(change.dependencies ?? [])) {
|
|
194
|
+
allDependencies.add(dependency);
|
|
195
|
+
}
|
|
196
|
+
recordChanges.push(change);
|
|
197
|
+
if (change.kind === 'delete') {
|
|
198
|
+
records.delete(change.key);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const current = records.get(change.key);
|
|
202
|
+
if (current !== undefined && current.model !== change.model) {
|
|
203
|
+
throw new TypeError(`record model changed for ${change.key}`);
|
|
204
|
+
}
|
|
205
|
+
records.set(change.key, {
|
|
206
|
+
key: change.key,
|
|
207
|
+
model: change.model,
|
|
208
|
+
fields: { ...(current?.fields ?? {}), ...change.fields }
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
baseRecords,
|
|
214
|
+
records,
|
|
215
|
+
recordChanges: Object.freeze(recordChanges),
|
|
216
|
+
relationshipChanges: Object.freeze(relationshipChanges),
|
|
217
|
+
invalidatedDependencies,
|
|
218
|
+
allDependencies
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export function validateIndexes(input) {
|
|
222
|
+
const indexes = new Map();
|
|
223
|
+
for (const index of input) {
|
|
224
|
+
validateName(index.key, 'index key');
|
|
225
|
+
if (indexes.has(index.key)) {
|
|
226
|
+
throw new TypeError(`duplicate maintenance index: ${index.key}`);
|
|
227
|
+
}
|
|
228
|
+
indexes.set(index.key, index);
|
|
229
|
+
}
|
|
230
|
+
return indexes;
|
|
231
|
+
}
|
|
232
|
+
export function planAffected(planValue, index, changes) {
|
|
233
|
+
if (planValue.kind === 'snapshot' &&
|
|
234
|
+
intersects(planValue.dependencies, changes.allDependencies)) {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
if (intersects(planValue.dependencies, changes.invalidatedDependencies)) {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
if (planValue.targetModel !== undefined &&
|
|
241
|
+
changes.recordChanges.some(({ model }) => model === planValue.targetModel)) {
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
if (planValue.kind === 'relationship' &&
|
|
245
|
+
planValue.parentModel !== undefined &&
|
|
246
|
+
changes.recordChanges.some((change) => parentRecordChangeAffectsRelationship(planValue, index, change))) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
return changes.relationshipChanges.some((change) => relationshipChangeMatches(planValue, index, change));
|
|
250
|
+
}
|
|
251
|
+
export function evaluatePlan(planValue, index, changes, trustedPresets) {
|
|
252
|
+
if (planValue.kind === 'snapshot') {
|
|
253
|
+
return stale(index.key, reason('aggregate_dependency_changed', planValue.path, 'an aggregate or embedded query snapshot dependency changed'));
|
|
254
|
+
}
|
|
255
|
+
if (intersects(planValue.dependencies, changes.invalidatedDependencies)) {
|
|
256
|
+
return stale(index.key, reason('dependency_changed', planValue.path, 'a declared dependency changed without locally provable row evidence'));
|
|
257
|
+
}
|
|
258
|
+
/*
|
|
259
|
+
* Freshness and structure are independent. Revalidation deliberately marks
|
|
260
|
+
* a complete index stale while retaining its last authoritative membership.
|
|
261
|
+
* Pending semantic layers must keep rebasing over that membership; the
|
|
262
|
+
* derived write preserves the stale metadata until the network result lands.
|
|
263
|
+
*/
|
|
264
|
+
if (!index.complete) {
|
|
265
|
+
return stale(index.key, reason('index_incomplete', ['indexes', index.key], 'only a structurally complete index can be maintained locally'));
|
|
266
|
+
}
|
|
267
|
+
if (planValue.cardinality !== 'many') {
|
|
268
|
+
const targetChanges = changes.recordChanges.filter((change) => change.model === planValue.targetModel);
|
|
269
|
+
if (changes.relationshipChanges.length === 0 &&
|
|
270
|
+
targetChanges.length > 0 &&
|
|
271
|
+
targetChanges.every((change) => change.kind === 'upsert' && index.records.includes(change.key))) {
|
|
272
|
+
// Updating fields on the already-selected singular identity cannot
|
|
273
|
+
// change that index's membership. The sparse record dependency will
|
|
274
|
+
// still publish selected field changes to the UI.
|
|
275
|
+
return unchanged(index.key);
|
|
276
|
+
}
|
|
277
|
+
return stale(index.key, reason('unsupported_cardinality', planValue.path, 'local query-index maintenance is limited to collection fields'));
|
|
278
|
+
}
|
|
279
|
+
if (index.metadata.field !== planValue.field ||
|
|
280
|
+
!sameValue(index.metadata.arguments, planValue.arguments)) {
|
|
281
|
+
return stale(index.key, reason('invalid_index_metadata', ['indexes', index.key], 'stored index metadata does not match its compiler plan'));
|
|
282
|
+
}
|
|
283
|
+
const duplicate = firstDuplicate(index.records);
|
|
284
|
+
if (duplicate !== undefined) {
|
|
285
|
+
return stale(index.key, reason('duplicate_index_record', ['indexes', index.key], `stored index contains duplicate record ${duplicate}`));
|
|
286
|
+
}
|
|
287
|
+
if (planValue.kind === 'relationship' &&
|
|
288
|
+
planValue.relationship?.maintenance !== 'local') {
|
|
289
|
+
return stale(index.key, reason('relationship_maintenance_revalidate', planValue.path, 'the compiler marked this relationship as revalidation-only'));
|
|
290
|
+
}
|
|
291
|
+
return maintainEntityIndex(planValue, index, changes, trustedPresets);
|
|
292
|
+
}
|
|
293
|
+
export function maintainEntityIndex(planValue, index, changes, trustedPresets) {
|
|
294
|
+
const current = [...index.records];
|
|
295
|
+
const members = new Set(current);
|
|
296
|
+
const candidates = new Set();
|
|
297
|
+
for (const change of changes.recordChanges) {
|
|
298
|
+
if (change.model === planValue.targetModel)
|
|
299
|
+
candidates.add(change.key);
|
|
300
|
+
}
|
|
301
|
+
const relationshipOverrides = new Map();
|
|
302
|
+
if (planValue.kind === 'relationship') {
|
|
303
|
+
const parentChanged = changes.recordChanges.some((change) => parentRecordChangeAffectsRelationship(planValue, index, change));
|
|
304
|
+
if (parentChanged) {
|
|
305
|
+
return stale(index.key, reason('relationship_mapping_unknown', planValue.path, 'a parent-key dependency changed and sparse target coverage cannot prove the new relationship'));
|
|
306
|
+
}
|
|
307
|
+
for (const change of changes.relationshipChanges) {
|
|
308
|
+
if (!relationshipChangeMatches(planValue, index, change))
|
|
309
|
+
continue;
|
|
310
|
+
const missing = planValue.relationship.dependencies.filter((dependency) => !change.dependencies.includes(dependency));
|
|
311
|
+
if (missing.length > 0) {
|
|
312
|
+
return stale(index.key, reason('relationship_dependency_missing', planValue.path, `relationship change omits declared dependencies: ${missing.join(', ')}`));
|
|
313
|
+
}
|
|
314
|
+
candidates.add(change.targetKey);
|
|
315
|
+
relationshipOverrides.set(change.targetKey, change.kind === 'link');
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
let inserted = false;
|
|
319
|
+
let removed = false;
|
|
320
|
+
for (const key of candidates) {
|
|
321
|
+
const record = changes.records.get(key);
|
|
322
|
+
let related = true;
|
|
323
|
+
if (planValue.kind === 'relationship') {
|
|
324
|
+
const override = relationshipOverrides.get(key);
|
|
325
|
+
if (override !== undefined) {
|
|
326
|
+
related = override;
|
|
327
|
+
}
|
|
328
|
+
else if (members.has(key)) {
|
|
329
|
+
related = true;
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
const relation = relationshipMembership(planValue, index, record, changes.records);
|
|
333
|
+
if ('reason' in relation)
|
|
334
|
+
return stale(index.key, relation.reason);
|
|
335
|
+
related = relation.related;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (record === undefined || !related) {
|
|
339
|
+
if (members.delete(key))
|
|
340
|
+
removed = true;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const filter = evaluateMembership(planValue, record, trustedPresets);
|
|
344
|
+
if ('reason' in filter)
|
|
345
|
+
return stale(index.key, filter.reason);
|
|
346
|
+
if (filter.matches) {
|
|
347
|
+
if (!members.has(key)) {
|
|
348
|
+
members.add(key);
|
|
349
|
+
inserted = true;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (members.delete(key)) {
|
|
353
|
+
removed = true;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
let activeOrderChanged = false;
|
|
357
|
+
if (planValue.order !== undefined) {
|
|
358
|
+
for (const key of candidates) {
|
|
359
|
+
if (!members.has(key))
|
|
360
|
+
continue;
|
|
361
|
+
const before = changes.baseRecords.get(key);
|
|
362
|
+
const after = changes.records.get(key);
|
|
363
|
+
if (before === undefined || after === undefined)
|
|
364
|
+
continue;
|
|
365
|
+
const compared = compareReplicaOrder(planValue.order, before.fields, after.fields, planValue.variables);
|
|
366
|
+
if (compared.result === 'unknown') {
|
|
367
|
+
return stale(index.key, compared.reason);
|
|
368
|
+
}
|
|
369
|
+
if (compared.result !== 'equal') {
|
|
370
|
+
activeOrderChanged = true;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
let records = current.filter((key) => members.has(key));
|
|
376
|
+
for (const key of members) {
|
|
377
|
+
if (!records.includes(key))
|
|
378
|
+
records.push(key);
|
|
379
|
+
}
|
|
380
|
+
const needsOrder = inserted || activeOrderChanged;
|
|
381
|
+
if (needsOrder) {
|
|
382
|
+
if (planValue.order === undefined && inserted) {
|
|
383
|
+
return stale(index.key, reason('missing_order_plan', planValue.path, 'an inserted record cannot be positioned without a compiler order plan'));
|
|
384
|
+
}
|
|
385
|
+
if (planValue.order !== undefined) {
|
|
386
|
+
const sorted = sortRecords(records, planValue.order, changes.records, planValue.variables, planValue.path);
|
|
387
|
+
if ('reason' in sorted)
|
|
388
|
+
return stale(index.key, sorted.reason);
|
|
389
|
+
records = [...sorted.records];
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const reordered = !inserted &&
|
|
393
|
+
!removed &&
|
|
394
|
+
(activeOrderChanged || !sameStringList(records, current));
|
|
395
|
+
const changeKind = inserted
|
|
396
|
+
? 'insert'
|
|
397
|
+
: removed
|
|
398
|
+
? 'delete'
|
|
399
|
+
: reordered
|
|
400
|
+
? 'reorder'
|
|
401
|
+
: 'stable_update';
|
|
402
|
+
if (planValue.pagination === undefined) {
|
|
403
|
+
if (changeKind === 'stable_update' &&
|
|
404
|
+
sameStringList(records, current)) {
|
|
405
|
+
// An uncertified collection can still prove a membership/order no-op;
|
|
406
|
+
// that stable update needs no index write or revalidation.
|
|
407
|
+
return unchanged(index.key);
|
|
408
|
+
}
|
|
409
|
+
return stale(index.key, reason('invalid_artifact', planValue.path, 'collection plan is missing its pagination maintenance contract'));
|
|
410
|
+
}
|
|
411
|
+
const coverageReason = certifyOffsetCoverage(planValue, index, current.length, changeKind);
|
|
412
|
+
if (coverageReason !== undefined) {
|
|
413
|
+
return stale(index.key, coverageReason);
|
|
414
|
+
}
|
|
415
|
+
const pagination = decideReplicaPaginationMaintenance(planValue.pagination, index.metadata.coverage, { kind: changeKind });
|
|
416
|
+
if (pagination.decision === 'revalidate') {
|
|
417
|
+
return stale(index.key, pagination.reason);
|
|
418
|
+
}
|
|
419
|
+
if (planValue.pagination.kind === 'offset' &&
|
|
420
|
+
index.metadata.coverage.kind === 'offset' &&
|
|
421
|
+
index.metadata.coverage.offset === 0 &&
|
|
422
|
+
index.metadata.coverage.limit !== undefined &&
|
|
423
|
+
records.length > index.metadata.coverage.limit) {
|
|
424
|
+
// Locality above is possible only because the base first page was
|
|
425
|
+
// non-full, so this is the complete optimistic ordered set. Preserve the
|
|
426
|
+
// operation's exact window when stacked optimistic inserts cross its
|
|
427
|
+
// limit.
|
|
428
|
+
records = records.slice(0, index.metadata.coverage.limit);
|
|
429
|
+
}
|
|
430
|
+
return sameStringList(records, current)
|
|
431
|
+
? unchanged(index.key)
|
|
432
|
+
: write(index.key, records);
|
|
433
|
+
}
|
|
434
|
+
export function certifyOffsetCoverage(planValue, index, confirmedRecords, changeKind) {
|
|
435
|
+
if (planValue.pagination?.kind !== 'offset' ||
|
|
436
|
+
changeKind === 'stable_update') {
|
|
437
|
+
return undefined;
|
|
438
|
+
}
|
|
439
|
+
const artifact = planValue.coverage;
|
|
440
|
+
const coverage = index.metadata.coverage;
|
|
441
|
+
if (artifact?.kind !== 'offset' || coverage.kind !== 'offset') {
|
|
442
|
+
return reason('invalid_index_metadata', ['indexes', index.key, 'coverage'], 'offset pagination locality requires its exact compiler coverage contract');
|
|
443
|
+
}
|
|
444
|
+
const expectedOffset = artifact.offsetArgument === undefined
|
|
445
|
+
? 0
|
|
446
|
+
: planValue.arguments[artifact.offsetArgument];
|
|
447
|
+
const configuredLimit = artifact.limitArgument === undefined
|
|
448
|
+
? artifact.defaultLimit
|
|
449
|
+
: planValue.arguments[artifact.limitArgument];
|
|
450
|
+
const expectedLimit = typeof configuredLimit === 'number' &&
|
|
451
|
+
typeof artifact.maxLimit === 'number'
|
|
452
|
+
? Math.min(configuredLimit, artifact.maxLimit)
|
|
453
|
+
: configuredLimit;
|
|
454
|
+
if (typeof expectedOffset !== 'number' ||
|
|
455
|
+
!Number.isSafeInteger(expectedOffset) ||
|
|
456
|
+
expectedOffset < 0 ||
|
|
457
|
+
(typeof expectedLimit !== 'number' &&
|
|
458
|
+
expectedLimit !== undefined) ||
|
|
459
|
+
(typeof expectedLimit === 'number' &&
|
|
460
|
+
(!Number.isSafeInteger(expectedLimit) || expectedLimit < 0)) ||
|
|
461
|
+
coverage.offset !== expectedOffset ||
|
|
462
|
+
coverage.limit !== expectedLimit ||
|
|
463
|
+
coverage.returned !== confirmedRecords ||
|
|
464
|
+
coverage.hasNext === true) {
|
|
465
|
+
return reason('invalid_index_metadata', ['indexes', index.key, 'coverage'], 'offset coverage does not match the request, confirmed index size, or observed page boundary');
|
|
466
|
+
}
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
export function parentRecordChangeAffectsRelationship(planValue, index, change) {
|
|
470
|
+
if (change.model !== planValue.parentModel ||
|
|
471
|
+
change.key !== index.metadata.parent) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
if (change.kind === 'delete')
|
|
475
|
+
return true;
|
|
476
|
+
const relationship = planValue.relationship;
|
|
477
|
+
if (relationship === undefined) {
|
|
478
|
+
// A forged relationship branch without its compiler-owned mapping must
|
|
479
|
+
// be evaluated and rejected instead of silently ignoring a parent change.
|
|
480
|
+
return true;
|
|
481
|
+
}
|
|
482
|
+
const mapping = relationship.keyMapping;
|
|
483
|
+
const mappingFields = mapping.kind === 'embedded' ? Object.freeze([]) : mapping.local;
|
|
484
|
+
if (mappingFields.some((field) => Object.prototype.hasOwnProperty.call(change.fields, field))) {
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
const dependencies = new Set(change.dependencies ?? []);
|
|
488
|
+
return relationship.dependencies.some((dependency) => dependencies.has(dependency));
|
|
489
|
+
}
|
|
490
|
+
export function relationshipMembership(planValue, index, target, records) {
|
|
491
|
+
if (target === undefined)
|
|
492
|
+
return { related: false };
|
|
493
|
+
const relationship = planValue.relationship;
|
|
494
|
+
const parentKey = index.metadata.parent;
|
|
495
|
+
if (relationship === undefined || parentKey === undefined) {
|
|
496
|
+
return {
|
|
497
|
+
reason: reason('relationship_mapping_unknown', planValue.path, 'relationship plan or parent identity is unavailable')
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
if (relationship.keyMapping.kind !== 'direct') {
|
|
501
|
+
return {
|
|
502
|
+
reason: reason('relationship_mapping_unknown', planValue.path, 'a non-member through/opaque relationship requires explicit link evidence')
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const parent = records.get(parentKey);
|
|
506
|
+
if (parent === undefined) {
|
|
507
|
+
return {
|
|
508
|
+
reason: reason('missing_parent_record', planValue.path, 'relationship parent record is absent')
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
for (let indexValue = 0; indexValue < relationship.keyMapping.local.length; indexValue += 1) {
|
|
512
|
+
const local = relationship.keyMapping.local[indexValue];
|
|
513
|
+
const remote = relationship.keyMapping.remote[indexValue];
|
|
514
|
+
if (!Object.prototype.hasOwnProperty.call(parent.fields, local) ||
|
|
515
|
+
!Object.prototype.hasOwnProperty.call(target.fields, remote)) {
|
|
516
|
+
return {
|
|
517
|
+
reason: reason('missing_field', [...planValue.path, 'relationship', local, remote], 'relationship key fields are not completely present')
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (!sameValue(parent.fields[local], target.fields[remote])) {
|
|
521
|
+
return { related: false };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return { related: true };
|
|
525
|
+
}
|
|
526
|
+
export function evaluateMembership(planValue, record, trustedPresets) {
|
|
527
|
+
if (planValue.filter === undefined)
|
|
528
|
+
return { matches: true };
|
|
529
|
+
const evaluated = evaluateReplicaFilter(planValue.filter, record.fields, planValue.variables, {
|
|
530
|
+
trustedPresets: {
|
|
531
|
+
descriptors: planValue.trustedPresets,
|
|
532
|
+
values: trustedPresets
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
return evaluated.result === 'unknown'
|
|
536
|
+
? { reason: evaluated.reason }
|
|
537
|
+
: { matches: evaluated.result === 'match' };
|
|
538
|
+
}
|
|
539
|
+
export function sortRecords(keys, order, records, variables, path) {
|
|
540
|
+
let unknown;
|
|
541
|
+
const sorted = [...keys].sort((leftKey, rightKey) => {
|
|
542
|
+
if (unknown !== undefined || leftKey === rightKey)
|
|
543
|
+
return 0;
|
|
544
|
+
const left = records.get(leftKey);
|
|
545
|
+
const right = records.get(rightKey);
|
|
546
|
+
if (left === undefined || right === undefined) {
|
|
547
|
+
unknown = reason('missing_index_record', path, `ordered index references missing record ${left === undefined ? leftKey : rightKey}`);
|
|
548
|
+
return 0;
|
|
549
|
+
}
|
|
550
|
+
const compared = compareReplicaOrder(order, left.fields, right.fields, variables);
|
|
551
|
+
if (compared.result === 'unknown') {
|
|
552
|
+
unknown = compared.reason;
|
|
553
|
+
return 0;
|
|
554
|
+
}
|
|
555
|
+
if (compared.result === 'equal') {
|
|
556
|
+
unknown = reason('non_unique_order', path, `distinct records ${leftKey} and ${rightKey} have no deterministic order`);
|
|
557
|
+
return 0;
|
|
558
|
+
}
|
|
559
|
+
return compared.result === 'less' ? -1 : 1;
|
|
560
|
+
});
|
|
561
|
+
return unknown === undefined
|
|
562
|
+
? { records: Object.freeze(sorted) }
|
|
563
|
+
: { reason: unknown };
|
|
564
|
+
}
|
|
565
|
+
export function relationshipChangeMatches(planValue, index, change) {
|
|
566
|
+
return (planValue.kind === 'relationship' &&
|
|
567
|
+
planValue.parentModel === change.sourceModel &&
|
|
568
|
+
planValue.field === change.field &&
|
|
569
|
+
planValue.targetModel === change.targetModel &&
|
|
570
|
+
index.metadata.parent === change.sourceKey);
|
|
571
|
+
}
|
|
572
|
+
export function isRelationshipChange(change) {
|
|
573
|
+
return change.kind === 'link' || change.kind === 'unlink';
|
|
574
|
+
}
|
|
575
|
+
export function validateDependencies(value) {
|
|
576
|
+
if (!Array.isArray(value))
|
|
577
|
+
throw new TypeError('dependencies must be an array');
|
|
578
|
+
const seen = new Set();
|
|
579
|
+
for (const dependency of value) {
|
|
580
|
+
validateName(dependency, 'dependency');
|
|
581
|
+
if (seen.has(dependency)) {
|
|
582
|
+
throw new TypeError(`duplicate dependency: ${dependency}`);
|
|
583
|
+
}
|
|
584
|
+
seen.add(dependency);
|
|
585
|
+
}
|
|
586
|
+
return value;
|
|
587
|
+
}
|
|
588
|
+
export function intersects(left, right) {
|
|
589
|
+
return left.some((value) => right.has(value));
|
|
590
|
+
}
|
|
591
|
+
export function recordKeyMatchesModel(key, model) {
|
|
592
|
+
return key.startsWith(`record:${encodeURIComponent(model)}:`);
|
|
593
|
+
}
|
|
594
|
+
export function firstDuplicate(values) {
|
|
595
|
+
const seen = new Set();
|
|
596
|
+
for (const value of values) {
|
|
597
|
+
if (seen.has(value))
|
|
598
|
+
return value;
|
|
599
|
+
seen.add(value);
|
|
600
|
+
}
|
|
601
|
+
return undefined;
|
|
602
|
+
}
|
|
603
|
+
export function write(indexKey, records) {
|
|
604
|
+
return Object.freeze({
|
|
605
|
+
kind: 'write',
|
|
606
|
+
indexKey,
|
|
607
|
+
records: Object.freeze([...records]),
|
|
608
|
+
complete: true
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
export function stale(indexKey, reasonValue) {
|
|
612
|
+
return Object.freeze({
|
|
613
|
+
kind: 'stale',
|
|
614
|
+
indexKey,
|
|
615
|
+
reason: reasonValue
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
export function unchanged(indexKey) {
|
|
619
|
+
return Object.freeze({ kind: 'unchanged', indexKey });
|
|
620
|
+
}
|
|
621
|
+
export function reason(code, path, message) {
|
|
622
|
+
return Object.freeze({
|
|
623
|
+
code,
|
|
624
|
+
path: Object.freeze([...path]),
|
|
625
|
+
message
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
export function sameStringList(left, right) {
|
|
629
|
+
return (left.length === right.length &&
|
|
630
|
+
left.every((value, index) => value === right[index]));
|
|
631
|
+
}
|
|
632
|
+
export function sameValue(left, right) {
|
|
633
|
+
return canonicalValue(left) === canonicalValue(right);
|
|
634
|
+
}
|
|
635
|
+
export function cloneRecord(value) {
|
|
636
|
+
return Object.freeze(Object.fromEntries(Object.entries(value)
|
|
637
|
+
.sort(([left], [right]) => compareCodeUnits(left, right))
|
|
638
|
+
.map(([key, entry]) => [key, cloneValue(entry)])));
|
|
639
|
+
}
|
|
640
|
+
export function cloneValue(value) {
|
|
641
|
+
if (Array.isArray(value))
|
|
642
|
+
return Object.freeze(value.map(cloneValue));
|
|
643
|
+
if (value !== null && typeof value === 'object') {
|
|
644
|
+
return cloneRecord(value);
|
|
645
|
+
}
|
|
646
|
+
return value;
|
|
647
|
+
}
|
|
648
|
+
export function canonicalValue(value) {
|
|
649
|
+
if (value === null)
|
|
650
|
+
return 'null';
|
|
651
|
+
if (typeof value === 'string')
|
|
652
|
+
return JSON.stringify(value);
|
|
653
|
+
if (typeof value === 'boolean')
|
|
654
|
+
return value ? 'true' : 'false';
|
|
655
|
+
if (typeof value === 'number') {
|
|
656
|
+
if (!Number.isFinite(value))
|
|
657
|
+
throw new TypeError('canonical values must be finite');
|
|
658
|
+
return Object.is(value, -0) ? '0' : JSON.stringify(value);
|
|
659
|
+
}
|
|
660
|
+
if (Array.isArray(value)) {
|
|
661
|
+
return `[${value.map(canonicalValue).join(',')}]`;
|
|
662
|
+
}
|
|
663
|
+
if (typeof value === 'object') {
|
|
664
|
+
const entries = Object.entries(value)
|
|
665
|
+
.filter(([, entry]) => entry !== undefined)
|
|
666
|
+
.sort(([left], [right]) => compareCodeUnits(left, right));
|
|
667
|
+
return `{${entries
|
|
668
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalValue(entry)}`)
|
|
669
|
+
.join(',')}}`;
|
|
670
|
+
}
|
|
671
|
+
throw new TypeError('canonical values must be JSON-compatible');
|
|
672
|
+
}
|
|
673
|
+
export function validateName(value, description) {
|
|
674
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
675
|
+
throw new TypeError(`${description} must be a non-empty string`);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export { createReplicaIndexMaintenanceRegistry, formatReplicaIndexStaleReason } from './implementation.js';
|
|
2
|
+
export type { ReplicaIndexDependencyChange, ReplicaIndexMaintenanceDecision, ReplicaIndexMaintenanceIndex, ReplicaIndexMaintenanceReason, ReplicaIndexMaintenanceReasonCode, ReplicaIndexMaintenanceRecord, ReplicaIndexMaintenanceRegistry, ReplicaIndexMaintenanceSnapshot, ReplicaIndexPlanRegistration, ReplicaIndexRecordChange, ReplicaIndexRelationshipChange, ReplicaIndexSemanticChange, ReplicaIndexSemanticLayer } from './implementation.js';
|