@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,734 @@
|
|
|
1
|
+
import { createCacheEngine } from '../../internal/cache-engine.js';
|
|
2
|
+
import { parseDistributedTrustedPresetInventory } from '../../protocol.js';
|
|
3
|
+
import { freezeRecord } from '../../lib/freeze-record.js';
|
|
4
|
+
export const DATABASE_VERSION = 1;
|
|
5
|
+
export const ENTRY_FORMAT_VERSION = 1;
|
|
6
|
+
export const STORE_NAME = 'confirmed-replicas';
|
|
7
|
+
export const DEFAULT_DATABASE_NAME = '@hops-ops/distributed:replica';
|
|
8
|
+
export const GRAPHQL_NAME = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
9
|
+
export const DECIMAL = /^(0|[1-9][0-9]*)$/;
|
|
10
|
+
export function normalizePolicy(policy) {
|
|
11
|
+
if (policy === undefined)
|
|
12
|
+
return new Map();
|
|
13
|
+
const raw = exactRecord(policy, 'persistence policy', ['models']);
|
|
14
|
+
const models = plainRecord(raw.models, 'persistence policy.models');
|
|
15
|
+
const result = new Map();
|
|
16
|
+
for (const [model, value] of Object.entries(models)) {
|
|
17
|
+
if (!GRAPHQL_NAME.test(model)) {
|
|
18
|
+
throw new TypeError(`invalid persistence policy model: ${model}`);
|
|
19
|
+
}
|
|
20
|
+
const entry = exactRecord(value, `persistence policy.models.${model}`, ['retention', 'sensitive']);
|
|
21
|
+
if (entry.retention !== 'persist-confirmed' &&
|
|
22
|
+
entry.retention !== 'memory-only') {
|
|
23
|
+
throw new TypeError(`invalid persistence retention for model ${model}`);
|
|
24
|
+
}
|
|
25
|
+
if (typeof entry.sensitive !== 'boolean') {
|
|
26
|
+
throw new TypeError(`persistence sensitivity for model ${model} must be boolean`);
|
|
27
|
+
}
|
|
28
|
+
result.set(model, Object.freeze({
|
|
29
|
+
retention: entry.retention,
|
|
30
|
+
sensitive: entry.sensitive
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
export function filterReplicaState(parsed, policy) {
|
|
36
|
+
const allowsModel = (model) => {
|
|
37
|
+
const decision = policy.get(model);
|
|
38
|
+
return (decision?.retention === 'persist-confirmed' &&
|
|
39
|
+
decision.sensitive === false);
|
|
40
|
+
};
|
|
41
|
+
const allowedRecord = (key) => {
|
|
42
|
+
const model = modelFromRecordKey(key);
|
|
43
|
+
return model !== undefined && allowsModel(model);
|
|
44
|
+
};
|
|
45
|
+
const recordClocks = parsed.payload.recordClocks.filter(([key]) => allowedRecord(key));
|
|
46
|
+
const recordClockKeys = new Set(recordClocks.map(([key]) => key));
|
|
47
|
+
const initialRecords = parsed.payload.cache.records.filter((record) => allowedRecord(record.key));
|
|
48
|
+
const liveRecordKeys = new Set(initialRecords
|
|
49
|
+
.filter((record) => record.tombstoneRevision === undefined)
|
|
50
|
+
.map((record) => record.key));
|
|
51
|
+
const records = initialRecords.map((record) => Object.freeze({
|
|
52
|
+
...record,
|
|
53
|
+
links: freezeRecord(Object.entries(record.links).filter(([, link]) => linkReferencesOnly(link.value, liveRecordKeys)))
|
|
54
|
+
}));
|
|
55
|
+
const candidateIndexes = new Map(parsed.payload.cache.indexes
|
|
56
|
+
.filter((index) => !index.deleted &&
|
|
57
|
+
index.records.length > 0 &&
|
|
58
|
+
index.records.every((key) => liveRecordKeys.has(key)) &&
|
|
59
|
+
(index.metadata?.parent === undefined ||
|
|
60
|
+
liveRecordKeys.has(index.metadata.parent)))
|
|
61
|
+
.map((index) => [index.key, index]));
|
|
62
|
+
const initiallyEligible = new Map();
|
|
63
|
+
for (const group of parsed.payload.operations) {
|
|
64
|
+
for (const state of [group.query, group.live]) {
|
|
65
|
+
if (state !== undefined &&
|
|
66
|
+
operationStateAllowed(state, candidateIndexes, recordClockKeys, allowedRecord)) {
|
|
67
|
+
initiallyEligible.set(state, state);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
let eligible = initiallyEligible;
|
|
72
|
+
let finalIndexKeys = indexesProvenByOperations(candidateIndexes, eligible.values());
|
|
73
|
+
for (;;) {
|
|
74
|
+
const nextEligible = new Map();
|
|
75
|
+
for (const state of eligible.values()) {
|
|
76
|
+
if (state.indexKeys.every((key) => finalIndexKeys.has(key))) {
|
|
77
|
+
nextEligible.set(state, state);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const nextIndexKeys = indexesProvenByOperations(candidateIndexes, nextEligible.values());
|
|
81
|
+
if (nextEligible.size === eligible.size &&
|
|
82
|
+
nextIndexKeys.size === finalIndexKeys.size) {
|
|
83
|
+
eligible = nextEligible;
|
|
84
|
+
finalIndexKeys = nextIndexKeys;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
eligible = nextEligible;
|
|
88
|
+
finalIndexKeys = nextIndexKeys;
|
|
89
|
+
}
|
|
90
|
+
const indexes = parsed.payload.cache.indexes.filter((index) => finalIndexKeys.has(index.key));
|
|
91
|
+
const operations = parsed.payload.operations.flatMap((group) => {
|
|
92
|
+
const query = group.query !== undefined && eligible.has(group.query)
|
|
93
|
+
? group.query
|
|
94
|
+
: undefined;
|
|
95
|
+
const live = group.live !== undefined && eligible.has(group.live)
|
|
96
|
+
? group.live
|
|
97
|
+
: undefined;
|
|
98
|
+
if (query === undefined && live === undefined)
|
|
99
|
+
return [];
|
|
100
|
+
const active = group.active === 'query' && query !== undefined
|
|
101
|
+
? 'query'
|
|
102
|
+
: group.active === 'live' && live !== undefined
|
|
103
|
+
? 'live'
|
|
104
|
+
: undefined;
|
|
105
|
+
return [
|
|
106
|
+
Object.freeze({
|
|
107
|
+
key: group.key,
|
|
108
|
+
...(query === undefined ? {} : { query }),
|
|
109
|
+
...(live === undefined ? {} : { live }),
|
|
110
|
+
...(active === undefined ? {} : { active }),
|
|
111
|
+
generation: group.generation
|
|
112
|
+
})
|
|
113
|
+
];
|
|
114
|
+
});
|
|
115
|
+
const anonymousRecordClocks = parsed.payload.anonymousRecordClocks.filter(([, value]) => allowsModel(value.model));
|
|
116
|
+
const cache = canonicalCacheSnapshot({
|
|
117
|
+
version: 1,
|
|
118
|
+
records,
|
|
119
|
+
indexes
|
|
120
|
+
});
|
|
121
|
+
if (cache.records.length === 0 &&
|
|
122
|
+
cache.indexes.length === 0 &&
|
|
123
|
+
recordClocks.length === 0 &&
|
|
124
|
+
anonymousRecordClocks.length === 0) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const payload = Object.freeze({
|
|
128
|
+
cache,
|
|
129
|
+
operations: Object.freeze(operations),
|
|
130
|
+
recordClocks: Object.freeze(recordClocks),
|
|
131
|
+
anonymousRecordClocks: Object.freeze(anonymousRecordClocks),
|
|
132
|
+
/*
|
|
133
|
+
* Presets are part of the exact server-issued cache scope contract. A
|
|
134
|
+
* restored cache cannot safely evaluate generated row policies or command
|
|
135
|
+
* effects without the matching inventory. The entry remains opt-in,
|
|
136
|
+
* confirmed-state-only, and keyed by that independently re-established
|
|
137
|
+
* opaque scope; transport credentials and command inputs are still absent.
|
|
138
|
+
*/
|
|
139
|
+
trustedPresets: parsed.payload.trustedPresets,
|
|
140
|
+
nextIndexRevision: parsed.payload.nextIndexRevision
|
|
141
|
+
});
|
|
142
|
+
assertMetadataConsistent(payload);
|
|
143
|
+
return Object.freeze({
|
|
144
|
+
version: 1,
|
|
145
|
+
scope: parsed.scope,
|
|
146
|
+
payload
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
export function operationStateAllowed(state, indexes, recordClockKeys, allowedRecord) {
|
|
150
|
+
if (state.indexKeys.length === 0 && state.pathRecords.length === 0) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (state.indexKeys.some((key) => !indexes.has(key)))
|
|
154
|
+
return false;
|
|
155
|
+
for (const [, key] of state.pathRecords) {
|
|
156
|
+
if (!allowedRecord(key) || !recordClockKeys.has(key))
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
export function indexesProvenByOperations(candidates, states) {
|
|
162
|
+
const result = new Set();
|
|
163
|
+
for (const state of states) {
|
|
164
|
+
if (state.indexRevision === undefined)
|
|
165
|
+
continue;
|
|
166
|
+
for (const key of state.indexKeys) {
|
|
167
|
+
if (candidates.get(key)?.revision === state.indexRevision) {
|
|
168
|
+
result.add(key);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
|
+
export function linkReferencesOnly(link, allowedLiveRecordKeys) {
|
|
175
|
+
if (link === null)
|
|
176
|
+
return true;
|
|
177
|
+
if (typeof link === 'string')
|
|
178
|
+
return allowedLiveRecordKeys.has(link);
|
|
179
|
+
return link.every((key) => allowedLiveRecordKeys.has(key));
|
|
180
|
+
}
|
|
181
|
+
export function parseReplicaState(value) {
|
|
182
|
+
const state = exactRecord(value, 'state', ['version', 'scope', 'payload']);
|
|
183
|
+
if (state.version !== 1)
|
|
184
|
+
throw new TypeError('unsupported replica state version');
|
|
185
|
+
const scope = parseAuthoritativeScope(state.scope);
|
|
186
|
+
const payloadValue = exactRecord(state.payload, 'state.payload', [
|
|
187
|
+
'cache',
|
|
188
|
+
'operations',
|
|
189
|
+
'recordClocks',
|
|
190
|
+
'anonymousRecordClocks',
|
|
191
|
+
'trustedPresets',
|
|
192
|
+
'nextIndexRevision'
|
|
193
|
+
]);
|
|
194
|
+
const cache = parseCacheSnapshot(payloadValue.cache);
|
|
195
|
+
const operations = parseOperations(payloadValue.operations);
|
|
196
|
+
const recordClocks = parseRecordClocks(payloadValue.recordClocks);
|
|
197
|
+
const anonymousRecordClocks = parseAnonymousRecordClocks(payloadValue.anonymousRecordClocks, recordClocks);
|
|
198
|
+
const trustedPresets = parseDistributedTrustedPresetInventory(payloadValue.trustedPresets, 'state.payload.trustedPresets');
|
|
199
|
+
const nextIndexRevision = decimalString(payloadValue.nextIndexRevision, 'state.payload.nextIndexRevision');
|
|
200
|
+
const payload = Object.freeze({
|
|
201
|
+
cache,
|
|
202
|
+
operations,
|
|
203
|
+
recordClocks,
|
|
204
|
+
anonymousRecordClocks,
|
|
205
|
+
trustedPresets,
|
|
206
|
+
nextIndexRevision
|
|
207
|
+
});
|
|
208
|
+
assertMetadataConsistent(payload);
|
|
209
|
+
const normalizedState = Object.freeze({
|
|
210
|
+
version: 1,
|
|
211
|
+
scope,
|
|
212
|
+
payload
|
|
213
|
+
});
|
|
214
|
+
return Object.freeze({ state: normalizedState, scope, payload });
|
|
215
|
+
}
|
|
216
|
+
export function parseAuthoritativeScope(value) {
|
|
217
|
+
const scope = exactRecord(value, 'authoritative scope', ['protocolVersion', 'schemaHash', 'cacheScope']);
|
|
218
|
+
if (scope.protocolVersion !== 1) {
|
|
219
|
+
throw new TypeError('unsupported authoritative protocol version');
|
|
220
|
+
}
|
|
221
|
+
return Object.freeze({
|
|
222
|
+
protocolVersion: 1,
|
|
223
|
+
schemaHash: nonEmptyString(scope.schemaHash, 'authoritative scope schemaHash'),
|
|
224
|
+
cacheScope: nonEmptyString(scope.cacheScope, 'authoritative scope cacheScope')
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
export function parseCacheSnapshot(value) {
|
|
228
|
+
const cache = exactRecord(value, 'state.payload.cache', [
|
|
229
|
+
'version',
|
|
230
|
+
'records',
|
|
231
|
+
'indexes'
|
|
232
|
+
]);
|
|
233
|
+
if (cache.version !== 1)
|
|
234
|
+
throw new TypeError('unsupported cache snapshot');
|
|
235
|
+
for (const [index, value] of arrayValue(cache.records, 'state.payload.cache.records').entries()) {
|
|
236
|
+
const path = `state.payload.cache.records[${index}]`;
|
|
237
|
+
const record = exactRecord(value, path, ['key', 'revision', 'incarnation', 'tombstoneRevision', 'fields', 'links'], ['key', 'revision', 'fields', 'links']);
|
|
238
|
+
nonEmptyString(record.key, `${path}.key`);
|
|
239
|
+
decimalString(record.revision, `${path}.revision`);
|
|
240
|
+
if (record.incarnation !== undefined) {
|
|
241
|
+
decimalString(record.incarnation, `${path}.incarnation`);
|
|
242
|
+
}
|
|
243
|
+
if (record.tombstoneRevision !== undefined) {
|
|
244
|
+
decimalString(record.tombstoneRevision, `${path}.tombstoneRevision`);
|
|
245
|
+
}
|
|
246
|
+
for (const [name, fieldValue] of Object.entries(plainRecord(record.fields, `${path}.fields`))) {
|
|
247
|
+
const field = exactRecord(fieldValue, `${path}.fields.${name}`, ['revision', 'value']);
|
|
248
|
+
decimalString(field.revision, `${path}.fields.${name}.revision`);
|
|
249
|
+
assertJsonValue(field.value, `${path}.fields.${name}.value`);
|
|
250
|
+
}
|
|
251
|
+
for (const [name, linkValue] of Object.entries(plainRecord(record.links, `${path}.links`))) {
|
|
252
|
+
const link = exactRecord(linkValue, `${path}.links.${name}`, ['revision', 'value']);
|
|
253
|
+
decimalString(link.revision, `${path}.links.${name}.revision`);
|
|
254
|
+
assertRecordLink(link.value, `${path}.links.${name}.value`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
for (const [index, value] of arrayValue(cache.indexes, 'state.payload.cache.indexes').entries()) {
|
|
258
|
+
const path = `state.payload.cache.indexes[${index}]`;
|
|
259
|
+
const entry = exactRecord(value, path, [
|
|
260
|
+
'key',
|
|
261
|
+
'revision',
|
|
262
|
+
'staleRevision',
|
|
263
|
+
'records',
|
|
264
|
+
'complete',
|
|
265
|
+
'deleted',
|
|
266
|
+
'metadata'
|
|
267
|
+
], ['key', 'revision', 'records', 'complete', 'deleted']);
|
|
268
|
+
nonEmptyString(entry.key, `${path}.key`);
|
|
269
|
+
decimalString(entry.revision, `${path}.revision`);
|
|
270
|
+
if (entry.staleRevision !== undefined) {
|
|
271
|
+
decimalString(entry.staleRevision, `${path}.staleRevision`);
|
|
272
|
+
}
|
|
273
|
+
const seen = new Set();
|
|
274
|
+
for (const [recordIndex, key] of arrayValue(entry.records, `${path}.records`).entries()) {
|
|
275
|
+
const recordKey = nonEmptyString(key, `${path}.records[${recordIndex}]`);
|
|
276
|
+
if (seen.has(recordKey)) {
|
|
277
|
+
throw new TypeError(`duplicate record at ${path}.records[${recordIndex}]`);
|
|
278
|
+
}
|
|
279
|
+
seen.add(recordKey);
|
|
280
|
+
}
|
|
281
|
+
booleanValue(entry.complete, `${path}.complete`);
|
|
282
|
+
booleanValue(entry.deleted, `${path}.deleted`);
|
|
283
|
+
if (entry.metadata !== undefined) {
|
|
284
|
+
assertIndexMetadata(entry.metadata, `${path}.metadata`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return canonicalCacheSnapshot(cache);
|
|
288
|
+
}
|
|
289
|
+
export function canonicalCacheSnapshot(value) {
|
|
290
|
+
const engine = createCacheEngine();
|
|
291
|
+
engine.restore(value);
|
|
292
|
+
return engine.extract();
|
|
293
|
+
}
|
|
294
|
+
export function assertIndexMetadata(value, path) {
|
|
295
|
+
const metadata = exactRecord(value, path, [
|
|
296
|
+
'parent',
|
|
297
|
+
'parentRevision',
|
|
298
|
+
'parentIncarnation',
|
|
299
|
+
'field',
|
|
300
|
+
'arguments',
|
|
301
|
+
'coverage',
|
|
302
|
+
'dependencies',
|
|
303
|
+
'staleReason',
|
|
304
|
+
'nullValue'
|
|
305
|
+
], ['field', 'arguments', 'coverage', 'dependencies']);
|
|
306
|
+
if (metadata.parent !== undefined)
|
|
307
|
+
nonEmptyString(metadata.parent, `${path}.parent`);
|
|
308
|
+
if (metadata.parentRevision !== undefined) {
|
|
309
|
+
decimalString(metadata.parentRevision, `${path}.parentRevision`);
|
|
310
|
+
}
|
|
311
|
+
if (metadata.parentIncarnation !== undefined) {
|
|
312
|
+
decimalString(metadata.parentIncarnation, `${path}.parentIncarnation`);
|
|
313
|
+
}
|
|
314
|
+
nonEmptyString(metadata.field, `${path}.field`);
|
|
315
|
+
const argumentsValue = plainRecord(metadata.arguments, `${path}.arguments`);
|
|
316
|
+
assertJsonValue(argumentsValue, `${path}.arguments`);
|
|
317
|
+
assertCoverage(metadata.coverage, `${path}.coverage`);
|
|
318
|
+
const seen = new Set();
|
|
319
|
+
for (const [index, dependency] of arrayValue(metadata.dependencies, `${path}.dependencies`).entries()) {
|
|
320
|
+
const name = nonEmptyString(dependency, `${path}.dependencies[${index}]`);
|
|
321
|
+
if (seen.has(name)) {
|
|
322
|
+
throw new TypeError(`duplicate dependency at ${path}.dependencies[${index}]`);
|
|
323
|
+
}
|
|
324
|
+
seen.add(name);
|
|
325
|
+
}
|
|
326
|
+
if (metadata.staleReason !== undefined) {
|
|
327
|
+
nonEmptyString(metadata.staleReason, `${path}.staleReason`);
|
|
328
|
+
}
|
|
329
|
+
if (metadata.nullValue !== undefined) {
|
|
330
|
+
booleanValue(metadata.nullValue, `${path}.nullValue`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
export function assertCoverage(value, path) {
|
|
334
|
+
const coverage = plainRecord(value, path);
|
|
335
|
+
if (coverage.kind === 'complete' || coverage.kind === 'unknown') {
|
|
336
|
+
exactKeys(coverage, path, ['kind']);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (coverage.kind === 'offset') {
|
|
340
|
+
exactKeys(coverage, path, ['kind', 'offset', 'limit', 'returned', 'hasNext'], ['kind', 'offset']);
|
|
341
|
+
nonNegativeInteger(coverage.offset, `${path}.offset`);
|
|
342
|
+
if (coverage.limit !== undefined)
|
|
343
|
+
nonNegativeInteger(coverage.limit, `${path}.limit`);
|
|
344
|
+
if (coverage.returned !== undefined) {
|
|
345
|
+
nonNegativeInteger(coverage.returned, `${path}.returned`);
|
|
346
|
+
}
|
|
347
|
+
if (coverage.hasNext !== undefined)
|
|
348
|
+
booleanValue(coverage.hasNext, `${path}.hasNext`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (coverage.kind === 'cursor') {
|
|
352
|
+
exactKeys(coverage, path, [
|
|
353
|
+
'kind',
|
|
354
|
+
'after',
|
|
355
|
+
'before',
|
|
356
|
+
'first',
|
|
357
|
+
'last',
|
|
358
|
+
'start',
|
|
359
|
+
'end',
|
|
360
|
+
'hasNext',
|
|
361
|
+
'hasPrevious'
|
|
362
|
+
], ['kind']);
|
|
363
|
+
for (const key of ['after', 'before', 'start', 'end']) {
|
|
364
|
+
if (coverage[key] !== undefined) {
|
|
365
|
+
assertJsonValue(coverage[key], `${path}.${key}`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const key of ['first', 'last']) {
|
|
369
|
+
if (coverage[key] !== undefined) {
|
|
370
|
+
nonNegativeInteger(coverage[key], `${path}.${key}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
for (const key of ['hasNext', 'hasPrevious']) {
|
|
374
|
+
if (coverage[key] !== undefined) {
|
|
375
|
+
booleanValue(coverage[key], `${path}.${key}`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
throw new TypeError(`unsupported index coverage at ${path}`);
|
|
381
|
+
}
|
|
382
|
+
export function parseOperations(value) {
|
|
383
|
+
const groups = [];
|
|
384
|
+
const keys = new Set();
|
|
385
|
+
for (const [index, entry] of arrayValue(value, 'state.payload.operations').entries()) {
|
|
386
|
+
const path = `state.payload.operations[${index}]`;
|
|
387
|
+
const raw = exactRecord(entry, path, ['key', 'query', 'live', 'active', 'generation'], ['key', 'generation']);
|
|
388
|
+
const key = nonEmptyString(raw.key, `${path}.key`);
|
|
389
|
+
if (!key.startsWith('protocol:') || keys.has(key)) {
|
|
390
|
+
throw new TypeError(`invalid or duplicate operation key at ${path}.key`);
|
|
391
|
+
}
|
|
392
|
+
keys.add(key);
|
|
393
|
+
const query = raw.query === undefined
|
|
394
|
+
? undefined
|
|
395
|
+
: parseOperationState(raw.query, `${path}.query`);
|
|
396
|
+
const live = raw.live === undefined
|
|
397
|
+
? undefined
|
|
398
|
+
: parseOperationState(raw.live, `${path}.live`);
|
|
399
|
+
if (query === undefined && live === undefined) {
|
|
400
|
+
throw new TypeError(`operation has no protocol state at ${path}`);
|
|
401
|
+
}
|
|
402
|
+
const active = raw.active === undefined
|
|
403
|
+
? undefined
|
|
404
|
+
: operationSource(raw.active, `${path}.active`);
|
|
405
|
+
if ((active === 'query' && query === undefined) ||
|
|
406
|
+
(active === 'live' && live === undefined)) {
|
|
407
|
+
throw new TypeError(`invalid active operation source at ${path}.active`);
|
|
408
|
+
}
|
|
409
|
+
groups.push(Object.freeze({
|
|
410
|
+
key,
|
|
411
|
+
...(query === undefined ? {} : { query }),
|
|
412
|
+
...(live === undefined ? {} : { live }),
|
|
413
|
+
...(active === undefined ? {} : { active }),
|
|
414
|
+
generation: nonNegativeInteger(raw.generation, `${path}.generation`)
|
|
415
|
+
}));
|
|
416
|
+
}
|
|
417
|
+
return Object.freeze(groups);
|
|
418
|
+
}
|
|
419
|
+
export function parseOperationState(value, path) {
|
|
420
|
+
const raw = exactRecord(value, path, [
|
|
421
|
+
'operation',
|
|
422
|
+
'snapshotScope',
|
|
423
|
+
'indexClocks',
|
|
424
|
+
'indexRevision',
|
|
425
|
+
'indexKeys',
|
|
426
|
+
'pathRecords',
|
|
427
|
+
'cursors'
|
|
428
|
+
], ['operation', 'indexClocks', 'indexKeys', 'pathRecords', 'cursors']);
|
|
429
|
+
const indexClocks = parseUniquePairs(raw.indexClocks, `${path}.indexClocks`, (entry, entryPath) => {
|
|
430
|
+
const clock = exactRecord(entry, entryPath, ['scopeToken', 'position']);
|
|
431
|
+
return Object.freeze({
|
|
432
|
+
scopeToken: nonEmptyString(clock.scopeToken, `${entryPath}.scopeToken`),
|
|
433
|
+
position: decimalString(clock.position, `${entryPath}.position`)
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
const indexKeys = uniqueStrings(raw.indexKeys, `${path}.indexKeys`);
|
|
437
|
+
const pathRecords = parseUniquePairs(raw.pathRecords, `${path}.pathRecords`, (entry, entryPath) => nonEmptyString(entry, entryPath));
|
|
438
|
+
const cursors = arrayValue(raw.cursors, `${path}.cursors`).map((entry, index) => {
|
|
439
|
+
const entryPath = `${path}.cursors[${index}]`;
|
|
440
|
+
const cursor = exactRecord(entry, entryPath, ['projection', 'position', 'token']);
|
|
441
|
+
return Object.freeze({
|
|
442
|
+
projection: nonEmptyString(cursor.projection, `${entryPath}.projection`),
|
|
443
|
+
position: decimalString(cursor.position, `${entryPath}.position`),
|
|
444
|
+
token: nonEmptyString(cursor.token, `${entryPath}.token`)
|
|
445
|
+
});
|
|
446
|
+
});
|
|
447
|
+
return Object.freeze({
|
|
448
|
+
operation: nonEmptyString(raw.operation, `${path}.operation`),
|
|
449
|
+
...(raw.snapshotScope === undefined
|
|
450
|
+
? {}
|
|
451
|
+
: {
|
|
452
|
+
snapshotScope: nonEmptyString(raw.snapshotScope, `${path}.snapshotScope`)
|
|
453
|
+
}),
|
|
454
|
+
indexClocks,
|
|
455
|
+
...(raw.indexRevision === undefined
|
|
456
|
+
? {}
|
|
457
|
+
: {
|
|
458
|
+
indexRevision: decimalString(raw.indexRevision, `${path}.indexRevision`)
|
|
459
|
+
}),
|
|
460
|
+
indexKeys,
|
|
461
|
+
pathRecords,
|
|
462
|
+
cursors: Object.freeze(cursors)
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
export function parseRecordClocks(value) {
|
|
466
|
+
return parseUniquePairs(value, 'state.payload.recordClocks', (entry, path) => parseRecordClock(entry, path));
|
|
467
|
+
}
|
|
468
|
+
export function parseAnonymousRecordClocks(value, recordClocks) {
|
|
469
|
+
const recordScopes = new Set(recordClocks.map(([, clock]) => clock.scopeToken));
|
|
470
|
+
const result = parseUniquePairs(value, 'state.payload.anonymousRecordClocks', (entry, path) => {
|
|
471
|
+
const raw = exactRecord(entry, path, ['model', 'clock']);
|
|
472
|
+
return Object.freeze({
|
|
473
|
+
model: nonEmptyString(raw.model, `${path}.model`),
|
|
474
|
+
clock: parseRecordClock(raw.clock, `${path}.clock`)
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
for (const [scopeToken, value] of result) {
|
|
478
|
+
if (value.clock.scopeToken !== scopeToken ||
|
|
479
|
+
recordScopes.has(scopeToken)) {
|
|
480
|
+
throw new TypeError(`invalid anonymous record scope at state.payload.anonymousRecordClocks`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
return result;
|
|
484
|
+
}
|
|
485
|
+
export function parseRecordClock(value, path) {
|
|
486
|
+
const clock = exactRecord(value, path, ['scopeToken', 'incarnation', 'revision', 'tombstone']);
|
|
487
|
+
return Object.freeze({
|
|
488
|
+
scopeToken: nonEmptyString(clock.scopeToken, `${path}.scopeToken`),
|
|
489
|
+
incarnation: decimalString(clock.incarnation, `${path}.incarnation`),
|
|
490
|
+
revision: decimalString(clock.revision, `${path}.revision`),
|
|
491
|
+
tombstone: booleanValue(clock.tombstone, `${path}.tombstone`)
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
export function assertMetadataConsistent(payload) {
|
|
495
|
+
const clocksByRecord = new Map(payload.recordClocks);
|
|
496
|
+
const recordScopes = new Set();
|
|
497
|
+
for (const [key, clock] of payload.recordClocks) {
|
|
498
|
+
if (recordScopes.has(clock.scopeToken)) {
|
|
499
|
+
throw new TypeError(`duplicate record scope token for ${key}`);
|
|
500
|
+
}
|
|
501
|
+
recordScopes.add(clock.scopeToken);
|
|
502
|
+
}
|
|
503
|
+
for (const record of payload.cache.records) {
|
|
504
|
+
if (modelFromRecordKey(record.key) === undefined)
|
|
505
|
+
continue;
|
|
506
|
+
const clock = clocksByRecord.get(record.key);
|
|
507
|
+
const incarnation = record.incarnation ?? record.revision;
|
|
508
|
+
if (clock === undefined ||
|
|
509
|
+
clock.incarnation !== incarnation ||
|
|
510
|
+
clock.revision !== record.revision ||
|
|
511
|
+
(clock.tombstone
|
|
512
|
+
? record.tombstoneRevision !== clock.revision
|
|
513
|
+
: record.tombstoneRevision !== undefined)) {
|
|
514
|
+
throw new TypeError(`inconsistent persisted record clock for ${record.key}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
const indexRevisions = new Map();
|
|
518
|
+
for (const group of payload.operations) {
|
|
519
|
+
for (const state of [group.query, group.live]) {
|
|
520
|
+
if (state === undefined)
|
|
521
|
+
continue;
|
|
522
|
+
if (state.indexRevision !== undefined &&
|
|
523
|
+
compareDecimal(state.indexRevision, payload.nextIndexRevision) > 0) {
|
|
524
|
+
throw new TypeError(`operation index revision exceeds checkpoint`);
|
|
525
|
+
}
|
|
526
|
+
if (state.indexRevision === undefined && state.indexKeys.length > 0) {
|
|
527
|
+
throw new TypeError(`operation index keys lack a revision`);
|
|
528
|
+
}
|
|
529
|
+
for (const [, recordKey] of state.pathRecords) {
|
|
530
|
+
if (modelFromRecordKey(recordKey) !== undefined &&
|
|
531
|
+
!clocksByRecord.has(recordKey)) {
|
|
532
|
+
throw new TypeError(`operation path lacks a record clock`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (state.indexRevision !== undefined) {
|
|
536
|
+
for (const key of state.indexKeys) {
|
|
537
|
+
const revisions = indexRevisions.get(key) ?? new Set();
|
|
538
|
+
revisions.add(state.indexRevision);
|
|
539
|
+
indexRevisions.set(key, revisions);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
for (const index of payload.cache.indexes) {
|
|
545
|
+
if (compareDecimal(index.revision, payload.nextIndexRevision) > 0 ||
|
|
546
|
+
!indexRevisions.get(index.key)?.has(index.revision)) {
|
|
547
|
+
throw new TypeError(`index lacks matching operation checkpoint: ${index.key}`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
export function parseStoredEntry(value) {
|
|
552
|
+
const entry = exactRecord(value, 'persisted replica entry', ['formatVersion', 'identity', 'storedAt', 'state']);
|
|
553
|
+
if (entry.formatVersion !== ENTRY_FORMAT_VERSION) {
|
|
554
|
+
throw new TypeError('unsupported persisted replica entry version');
|
|
555
|
+
}
|
|
556
|
+
const storedAt = nonNegativeInteger(entry.storedAt, 'persisted replica storedAt');
|
|
557
|
+
return Object.freeze({
|
|
558
|
+
formatVersion: 1,
|
|
559
|
+
identity: nonEmptyString(entry.identity, 'persisted replica identity'),
|
|
560
|
+
storedAt,
|
|
561
|
+
state: entry.state
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
export function persistenceIdentity(scope) {
|
|
565
|
+
// JSON tuple encoding is unambiguous even when opaque scope values contain
|
|
566
|
+
// punctuation used by human-readable key formats.
|
|
567
|
+
return JSON.stringify([
|
|
568
|
+
'distributed-confirmed-replica',
|
|
569
|
+
ENTRY_FORMAT_VERSION,
|
|
570
|
+
scope.protocolVersion,
|
|
571
|
+
scope.schemaHash,
|
|
572
|
+
scope.cacheScope
|
|
573
|
+
]);
|
|
574
|
+
}
|
|
575
|
+
export function sameScope(left, right) {
|
|
576
|
+
return (left.protocolVersion === right.protocolVersion &&
|
|
577
|
+
left.schemaHash === right.schemaHash &&
|
|
578
|
+
left.cacheScope === right.cacheScope);
|
|
579
|
+
}
|
|
580
|
+
export function modelFromRecordKey(key) {
|
|
581
|
+
if (!key.startsWith('record:'))
|
|
582
|
+
return undefined;
|
|
583
|
+
const separator = key.indexOf(':', 'record:'.length);
|
|
584
|
+
if (separator < 0 || separator === key.length - 1)
|
|
585
|
+
return undefined;
|
|
586
|
+
try {
|
|
587
|
+
const model = decodeURIComponent(key.slice('record:'.length, separator));
|
|
588
|
+
return GRAPHQL_NAME.test(model) ? model : undefined;
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
return undefined;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
export function parseUniquePairs(value, path, parseValue) {
|
|
595
|
+
const result = [];
|
|
596
|
+
const keys = new Set();
|
|
597
|
+
for (const [index, entry] of arrayValue(value, path).entries()) {
|
|
598
|
+
const entryPath = `${path}[${index}]`;
|
|
599
|
+
if (!Array.isArray(entry) || entry.length !== 2) {
|
|
600
|
+
throw new TypeError(`invalid pair at ${entryPath}`);
|
|
601
|
+
}
|
|
602
|
+
const key = nonEmptyString(entry[0], `${entryPath}[0]`);
|
|
603
|
+
if (keys.has(key))
|
|
604
|
+
throw new TypeError(`duplicate key at ${entryPath}[0]`);
|
|
605
|
+
keys.add(key);
|
|
606
|
+
result.push(Object.freeze([
|
|
607
|
+
key,
|
|
608
|
+
parseValue(entry[1], `${entryPath}[1]`)
|
|
609
|
+
]));
|
|
610
|
+
}
|
|
611
|
+
return Object.freeze(result);
|
|
612
|
+
}
|
|
613
|
+
export function uniqueStrings(value, path) {
|
|
614
|
+
const result = [];
|
|
615
|
+
const seen = new Set();
|
|
616
|
+
for (const [index, entry] of arrayValue(value, path).entries()) {
|
|
617
|
+
const string = nonEmptyString(entry, `${path}[${index}]`);
|
|
618
|
+
if (seen.has(string))
|
|
619
|
+
throw new TypeError(`duplicate value at ${path}[${index}]`);
|
|
620
|
+
seen.add(string);
|
|
621
|
+
result.push(string);
|
|
622
|
+
}
|
|
623
|
+
return Object.freeze(result);
|
|
624
|
+
}
|
|
625
|
+
export function assertRecordLink(value, path) {
|
|
626
|
+
if (value === null)
|
|
627
|
+
return;
|
|
628
|
+
if (typeof value === 'string' && value.length > 0)
|
|
629
|
+
return;
|
|
630
|
+
if (!Array.isArray(value))
|
|
631
|
+
throw new TypeError(`invalid record link at ${path}`);
|
|
632
|
+
const seen = new Set();
|
|
633
|
+
for (const [index, entry] of value.entries()) {
|
|
634
|
+
const key = nonEmptyString(entry, `${path}[${index}]`);
|
|
635
|
+
if (seen.has(key))
|
|
636
|
+
throw new TypeError(`duplicate record link at ${path}[${index}]`);
|
|
637
|
+
seen.add(key);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
export function assertJsonValue(value, path, ancestors = new Set()) {
|
|
641
|
+
if (value === null ||
|
|
642
|
+
typeof value === 'string' ||
|
|
643
|
+
typeof value === 'boolean') {
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (typeof value === 'number') {
|
|
647
|
+
if (!Number.isFinite(value))
|
|
648
|
+
throw new TypeError(`non-finite number at ${path}`);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
if (typeof value !== 'object')
|
|
652
|
+
throw new TypeError(`non-JSON value at ${path}`);
|
|
653
|
+
if (ancestors.has(value))
|
|
654
|
+
throw new TypeError(`cyclic JSON value at ${path}`);
|
|
655
|
+
ancestors.add(value);
|
|
656
|
+
if (Array.isArray(value)) {
|
|
657
|
+
for (const [index, entry] of value.entries()) {
|
|
658
|
+
assertJsonValue(entry, `${path}[${index}]`, ancestors);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
else {
|
|
662
|
+
const record = plainRecord(value, path);
|
|
663
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
664
|
+
assertJsonValue(entry, `${path}.${key}`, ancestors);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
ancestors.delete(value);
|
|
668
|
+
}
|
|
669
|
+
export function exactRecord(value, path, allowed, required = allowed) {
|
|
670
|
+
const record = plainRecord(value, path);
|
|
671
|
+
exactKeys(record, path, allowed, required);
|
|
672
|
+
return record;
|
|
673
|
+
}
|
|
674
|
+
export function exactKeys(record, path, allowed, required = allowed) {
|
|
675
|
+
const allowedKeys = new Set(allowed);
|
|
676
|
+
for (const key of Object.keys(record)) {
|
|
677
|
+
if (!allowedKeys.has(key))
|
|
678
|
+
throw new TypeError(`unknown field ${path}.${key}`);
|
|
679
|
+
}
|
|
680
|
+
for (const key of required) {
|
|
681
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) {
|
|
682
|
+
throw new TypeError(`missing field ${path}.${key}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
export function plainRecord(value, path) {
|
|
687
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
688
|
+
throw new TypeError(`expected object at ${path}`);
|
|
689
|
+
}
|
|
690
|
+
const prototype = Object.getPrototypeOf(value);
|
|
691
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
692
|
+
throw new TypeError(`expected plain object at ${path}`);
|
|
693
|
+
}
|
|
694
|
+
return value;
|
|
695
|
+
}
|
|
696
|
+
export function arrayValue(value, path) {
|
|
697
|
+
if (!Array.isArray(value))
|
|
698
|
+
throw new TypeError(`expected array at ${path}`);
|
|
699
|
+
return value;
|
|
700
|
+
}
|
|
701
|
+
export function nonEmptyString(value, path) {
|
|
702
|
+
if (typeof value !== 'string' || value.length === 0) {
|
|
703
|
+
throw new TypeError(`expected non-empty string at ${path}`);
|
|
704
|
+
}
|
|
705
|
+
return value;
|
|
706
|
+
}
|
|
707
|
+
export function decimalString(value, path) {
|
|
708
|
+
const string = nonEmptyString(value, path);
|
|
709
|
+
if (!DECIMAL.test(string))
|
|
710
|
+
throw new TypeError(`expected decimal at ${path}`);
|
|
711
|
+
return string;
|
|
712
|
+
}
|
|
713
|
+
export function booleanValue(value, path) {
|
|
714
|
+
if (typeof value !== 'boolean')
|
|
715
|
+
throw new TypeError(`expected boolean at ${path}`);
|
|
716
|
+
return value;
|
|
717
|
+
}
|
|
718
|
+
export function operationSource(value, path) {
|
|
719
|
+
if (value !== 'query' && value !== 'live') {
|
|
720
|
+
throw new TypeError(`expected operation source at ${path}`);
|
|
721
|
+
}
|
|
722
|
+
return value;
|
|
723
|
+
}
|
|
724
|
+
export function nonNegativeInteger(value, path) {
|
|
725
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
726
|
+
throw new TypeError(`expected non-negative safe integer at ${path}`);
|
|
727
|
+
}
|
|
728
|
+
return value;
|
|
729
|
+
}
|
|
730
|
+
export function compareDecimal(left, right) {
|
|
731
|
+
const leftValue = BigInt(left);
|
|
732
|
+
const rightValue = BigInt(right);
|
|
733
|
+
return leftValue < rightValue ? -1 : leftValue > rightValue ? 1 : 0;
|
|
734
|
+
}
|