@adobe/data-persistence 0.9.90

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 (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/dist/backend/memory-backend.d.ts +6 -0
  4. package/dist/backend/memory-backend.js +92 -0
  5. package/dist/backend/memory-backend.js.map +1 -0
  6. package/dist/backend/memory-backend.test.d.ts +1 -0
  7. package/dist/backend/memory-backend.test.js +7 -0
  8. package/dist/backend/memory-backend.test.js.map +1 -0
  9. package/dist/backend/memory-file.d.ts +7 -0
  10. package/dist/backend/memory-file.js +90 -0
  11. package/dist/backend/memory-file.js.map +1 -0
  12. package/dist/backend/memory-file.test.d.ts +1 -0
  13. package/dist/backend/memory-file.test.js +5 -0
  14. package/dist/backend/memory-file.test.js.map +1 -0
  15. package/dist/backend/persistence-backend.conformance.d.ts +10 -0
  16. package/dist/backend/persistence-backend.conformance.js +104 -0
  17. package/dist/backend/persistence-backend.conformance.js.map +1 -0
  18. package/dist/backend/persistence-backend.d.ts +54 -0
  19. package/dist/backend/persistence-backend.js +3 -0
  20. package/dist/backend/persistence-backend.js.map +1 -0
  21. package/dist/backend/random-access-file.conformance.d.ts +20 -0
  22. package/dist/backend/random-access-file.conformance.js +156 -0
  23. package/dist/backend/random-access-file.conformance.js.map +1 -0
  24. package/dist/backend/random-access-file.d.ts +39 -0
  25. package/dist/backend/random-access-file.js +3 -0
  26. package/dist/backend/random-access-file.js.map +1 -0
  27. package/dist/backend/validate-rel-path.d.ts +14 -0
  28. package/dist/backend/validate-rel-path.js +44 -0
  29. package/dist/backend/validate-rel-path.js.map +1 -0
  30. package/dist/backend/validate-rel-path.test.d.ts +1 -0
  31. package/dist/backend/validate-rel-path.test.js +37 -0
  32. package/dist/backend/validate-rel-path.test.js.map +1 -0
  33. package/dist/browser/browser-worker-bootstrap.d.ts +1 -0
  34. package/dist/browser/browser-worker-bootstrap.js +97 -0
  35. package/dist/browser/browser-worker-bootstrap.js.map +1 -0
  36. package/dist/browser/browser-worker-transport.browser.test.d.ts +1 -0
  37. package/dist/browser/browser-worker-transport.browser.test.js +156 -0
  38. package/dist/browser/browser-worker-transport.browser.test.js.map +1 -0
  39. package/dist/browser/browser-worker-transport.d.ts +33 -0
  40. package/dist/browser/browser-worker-transport.js +120 -0
  41. package/dist/browser/browser-worker-transport.js.map +1 -0
  42. package/dist/browser/create-opfs-provider.d.ts +23 -0
  43. package/dist/browser/create-opfs-provider.js +49 -0
  44. package/dist/browser/create-opfs-provider.js.map +1 -0
  45. package/dist/browser/index.d.ts +6 -0
  46. package/dist/browser/index.js +12 -0
  47. package/dist/browser/index.js.map +1 -0
  48. package/dist/browser/mount-opfs.d.ts +12 -0
  49. package/dist/browser/mount-opfs.js +15 -0
  50. package/dist/browser/mount-opfs.js.map +1 -0
  51. package/dist/browser/opfs-backend-lock.browser.test.d.ts +1 -0
  52. package/dist/browser/opfs-backend-lock.browser.test.js +127 -0
  53. package/dist/browser/opfs-backend-lock.browser.test.js.map +1 -0
  54. package/dist/browser/opfs-backend.d.ts +6 -0
  55. package/dist/browser/opfs-backend.js +162 -0
  56. package/dist/browser/opfs-backend.js.map +1 -0
  57. package/dist/browser/opfs-lock-test-worker.d.ts +1 -0
  58. package/dist/browser/opfs-lock-test-worker.js +50 -0
  59. package/dist/browser/opfs-lock-test-worker.js.map +1 -0
  60. package/dist/browser/opfs-sync-file.d.ts +10 -0
  61. package/dist/browser/opfs-sync-file.js +58 -0
  62. package/dist/browser/opfs-sync-file.js.map +1 -0
  63. package/dist/encoder/create-column-encoder.d.ts +15 -0
  64. package/dist/encoder/create-column-encoder.js +110 -0
  65. package/dist/encoder/create-column-encoder.js.map +1 -0
  66. package/dist/encoder/journal-snapshot-codec.d.ts +21 -0
  67. package/dist/encoder/journal-snapshot-codec.js +103 -0
  68. package/dist/encoder/journal-snapshot-codec.js.map +1 -0
  69. package/dist/encoder/journal-snapshot-codec.test.d.ts +1 -0
  70. package/dist/encoder/journal-snapshot-codec.test.js +96 -0
  71. package/dist/encoder/journal-snapshot-codec.test.js.map +1 -0
  72. package/dist/encoder/types.d.ts +72 -0
  73. package/dist/encoder/types.js +3 -0
  74. package/dist/encoder/types.js.map +1 -0
  75. package/dist/index.d.ts +24 -0
  76. package/dist/index.js +15 -0
  77. package/dist/index.js.map +1 -0
  78. package/dist/journal/journal-codec.d.ts +28 -0
  79. package/dist/journal/journal-codec.js +82 -0
  80. package/dist/journal/journal-codec.js.map +1 -0
  81. package/dist/journal/journal-codec.test.d.ts +1 -0
  82. package/dist/journal/journal-codec.test.js +106 -0
  83. package/dist/journal/journal-codec.test.js.map +1 -0
  84. package/dist/journal/journal-entry.d.ts +63 -0
  85. package/dist/journal/journal-entry.js +28 -0
  86. package/dist/journal/journal-entry.js.map +1 -0
  87. package/dist/node/create-node-fs-provider.d.ts +34 -0
  88. package/dist/node/create-node-fs-provider.js +48 -0
  89. package/dist/node/create-node-fs-provider.js.map +1 -0
  90. package/dist/node/index.d.ts +6 -0
  91. package/dist/node/index.js +10 -0
  92. package/dist/node/index.js.map +1 -0
  93. package/dist/node/mount-node-fs.d.ts +12 -0
  94. package/dist/node/mount-node-fs.js +15 -0
  95. package/dist/node/mount-node-fs.js.map +1 -0
  96. package/dist/node/node-fs-backend-lock.node.test.d.ts +1 -0
  97. package/dist/node/node-fs-backend-lock.node.test.js +160 -0
  98. package/dist/node/node-fs-backend-lock.node.test.js.map +1 -0
  99. package/dist/node/node-fs-backend.d.ts +21 -0
  100. package/dist/node/node-fs-backend.js +186 -0
  101. package/dist/node/node-fs-backend.js.map +1 -0
  102. package/dist/node/node-fs-backend.node.test.d.ts +1 -0
  103. package/dist/node/node-fs-backend.node.test.js +17 -0
  104. package/dist/node/node-fs-backend.node.test.js.map +1 -0
  105. package/dist/node/node-fs-file.d.ts +6 -0
  106. package/dist/node/node-fs-file.js +57 -0
  107. package/dist/node/node-fs-file.js.map +1 -0
  108. package/dist/node/node-fs-file.node.test.d.ts +1 -0
  109. package/dist/node/node-fs-file.node.test.js +13 -0
  110. package/dist/node/node-fs-file.node.test.js.map +1 -0
  111. package/dist/node/node-fs-provider.node.test.d.ts +1 -0
  112. package/dist/node/node-fs-provider.node.test.js +119 -0
  113. package/dist/node/node-fs-provider.node.test.js.map +1 -0
  114. package/dist/node/node-worker-bootstrap.d.ts +1 -0
  115. package/dist/node/node-worker-bootstrap.js +104 -0
  116. package/dist/node/node-worker-bootstrap.js.map +1 -0
  117. package/dist/node/node-worker-transport.d.ts +27 -0
  118. package/dist/node/node-worker-transport.js +152 -0
  119. package/dist/node/node-worker-transport.js.map +1 -0
  120. package/dist/node/node-worker-transport.node.test.d.ts +1 -0
  121. package/dist/node/node-worker-transport.node.test.js +147 -0
  122. package/dist/node/node-worker-transport.node.test.js.map +1 -0
  123. package/dist/perftest/perf-harness.d.ts +74 -0
  124. package/dist/perftest/perf-harness.js +147 -0
  125. package/dist/perftest/perf-harness.js.map +1 -0
  126. package/dist/perftest/persistence-perf.d.ts +2 -0
  127. package/dist/perftest/persistence-perf.js +290 -0
  128. package/dist/perftest/persistence-perf.js.map +1 -0
  129. package/dist/perftest/run.d.ts +1 -0
  130. package/dist/perftest/run.js +28 -0
  131. package/dist/perftest/run.js.map +1 -0
  132. package/dist/provider/create-memory-provider.d.ts +16 -0
  133. package/dist/provider/create-memory-provider.js +44 -0
  134. package/dist/provider/create-memory-provider.js.map +1 -0
  135. package/dist/provider/mount.d.ts +20 -0
  136. package/dist/provider/mount.js +20 -0
  137. package/dist/provider/mount.js.map +1 -0
  138. package/dist/provider/persistence-provider.d.ts +63 -0
  139. package/dist/provider/persistence-provider.js +3 -0
  140. package/dist/provider/persistence-provider.js.map +1 -0
  141. package/dist/provider/persistence-provider.test.d.ts +9 -0
  142. package/dist/provider/persistence-provider.test.js +139 -0
  143. package/dist/provider/persistence-provider.test.js.map +1 -0
  144. package/dist/service/component-nonpersistent-durability.test.d.ts +1 -0
  145. package/dist/service/component-nonpersistent-durability.test.js +57 -0
  146. package/dist/service/component-nonpersistent-durability.test.js.map +1 -0
  147. package/dist/service/create-incremental-persistence-service.d.ts +7 -0
  148. package/dist/service/create-incremental-persistence-service.js +960 -0
  149. package/dist/service/create-incremental-persistence-service.js.map +1 -0
  150. package/dist/service/create-worker-persistence-service.d.ts +1 -0
  151. package/dist/service/create-worker-persistence-service.js +7 -0
  152. package/dist/service/create-worker-persistence-service.js.map +1 -0
  153. package/dist/service/create-worker-persistence-service.test.d.ts +1 -0
  154. package/dist/service/create-worker-persistence-service.test.js +291 -0
  155. package/dist/service/create-worker-persistence-service.test.js.map +1 -0
  156. package/dist/service/entity-location-cache.d.ts +15 -0
  157. package/dist/service/entity-location-cache.js +75 -0
  158. package/dist/service/entity-location-cache.js.map +1 -0
  159. package/dist/service/entity-location-density.test.d.ts +1 -0
  160. package/dist/service/entity-location-density.test.js +42 -0
  161. package/dist/service/entity-location-density.test.js.map +1 -0
  162. package/dist/service/incremental-persistence-service.d.ts +57 -0
  163. package/dist/service/incremental-persistence-service.js +3 -0
  164. package/dist/service/incremental-persistence-service.js.map +1 -0
  165. package/dist/service/intermediate-relocation-durability.test.d.ts +1 -0
  166. package/dist/service/intermediate-relocation-durability.test.js +89 -0
  167. package/dist/service/intermediate-relocation-durability.test.js.map +1 -0
  168. package/dist/service/internal-access.d.ts +34 -0
  169. package/dist/service/internal-access.js +43 -0
  170. package/dist/service/internal-access.js.map +1 -0
  171. package/dist/service/journal-replay.test.d.ts +1 -0
  172. package/dist/service/journal-replay.test.js +424 -0
  173. package/dist/service/journal-replay.test.js.map +1 -0
  174. package/dist/service/journal-storage-durability.test.d.ts +1 -0
  175. package/dist/service/journal-storage-durability.test.js +171 -0
  176. package/dist/service/journal-storage-durability.test.js.map +1 -0
  177. package/dist/service/load-roundtrip.test.d.ts +1 -0
  178. package/dist/service/load-roundtrip.test.js +224 -0
  179. package/dist/service/load-roundtrip.test.js.map +1 -0
  180. package/dist/service/persistence-complexity.test.d.ts +1 -0
  181. package/dist/service/persistence-complexity.test.js +275 -0
  182. package/dist/service/persistence-complexity.test.js.map +1 -0
  183. package/dist/service/worker-persistence-service.d.ts +24 -0
  184. package/dist/service/worker-persistence-service.js +8 -0
  185. package/dist/service/worker-persistence-service.js.map +1 -0
  186. package/dist/transport/inprocess-transport.d.ts +15 -0
  187. package/dist/transport/inprocess-transport.js +64 -0
  188. package/dist/transport/inprocess-transport.js.map +1 -0
  189. package/dist/transport/inprocess-transport.test.d.ts +1 -0
  190. package/dist/transport/inprocess-transport.test.js +100 -0
  191. package/dist/transport/inprocess-transport.test.js.map +1 -0
  192. package/dist/transport/router.d.ts +12 -0
  193. package/dist/transport/router.js +146 -0
  194. package/dist/transport/router.js.map +1 -0
  195. package/dist/transport/transport.d.ts +105 -0
  196. package/dist/transport/transport.js +3 -0
  197. package/dist/transport/transport.js.map +1 -0
  198. package/package.json +59 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025-2026 Adobe, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,258 @@
1
+ # @adobe/data-persistence
2
+
3
+ Incremental persistence for `@adobe/data` ECS databases — O(1) writes,
4
+ append-only journal, crash recovery.
5
+
6
+ This package is **purely additive** — it does not replace `db.toData()` /
7
+ `db.fromData()`, which remain the canonical whole-world snapshot mechanism.
8
+ Use this package when worlds are large enough that re-serializing the
9
+ entire database on every save is prohibitive.
10
+
11
+ ## Status
12
+
13
+ Functionally complete. Crash-recovery and journal-replay are tested across both runtimes; see the Crash safety section below for the precise contract and the Performance section for benchmarked baselines.
14
+
15
+ ## Design
16
+
17
+ - **O(1) byte-range writes** to per-archetype × column files
18
+ - **Append-only journal** for changes between checkpoints (true
19
+ write-ahead log: each tx is closed by a `commit` entry; replay only
20
+ applies entries belonging to a tx whose commit reached disk)
21
+ - **Pluggable storage** via `PersistenceProvider` — OPFS, Node `fs`,
22
+ in-memory, or a custom cloud implementation
23
+ - Workers are **optional** for Node (libuv is already async); always
24
+ required for OPFS (`FileSystemSyncAccessHandle` needs a worker context)
25
+ - Single core implementation; only the `RandomAccessFile`,
26
+ `PersistenceBackend`, and `Transport` factories differ per runtime
27
+
28
+ ## Quick start
29
+
30
+ ### Node.js
31
+
32
+ ```ts
33
+ import { mount } from "@adobe/data-persistence";
34
+ import { createNodeFsProvider } from "@adobe/data-persistence/node";
35
+ import { Database } from "@adobe/data/ecs";
36
+
37
+ const db = Database.create(myPlugin);
38
+ const provider = createNodeFsProvider("/tmp/my-save");
39
+ const m = await mount(provider, db);
40
+
41
+ await m.service.load(); // restore previous session
42
+
43
+ // ... run game / app ...
44
+
45
+ await m.service.flush();
46
+ await m.service.checkpoint();
47
+ await m.dispose(); // flushes, closes transport, releases lock
48
+ ```
49
+
50
+ Pass `{ worker: true }` to move I/O off the calling thread into a
51
+ `node:worker_threads` worker:
52
+
53
+ ```ts
54
+ const provider = createNodeFsProvider("/tmp/my-save", { worker: true });
55
+ ```
56
+
57
+ ### Browser (inside a Vite app)
58
+
59
+ ```ts
60
+ import { mount } from "@adobe/data-persistence";
61
+ import { createOpfsProvider } from "@adobe/data-persistence/browser";
62
+ import { Database } from "@adobe/data/ecs";
63
+
64
+ const db = Database.create(myPlugin);
65
+ // Omit the argument to use the per-origin OPFS root (the common case).
66
+ const provider = createOpfsProvider();
67
+ const m = await mount(provider, db);
68
+
69
+ await m.service.load(); // restore previous session
70
+
71
+ // ... run app ...
72
+
73
+ await m.service.flush();
74
+ await m.service.checkpoint();
75
+ await m.dispose(); // flushes, closes OPFS worker, releases lock
76
+ ```
77
+
78
+ See [`examples/node-basic.ts`](./examples/node-basic.ts) and
79
+ [`examples/browser-main.ts`](./examples/browser-main.ts) for complete
80
+ annotated examples with write and read passes.
81
+
82
+ ## Public entry points
83
+
84
+ | Import path | Use from |
85
+ |---|---|
86
+ | `@adobe/data-persistence` | runtime-agnostic surface (interfaces, `mount`, `createMemoryProvider`, service factory) |
87
+ | `@adobe/data-persistence/browser` | OPFS backend + browser worker transport + `createOpfsProvider` |
88
+ | `@adobe/data-persistence/node` | `node:fs` backend + `node:worker_threads` transport + `createNodeFsProvider` |
89
+
90
+ ## API reference
91
+
92
+ ### `mount(provider, database, options?)`
93
+
94
+ The single entry point for all persistence. Returns a `PersistenceMount`.
95
+
96
+ ```ts
97
+ const m = await mount(provider, database, {
98
+ autoPersist: true, // default: true
99
+ checkpoint: {
100
+ everyNTransactions: 1000, // default: 1000
101
+ idleMs: 5000, // default: 5000
102
+ },
103
+ });
104
+ ```
105
+
106
+ ### `PersistenceMount`
107
+
108
+ ```ts
109
+ interface PersistenceMount {
110
+ service: IncrementalPersistenceService;
111
+ dispose(): Promise<void>;
112
+ }
113
+ ```
114
+
115
+ ### `IncrementalPersistenceService`
116
+
117
+ ```ts
118
+ interface IncrementalPersistenceService {
119
+ serviceName: string;
120
+ save(): Promise<void>;
121
+ load(): Promise<void>;
122
+ flush(): Promise<void>;
123
+ checkpoint(): Promise<void>;
124
+ dispose(): Promise<void>;
125
+ }
126
+ ```
127
+
128
+ ## Writing a custom cloud provider
129
+
130
+ External implementers choose one of two seams depending on whether their
131
+ storage supports byte-range writes.
132
+
133
+ ### Pattern A — byte-range capable storage
134
+
135
+ Implement `PersistenceBackend` + `RandomAccessFile`. Then wrap with
136
+ `createInprocessTransport` + `createIncrementalPersistenceService`. This
137
+ reuses 95% of the existing code and supports the full journal + checkpoint
138
+ strategy automatically.
139
+
140
+ ```ts
141
+ import type { PersistenceProvider } from "@adobe/data-persistence";
142
+ import { createInprocessTransport, createIncrementalPersistenceService } from "@adobe/data-persistence";
143
+
144
+ export const createMyCloudProvider = (client: CloudClient): PersistenceProvider => ({
145
+ providerName: "MyCloudProvider",
146
+ async mount(database, opts) {
147
+ const backend = createMyCloudBackend(client); // implements PersistenceBackend
148
+ const transport = createInprocessTransport(backend);
149
+ const service = await createIncrementalPersistenceService({
150
+ database, backend, transport,
151
+ autoPersist: opts?.autoPersist,
152
+ checkpoint: opts?.checkpoint,
153
+ });
154
+ return {
155
+ service,
156
+ dispose: async () => { await service.dispose(); await transport.close(); },
157
+ };
158
+ },
159
+ });
160
+ ```
161
+
162
+ If your backend does not currently support byte-range writes but will in
163
+ the future, declare it via the optional `capabilities` field:
164
+
165
+ ```ts
166
+ const backend: PersistenceBackend = {
167
+ capabilities: { byteRangeWrites: false },
168
+ async open(relPath) { /* ... */ },
169
+ // ...
170
+ };
171
+ ```
172
+
173
+ ### Pattern B — whole-file storage
174
+
175
+ Implement `PersistenceProvider` directly. Inside `mount()`, observe
176
+ `database.observe.transactions` and accumulate deltas; on
177
+ `flush`/`checkpoint`/`dispose`, push a complete blob to the cloud. Skip
178
+ the journal and manifest entirely — the provider author owns the delta
179
+ strategy.
180
+
181
+ ```ts
182
+ import type { PersistenceProvider, PersistenceMount } from "@adobe/data-persistence";
183
+
184
+ export const createBlobProvider = (client: BlobClient): PersistenceProvider => ({
185
+ providerName: "BlobProvider",
186
+ async mount(database, opts): Promise<PersistenceMount> {
187
+ // Observe transactions, accumulate state, push blob on checkpoint/dispose.
188
+ const service = createBlobPersistenceService(client, database, opts);
189
+ return { service, dispose: () => service.dispose() };
190
+ },
191
+ });
192
+ ```
193
+
194
+ ## Crash safety
195
+
196
+ What we guarantee:
197
+
198
+ - Clean shutdown via `dispose()` → next `load()` is byte-identical.
199
+ - Crash between transactions → next `load()` is the last fully-committed
200
+ transaction. Snapshot + journal replay reconstructs the world.
201
+ - Crash inside the journal stream itself (torn append) → the affected
202
+ entry is dropped; everything before it is recovered.
203
+ - Crash with the commit marker missing for a tx → replay drops every
204
+ entry of that tx, so it never half-applies a multi-entity transaction.
205
+
206
+ What we do **not** guarantee:
207
+
208
+ - Row-level rollback of an in-flight transaction. Column-slice writes
209
+ are eager, so if a crash happens *during* a tx's writes, individual
210
+ column files may already reflect bytes from the dropped tx. Replay
211
+ refuses to extend the dropped tx into other entities, but bytes
212
+ written to its own row may persist.
213
+
214
+ Full row-level atomicity would require shadow paging or a separate
215
+ undo log, both of which trade away the O(1) write property this
216
+ package is designed for.
217
+
218
+ ## Performance
219
+
220
+ Two complementary guarantees:
221
+
222
+ ### Algorithmic complexity (O(changes))
223
+
224
+ The cost of persisting a transaction T is **O(touched_columns + changed_entities)** — independent of total world size, archetype row capacity, or the number of registered archetypes. Verified by `src/service/persistence-complexity.test.ts`, which counts `PersistOp` messages emitted per transaction across worlds of N=10, 100, and 500 entities and asserts the count is identical:
225
+
226
+ | Transaction shape | Ops emitted (any N) |
227
+ |---|---|
228
+ | Update 1 component on 1 entity | 4 (journal entry + column slice + ELT + commit) |
229
+ | Update 3 components on 1 entity | 8 |
230
+ | Insert (3-column archetype) | 8 |
231
+ | Delete + 1 swap-remove side effect | 10 |
232
+
233
+ For **fixed-stride columns** (number, struct, enum) `checkpoint()` scales with archetype × column count (manifest schema), not row count — column slices are written incrementally per transaction, so the checkpoint just writes the manifest and truncates the journal.
234
+
235
+ For **journal-storage columns** (`"array"` typed buffers — variable-length per-row values) the checkpoint additionally writes a full per-column snapshot file whose cost is O(rowCount × mean value size) for each such column. At load time that snapshot is restored before journal replay, so these columns are fully durable across checkpoints. If your schema has no `"array"` columns, checkpoint cost remains independent of row count.
236
+
237
+ ### Wall-clock baseline
238
+
239
+ Run with:
240
+
241
+ ```bash
242
+ pnpm --filter @adobe/data-persistence run perf
243
+ ```
244
+
245
+ Numbers below are from a steady-state run on an in-process transport (no worker postMessage overhead), 1000-entity world, three-component archetype:
246
+
247
+ | Scenario | μs/op (mean) | μs/op (p99) | ops/sec |
248
+ |---|---:|---:|---:|
249
+ | tx: update 1 component on 1 entity | ~160 | ~250 | ~6,200 |
250
+ | tx: update 3 components on 1 entity | ~350 | ~490 | ~2,850 |
251
+ | tx: insert (3 columns) | ~350 | ~480 | ~2,900 |
252
+ | tx: delete + swap-remove side effect | ~470 | ~620 | ~2,100 |
253
+ | checkpoint after 1000 dirty rows | ~140 | ~350 | ~7,000 |
254
+ | load 1000-entity snapshot | ~58 | ~125 | ~17,000 |
255
+
256
+ Read these as comparative ratios, not absolute targets — the 2.2× ratio between 1-column and 3-column updates confirms the per-component-write optimization is doing useful work. The flat checkpoint cost (~140 μs regardless of row count) holds for fixed-stride-only schemas; schemas with `"array"` (journal-storage) columns incur an additional O(rowCount × mean value size) write per such column at checkpoint time.
257
+
258
+ Worker-thread transports (browser OPFS, Node `worker_threads`) add 1–10 μs of `postMessage` round-trip per op on top of these numbers, plus real disk I/O at checkpoint and load.
@@ -0,0 +1,6 @@
1
+ import type { PersistenceBackend } from "./persistence-backend.js";
2
+ /**
3
+ * In-memory backend used by tests and the in-process service. Stores
4
+ * file contents in a Map keyed by normalized relative path.
5
+ */
6
+ export declare const createMemoryBackend: () => PersistenceBackend;
@@ -0,0 +1,92 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { createMemoryFile } from "./memory-file.js";
3
+ import { validateRelPath } from "./validate-rel-path.js";
4
+ const normalize = (relPath) => {
5
+ // Strip './' prefixes and trailing slashes; collapse "//".
6
+ return relPath
7
+ .split("/")
8
+ .filter(seg => seg !== "" && seg !== ".")
9
+ .join("/");
10
+ };
11
+ /**
12
+ * In-memory backend used by tests and the in-process service. Stores
13
+ * file contents in a Map keyed by normalized relative path.
14
+ */
15
+ export const createMemoryBackend = () => {
16
+ // Stored bytes per file. Each "open" returns a fresh handle that
17
+ // syncs back to this map on writes.
18
+ const store = new Map();
19
+ const readSnapshot = (key) => store.get(key) ?? new Uint8Array(0);
20
+ const writeSnapshot = (key, bytes) => {
21
+ store.set(key, bytes);
22
+ };
23
+ return {
24
+ async open(relPath) {
25
+ validateRelPath(relPath);
26
+ const key = normalize(relPath);
27
+ const initial = readSnapshot(key);
28
+ const inner = createMemoryFile(initial);
29
+ return {
30
+ async readAt(offset, length) {
31
+ return inner.readAt(offset, length);
32
+ },
33
+ async writeAt(offset, bytes) {
34
+ await inner.writeAt(offset, bytes);
35
+ const size = await inner.size();
36
+ writeSnapshot(key, await inner.readAt(0, size));
37
+ },
38
+ async appendAt(bytes) {
39
+ const newSize = await inner.appendAt(bytes);
40
+ writeSnapshot(key, await inner.readAt(0, newSize));
41
+ return newSize;
42
+ },
43
+ async size() {
44
+ return inner.size();
45
+ },
46
+ async truncate(length) {
47
+ await inner.truncate(length);
48
+ const size = await inner.size();
49
+ writeSnapshot(key, await inner.readAt(0, size));
50
+ },
51
+ async sync() {
52
+ await inner.sync();
53
+ },
54
+ async close() {
55
+ await inner.close();
56
+ },
57
+ };
58
+ },
59
+ async list(relPath) {
60
+ validateRelPath(relPath === "" ? "." : relPath);
61
+ const prefix = normalize(relPath);
62
+ const directKey = prefix.length === 0 ? "" : prefix + "/";
63
+ const out = new Set();
64
+ for (const key of store.keys()) {
65
+ if (prefix.length === 0 || key.startsWith(directKey)) {
66
+ const rest = prefix.length === 0 ? key : key.slice(directKey.length);
67
+ const head = rest.split("/")[0];
68
+ if (head !== undefined && head.length > 0)
69
+ out.add(head);
70
+ }
71
+ }
72
+ return [...out];
73
+ },
74
+ async remove(relPath) {
75
+ validateRelPath(relPath);
76
+ store.delete(normalize(relPath));
77
+ },
78
+ async rename(from, to) {
79
+ validateRelPath(from);
80
+ validateRelPath(to);
81
+ const fromKey = normalize(from);
82
+ const toKey = normalize(to);
83
+ const data = store.get(fromKey);
84
+ if (data === undefined) {
85
+ throw new Error(`rename: source does not exist: ${JSON.stringify(from)}`);
86
+ }
87
+ store.set(toKey, data);
88
+ store.delete(fromKey);
89
+ },
90
+ };
91
+ };
92
+ //# sourceMappingURL=memory-backend.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-backend.js","sourceRoot":"","sources":["../../src/backend/memory-backend.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { createMemoryBackend } from "./memory-backend.js";
3
+ import { runPersistenceBackendConformance } from "./persistence-backend.conformance.js";
4
+ runPersistenceBackendConformance("memory", async () => ({
5
+ backend: createMemoryBackend(),
6
+ }));
7
+ //# sourceMappingURL=memory-backend.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-backend.test.js","sourceRoot":"","sources":["../../src/backend/memory-backend.test.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,gCAAgC,EAAE,MAAM,sCAAsC,CAAC;AAExF,gCAAgC,CAAC,QAAQ,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC;IACpD,OAAO,EAAE,mBAAmB,EAAE;CACjC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,7 @@
1
+ import { RandomAccessFile } from "./random-access-file.js";
2
+ /**
3
+ * In-memory RandomAccessFile backed by a growable Uint8Array. Used by
4
+ * tests and as the in-process backend implementation. Behaves
5
+ * identically across Node and browser runtimes.
6
+ */
7
+ export declare const createMemoryFile: (initial?: Uint8Array) => RandomAccessFile;
@@ -0,0 +1,90 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ /**
3
+ * In-memory RandomAccessFile backed by a growable Uint8Array. Used by
4
+ * tests and as the in-process backend implementation. Behaves
5
+ * identically across Node and browser runtimes.
6
+ */
7
+ export const createMemoryFile = (initial) => {
8
+ let buffer = initial ? new Uint8Array(initial) : new Uint8Array(0);
9
+ let length = buffer.byteLength;
10
+ let closed = false;
11
+ const ensureOpen = () => {
12
+ if (closed)
13
+ throw new Error("MemoryFile is closed");
14
+ };
15
+ const ensureCapacity = (required) => {
16
+ if (required <= buffer.byteLength)
17
+ return;
18
+ // Grow by powers of two so amortized cost stays O(1).
19
+ let newCapacity = Math.max(buffer.byteLength * 2, 64);
20
+ while (newCapacity < required)
21
+ newCapacity *= 2;
22
+ const grown = new Uint8Array(newCapacity);
23
+ grown.set(buffer.subarray(0, length));
24
+ buffer = grown;
25
+ };
26
+ return {
27
+ async readAt(offset, requestedLength) {
28
+ ensureOpen();
29
+ if (offset < 0 || requestedLength < 0) {
30
+ throw new RangeError(`readAt: offset and length must be non-negative (offset=${offset}, length=${requestedLength})`);
31
+ }
32
+ const end = Math.min(offset + requestedLength, length);
33
+ const actualLength = Math.max(0, end - offset);
34
+ if (actualLength === 0)
35
+ return new Uint8Array(0);
36
+ // Copy out so the caller cannot mutate our internal buffer.
37
+ return new Uint8Array(buffer.buffer, buffer.byteOffset + offset, actualLength).slice();
38
+ },
39
+ async writeAt(offset, bytes) {
40
+ ensureOpen();
41
+ if (offset < 0) {
42
+ throw new RangeError(`writeAt: offset must be non-negative (offset=${offset})`);
43
+ }
44
+ const end = offset + bytes.byteLength;
45
+ ensureCapacity(end);
46
+ buffer.set(bytes, offset);
47
+ if (end > length)
48
+ length = end;
49
+ },
50
+ async appendAt(bytes) {
51
+ ensureOpen();
52
+ const start = length;
53
+ const end = start + bytes.byteLength;
54
+ ensureCapacity(end);
55
+ buffer.set(bytes, start);
56
+ length = end;
57
+ return length;
58
+ },
59
+ async size() {
60
+ ensureOpen();
61
+ return length;
62
+ },
63
+ async truncate(newLength) {
64
+ ensureOpen();
65
+ if (newLength < 0) {
66
+ throw new RangeError(`truncate: length must be non-negative (length=${newLength})`);
67
+ }
68
+ if (newLength > length) {
69
+ ensureCapacity(newLength);
70
+ // Zero-fill the grown region.
71
+ buffer.fill(0, length, newLength);
72
+ }
73
+ else {
74
+ // Zero-fill the freed region so subsequent grows do not
75
+ // expose stale bytes.
76
+ buffer.fill(0, newLength, length);
77
+ }
78
+ length = newLength;
79
+ },
80
+ async sync() {
81
+ ensureOpen();
82
+ },
83
+ async close() {
84
+ closed = true;
85
+ buffer = new Uint8Array(0);
86
+ length = 0;
87
+ },
88
+ };
89
+ };
90
+ //# sourceMappingURL=memory-file.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-file.js","sourceRoot":"","sources":["../../src/backend/memory-file.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { createMemoryFile } from "./memory-file.js";
3
+ import { runRandomAccessFileConformance } from "./random-access-file.conformance.js";
4
+ runRandomAccessFileConformance("memory", () => createMemoryFile());
5
+ //# sourceMappingURL=memory-file.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-file.test.js","sourceRoot":"","sources":["../../src/backend/memory-file.test.ts"],"names":[],"mappings":"AAAA,uDAAuD;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,8BAA8B,EAAE,MAAM,qCAAqC,CAAC;AAErF,8BAA8B,CAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,gBAAgB,EAAE,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import type { PersistenceBackend } from "./persistence-backend.js";
2
+ /**
3
+ * Conformance suite for PersistenceBackend implementations. The factory
4
+ * is invoked per test (returning a fresh backend) and the optional
5
+ * teardown is invoked after each test.
6
+ */
7
+ export declare const runPersistenceBackendConformance: (name: string, setup: () => Promise<{
8
+ backend: PersistenceBackend;
9
+ cleanup?: () => Promise<void>;
10
+ }>) => void;
@@ -0,0 +1,104 @@
1
+ // © 2026 Adobe. MIT License. See /LICENSE for details.
2
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
3
+ /**
4
+ * Conformance suite for PersistenceBackend implementations. The factory
5
+ * is invoked per test (returning a fresh backend) and the optional
6
+ * teardown is invoked after each test.
7
+ */
8
+ export const runPersistenceBackendConformance = (name, setup) => {
9
+ describe(`PersistenceBackend conformance: ${name}`, () => {
10
+ let backend;
11
+ let cleanup;
12
+ beforeEach(async () => {
13
+ const created = await setup();
14
+ backend = created.backend;
15
+ cleanup = created.cleanup;
16
+ });
17
+ afterEach(async () => {
18
+ if (cleanup)
19
+ await cleanup();
20
+ });
21
+ test("open creates an empty file", async () => {
22
+ const file = await backend.open("foo.bin");
23
+ try {
24
+ expect(await file.size()).toBe(0);
25
+ }
26
+ finally {
27
+ await file.close();
28
+ }
29
+ });
30
+ test("write then re-open round-trips contents", async () => {
31
+ {
32
+ const file = await backend.open("foo.bin");
33
+ await file.writeAt(0, new Uint8Array([1, 2, 3]));
34
+ await file.close();
35
+ }
36
+ {
37
+ const file = await backend.open("foo.bin");
38
+ try {
39
+ const read = await file.readAt(0, 3);
40
+ expect(Array.from(read)).toEqual([1, 2, 3]);
41
+ }
42
+ finally {
43
+ await file.close();
44
+ }
45
+ }
46
+ });
47
+ test("nested paths work", async () => {
48
+ const file = await backend.open("a/b/c.bin");
49
+ try {
50
+ await file.writeAt(0, new Uint8Array([42]));
51
+ expect(await file.size()).toBe(1);
52
+ }
53
+ finally {
54
+ await file.close();
55
+ }
56
+ });
57
+ test("list returns immediate children", async () => {
58
+ for (const relPath of ["a/x.bin", "a/y.bin", "a/sub/z.bin", "b/q.bin"]) {
59
+ const file = await backend.open(relPath);
60
+ await file.writeAt(0, new Uint8Array([1]));
61
+ await file.close();
62
+ }
63
+ const aChildren = [...(await backend.list("a"))].sort();
64
+ expect(aChildren).toEqual(["sub", "x.bin", "y.bin"]);
65
+ const root = [...(await backend.list("."))].sort();
66
+ expect(root).toEqual(["a", "b"]);
67
+ });
68
+ test("remove deletes a file", async () => {
69
+ const file = await backend.open("foo.bin");
70
+ await file.writeAt(0, new Uint8Array([1]));
71
+ await file.close();
72
+ await backend.remove("foo.bin");
73
+ const reopened = await backend.open("foo.bin");
74
+ try {
75
+ expect(await reopened.size()).toBe(0);
76
+ }
77
+ finally {
78
+ await reopened.close();
79
+ }
80
+ });
81
+ test("remove of non-existent file is a no-op", async () => {
82
+ await expect(backend.remove("does/not/exist.bin")).resolves.toBeUndefined();
83
+ });
84
+ test("rename moves a file's contents", async () => {
85
+ const file = await backend.open("from.bin");
86
+ await file.writeAt(0, new Uint8Array([7, 8, 9]));
87
+ await file.close();
88
+ await backend.rename("from.bin", "to.bin");
89
+ const renamed = await backend.open("to.bin");
90
+ try {
91
+ const read = await renamed.readAt(0, 3);
92
+ expect(Array.from(read)).toEqual([7, 8, 9]);
93
+ }
94
+ finally {
95
+ await renamed.close();
96
+ }
97
+ });
98
+ test("rejects path traversal attempts", async () => {
99
+ await expect(backend.open("../etc/passwd")).rejects.toThrow();
100
+ await expect(backend.open("/etc/passwd")).rejects.toThrow();
101
+ });
102
+ });
103
+ };
104
+ //# sourceMappingURL=persistence-backend.conformance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"persistence-backend.conformance.js","sourceRoot":"","sources":["../../src/backend/persistence-backend.conformance.ts"],"names":[],"mappings":"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"}