@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,1287 @@
1
+ import { CacheRevisionConflictError } from './errors.js';
2
+ import { assertName, assertSynchronousResult, assertWriterActive, cloneCacheValue, cloneDerivedIndexOperations, cloneFields, cloneIndexMetadata, cloneLink, cloneLinks, compareRecordTuple, deepEqual, dependenciesChanged, derivedIndexKeys, emptyVisibleRecord, freezeRecord, indexDependency, indexMetadataWithoutStaleReason, isOrderedSubsequence, isVisibleRecordLive, linkKeys, operationDependencies, parseSnapshot, recordFieldDependency, recordSeenDependency, recordWildcardDependency, refinementMetadataCompatible, reportSafely, reportUnhandledWatcherError, revisionString, revisionToken, runDerivedIndexReconciler, validateIndexWrite, validateRecordKey } from './helpers.js';
3
+ /**
4
+ * Minimum purpose-built implementation selected by the executable spike.
5
+ *
6
+ * It stores authoritative sparse records and exact indexes, then materializes
7
+ * named optimistic operation layers above them. Confirmation advances a
8
+ * per-dependency causal floor before removing the layer, so an older pending
9
+ * layer cannot become visible again after a newer command confirms.
10
+ */
11
+ export class PurposeBuiltCacheEngine {
12
+ #records = new Map();
13
+ #indexes = new Map();
14
+ #layers = [];
15
+ #derivedIndexOperations = [];
16
+ #derivedIndexReconciler;
17
+ #reconcilingDerivedIndexes = false;
18
+ #confirmedFloors = new Map();
19
+ #retained = new Map();
20
+ #nextLayerSequence = 0;
21
+ #watchers = new Set();
22
+ #transactionDepth = 0;
23
+ #transactionLifecycleIndexes = new Set();
24
+ #dirty = false;
25
+ #changedDependencies = new Set();
26
+ #reportWatcherError;
27
+ constructor(options = {}) {
28
+ this.#reportWatcherError = options.onWatcherError ?? reportUnhandledWatcherError;
29
+ }
30
+ read(selector) {
31
+ if (typeof selector !== 'function')
32
+ throw new TypeError('cache selector must be a function');
33
+ return selector(this.#reader());
34
+ }
35
+ readConfirmed(selector) {
36
+ if (typeof selector !== 'function')
37
+ throw new TypeError('cache selector must be a function');
38
+ return selector(this.#reader(undefined, false));
39
+ }
40
+ confirmedIndexFences(keys) {
41
+ const fences = new Map();
42
+ for (const key of new Set(keys)) {
43
+ assertName(key, 'index key');
44
+ const index = this.#indexes.get(key);
45
+ if (index === undefined)
46
+ continue;
47
+ const fence = index.staleRevision !== undefined &&
48
+ index.staleRevision > index.revision
49
+ ? index.staleRevision
50
+ : index.revision;
51
+ fences.set(key, revisionString(fence));
52
+ }
53
+ return fences;
54
+ }
55
+ watch(selector, listener, options = {}) {
56
+ if (typeof listener !== 'function')
57
+ throw new TypeError('cache listener must be a function');
58
+ const { value, dependencies } = this.#select(selector);
59
+ const watcher = { selector, listener, value, dependencies };
60
+ this.#watchers.add(watcher);
61
+ if (options.immediate) {
62
+ try {
63
+ listener(value, undefined);
64
+ }
65
+ catch (error) {
66
+ this.#watchers.delete(watcher);
67
+ reportSafely(this.#reportWatcherError, new AggregateError([error], 'initial cache watcher delivery failed'));
68
+ }
69
+ }
70
+ return () => this.#watchers.delete(watcher);
71
+ }
72
+ batch(update) {
73
+ if (typeof update !== 'function')
74
+ throw new TypeError('cache update must be a function');
75
+ if (this.#transactionDepth !== 0) {
76
+ throw new Error('nested cache batches are not supported');
77
+ }
78
+ return this.#transaction(() => {
79
+ const result = this.#runBaseUpdate(update);
80
+ this.#reconcileDerivedIndexes();
81
+ return result;
82
+ });
83
+ }
84
+ createOptimisticLayer(id, update, context) {
85
+ assertName(id, 'optimistic layer id');
86
+ if (this.#layers.some((layer) => layer.id === id)) {
87
+ throw new Error(`optimistic layer already exists: ${id}`);
88
+ }
89
+ if (typeof update !== 'function') {
90
+ throw new TypeError('optimistic layer update must be a function');
91
+ }
92
+ const operations = [];
93
+ this.#runOptimisticUpdate(update, operations);
94
+ const stableContext = context === undefined ? undefined : cloneCacheValue(context);
95
+ this.#transaction(() => {
96
+ const before = this.#materialize();
97
+ this.#layers.push({
98
+ id,
99
+ sequence: ++this.#nextLayerSequence,
100
+ state: 'optimistic',
101
+ operations,
102
+ ...(stableContext === undefined ? {} : { context: stableContext })
103
+ });
104
+ this.#markOverlayChanges(operations, before, this.#materialize());
105
+ this.#dirty = true;
106
+ this.#reconcileDerivedIndexes();
107
+ });
108
+ }
109
+ setDerivedIndexReconciler(reconciler) {
110
+ if (reconciler !== undefined && typeof reconciler !== 'function') {
111
+ throw new TypeError('derived index reconciler must be a function');
112
+ }
113
+ const previous = this.#derivedIndexReconciler;
114
+ try {
115
+ this.#transaction(() => {
116
+ this.#derivedIndexReconciler = reconciler;
117
+ this.#reconcileDerivedIndexes();
118
+ });
119
+ }
120
+ catch (error) {
121
+ this.#derivedIndexReconciler = previous;
122
+ throw error;
123
+ }
124
+ }
125
+ markOptimisticLayerAccepted(id) {
126
+ const layer = this.#layers.find((candidate) => candidate.id === id);
127
+ if (!layer)
128
+ return false;
129
+ this.#transaction(() => {
130
+ layer.state = 'accepted';
131
+ this.#reconcileDerivedIndexes();
132
+ });
133
+ return true;
134
+ }
135
+ confirmOptimisticLayer(id, update) {
136
+ return this.confirmOptimisticLayers([id], update);
137
+ }
138
+ confirmOptimisticLayers(ids, update) {
139
+ const unique = [...new Set(ids)];
140
+ if (unique.length !== ids.length) {
141
+ throw new Error('optimistic layer confirmation contains duplicate ids');
142
+ }
143
+ const layers = unique.map((id) => {
144
+ const layer = this.#layers.find((candidate) => candidate.id === id);
145
+ if (!layer)
146
+ throw new Error(`unknown optimistic layer: ${id}`);
147
+ return layer;
148
+ });
149
+ return this.#transaction(() => {
150
+ const before = this.#materialize();
151
+ const layerDependencies = new Map();
152
+ for (const layer of layers) {
153
+ for (const operation of layer.operations) {
154
+ for (const dependency of operationDependencies(operation)) {
155
+ layerDependencies.set(dependency, Math.max(layerDependencies.get(dependency) ?? 0, layer.sequence));
156
+ }
157
+ }
158
+ }
159
+ // A command result may contain a full row, but only fields owned by this
160
+ // optimistic layer are confirmed. Advancing floors for every field in the
161
+ // server payload would incorrectly erase independent, older overlays.
162
+ const result = this.#runBaseUpdate(update);
163
+ for (const [dependency, sequence] of layerDependencies) {
164
+ const previous = this.#confirmedFloors.get(dependency) ?? 0;
165
+ if (sequence > previous)
166
+ this.#confirmedFloors.set(dependency, sequence);
167
+ }
168
+ const removed = new Set(layers);
169
+ this.#layers = this.#layers.filter((candidate) => !removed.has(candidate));
170
+ for (const layer of layers) {
171
+ this.#markOverlayChanges(layer.operations, before, this.#materialize());
172
+ }
173
+ if (this.#layers.length === 0)
174
+ this.#confirmedFloors.clear();
175
+ this.#dirty = true;
176
+ this.#reconcileDerivedIndexes();
177
+ return result;
178
+ });
179
+ }
180
+ rejectOptimisticLayer(id) {
181
+ const layer = this.#layers.find((candidate) => candidate.id === id);
182
+ if (!layer)
183
+ return false;
184
+ this.#transaction(() => {
185
+ const before = this.#materialize();
186
+ this.#layers = this.#layers.filter((candidate) => candidate !== layer);
187
+ this.#markOverlayChanges(layer.operations, before, this.#materialize());
188
+ if (this.#layers.length === 0)
189
+ this.#confirmedFloors.clear();
190
+ this.#dirty = true;
191
+ this.#reconcileDerivedIndexes();
192
+ });
193
+ return true;
194
+ }
195
+ optimisticLayerState(id) {
196
+ return this.#layers.find((layer) => layer.id === id)?.state;
197
+ }
198
+ extract() {
199
+ return Object.freeze({
200
+ version: 1,
201
+ records: Object.freeze([...this.#records]
202
+ .sort(([left], [right]) => left.localeCompare(right))
203
+ .map(([key, record]) => Object.freeze({
204
+ key,
205
+ revision: revisionString(record.revision),
206
+ incarnation: revisionString(record.incarnation),
207
+ ...(record.tombstoneRevision === undefined
208
+ ? {}
209
+ : { tombstoneRevision: revisionString(record.tombstoneRevision) }),
210
+ fields: freezeRecord([...record.fields]
211
+ .sort(([left], [right]) => left.localeCompare(right))
212
+ .map(([name, field]) => [
213
+ name,
214
+ Object.freeze({
215
+ revision: revisionString(field.revision),
216
+ value: cloneCacheValue(field.value)
217
+ })
218
+ ])),
219
+ links: freezeRecord([...record.links]
220
+ .sort(([left], [right]) => left.localeCompare(right))
221
+ .map(([name, link]) => [
222
+ name,
223
+ Object.freeze({
224
+ revision: revisionString(link.revision),
225
+ value: cloneLink(link.value)
226
+ })
227
+ ]))
228
+ }))),
229
+ indexes: Object.freeze([...this.#indexes]
230
+ .sort(([left], [right]) => left.localeCompare(right))
231
+ .map(([key, index]) => Object.freeze({
232
+ key,
233
+ revision: revisionString(index.revision),
234
+ ...(index.staleRevision === undefined
235
+ ? {}
236
+ : { staleRevision: revisionString(index.staleRevision) }),
237
+ records: Object.freeze([...index.records]),
238
+ complete: index.complete,
239
+ deleted: index.deleted,
240
+ ...(index.metadata === undefined
241
+ ? {}
242
+ : { metadata: cloneIndexMetadata(index.metadata) })
243
+ })))
244
+ });
245
+ }
246
+ restore(snapshot) {
247
+ const restored = parseSnapshot(snapshot);
248
+ this.#transaction(() => {
249
+ this.#records = restored.records;
250
+ this.#indexes = restored.indexes;
251
+ this.#layers = [];
252
+ this.#derivedIndexOperations = [];
253
+ this.#confirmedFloors.clear();
254
+ this.#nextLayerSequence = 0;
255
+ this.#changedDependencies.add('*');
256
+ this.#dirty = true;
257
+ });
258
+ }
259
+ restoreConfirmed(snapshot) {
260
+ const restored = parseSnapshot(snapshot);
261
+ this.#transaction(() => {
262
+ this.#records = restored.records;
263
+ this.#indexes = restored.indexes;
264
+ this.#derivedIndexOperations = [];
265
+ this.#changedDependencies.add('*');
266
+ this.#dirty = true;
267
+ this.#reconcileDerivedIndexes();
268
+ });
269
+ }
270
+ discardIndexes(keys) {
271
+ const unique = new Set();
272
+ for (const key of keys) {
273
+ assertName(key, 'index key');
274
+ unique.add(key);
275
+ }
276
+ if (unique.size === 0)
277
+ return;
278
+ this.#transaction(() => {
279
+ let changed = false;
280
+ for (const key of unique) {
281
+ if (!this.#indexes.delete(key))
282
+ continue;
283
+ this.#changedDependencies.add(indexDependency(key));
284
+ changed = true;
285
+ }
286
+ if (changed)
287
+ this.#dirty = true;
288
+ this.#reconcileDerivedIndexes();
289
+ });
290
+ }
291
+ retain(key) {
292
+ assertName(key, 'record key');
293
+ this.#retained.set(key, (this.#retained.get(key) ?? 0) + 1);
294
+ }
295
+ release(key) {
296
+ const count = this.#retained.get(key);
297
+ if (count === undefined)
298
+ return;
299
+ if (count <= 1)
300
+ this.#retained.delete(key);
301
+ else
302
+ this.#retained.set(key, count - 1);
303
+ }
304
+ gc() {
305
+ return this.#transaction(() => {
306
+ // A destructive optimistic overlay must not make confirmed state
307
+ // collectible: rejecting that layer has to reveal the complete base
308
+ // graph again. Conversely, an optimistic index/link may be the only
309
+ // current root for an authoritative record. Preserve the union.
310
+ const optimisticRoots = this.#optimisticRecordRoots();
311
+ const baseGraph = this.#materialize(false);
312
+ const visibleGraph = this.#materialize(true);
313
+ const reachable = new Set([
314
+ ...this.#reachableRecords(baseGraph, optimisticRoots),
315
+ ...this.#reachableRecords(visibleGraph, optimisticRoots)
316
+ ]);
317
+ const collected = [];
318
+ for (const [key, record] of this.#records) {
319
+ // Tombstones are revision fences and cannot be collected like data rows.
320
+ if (record.tombstoneRevision === undefined && !reachable.has(key)) {
321
+ this.#records.delete(key);
322
+ this.#changedDependencies.add(recordSeenDependency(key));
323
+ this.#changedDependencies.add(recordWildcardDependency(key));
324
+ collected.push(key);
325
+ }
326
+ }
327
+ let indexesCollected = false;
328
+ for (const [key, index] of this.#indexes) {
329
+ const parent = index.metadata?.parent;
330
+ if (parent === undefined)
331
+ continue;
332
+ const parentIsLive = isVisibleRecordLive(baseGraph.records.get(parent)) ||
333
+ isVisibleRecordLive(visibleGraph.records.get(parent));
334
+ if (reachable.has(parent) && parentIsLive)
335
+ continue;
336
+ this.#indexes.delete(key);
337
+ this.#changedDependencies.add(indexDependency(key));
338
+ indexesCollected = true;
339
+ }
340
+ if (collected.length > 0 || indexesCollected)
341
+ this.#dirty = true;
342
+ this.#reconcileDerivedIndexes();
343
+ return Object.freeze(collected.sort());
344
+ });
345
+ }
346
+ #reader(dependencies, includeOptimistic = true) {
347
+ // V1 executable-spike tradeoff: materializing the visible graph is
348
+ // O(records + indexes + overlay operations) per selector. The private seam
349
+ // keeps this replaceable with an incrementally indexed graph without
350
+ // changing generated artifacts or the public replica API.
351
+ const { records, indexes } = this.#materialize(includeOptimistic);
352
+ return Object.freeze({
353
+ recordMeta(key) {
354
+ dependencies?.add(recordSeenDependency(key));
355
+ const record = records.get(key);
356
+ if (!record || record.tombstoned)
357
+ return undefined;
358
+ return Object.freeze({
359
+ key,
360
+ incarnation: revisionString(record.incarnation)
361
+ });
362
+ },
363
+ field(key, name) {
364
+ assertName(name, 'record field');
365
+ dependencies?.add(recordSeenDependency(key));
366
+ dependencies?.add(recordFieldDependency(key, `field:${name}`));
367
+ const record = records.get(key);
368
+ if (!record || record.tombstoned || !record.fields.has(name)) {
369
+ return Object.freeze({ present: false });
370
+ }
371
+ return Object.freeze({
372
+ present: true,
373
+ value: cloneCacheValue(record.fields.get(name))
374
+ });
375
+ },
376
+ link(key, name) {
377
+ assertName(name, 'record link');
378
+ dependencies?.add(recordSeenDependency(key));
379
+ dependencies?.add(recordFieldDependency(key, `link:${name}`));
380
+ const record = records.get(key);
381
+ if (!record || record.tombstoned || !record.links.has(name)) {
382
+ return Object.freeze({ present: false });
383
+ }
384
+ return Object.freeze({
385
+ present: true,
386
+ value: cloneLink(record.links.get(name))
387
+ });
388
+ },
389
+ record(key) {
390
+ dependencies?.add(recordWildcardDependency(key));
391
+ const record = records.get(key);
392
+ if (!record || record.tombstoned)
393
+ return undefined;
394
+ return Object.freeze({
395
+ key,
396
+ revision: revisionString(record.revision),
397
+ incarnation: revisionString(record.incarnation),
398
+ fields: freezeRecord([...record.fields].map(([name, value]) => [name, cloneCacheValue(value)])),
399
+ links: freezeRecord([...record.links].map(([name, value]) => [name, cloneLink(value)]))
400
+ });
401
+ },
402
+ index(key) {
403
+ dependencies?.add(indexDependency(key));
404
+ const index = indexes.get(key);
405
+ if (!index || index.deleted)
406
+ return undefined;
407
+ return Object.freeze({
408
+ key,
409
+ revision: revisionString(index.revision),
410
+ ...(index.staleRevision === undefined
411
+ ? {}
412
+ : { staleRevision: revisionString(index.staleRevision) }),
413
+ records: Object.freeze([...index.records]),
414
+ complete: index.complete,
415
+ ...(index.metadata === undefined
416
+ ? {}
417
+ : { metadata: cloneIndexMetadata(index.metadata) })
418
+ });
419
+ }
420
+ });
421
+ }
422
+ #select(selector) {
423
+ const dependencies = new Set();
424
+ return { value: selector(this.#reader(dependencies)), dependencies };
425
+ }
426
+ #optimisticRecordRoots() {
427
+ const roots = new Set();
428
+ for (const layer of this.#layers) {
429
+ for (const operation of layer.operations) {
430
+ if (operation.kind === 'write-record') {
431
+ roots.add(operation.write.key);
432
+ for (const link of Object.values(operation.write.links ?? {})) {
433
+ for (const key of linkKeys(link))
434
+ roots.add(key);
435
+ }
436
+ }
437
+ else if (operation.kind === 'tombstone-record') {
438
+ roots.add(operation.key);
439
+ }
440
+ else if (operation.kind === 'write-index') {
441
+ for (const key of operation.write.records)
442
+ roots.add(key);
443
+ }
444
+ }
445
+ }
446
+ for (const operation of this.#derivedIndexOperations) {
447
+ if (operation.kind !== 'write-index')
448
+ continue;
449
+ for (const key of operation.write.records)
450
+ roots.add(key);
451
+ }
452
+ return roots;
453
+ }
454
+ #reachableRecords(graph, extraRoots) {
455
+ const reachable = new Set([...this.#retained.keys(), ...extraRoots]);
456
+ const pending = [...reachable];
457
+ const relationshipIndexes = new Map();
458
+ for (const index of graph.indexes.values()) {
459
+ if (index.deleted)
460
+ continue;
461
+ const parent = index.metadata?.parent;
462
+ if (parent !== undefined) {
463
+ const indexes = relationshipIndexes.get(parent) ?? [];
464
+ indexes.push(index);
465
+ relationshipIndexes.set(parent, indexes);
466
+ continue;
467
+ }
468
+ for (const key of index.records) {
469
+ if (reachable.has(key))
470
+ continue;
471
+ reachable.add(key);
472
+ pending.push(key);
473
+ }
474
+ }
475
+ while (pending.length > 0) {
476
+ const key = pending.pop();
477
+ const record = graph.records.get(key);
478
+ if (!record || record.tombstoned)
479
+ continue;
480
+ for (const link of record.links.values()) {
481
+ for (const key of linkKeys(link)) {
482
+ if (reachable.has(key))
483
+ continue;
484
+ reachable.add(key);
485
+ pending.push(key);
486
+ }
487
+ }
488
+ for (const index of relationshipIndexes.get(key) ?? []) {
489
+ for (const child of index.records) {
490
+ if (reachable.has(child))
491
+ continue;
492
+ reachable.add(child);
493
+ pending.push(child);
494
+ }
495
+ }
496
+ }
497
+ return reachable;
498
+ }
499
+ #materialize(includeOptimistic = true) {
500
+ const records = new Map();
501
+ const indexes = new Map();
502
+ for (const [key, record] of this.#records) {
503
+ records.set(key, {
504
+ revision: record.revision,
505
+ incarnation: record.incarnation,
506
+ tombstoned: record.tombstoneRevision !== undefined,
507
+ fields: new Map([...record.fields].map(([name, field]) => [name, field.value])),
508
+ links: new Map([...record.links].map(([name, link]) => [name, link.value]))
509
+ });
510
+ }
511
+ for (const [key, index] of this.#indexes) {
512
+ indexes.set(key, {
513
+ revision: index.revision,
514
+ staleRevision: index.staleRevision,
515
+ records: [...index.records],
516
+ complete: index.complete,
517
+ deleted: index.deleted,
518
+ metadata: index.metadata
519
+ });
520
+ }
521
+ if (includeOptimistic) {
522
+ for (const layer of this.#layers) {
523
+ for (const operation of layer.operations) {
524
+ this.#applyOverlay(records, indexes, layer.sequence, operation);
525
+ }
526
+ }
527
+ for (const operation of this.#derivedIndexOperations) {
528
+ this.#applyDerivedIndexOperation(records, indexes, operation);
529
+ }
530
+ }
531
+ return { records, indexes };
532
+ }
533
+ #markOverlayChanges(operations, before, after) {
534
+ for (const operation of operations) {
535
+ if (operation.kind === 'write-record') {
536
+ const key = operation.write.key;
537
+ this.#changedDependencies.add(recordWildcardDependency(key));
538
+ for (const name of Object.keys(operation.write.fields ?? {})) {
539
+ this.#changedDependencies.add(recordFieldDependency(key, `field:${name}`));
540
+ }
541
+ for (const name of Object.keys(operation.write.links ?? {})) {
542
+ this.#changedDependencies.add(recordFieldDependency(key, `link:${name}`));
543
+ }
544
+ const previous = before.records.get(key);
545
+ const next = after.records.get(key);
546
+ if ((previous !== undefined && !previous.tombstoned) !==
547
+ (next !== undefined && !next.tombstoned) ||
548
+ previous?.incarnation !== next?.incarnation) {
549
+ this.#changedDependencies.add(recordSeenDependency(key));
550
+ }
551
+ continue;
552
+ }
553
+ if (operation.kind === 'tombstone-record') {
554
+ this.#changedDependencies.add(recordSeenDependency(operation.key));
555
+ this.#changedDependencies.add(recordWildcardDependency(operation.key));
556
+ continue;
557
+ }
558
+ this.#changedDependencies.add(indexDependency(operation.kind === 'write-index' ? operation.write.key : operation.key));
559
+ }
560
+ }
561
+ #applyOverlay(records, indexes, sequence, operation) {
562
+ if (operation.kind === 'write-record') {
563
+ const { key, fields = {}, links = {} } = operation.write;
564
+ let record = records.get(key);
565
+ let wrote = false;
566
+ for (const [name, value] of Object.entries(fields)) {
567
+ if (sequence <= this.#recordFieldFloor(key, `field:${name}`))
568
+ continue;
569
+ if (!record)
570
+ record = emptyVisibleRecord();
571
+ record.fields.set(name, value);
572
+ wrote = true;
573
+ }
574
+ for (const [name, value] of Object.entries(links)) {
575
+ if (sequence <= this.#recordFieldFloor(key, `link:${name}`))
576
+ continue;
577
+ if (!record)
578
+ record = emptyVisibleRecord();
579
+ record.links.set(name, value);
580
+ wrote = true;
581
+ }
582
+ if (wrote && record) {
583
+ record.tombstoned = false;
584
+ records.set(key, record);
585
+ }
586
+ return;
587
+ }
588
+ if (operation.kind === 'tombstone-record') {
589
+ if (sequence <= (this.#confirmedFloors.get(recordSeenDependency(operation.key)) ?? 0)) {
590
+ return;
591
+ }
592
+ const record = records.get(operation.key) ?? emptyVisibleRecord();
593
+ record.tombstoned = true;
594
+ record.fields.clear();
595
+ record.links.clear();
596
+ records.set(operation.key, record);
597
+ return;
598
+ }
599
+ const key = operation.kind === 'write-index' ? operation.write.key : operation.key;
600
+ if (sequence <= (this.#confirmedFloors.get(indexDependency(key)) ?? 0))
601
+ return;
602
+ if (operation.kind === 'write-index') {
603
+ let metadata = operation.write.metadata;
604
+ if (metadata?.parent !== undefined) {
605
+ const parent = records.get(metadata.parent);
606
+ if (!parent || parent.tombstoned)
607
+ return;
608
+ if (metadata.parentRevision !== undefined &&
609
+ revisionToken(metadata.parentRevision) !== parent.revision) {
610
+ return;
611
+ }
612
+ if (metadata.parentIncarnation !== undefined &&
613
+ revisionToken(metadata.parentIncarnation) !== parent.incarnation) {
614
+ return;
615
+ }
616
+ metadata = cloneIndexMetadata({
617
+ ...metadata,
618
+ parentIncarnation: revisionString(parent.incarnation)
619
+ });
620
+ }
621
+ indexes.set(key, {
622
+ revision: indexes.get(key)?.revision ?? 0n,
623
+ staleRevision: indexes.get(key)?.staleRevision,
624
+ records: [...operation.write.records],
625
+ complete: operation.write.complete ?? false,
626
+ deleted: false,
627
+ metadata
628
+ });
629
+ }
630
+ else {
631
+ const index = indexes.get(key) ?? {
632
+ revision: 0n,
633
+ staleRevision: undefined,
634
+ records: [],
635
+ complete: false,
636
+ deleted: true,
637
+ metadata: undefined
638
+ };
639
+ index.deleted = true;
640
+ index.records = [];
641
+ indexes.set(key, index);
642
+ }
643
+ }
644
+ #applyDerivedIndexOperation(records, indexes, operation) {
645
+ if (operation.kind === 'mark-index-stale') {
646
+ const index = indexes.get(operation.key);
647
+ if (!index || index.deleted)
648
+ return;
649
+ /*
650
+ * Staleness is a freshness claim, not structural data loss. Keep a
651
+ * previously complete visible index renderable while the owner
652
+ * revalidates it. Operations that actually remove membership or
653
+ * records explicitly clear `complete` in their own lifecycle path.
654
+ */
655
+ if (index.staleRevision === undefined ||
656
+ index.staleRevision < index.revision) {
657
+ index.staleRevision = index.revision;
658
+ }
659
+ if (index.metadata !== undefined) {
660
+ index.metadata = cloneIndexMetadata({
661
+ ...index.metadata,
662
+ staleReason: operation.reason
663
+ });
664
+ }
665
+ return;
666
+ }
667
+ const key = operation.kind === 'write-index' ? operation.write.key : operation.key;
668
+ if (operation.kind === 'delete-index') {
669
+ const index = indexes.get(key) ?? {
670
+ revision: 0n,
671
+ staleRevision: undefined,
672
+ records: [],
673
+ complete: false,
674
+ deleted: true,
675
+ metadata: undefined
676
+ };
677
+ index.deleted = true;
678
+ index.records = [];
679
+ indexes.set(key, index);
680
+ return;
681
+ }
682
+ let metadata = operation.write.metadata;
683
+ if (metadata?.parent !== undefined) {
684
+ const parent = records.get(metadata.parent);
685
+ if (!parent || parent.tombstoned)
686
+ return;
687
+ if (metadata.parentRevision !== undefined &&
688
+ revisionToken(metadata.parentRevision) !== parent.revision) {
689
+ return;
690
+ }
691
+ if (metadata.parentIncarnation !== undefined &&
692
+ revisionToken(metadata.parentIncarnation) !== parent.incarnation) {
693
+ return;
694
+ }
695
+ metadata = cloneIndexMetadata({
696
+ ...metadata,
697
+ parentIncarnation: revisionString(parent.incarnation)
698
+ });
699
+ }
700
+ indexes.set(key, {
701
+ revision: indexes.get(key)?.revision ?? 0n,
702
+ staleRevision: indexes.get(key)?.staleRevision,
703
+ records: [...operation.write.records],
704
+ complete: operation.write.complete ?? false,
705
+ deleted: false,
706
+ metadata
707
+ });
708
+ }
709
+ #reconcileDerivedIndexes() {
710
+ if (this.#reconcilingDerivedIndexes) {
711
+ throw new Error('derived index reconciliation cannot be re-entered');
712
+ }
713
+ const reconciler = this.#derivedIndexReconciler;
714
+ let next;
715
+ this.#reconcilingDerivedIndexes = true;
716
+ try {
717
+ next =
718
+ reconciler === undefined
719
+ ? []
720
+ : cloneDerivedIndexOperations(runDerivedIndexReconciler(reconciler, this.extract(), this.#layers.map((layer) => Object.freeze({
721
+ id: layer.id,
722
+ sequence: layer.sequence,
723
+ state: layer.state,
724
+ ...(layer.context === undefined
725
+ ? {}
726
+ : { context: layer.context })
727
+ }))));
728
+ }
729
+ finally {
730
+ this.#reconcilingDerivedIndexes = false;
731
+ }
732
+ const before = this.#materialize();
733
+ const previous = this.#derivedIndexOperations;
734
+ this.#derivedIndexOperations = [...next];
735
+ const after = this.#materialize();
736
+ let changed = false;
737
+ for (const key of derivedIndexKeys([...previous, ...next])) {
738
+ if (deepEqual(before.indexes.get(key), after.indexes.get(key)))
739
+ continue;
740
+ this.#changedDependencies.add(indexDependency(key));
741
+ changed = true;
742
+ }
743
+ if (changed)
744
+ this.#dirty = true;
745
+ }
746
+ #recordFieldFloor(key, field) {
747
+ return Math.max(this.#confirmedFloors.get(recordWildcardDependency(key)) ?? 0, this.#confirmedFloors.get(recordFieldDependency(key, field)) ?? 0);
748
+ }
749
+ #baseWriter(touched, isActive = () => true) {
750
+ return Object.freeze({
751
+ recordClock: (key) => {
752
+ assertWriterActive(isActive());
753
+ validateRecordKey(key);
754
+ const record = this.#records.get(key);
755
+ if (!record)
756
+ return undefined;
757
+ return Object.freeze({
758
+ revision: revisionString(record.revision),
759
+ incarnation: revisionString(record.incarnation),
760
+ tombstoned: record.tombstoneRevision !== undefined
761
+ });
762
+ },
763
+ writeRecord: (write) => {
764
+ assertWriterActive(isActive());
765
+ return this.#writeBaseRecord(write, touched);
766
+ },
767
+ tombstoneRecord: (key, revision, incarnation) => {
768
+ assertWriterActive(isActive());
769
+ return this.#tombstoneBaseRecord(key, revision, incarnation, touched);
770
+ },
771
+ discardRecord: (key) => {
772
+ assertWriterActive(isActive());
773
+ return this.#discardBaseRecord(key, touched);
774
+ },
775
+ writeIndex: (write) => {
776
+ assertWriterActive(isActive());
777
+ return this.#writeBaseIndex(write, touched);
778
+ },
779
+ markIndexStale: (key, reason, revision) => {
780
+ assertWriterActive(isActive());
781
+ return this.#markBaseIndexStale(key, reason, revision, touched);
782
+ },
783
+ deleteIndex: (key, revision) => {
784
+ assertWriterActive(isActive());
785
+ return this.#deleteBaseIndex(key, revision, touched);
786
+ }
787
+ });
788
+ }
789
+ #optimisticWriter(operations, isActive = () => true) {
790
+ return Object.freeze({
791
+ writeRecord(write) {
792
+ assertWriterActive(isActive());
793
+ validateRecordKey(write.key);
794
+ const fields = cloneFields(write.fields);
795
+ const links = cloneLinks(write.links);
796
+ if (Object.keys(fields).length === 0 && Object.keys(links).length === 0)
797
+ return;
798
+ operations.push({ kind: 'write-record', write: { key: write.key, fields, links } });
799
+ },
800
+ tombstoneRecord(key) {
801
+ assertWriterActive(isActive());
802
+ validateRecordKey(key);
803
+ operations.push({ kind: 'tombstone-record', key });
804
+ },
805
+ writeIndex(write) {
806
+ assertWriterActive(isActive());
807
+ validateIndexWrite(write);
808
+ operations.push({
809
+ kind: 'write-index',
810
+ write: {
811
+ key: write.key,
812
+ records: Object.freeze([...write.records]),
813
+ complete: write.complete ?? false,
814
+ ...(write.metadata === undefined
815
+ ? {}
816
+ : { metadata: cloneIndexMetadata(write.metadata) })
817
+ }
818
+ });
819
+ },
820
+ deleteIndex(key) {
821
+ assertWriterActive(isActive());
822
+ assertName(key, 'index key');
823
+ operations.push({ kind: 'delete-index', key });
824
+ }
825
+ });
826
+ }
827
+ #runBaseUpdate(update, touched) {
828
+ let active = true;
829
+ const writer = this.#baseWriter(touched, () => active);
830
+ try {
831
+ const result = update(writer);
832
+ assertSynchronousResult(result, 'cache update');
833
+ return result;
834
+ }
835
+ finally {
836
+ active = false;
837
+ }
838
+ }
839
+ #runOptimisticUpdate(update, operations) {
840
+ let active = true;
841
+ const writer = this.#optimisticWriter(operations, () => active);
842
+ try {
843
+ const result = update(writer);
844
+ assertSynchronousResult(result, 'optimistic layer update');
845
+ }
846
+ finally {
847
+ active = false;
848
+ }
849
+ }
850
+ #writeBaseRecord(write, touched) {
851
+ validateRecordKey(write.key);
852
+ const revision = revisionToken(write.revision);
853
+ const requestedIncarnation = write.incarnation === undefined ? undefined : revisionToken(write.incarnation);
854
+ const fields = cloneFields(write.fields);
855
+ const links = cloneLinks(write.links);
856
+ for (const name of Object.keys(fields)) {
857
+ touched?.add(recordFieldDependency(write.key, `field:${name}`));
858
+ }
859
+ for (const name of Object.keys(links)) {
860
+ touched?.add(recordFieldDependency(write.key, `link:${name}`));
861
+ }
862
+ if (Object.keys(fields).length > 0 || Object.keys(links).length > 0) {
863
+ touched?.add(recordSeenDependency(write.key));
864
+ }
865
+ let record = this.#records.get(write.key);
866
+ if (requestedIncarnation === undefined &&
867
+ record?.tombstoneRevision !== undefined) {
868
+ if (revision < record.revision)
869
+ return false;
870
+ if (revision === record.revision) {
871
+ throw new CacheRevisionConflictError(recordSeenDependency(write.key), revision);
872
+ }
873
+ }
874
+ const incarnation = requestedIncarnation ??
875
+ (record === undefined
876
+ ? revision
877
+ : record.tombstoneRevision === undefined
878
+ ? record.incarnation
879
+ : revision);
880
+ if (record !== undefined) {
881
+ const comparison = compareRecordTuple(incarnation, revision, record.incarnation, record.revision);
882
+ if (comparison < 0)
883
+ return false;
884
+ if (comparison === 0 && record.tombstoneRevision !== undefined) {
885
+ throw new CacheRevisionConflictError(recordSeenDependency(write.key), revision);
886
+ }
887
+ if (comparison > 0 &&
888
+ record.tombstoneRevision !== undefined &&
889
+ incarnation === record.incarnation) {
890
+ throw new CacheRevisionConflictError(recordSeenDependency(write.key), revision);
891
+ }
892
+ }
893
+ let changed = false;
894
+ let presenceChanged = false;
895
+ if (!record) {
896
+ this.#invalidateIndexesForRecordLifecycle(write.key, touched);
897
+ record = {
898
+ revision,
899
+ incarnation,
900
+ fields: new Map(),
901
+ links: new Map()
902
+ };
903
+ this.#records.set(write.key, record);
904
+ changed = true;
905
+ presenceChanged = true;
906
+ }
907
+ else if (incarnation > record.incarnation) {
908
+ this.#invalidateIndexesForRecordLifecycle(write.key, touched);
909
+ record.fields.clear();
910
+ record.links.clear();
911
+ record.tombstoneRevision = undefined;
912
+ record.incarnation = incarnation;
913
+ record.revision = revision;
914
+ changed = true;
915
+ presenceChanged = true;
916
+ }
917
+ for (const [name, value] of Object.entries(fields)) {
918
+ const current = record.fields.get(name);
919
+ if (current && revision < current.revision)
920
+ continue;
921
+ if (current && revision === current.revision) {
922
+ if (deepEqual(current.value, value))
923
+ continue;
924
+ throw new CacheRevisionConflictError(recordFieldDependency(write.key, `field:${name}`), revision);
925
+ }
926
+ record.fields.set(name, { revision, value });
927
+ changed = true;
928
+ }
929
+ for (const [name, value] of Object.entries(links)) {
930
+ const current = record.links.get(name);
931
+ if (current && revision < current.revision)
932
+ continue;
933
+ if (current && revision === current.revision) {
934
+ if (deepEqual(current.value, value))
935
+ continue;
936
+ throw new CacheRevisionConflictError(recordFieldDependency(write.key, `link:${name}`), revision);
937
+ }
938
+ record.links.set(name, { revision, value });
939
+ changed = true;
940
+ }
941
+ if (incarnation === record.incarnation && revision > record.revision) {
942
+ record.revision = revision;
943
+ changed = true;
944
+ }
945
+ if (changed) {
946
+ this.#changedDependencies.add(recordWildcardDependency(write.key));
947
+ if (presenceChanged)
948
+ this.#changedDependencies.add(recordSeenDependency(write.key));
949
+ for (const name of Object.keys(fields)) {
950
+ this.#changedDependencies.add(recordFieldDependency(write.key, `field:${name}`));
951
+ }
952
+ for (const name of Object.keys(links)) {
953
+ this.#changedDependencies.add(recordFieldDependency(write.key, `link:${name}`));
954
+ }
955
+ this.#dirty = true;
956
+ }
957
+ return changed;
958
+ }
959
+ #tombstoneBaseRecord(key, revisionValue, incarnationValue, touched) {
960
+ validateRecordKey(key);
961
+ const revision = revisionToken(revisionValue);
962
+ touched?.add(recordWildcardDependency(key));
963
+ touched?.add(recordSeenDependency(key));
964
+ const record = this.#records.get(key);
965
+ const incarnation = incarnationValue === undefined
966
+ ? (record?.incarnation ?? revision)
967
+ : revisionToken(incarnationValue);
968
+ if (record) {
969
+ const comparison = compareRecordTuple(incarnation, revision, record.incarnation, record.revision);
970
+ if (comparison < 0)
971
+ return false;
972
+ if (comparison === 0) {
973
+ if (record.tombstoneRevision !== undefined)
974
+ return false;
975
+ throw new CacheRevisionConflictError(recordSeenDependency(key), revision);
976
+ }
977
+ record.incarnation = incarnation;
978
+ record.revision = revision;
979
+ record.tombstoneRevision = revision;
980
+ record.fields.clear();
981
+ record.links.clear();
982
+ }
983
+ else {
984
+ this.#records.set(key, {
985
+ revision,
986
+ incarnation,
987
+ tombstoneRevision: revision,
988
+ fields: new Map(),
989
+ links: new Map()
990
+ });
991
+ }
992
+ this.#invalidateIndexesForRecordLifecycle(key, touched);
993
+ this.#changedDependencies.add(recordSeenDependency(key));
994
+ this.#changedDependencies.add(recordWildcardDependency(key));
995
+ this.#dirty = true;
996
+ return true;
997
+ }
998
+ #discardBaseRecord(key, touched) {
999
+ validateRecordKey(key);
1000
+ const hadRecord = this.#records.has(key);
1001
+ const hasIndexReference = [...this.#indexes.values()].some((index) => !index.deleted &&
1002
+ (index.metadata?.parent === key || index.records.includes(key)));
1003
+ if (!hadRecord && !hasIndexReference)
1004
+ return false;
1005
+ this.#records.delete(key);
1006
+ touched?.add(recordSeenDependency(key));
1007
+ touched?.add(recordWildcardDependency(key));
1008
+ this.#invalidateIndexesForRecordLifecycle(key, touched);
1009
+ this.#changedDependencies.add(recordSeenDependency(key));
1010
+ this.#changedDependencies.add(recordWildcardDependency(key));
1011
+ this.#dirty = true;
1012
+ return true;
1013
+ }
1014
+ #invalidateIndexesForRecordLifecycle(recordKey, touched) {
1015
+ for (const [key, index] of this.#indexes) {
1016
+ if (index.deleted)
1017
+ continue;
1018
+ const ownedByRecord = index.metadata?.parent === recordKey;
1019
+ const referencesRecord = index.records.includes(recordKey);
1020
+ if (!ownedByRecord && !referencesRecord)
1021
+ continue;
1022
+ this.#transactionLifecycleIndexes.add(key);
1023
+ index.deleted = ownedByRecord;
1024
+ index.records = ownedByRecord
1025
+ ? []
1026
+ : index.records.filter((candidate) => candidate !== recordKey);
1027
+ index.complete = false;
1028
+ if (index.metadata !== undefined) {
1029
+ index.metadata = cloneIndexMetadata({
1030
+ ...index.metadata,
1031
+ staleReason: 'record-lifecycle-changed'
1032
+ });
1033
+ }
1034
+ if (index.staleRevision === undefined || index.staleRevision < index.revision) {
1035
+ index.staleRevision = index.revision;
1036
+ }
1037
+ touched?.add(indexDependency(key));
1038
+ this.#changedDependencies.add(indexDependency(key));
1039
+ this.#dirty = true;
1040
+ }
1041
+ }
1042
+ #writeBaseIndex(write, touched) {
1043
+ validateIndexWrite(write);
1044
+ const revision = revisionToken(write.revision);
1045
+ touched?.add(indexDependency(write.key));
1046
+ const current = this.#indexes.get(write.key);
1047
+ if (current?.staleRevision !== undefined && revision < current.staleRevision) {
1048
+ return false;
1049
+ }
1050
+ if (current && revision < current.revision)
1051
+ return false;
1052
+ const records = [...write.records];
1053
+ const complete = write.complete ?? false;
1054
+ let metadata = write.metadata === undefined ? undefined : cloneIndexMetadata(write.metadata);
1055
+ if (metadata?.parent !== undefined) {
1056
+ const parent = this.#records.get(metadata.parent);
1057
+ if (!parent || parent.tombstoneRevision !== undefined)
1058
+ return false;
1059
+ if (metadata.parentRevision !== undefined &&
1060
+ revisionToken(metadata.parentRevision) !== parent.revision) {
1061
+ throw new CacheRevisionConflictError(recordSeenDependency(metadata.parent), revisionToken(metadata.parentRevision));
1062
+ }
1063
+ if (metadata.parentIncarnation !== undefined &&
1064
+ revisionToken(metadata.parentIncarnation) !== parent.incarnation) {
1065
+ throw new CacheRevisionConflictError(recordSeenDependency(metadata.parent), revisionToken(metadata.parentIncarnation));
1066
+ }
1067
+ metadata = cloneIndexMetadata({
1068
+ ...metadata,
1069
+ parentIncarnation: revisionString(parent.incarnation)
1070
+ });
1071
+ }
1072
+ const staleRevision = metadata?.staleReason === undefined ? undefined : revision;
1073
+ if (current && revision === current.revision) {
1074
+ if (current.deleted &&
1075
+ current.metadata === undefined &&
1076
+ current.records.length === 0 &&
1077
+ current.staleRevision === revision) {
1078
+ // A hidden fence uses an empty deleted index so it materializes no
1079
+ // membership. Revision zero is valid, so an equal-checkpoint success
1080
+ // must be able to replace that sentinel state.
1081
+ current.records = records;
1082
+ current.complete = complete;
1083
+ current.deleted = false;
1084
+ current.metadata = metadata;
1085
+ current.staleRevision = staleRevision;
1086
+ this.#changedDependencies.add(indexDependency(write.key));
1087
+ this.#dirty = true;
1088
+ return true;
1089
+ }
1090
+ if (complete &&
1091
+ metadata?.staleReason === undefined &&
1092
+ (this.#transactionLifecycleIndexes.has(write.key) ||
1093
+ (!current.deleted &&
1094
+ !current.complete &&
1095
+ current.metadata?.staleReason !== 'record-lifecycle-changed' &&
1096
+ isOrderedSubsequence(current.records, records) &&
1097
+ refinementMetadataCompatible(current.metadata, metadata)))) {
1098
+ // A partial GraphQL result may be retried without the underlying read
1099
+ // model advancing. Permit only the monotonic incomplete -> complete
1100
+ // refinement; an already-authoritative membership still conflicts on
1101
+ // any same-revision disagreement.
1102
+ current.records = records;
1103
+ current.complete = true;
1104
+ current.deleted = false;
1105
+ current.metadata = metadata;
1106
+ current.staleRevision = undefined;
1107
+ this.#changedDependencies.add(indexDependency(write.key));
1108
+ this.#dirty = true;
1109
+ return true;
1110
+ }
1111
+ const currentComparable = indexMetadataWithoutStaleReason(current.metadata);
1112
+ const nextComparable = indexMetadataWithoutStaleReason(metadata);
1113
+ if (!current.deleted &&
1114
+ current.complete === complete &&
1115
+ deepEqual(current.records, records) &&
1116
+ deepEqual(currentComparable, nextComparable)) {
1117
+ if (deepEqual(current.metadata, metadata) &&
1118
+ current.staleRevision === staleRevision) {
1119
+ return false;
1120
+ }
1121
+ current.metadata = metadata;
1122
+ current.staleRevision = staleRevision;
1123
+ this.#changedDependencies.add(indexDependency(write.key));
1124
+ this.#dirty = true;
1125
+ return true;
1126
+ }
1127
+ throw new CacheRevisionConflictError(indexDependency(write.key), revision);
1128
+ }
1129
+ this.#indexes.set(write.key, {
1130
+ revision,
1131
+ records,
1132
+ complete,
1133
+ deleted: false,
1134
+ metadata,
1135
+ staleRevision
1136
+ });
1137
+ this.#changedDependencies.add(indexDependency(write.key));
1138
+ this.#dirty = true;
1139
+ return true;
1140
+ }
1141
+ #markBaseIndexStale(key, reason, revisionValue, touched) {
1142
+ assertName(key, 'index key');
1143
+ assertName(reason, 'index stale reason');
1144
+ let current = this.#indexes.get(key);
1145
+ if (!current) {
1146
+ if (revisionValue === undefined)
1147
+ return false;
1148
+ const revision = revisionToken(revisionValue);
1149
+ this.#indexes.set(key, {
1150
+ revision: 0n,
1151
+ staleRevision: revision,
1152
+ records: [],
1153
+ complete: false,
1154
+ deleted: true,
1155
+ metadata: undefined
1156
+ });
1157
+ touched?.add(indexDependency(key));
1158
+ this.#changedDependencies.add(indexDependency(key));
1159
+ this.#dirty = true;
1160
+ return true;
1161
+ }
1162
+ if (current.deleted && revisionValue === undefined)
1163
+ return false;
1164
+ const revision = revisionValue === undefined ? current.revision : revisionToken(revisionValue);
1165
+ const fence = current.staleRevision !== undefined && current.staleRevision > current.revision
1166
+ ? current.staleRevision
1167
+ : current.revision;
1168
+ if (revision < fence)
1169
+ return false;
1170
+ if (current.staleRevision === revision &&
1171
+ (current.metadata === undefined || current.metadata.staleReason === reason)) {
1172
+ return false;
1173
+ }
1174
+ touched?.add(indexDependency(key));
1175
+ if (current.metadata !== undefined) {
1176
+ current.metadata = cloneIndexMetadata({ ...current.metadata, staleReason: reason });
1177
+ }
1178
+ current.staleRevision = revision;
1179
+ this.#changedDependencies.add(indexDependency(key));
1180
+ this.#dirty = true;
1181
+ return true;
1182
+ }
1183
+ #deleteBaseIndex(key, revisionValue, touched) {
1184
+ assertName(key, 'index key');
1185
+ const revision = revisionToken(revisionValue);
1186
+ touched?.add(indexDependency(key));
1187
+ const current = this.#indexes.get(key);
1188
+ if (current?.staleRevision !== undefined && revision < current.staleRevision) {
1189
+ return false;
1190
+ }
1191
+ if (current && revision < current.revision)
1192
+ return false;
1193
+ if (current && revision === current.revision) {
1194
+ if (current.deleted)
1195
+ return false;
1196
+ throw new CacheRevisionConflictError(indexDependency(key), revision);
1197
+ }
1198
+ this.#indexes.set(key, {
1199
+ revision,
1200
+ records: [],
1201
+ complete: false,
1202
+ deleted: true,
1203
+ metadata: current?.metadata,
1204
+ staleRevision: undefined
1205
+ });
1206
+ this.#changedDependencies.add(indexDependency(key));
1207
+ this.#dirty = true;
1208
+ return true;
1209
+ }
1210
+ #transaction(update) {
1211
+ const outermost = this.#transactionDepth === 0;
1212
+ const backup = outermost ? this.#backup() : undefined;
1213
+ if (outermost)
1214
+ this.#transactionLifecycleIndexes = new Set();
1215
+ this.#transactionDepth += 1;
1216
+ let result;
1217
+ try {
1218
+ result = update();
1219
+ }
1220
+ catch (error) {
1221
+ this.#transactionDepth -= 1;
1222
+ if (outermost && backup) {
1223
+ this.#restoreBackup(backup);
1224
+ this.#dirty = false;
1225
+ this.#transactionLifecycleIndexes = new Set();
1226
+ }
1227
+ throw error;
1228
+ }
1229
+ this.#transactionDepth -= 1;
1230
+ if (outermost) {
1231
+ const shouldFlush = this.#dirty;
1232
+ const changedDependencies = this.#changedDependencies;
1233
+ this.#dirty = false;
1234
+ this.#changedDependencies = new Set();
1235
+ this.#transactionLifecycleIndexes = new Set();
1236
+ if (shouldFlush)
1237
+ this.#flushWatchers(changedDependencies);
1238
+ }
1239
+ return result;
1240
+ }
1241
+ #flushWatchers(changedDependencies) {
1242
+ const errors = [];
1243
+ for (const watcher of this.#watchers) {
1244
+ if (!dependenciesChanged(watcher.dependencies, changedDependencies))
1245
+ continue;
1246
+ try {
1247
+ const { value: next, dependencies } = this.#select(watcher.selector);
1248
+ watcher.dependencies = dependencies;
1249
+ if (deepEqual(next, watcher.value))
1250
+ continue;
1251
+ const previous = watcher.value;
1252
+ watcher.value = next;
1253
+ watcher.listener(next, previous);
1254
+ }
1255
+ catch (error) {
1256
+ errors.push(error);
1257
+ }
1258
+ }
1259
+ if (errors.length > 0) {
1260
+ reportSafely(this.#reportWatcherError, new AggregateError(errors, 'cache transaction committed, but watcher delivery failed'));
1261
+ }
1262
+ }
1263
+ #backup() {
1264
+ const backup = structuredClone({
1265
+ records: this.#records,
1266
+ indexes: this.#indexes,
1267
+ layers: this.#layers,
1268
+ derivedIndexOperations: this.#derivedIndexOperations,
1269
+ confirmedFloors: this.#confirmedFloors,
1270
+ retained: this.#retained,
1271
+ nextLayerSequence: this.#nextLayerSequence,
1272
+ changedDependencies: new Set()
1273
+ });
1274
+ backup.changedDependencies = new Set(this.#changedDependencies);
1275
+ return backup;
1276
+ }
1277
+ #restoreBackup(backup) {
1278
+ this.#records = backup.records;
1279
+ this.#indexes = backup.indexes;
1280
+ this.#layers = backup.layers;
1281
+ this.#derivedIndexOperations = backup.derivedIndexOperations;
1282
+ this.#confirmedFloors = backup.confirmedFloors;
1283
+ this.#retained = backup.retained;
1284
+ this.#nextLayerSequence = backup.nextLayerSequence;
1285
+ this.#changedDependencies = new Set(backup.changedDependencies);
1286
+ }
1287
+ }