@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.
Files changed (242) hide show
  1. package/README.md +454 -0
  2. package/dist/auth-headers.d.ts +8 -0
  3. package/dist/auth-headers.js +35 -0
  4. package/dist/diagnostics.d.ts +2 -0
  5. package/dist/diagnostics.js +1 -0
  6. package/dist/document.d.ts +8 -0
  7. package/dist/document.js +6 -0
  8. package/dist/identity.d.ts +23 -0
  9. package/dist/identity.js +73 -0
  10. package/dist/index.d.ts +7 -0
  11. package/dist/index.js +6 -0
  12. package/dist/internal/cache-engine/create.d.ts +5 -0
  13. package/dist/internal/cache-engine/create.js +7 -0
  14. package/dist/internal/cache-engine/engine.d.ts +32 -0
  15. package/dist/internal/cache-engine/engine.js +1287 -0
  16. package/dist/internal/cache-engine/errors.d.ts +5 -0
  17. package/dist/internal/cache-engine/errors.js +10 -0
  18. package/dist/internal/cache-engine/helpers.d.ts +55 -0
  19. package/dist/internal/cache-engine/helpers.js +545 -0
  20. package/dist/internal/cache-engine/index.d.ts +3 -0
  21. package/dist/internal/cache-engine/index.js +2 -0
  22. package/dist/internal/cache-engine/types.d.ts +300 -0
  23. package/dist/internal/cache-engine/types.js +1 -0
  24. package/dist/internal/cache-engine.d.ts +3 -0
  25. package/dist/internal/cache-engine.js +2 -0
  26. package/dist/lib/assert-name.d.ts +2 -0
  27. package/dist/lib/assert-name.js +6 -0
  28. package/dist/lib/compare-code-units.d.ts +2 -0
  29. package/dist/lib/compare-code-units.js +4 -0
  30. package/dist/lib/deep-equal.d.ts +2 -0
  31. package/dist/lib/deep-equal.js +24 -0
  32. package/dist/lib/freeze-record.d.ts +2 -0
  33. package/dist/lib/freeze-record.js +13 -0
  34. package/dist/lib/index.d.ts +6 -0
  35. package/dist/lib/index.js +6 -0
  36. package/dist/lib/is-plain-record.d.ts +2 -0
  37. package/dist/lib/is-plain-record.js +8 -0
  38. package/dist/lib/report.d.ts +7 -0
  39. package/dist/lib/report.js +26 -0
  40. package/dist/protocol.d.ts +179 -0
  41. package/dist/protocol.js +558 -0
  42. package/dist/react/context.d.ts +20 -0
  43. package/dist/react/context.js +24 -0
  44. package/dist/react/index.d.ts +2 -0
  45. package/dist/react/index.js +3 -0
  46. package/dist/react/query.d.ts +20 -0
  47. package/dist/react/query.js +99 -0
  48. package/dist/replica/command-id.d.ts +2 -0
  49. package/dist/replica/command-id.js +19 -0
  50. package/dist/replica/command-runtime/constants.d.ts +5 -0
  51. package/dist/replica/command-runtime/constants.js +5 -0
  52. package/dist/replica/command-runtime/create.d.ts +2 -0
  53. package/dist/replica/command-runtime/create.js +644 -0
  54. package/dist/replica/command-runtime/errors.d.ts +16 -0
  55. package/dist/replica/command-runtime/errors.js +41 -0
  56. package/dist/replica/command-runtime/index.d.ts +5 -0
  57. package/dist/replica/command-runtime/index.js +4 -0
  58. package/dist/replica/command-runtime/lib/binding.d.ts +4 -0
  59. package/dist/replica/command-runtime/lib/binding.js +64 -0
  60. package/dist/replica/command-runtime/lib/effects.d.ts +11 -0
  61. package/dist/replica/command-runtime/lib/effects.js +89 -0
  62. package/dist/replica/command-runtime/lib/index.d.ts +9 -0
  63. package/dist/replica/command-runtime/lib/index.js +9 -0
  64. package/dist/replica/command-runtime/lib/inventory.d.ts +10 -0
  65. package/dist/replica/command-runtime/lib/inventory.js +141 -0
  66. package/dist/replica/command-runtime/lib/output.d.ts +13 -0
  67. package/dist/replica/command-runtime/lib/output.js +190 -0
  68. package/dist/replica/command-runtime/lib/projection.d.ts +20 -0
  69. package/dist/replica/command-runtime/lib/projection.js +255 -0
  70. package/dist/replica/command-runtime/lib/status.d.ts +7 -0
  71. package/dist/replica/command-runtime/lib/status.js +127 -0
  72. package/dist/replica/command-runtime/lib/transport.d.ts +16 -0
  73. package/dist/replica/command-runtime/lib/transport.js +112 -0
  74. package/dist/replica/command-runtime/lib/util.d.ts +16 -0
  75. package/dist/replica/command-runtime/lib/util.js +127 -0
  76. package/dist/replica/command-runtime/lifecycle.d.ts +11 -0
  77. package/dist/replica/command-runtime/lifecycle.js +13 -0
  78. package/dist/replica/command-runtime/symbols.d.ts +30 -0
  79. package/dist/replica/command-runtime/symbols.js +30 -0
  80. package/dist/replica/command-runtime/types.d.ts +222 -0
  81. package/dist/replica/command-runtime/types.js +1 -0
  82. package/dist/replica/command-runtime.d.ts +3 -0
  83. package/dist/replica/command-runtime.js +2 -0
  84. package/dist/replica/commands/clone.d.ts +12 -0
  85. package/dist/replica/commands/clone.js +187 -0
  86. package/dist/replica/commands/constants.d.ts +6 -0
  87. package/dist/replica/commands/constants.js +6 -0
  88. package/dist/replica/commands/errors.d.ts +7 -0
  89. package/dist/replica/commands/errors.js +22 -0
  90. package/dist/replica/commands/implementation.d.ts +8 -0
  91. package/dist/replica/commands/implementation.js +8 -0
  92. package/dist/replica/commands/index.d.ts +5 -0
  93. package/dist/replica/commands/index.js +4 -0
  94. package/dist/replica/commands/prepare.d.ts +27 -0
  95. package/dist/replica/commands/prepare.js +77 -0
  96. package/dist/replica/commands/presets.d.ts +12 -0
  97. package/dist/replica/commands/presets.js +84 -0
  98. package/dist/replica/commands/receipt.d.ts +3 -0
  99. package/dist/replica/commands/receipt.js +36 -0
  100. package/dist/replica/commands/resolve.d.ts +11 -0
  101. package/dist/replica/commands/resolve.js +165 -0
  102. package/dist/replica/commands/types.d.ts +278 -0
  103. package/dist/replica/commands/types.js +2 -0
  104. package/dist/replica/commands/util.d.ts +21 -0
  105. package/dist/replica/commands/util.js +96 -0
  106. package/dist/replica/commands/validate.d.ts +18 -0
  107. package/dist/replica/commands/validate.js +477 -0
  108. package/dist/replica/commands.d.ts +3 -0
  109. package/dist/replica/commands.js +2 -0
  110. package/dist/replica/diagnostics/event-log.d.ts +4 -0
  111. package/dist/replica/diagnostics/event-log.js +463 -0
  112. package/dist/replica/diagnostics/implementation.d.ts +4 -0
  113. package/dist/replica/diagnostics/implementation.js +4 -0
  114. package/dist/replica/diagnostics/index.d.ts +2 -0
  115. package/dist/replica/diagnostics/index.js +1 -0
  116. package/dist/replica/diagnostics/inspect.d.ts +6 -0
  117. package/dist/replica/diagnostics/inspect.js +170 -0
  118. package/dist/replica/diagnostics/types.d.ts +295 -0
  119. package/dist/replica/diagnostics/types.js +2 -0
  120. package/dist/replica/diagnostics.d.ts +3 -0
  121. package/dist/replica/diagnostics.js +2 -0
  122. package/dist/replica/distributed-replica/clocks.d.ts +23 -0
  123. package/dist/replica/distributed-replica/clocks.js +156 -0
  124. package/dist/replica/distributed-replica/constants.d.ts +12 -0
  125. package/dist/replica/distributed-replica/constants.js +10 -0
  126. package/dist/replica/distributed-replica/helpers.d.ts +48 -0
  127. package/dist/replica/distributed-replica/helpers.js +445 -0
  128. package/dist/replica/distributed-replica/hydration.d.ts +19 -0
  129. package/dist/replica/distributed-replica/hydration.js +380 -0
  130. package/dist/replica/distributed-replica/impl-diagnostics.d.ts +22 -0
  131. package/dist/replica/distributed-replica/impl-diagnostics.js +189 -0
  132. package/dist/replica/distributed-replica/impl-fetch-live.d.ts +39 -0
  133. package/dist/replica/distributed-replica/impl-fetch-live.js +395 -0
  134. package/dist/replica/distributed-replica/impl-hydration-orchestrate.d.ts +56 -0
  135. package/dist/replica/distributed-replica/impl-hydration-orchestrate.js +299 -0
  136. package/dist/replica/distributed-replica/impl-optimistic.d.ts +31 -0
  137. package/dist/replica/distributed-replica/impl-optimistic.js +172 -0
  138. package/dist/replica/distributed-replica/impl-protocol.d.ts +53 -0
  139. package/dist/replica/distributed-replica/impl-protocol.js +108 -0
  140. package/dist/replica/distributed-replica/impl.d.ts +44 -0
  141. package/dist/replica/distributed-replica/impl.js +1776 -0
  142. package/dist/replica/distributed-replica/index.d.ts +2 -0
  143. package/dist/replica/distributed-replica/index.js +4 -0
  144. package/dist/replica/distributed-replica/optimistic.d.ts +30 -0
  145. package/dist/replica/distributed-replica/optimistic.js +221 -0
  146. package/dist/replica/distributed-replica/types.d.ts +147 -0
  147. package/dist/replica/distributed-replica/types.js +1 -0
  148. package/dist/replica/distributed-replica/watch.d.ts +19 -0
  149. package/dist/replica/distributed-replica/watch.js +95 -0
  150. package/dist/replica/distributed-replica.d.ts +2 -0
  151. package/dist/replica/distributed-replica.js +2 -0
  152. package/dist/replica/graphql-transport.d.ts +24 -0
  153. package/dist/replica/graphql-transport.js +122 -0
  154. package/dist/replica/identity/clone.d.ts +6 -0
  155. package/dist/replica/identity/clone.js +54 -0
  156. package/dist/replica/identity/codec.d.ts +48 -0
  157. package/dist/replica/identity/codec.js +789 -0
  158. package/dist/replica/identity/constants.d.ts +3 -0
  159. package/dist/replica/identity/constants.js +18 -0
  160. package/dist/replica/identity/implementation.d.ts +5 -0
  161. package/dist/replica/identity/implementation.js +5 -0
  162. package/dist/replica/identity/index.d.ts +3 -0
  163. package/dist/replica/identity/index.js +3 -0
  164. package/dist/replica/identity/keys.d.ts +29 -0
  165. package/dist/replica/identity/keys.js +173 -0
  166. package/dist/replica/identity.d.ts +2 -0
  167. package/dist/replica/identity.js +2 -0
  168. package/dist/replica/index-maintenance/engine.d.ts +84 -0
  169. package/dist/replica/index-maintenance/engine.js +677 -0
  170. package/dist/replica/index-maintenance/implementation.d.ts +4 -0
  171. package/dist/replica/index-maintenance/implementation.js +4 -0
  172. package/dist/replica/index-maintenance/index.d.ts +2 -0
  173. package/dist/replica/index-maintenance/index.js +1 -0
  174. package/dist/replica/index-maintenance/registry.d.ts +2 -0
  175. package/dist/replica/index-maintenance/registry.js +49 -0
  176. package/dist/replica/index-maintenance/types.d.ts +58 -0
  177. package/dist/replica/index-maintenance/types.js +1 -0
  178. package/dist/replica/index-maintenance.d.ts +3 -0
  179. package/dist/replica/index-maintenance.js +2 -0
  180. package/dist/replica/index.d.ts +17 -0
  181. package/dist/replica/index.js +9 -0
  182. package/dist/replica/materialize.d.ts +10 -0
  183. package/dist/replica/materialize.js +163 -0
  184. package/dist/replica/normalize.d.ts +27 -0
  185. package/dist/replica/normalize.js +298 -0
  186. package/dist/replica/operation-binding.d.ts +14 -0
  187. package/dist/replica/operation-binding.js +76 -0
  188. package/dist/replica/persistence/idb.d.ts +20 -0
  189. package/dist/replica/persistence/idb.js +193 -0
  190. package/dist/replica/persistence/implementation.d.ts +4 -0
  191. package/dist/replica/persistence/implementation.js +4 -0
  192. package/dist/replica/persistence/index.d.ts +2 -0
  193. package/dist/replica/persistence/index.js +1 -0
  194. package/dist/replica/persistence/state.d.ts +104 -0
  195. package/dist/replica/persistence/state.js +734 -0
  196. package/dist/replica/persistence/types.d.ts +54 -0
  197. package/dist/replica/persistence/types.js +1 -0
  198. package/dist/replica/persistence.d.ts +3 -0
  199. package/dist/replica/persistence.js +2 -0
  200. package/dist/replica/query-plan/constants.d.ts +13 -0
  201. package/dist/replica/query-plan/constants.js +7 -0
  202. package/dist/replica/query-plan/filter.d.ts +39 -0
  203. package/dist/replica/query-plan/filter.js +544 -0
  204. package/dist/replica/query-plan/index.d.ts +4 -0
  205. package/dist/replica/query-plan/index.js +3 -0
  206. package/dist/replica/query-plan/order.d.ts +11 -0
  207. package/dist/replica/query-plan/order.js +158 -0
  208. package/dist/replica/query-plan/pagination.d.ts +6 -0
  209. package/dist/replica/query-plan/pagination.js +90 -0
  210. package/dist/replica/query-plan/resolve.d.ts +14 -0
  211. package/dist/replica/query-plan/resolve.js +163 -0
  212. package/dist/replica/query-plan/types.d.ts +89 -0
  213. package/dist/replica/query-plan/types.js +1 -0
  214. package/dist/replica/query-plan/util.d.ts +13 -0
  215. package/dist/replica/query-plan/util.js +62 -0
  216. package/dist/replica/query-plan.d.ts +3 -0
  217. package/dist/replica/query-plan.js +2 -0
  218. package/dist/replica/revalidation.d.ts +9 -0
  219. package/dist/replica/revalidation.js +86 -0
  220. package/dist/replica/selection.d.ts +20 -0
  221. package/dist/replica/selection.js +109 -0
  222. package/dist/replica/types.d.ts +657 -0
  223. package/dist/replica/types.js +1 -0
  224. package/dist/request.d.ts +17 -0
  225. package/dist/request.js +123 -0
  226. package/dist/sveltekit/auth.d.ts +18 -0
  227. package/dist/sveltekit/auth.js +9 -0
  228. package/dist/sveltekit/context.d.ts +28 -0
  229. package/dist/sveltekit/context.js +58 -0
  230. package/dist/sveltekit/index.d.ts +4 -0
  231. package/dist/sveltekit/index.js +4 -0
  232. package/dist/sveltekit/replica.d.ts +145 -0
  233. package/dist/sveltekit/replica.js +491 -0
  234. package/dist/sveltekit/server-replica.d.ts +52 -0
  235. package/dist/sveltekit/server-replica.js +176 -0
  236. package/dist/sveltekit/vite.d.ts +103 -0
  237. package/dist/sveltekit/vite.js +878 -0
  238. package/dist/types.d.ts +33 -0
  239. package/dist/types.js +1 -0
  240. package/dist/websocket.d.ts +41 -0
  241. package/dist/websocket.js +187 -0
  242. 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
+ }