@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.
- package/LICENSE +21 -0
- package/README.md +258 -0
- package/dist/backend/memory-backend.d.ts +6 -0
- package/dist/backend/memory-backend.js +92 -0
- package/dist/backend/memory-backend.js.map +1 -0
- package/dist/backend/memory-backend.test.d.ts +1 -0
- package/dist/backend/memory-backend.test.js +7 -0
- package/dist/backend/memory-backend.test.js.map +1 -0
- package/dist/backend/memory-file.d.ts +7 -0
- package/dist/backend/memory-file.js +90 -0
- package/dist/backend/memory-file.js.map +1 -0
- package/dist/backend/memory-file.test.d.ts +1 -0
- package/dist/backend/memory-file.test.js +5 -0
- package/dist/backend/memory-file.test.js.map +1 -0
- package/dist/backend/persistence-backend.conformance.d.ts +10 -0
- package/dist/backend/persistence-backend.conformance.js +104 -0
- package/dist/backend/persistence-backend.conformance.js.map +1 -0
- package/dist/backend/persistence-backend.d.ts +54 -0
- package/dist/backend/persistence-backend.js +3 -0
- package/dist/backend/persistence-backend.js.map +1 -0
- package/dist/backend/random-access-file.conformance.d.ts +20 -0
- package/dist/backend/random-access-file.conformance.js +156 -0
- package/dist/backend/random-access-file.conformance.js.map +1 -0
- package/dist/backend/random-access-file.d.ts +39 -0
- package/dist/backend/random-access-file.js +3 -0
- package/dist/backend/random-access-file.js.map +1 -0
- package/dist/backend/validate-rel-path.d.ts +14 -0
- package/dist/backend/validate-rel-path.js +44 -0
- package/dist/backend/validate-rel-path.js.map +1 -0
- package/dist/backend/validate-rel-path.test.d.ts +1 -0
- package/dist/backend/validate-rel-path.test.js +37 -0
- package/dist/backend/validate-rel-path.test.js.map +1 -0
- package/dist/browser/browser-worker-bootstrap.d.ts +1 -0
- package/dist/browser/browser-worker-bootstrap.js +97 -0
- package/dist/browser/browser-worker-bootstrap.js.map +1 -0
- package/dist/browser/browser-worker-transport.browser.test.d.ts +1 -0
- package/dist/browser/browser-worker-transport.browser.test.js +156 -0
- package/dist/browser/browser-worker-transport.browser.test.js.map +1 -0
- package/dist/browser/browser-worker-transport.d.ts +33 -0
- package/dist/browser/browser-worker-transport.js +120 -0
- package/dist/browser/browser-worker-transport.js.map +1 -0
- package/dist/browser/create-opfs-provider.d.ts +23 -0
- package/dist/browser/create-opfs-provider.js +49 -0
- package/dist/browser/create-opfs-provider.js.map +1 -0
- package/dist/browser/index.d.ts +6 -0
- package/dist/browser/index.js +12 -0
- package/dist/browser/index.js.map +1 -0
- package/dist/browser/mount-opfs.d.ts +12 -0
- package/dist/browser/mount-opfs.js +15 -0
- package/dist/browser/mount-opfs.js.map +1 -0
- package/dist/browser/opfs-backend-lock.browser.test.d.ts +1 -0
- package/dist/browser/opfs-backend-lock.browser.test.js +127 -0
- package/dist/browser/opfs-backend-lock.browser.test.js.map +1 -0
- package/dist/browser/opfs-backend.d.ts +6 -0
- package/dist/browser/opfs-backend.js +162 -0
- package/dist/browser/opfs-backend.js.map +1 -0
- package/dist/browser/opfs-lock-test-worker.d.ts +1 -0
- package/dist/browser/opfs-lock-test-worker.js +50 -0
- package/dist/browser/opfs-lock-test-worker.js.map +1 -0
- package/dist/browser/opfs-sync-file.d.ts +10 -0
- package/dist/browser/opfs-sync-file.js +58 -0
- package/dist/browser/opfs-sync-file.js.map +1 -0
- package/dist/encoder/create-column-encoder.d.ts +15 -0
- package/dist/encoder/create-column-encoder.js +110 -0
- package/dist/encoder/create-column-encoder.js.map +1 -0
- package/dist/encoder/journal-snapshot-codec.d.ts +21 -0
- package/dist/encoder/journal-snapshot-codec.js +103 -0
- package/dist/encoder/journal-snapshot-codec.js.map +1 -0
- package/dist/encoder/journal-snapshot-codec.test.d.ts +1 -0
- package/dist/encoder/journal-snapshot-codec.test.js +96 -0
- package/dist/encoder/journal-snapshot-codec.test.js.map +1 -0
- package/dist/encoder/types.d.ts +72 -0
- package/dist/encoder/types.js +3 -0
- package/dist/encoder/types.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/journal/journal-codec.d.ts +28 -0
- package/dist/journal/journal-codec.js +82 -0
- package/dist/journal/journal-codec.js.map +1 -0
- package/dist/journal/journal-codec.test.d.ts +1 -0
- package/dist/journal/journal-codec.test.js +106 -0
- package/dist/journal/journal-codec.test.js.map +1 -0
- package/dist/journal/journal-entry.d.ts +63 -0
- package/dist/journal/journal-entry.js +28 -0
- package/dist/journal/journal-entry.js.map +1 -0
- package/dist/node/create-node-fs-provider.d.ts +34 -0
- package/dist/node/create-node-fs-provider.js +48 -0
- package/dist/node/create-node-fs-provider.js.map +1 -0
- package/dist/node/index.d.ts +6 -0
- package/dist/node/index.js +10 -0
- package/dist/node/index.js.map +1 -0
- package/dist/node/mount-node-fs.d.ts +12 -0
- package/dist/node/mount-node-fs.js +15 -0
- package/dist/node/mount-node-fs.js.map +1 -0
- package/dist/node/node-fs-backend-lock.node.test.d.ts +1 -0
- package/dist/node/node-fs-backend-lock.node.test.js +160 -0
- package/dist/node/node-fs-backend-lock.node.test.js.map +1 -0
- package/dist/node/node-fs-backend.d.ts +21 -0
- package/dist/node/node-fs-backend.js +186 -0
- package/dist/node/node-fs-backend.js.map +1 -0
- package/dist/node/node-fs-backend.node.test.d.ts +1 -0
- package/dist/node/node-fs-backend.node.test.js +17 -0
- package/dist/node/node-fs-backend.node.test.js.map +1 -0
- package/dist/node/node-fs-file.d.ts +6 -0
- package/dist/node/node-fs-file.js +57 -0
- package/dist/node/node-fs-file.js.map +1 -0
- package/dist/node/node-fs-file.node.test.d.ts +1 -0
- package/dist/node/node-fs-file.node.test.js +13 -0
- package/dist/node/node-fs-file.node.test.js.map +1 -0
- package/dist/node/node-fs-provider.node.test.d.ts +1 -0
- package/dist/node/node-fs-provider.node.test.js +119 -0
- package/dist/node/node-fs-provider.node.test.js.map +1 -0
- package/dist/node/node-worker-bootstrap.d.ts +1 -0
- package/dist/node/node-worker-bootstrap.js +104 -0
- package/dist/node/node-worker-bootstrap.js.map +1 -0
- package/dist/node/node-worker-transport.d.ts +27 -0
- package/dist/node/node-worker-transport.js +152 -0
- package/dist/node/node-worker-transport.js.map +1 -0
- package/dist/node/node-worker-transport.node.test.d.ts +1 -0
- package/dist/node/node-worker-transport.node.test.js +147 -0
- package/dist/node/node-worker-transport.node.test.js.map +1 -0
- package/dist/perftest/perf-harness.d.ts +74 -0
- package/dist/perftest/perf-harness.js +147 -0
- package/dist/perftest/perf-harness.js.map +1 -0
- package/dist/perftest/persistence-perf.d.ts +2 -0
- package/dist/perftest/persistence-perf.js +290 -0
- package/dist/perftest/persistence-perf.js.map +1 -0
- package/dist/perftest/run.d.ts +1 -0
- package/dist/perftest/run.js +28 -0
- package/dist/perftest/run.js.map +1 -0
- package/dist/provider/create-memory-provider.d.ts +16 -0
- package/dist/provider/create-memory-provider.js +44 -0
- package/dist/provider/create-memory-provider.js.map +1 -0
- package/dist/provider/mount.d.ts +20 -0
- package/dist/provider/mount.js +20 -0
- package/dist/provider/mount.js.map +1 -0
- package/dist/provider/persistence-provider.d.ts +63 -0
- package/dist/provider/persistence-provider.js +3 -0
- package/dist/provider/persistence-provider.js.map +1 -0
- package/dist/provider/persistence-provider.test.d.ts +9 -0
- package/dist/provider/persistence-provider.test.js +139 -0
- package/dist/provider/persistence-provider.test.js.map +1 -0
- package/dist/service/component-nonpersistent-durability.test.d.ts +1 -0
- package/dist/service/component-nonpersistent-durability.test.js +57 -0
- package/dist/service/component-nonpersistent-durability.test.js.map +1 -0
- package/dist/service/create-incremental-persistence-service.d.ts +7 -0
- package/dist/service/create-incremental-persistence-service.js +960 -0
- package/dist/service/create-incremental-persistence-service.js.map +1 -0
- package/dist/service/create-worker-persistence-service.d.ts +1 -0
- package/dist/service/create-worker-persistence-service.js +7 -0
- package/dist/service/create-worker-persistence-service.js.map +1 -0
- package/dist/service/create-worker-persistence-service.test.d.ts +1 -0
- package/dist/service/create-worker-persistence-service.test.js +291 -0
- package/dist/service/create-worker-persistence-service.test.js.map +1 -0
- package/dist/service/entity-location-cache.d.ts +15 -0
- package/dist/service/entity-location-cache.js +75 -0
- package/dist/service/entity-location-cache.js.map +1 -0
- package/dist/service/entity-location-density.test.d.ts +1 -0
- package/dist/service/entity-location-density.test.js +42 -0
- package/dist/service/entity-location-density.test.js.map +1 -0
- package/dist/service/incremental-persistence-service.d.ts +57 -0
- package/dist/service/incremental-persistence-service.js +3 -0
- package/dist/service/incremental-persistence-service.js.map +1 -0
- package/dist/service/intermediate-relocation-durability.test.d.ts +1 -0
- package/dist/service/intermediate-relocation-durability.test.js +89 -0
- package/dist/service/intermediate-relocation-durability.test.js.map +1 -0
- package/dist/service/internal-access.d.ts +34 -0
- package/dist/service/internal-access.js +43 -0
- package/dist/service/internal-access.js.map +1 -0
- package/dist/service/journal-replay.test.d.ts +1 -0
- package/dist/service/journal-replay.test.js +424 -0
- package/dist/service/journal-replay.test.js.map +1 -0
- package/dist/service/journal-storage-durability.test.d.ts +1 -0
- package/dist/service/journal-storage-durability.test.js +171 -0
- package/dist/service/journal-storage-durability.test.js.map +1 -0
- package/dist/service/load-roundtrip.test.d.ts +1 -0
- package/dist/service/load-roundtrip.test.js +224 -0
- package/dist/service/load-roundtrip.test.js.map +1 -0
- package/dist/service/persistence-complexity.test.d.ts +1 -0
- package/dist/service/persistence-complexity.test.js +275 -0
- package/dist/service/persistence-complexity.test.js.map +1 -0
- package/dist/service/worker-persistence-service.d.ts +24 -0
- package/dist/service/worker-persistence-service.js +8 -0
- package/dist/service/worker-persistence-service.js.map +1 -0
- package/dist/transport/inprocess-transport.d.ts +15 -0
- package/dist/transport/inprocess-transport.js +64 -0
- package/dist/transport/inprocess-transport.js.map +1 -0
- package/dist/transport/inprocess-transport.test.d.ts +1 -0
- package/dist/transport/inprocess-transport.test.js +100 -0
- package/dist/transport/inprocess-transport.test.js.map +1 -0
- package/dist/transport/router.d.ts +12 -0
- package/dist/transport/router.js +146 -0
- package/dist/transport/router.js.map +1 -0
- package/dist/transport/transport.d.ts +105 -0
- package/dist/transport/transport.js +3 -0
- package/dist/transport/transport.js.map +1 -0
- 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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"}
|