@jarenjs/db 0.56.0 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +412 -56
- package/README.md +600 -57
- package/docs/HOSTS.md +269 -0
- package/docs/JOBS-FORMAT.md +293 -45
- package/docs/LIVE-FORMAT.md +169 -20
- package/docs/MIGRATION-FORMAT.md +142 -17
- package/docs/MODEL-FORMAT.md +752 -64
- package/docs/REPLICATION-FORMAT.md +208 -0
- package/package.json +21 -7
- package/schemas/jaren-model.draft-07.schema.json +224 -162
- package/schemas/jaren-model.schema.json +224 -162
- package/schemas/jaren-replication-snapshot.draft-07.schema.json +83 -0
- package/schemas/jaren-replication-snapshot.schema.json +83 -0
- package/schemas/jaren-replication.draft-07.schema.json +82 -0
- package/schemas/jaren-replication.schema.json +82 -0
- package/src/algebra.js +227 -9
- package/src/backup.js +161 -0
- package/src/cancellation.js +48 -0
- package/src/capture.js +230 -47
- package/src/cli.js +165 -59
- package/src/cursor.js +417 -0
- package/src/dag-job.js +154 -21
- package/src/ddl.js +102 -8
- package/src/dialect.js +268 -113
- package/src/dialects/expression-read.js +158 -0
- package/src/dialects/postgres.js +618 -0
- package/src/dialects/rtree-ddl.js +129 -0
- package/src/dialects/sqlite.js +244 -11
- package/src/document-files.js +311 -0
- package/src/document-steps.js +422 -0
- package/src/documents.js +335 -0
- package/src/driver.js +448 -61
- package/src/drivers/bun.js +37 -1
- package/src/drivers/indexeddb-snapshot.js +149 -0
- package/src/drivers/node-pool.js +11 -0
- package/src/drivers/node-worker-endpoint.js +105 -0
- package/src/drivers/node-worker.js +204 -0
- package/src/drivers/node.js +41 -7
- package/src/drivers/postgres.js +331 -0
- package/src/drivers/wasm-oo1.js +97 -0
- package/src/drivers/wasm-session.js +67 -0
- package/src/drivers/wasm.js +17 -83
- package/src/drivers/worker-pool.js +183 -0
- package/src/drivers/worker-protocol.js +79 -0
- package/src/drivers/worker-queue.js +60 -0
- package/src/emit.js +339 -48
- package/src/entity.js +20 -22
- package/src/errors.js +430 -19
- package/src/expression.js +284 -0
- package/src/graph.js +64 -8
- package/src/index.js +48 -17
- package/src/introspect.js +583 -0
- package/src/jobs.js +843 -107
- package/src/json-bytes.js +58 -0
- package/src/live-join.js +250 -0
- package/src/live-nested.js +120 -0
- package/src/live.js +18 -4
- package/src/logical-rows.js +90 -0
- package/src/maintenance.js +175 -0
- package/src/migrate.js +248 -181
- package/src/model.js +68 -0
- package/src/plan.js +1119 -138
- package/src/pragmas.js +314 -0
- package/src/profile.js +151 -3
- package/src/query.js +1634 -323
- package/src/replication-format.js +115 -0
- package/src/replication.js +332 -0
- package/src/residual.js +17 -0
- package/src/series.js +12 -4
- package/src/store.js +1567 -273
- package/src/tracker.js +203 -29
- package/src/udf.js +88 -7
- package/types/index.d.ts +1158 -27
- package/types/node-pool.d.ts +28 -0
- package/types/node-worker.d.ts +54 -0
- package/types/node.d.ts +69 -2
- package/types/postgres.d.ts +46 -0
- package/types/typed.d.ts +27 -4
- package/types/wasm.d.ts +14 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# Replication format and runtime
|
|
2
|
+
|
|
3
|
+
`jaren-replication` 0.1 is a transport-neutral logical transaction. Open an empty
|
|
4
|
+
store with `replication: { replica: 'host-issued-id' }`; capture is enabled when
|
|
5
|
+
omitted. Existing replication stores must reopen with the same identity and
|
|
6
|
+
model. Every writer of that file must enable replication. External SQL writes
|
|
7
|
+
and opening a replicated file without replication are outside the history
|
|
8
|
+
contract; a detected before-image disagreement refuses with `JD2104`.
|
|
9
|
+
|
|
10
|
+
The host owns network transport, authentication, replica identity allocation and
|
|
11
|
+
resolver policy. A replica id must remain unique to its writer lineage. The
|
|
12
|
+
runtime works on SQLite Node, worker and wasm hosts through session or journal
|
|
13
|
+
capture. Drivers without `changeCapture` refuse at open; PostgreSQL capture is
|
|
14
|
+
not implemented. Journal replication refuses models with cascading or set-null
|
|
15
|
+
child foreign keys (`JD0051`), because the journal cannot observe those effects.
|
|
16
|
+
Session capture includes them. Both modes support membership cascades and
|
|
17
|
+
roll back an envelope whose side effects are absent from its logical operations.
|
|
18
|
+
|
|
19
|
+
## Envelope
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"$replication": "0.1",
|
|
24
|
+
"replica": "site-a",
|
|
25
|
+
"seq": 1,
|
|
26
|
+
"frontier": {},
|
|
27
|
+
"model": "application-model-revision",
|
|
28
|
+
"operations": [
|
|
29
|
+
{ "table": "notes", "key": "note/one", "before": null,
|
|
30
|
+
"after": { "id": "note/one", "body": "hello" } }
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`model` is the store's `shapeHash(model)` revision. `seq` is a positive safe
|
|
36
|
+
integer, allocated from the replica's durable frontier in the data transaction.
|
|
37
|
+
The identity is `replicationIdentity(replica, seq)`, the JSON encoding of their
|
|
38
|
+
pair, so separators inside host ids cannot collide. A frontier maps host ids to
|
|
39
|
+
non-negative safe sequences. The sender's frontier entry must equal `seq - 1`,
|
|
40
|
+
with an absent entry meaning zero. Property names such as `__proto__` and
|
|
41
|
+
`constructor` remain ordinary replica ids.
|
|
42
|
+
|
|
43
|
+
Each operation is one net row transition. `before: null` inserts, `after: null`
|
|
44
|
+
deletes, and two objects replace the complete logical row. A table/key occurs
|
|
45
|
+
once per envelope. Composite keys use the capture format's JSON-array token;
|
|
46
|
+
single keys use their scalar text. Entity documents include mapped scalar and
|
|
47
|
+
foreign-key fields, omit relation projections and retain authored version and
|
|
48
|
+
timestamp values. Membership tables carry their two keys. Application performs
|
|
49
|
+
schema validation through the store's configured validator and reuses physical
|
|
50
|
+
column plans; it does not generate fresh defaults or version stamps.
|
|
51
|
+
|
|
52
|
+
`normalizeReplication` validates and detaches the document. `encodeReplication`
|
|
53
|
+
produces deterministic JSON. Object members are canonically ordered; operation
|
|
54
|
+
array order remains part of identity. Capture sorts net rows by table/key, making
|
|
55
|
+
physical session and journal output equal. Envelope order preserves transaction
|
|
56
|
+
order. Physical insertion order inside a netted transaction or snapshot is not a
|
|
57
|
+
portable query ordering: use an explicit query order when comparing replicas.
|
|
58
|
+
Receipts retain the entire canonical payload, avoiding hash-collision ambiguity.
|
|
59
|
+
|
|
60
|
+
Unknown members and versions, empty ids, unsafe sequences, non-JSON values,
|
|
61
|
+
cycles, duplicate net rows and unchanged operations are `JD0060`. Nesting is
|
|
62
|
+
limited to 128 object/array ancestors. JSON Schema artifacts describe structural
|
|
63
|
+
validation; cross-field sequence, uniqueness and causal checks are runtime
|
|
64
|
+
constraints. Current and draft-07 artifacts ship for envelopes and snapshots:
|
|
65
|
+
|
|
66
|
+
- `@jarenjs/db/schemas/jaren-replication.schema.json`
|
|
67
|
+
- `@jarenjs/db/schemas/jaren-replication.draft-07.schema.json`
|
|
68
|
+
- `@jarenjs/db/schemas/jaren-replication-snapshot.schema.json`
|
|
69
|
+
- `@jarenjs/db/schemas/jaren-replication-snapshot.draft-07.schema.json`
|
|
70
|
+
|
|
71
|
+
## Applying and exporting
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
const source = await openStore(model, {
|
|
75
|
+
driver, replication: { replica: 'site-a', retention: 1000 }
|
|
76
|
+
});
|
|
77
|
+
const page = await source.replication.page({ after: 0, limit: 100, maxBytes: 1048576 });
|
|
78
|
+
for (const envelope of page.items) {
|
|
79
|
+
const result = await target.replication.apply(envelope);
|
|
80
|
+
if (result.status === 'conflict') hostReport(result.conflicts);
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`apply` returns `applied`, `duplicate` or `conflict`, together with the committed
|
|
85
|
+
frontier and conflict records. Successful duplicate delivery performs no row
|
|
86
|
+
write or live emission. An acknowledged identity with different bytes is
|
|
87
|
+
`JD2101`. A sequence or causal gap is `JD2100`: no suffix is applied. Deliver
|
|
88
|
+
missing envelopes first, then retry, or perform an explicit reset. Model mismatch
|
|
89
|
+
is `JD2102`. Imported envelopes keep their original identity and produce no local
|
|
90
|
+
outbox entry, preventing echoes. A host forwarding between peers must preserve
|
|
91
|
+
the original envelopes; `page()` exports this replica's own commits only.
|
|
92
|
+
|
|
93
|
+
Data, receipt, per-row causal metadata, capture record and frontier settle in
|
|
94
|
+
the same transaction. Replication is available only on the root Store; transaction
|
|
95
|
+
views expose no replication API. Failed validation, cancellation, constraint checks or
|
|
96
|
+
commit roll everything back. Subscribers run through the existing committed
|
|
97
|
+
capture delivery path. Tests kill a process immediately before and after commit
|
|
98
|
+
and verify both data and frontier after reopening.
|
|
99
|
+
|
|
100
|
+
Pages use the shared cursor and byte-credit drain. `limit` counts whole envelopes;
|
|
101
|
+
`maxBytes` counts complete canonical envelope bytes. An indivisible oversize
|
|
102
|
+
envelope is `JD2074`. Retention loss returns `resetRequired: true`, no items and
|
|
103
|
+
no continuation. `earliestAvailable`, `highWatermark`, `next` and `hasMore` describe
|
|
104
|
+
the local outbox. Pages and snapshots take a consistent store transaction.
|
|
105
|
+
`signal` and `deadline` are honored through existing cancellation contracts:
|
|
106
|
+
queue cancellation is `JD2064`, operation-boundary cancellation `JD2072`, and an
|
|
107
|
+
expired deadline `JD2075`.
|
|
108
|
+
|
|
109
|
+
Defaults are `retention: 1000`, `maxOperations: 10000`, `maxBytes: 4194304`.
|
|
110
|
+
Operation overflow is `JD2106`; all credits are positive safe integers. Retention
|
|
111
|
+
prunes only outgoing envelope history. Replay receipts, row tombstones and
|
|
112
|
+
conflict evidence remain durable. They are deliberately not claimed to be a
|
|
113
|
+
bounded total database size. Internal tables use the reserved `_jaren_replica`
|
|
114
|
+
prefix. No schema migration or revision rewrite of this protocol is implicit.
|
|
115
|
+
|
|
116
|
+
## Conflicts
|
|
117
|
+
|
|
118
|
+
Concurrent incompatible whole-row writes reject by default. A `conflict` result
|
|
119
|
+
does not acknowledge the remote envelope and leaves the local row intact.
|
|
120
|
+
`replication.conflicts({ limit, maxBytes, signal, deadline })` reads persisted evidence: the common base,
|
|
121
|
+
local value and frontier, remote value and envelope position, resolver identity
|
|
122
|
+
and decision. A rejected envelope reserves its canonical identity too, so retries
|
|
123
|
+
cannot replace its contender with different content.
|
|
124
|
+
|
|
125
|
+
An optional `replication.resolver` has `{ id, resolve }`. Its synchronous pure
|
|
126
|
+
function receives frozen evidence and returns `{ action: 'local' }`,
|
|
127
|
+
`{ action: 'remote' }`, or `{ action: 'merged', value }`; a merged value may be
|
|
128
|
+
null for deletion. Invalid decisions refuse with `JD2103`. Both contenders and
|
|
129
|
+
the resolver id are retained for every resolution. No resolver is bundled.
|
|
130
|
+
Determinism and convergence of a custom policy are the host's responsibility:
|
|
131
|
+
choosing the receiver's local value on each replica is deterministic but does
|
|
132
|
+
not make replicas agree. Independent non-conflicting deliveries converge without
|
|
133
|
+
resolver policy. Concurrent changes to different fields of the same row are
|
|
134
|
+
conservatively whole-row conflicts in this version.
|
|
135
|
+
|
|
136
|
+
## Reset handshake
|
|
137
|
+
|
|
138
|
+
`source.replication.snapshot()` returns a `jaren-replication-snapshot` 0.1
|
|
139
|
+
document containing `model`, `frontier`, logical `rows` with their causal
|
|
140
|
+
frontiers, and complete acknowledged `receipts`.
|
|
141
|
+
`target.replication.reset(snapshot)` validates and atomically installs it while
|
|
142
|
+
emitting ordinary live invalidations. The snapshot must dominate all target
|
|
143
|
+
acknowledgements and preserve every known receipt's bytes. It cannot discard
|
|
144
|
+
unsynchronized local history (`JD2105`) or rewrite identities (`JD2101`).
|
|
145
|
+
Snapshot receipt sequences must completely cover their frontier, and row
|
|
146
|
+
frontiers cannot be ahead of it. Subsequent duplicate deliveries remain
|
|
147
|
+
verifiable because the receipts travel with the snapshot.
|
|
148
|
+
|
|
149
|
+
Resets are intentionally bounded single documents: rows plus receipts must fit
|
|
150
|
+
`maxOperations`, and the complete encoding must fit `maxBytes`. Snapshot and
|
|
151
|
+
conflict readers pull through the shared cursor and stop before accumulating
|
|
152
|
+
beyond the configured credits. Resolver outputs and conflict evidence also
|
|
153
|
+
obey `maxBytes`. Very large
|
|
154
|
+
histories require a separately designed paged snapshot/receipt compaction
|
|
155
|
+
protocol; this implementation refuses them rather than splicing partial state.
|
|
156
|
+
A reset preserves the receiver's own replica identity and clears its outgoing
|
|
157
|
+
history. Future writes continue its sequence from the imported frontier.
|
|
158
|
+
|
|
159
|
+
## Authoring
|
|
160
|
+
|
|
161
|
+
`@jarenjs/linq/db` exports a deterministic pen using the same validator:
|
|
162
|
+
|
|
163
|
+
```js
|
|
164
|
+
const envelope = defineReplication({ replica: 'site-a', seq: 1,
|
|
165
|
+
frontier: {}, model: revision })
|
|
166
|
+
.change('notes', 'one', null, { id: 'one', body: 'hello' })
|
|
167
|
+
.toDocument();
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The pen snapshots its input and emits the same canonical bytes as hand-authored
|
|
171
|
+
JSON. Normal applications export committed Store envelopes; constructing an
|
|
172
|
+
envelope does not allocate or acknowledge a Store sequence.
|
|
173
|
+
|
|
174
|
+
## Measurements
|
|
175
|
+
|
|
176
|
+
Run `npm run benchmark:changeflow`. Every timed mutation is followed by a fresh
|
|
177
|
+
query and a patch-only consumer comparison; every measured mutation changes its
|
|
178
|
+
visible result. Graph measurements mutate a child of one owner. Replication deliveries compare
|
|
179
|
+
complete database state, including relations. Conflict timings retry one
|
|
180
|
+
rejected contender and assert unchanged data/frontier plus durable evidence. Timing includes capture and live
|
|
181
|
+
delivery; oracle time is reported separately. Heap deltas are uncollected
|
|
182
|
+
allocations observed after initialization, not precise retained-heap sizes.
|
|
183
|
+
|
|
184
|
+
<!--fact:db.changeflow-->
|
|
185
|
+
|
|
186
|
+
Measured 2026-09-08, v24.19.0, AMD Ryzen 9 5900HX with Radeon Graphics; 15 mutations per case.
|
|
187
|
+
|
|
188
|
+
| Shape | Strategy | Initialize ms | Mutation p50 ms | Mutation p95 ms | Initialization heap bytes |
|
|
189
|
+
|---|---|---:|---:|---:|---:|
|
|
190
|
+
| selective join | join | 16.663 | 0.323 | 2.119 | 4878848 |
|
|
191
|
+
| selective join | rerun | 0.899 | 0.746 | 1.622 | 347160 |
|
|
192
|
+
| high fan-out join | join | 7.314 | 1.901 | 3.179 | 4851232 |
|
|
193
|
+
| high fan-out join | rerun | 0.860 | 0.850 | 1.188 | 340328 |
|
|
194
|
+
| graph | graph | 7.462 | 0.459 | 0.750 | 4811712 |
|
|
195
|
+
| graph | rerun | 4.539 | 4.042 | 4.979 | 1417928 |
|
|
196
|
+
| nested groups | nested-group | 6.775 | 0.252 | 0.795 | 2057832 |
|
|
197
|
+
| nested groups | rerun | 0.897 | 0.472 | 0.685 | 295808 |
|
|
198
|
+
| offset groups | rerun | 1.519 | 0.459 | 0.547 | 383648 |
|
|
199
|
+
| offset groups | rerun | 0.884 | 0.426 | 0.585 | 282280 |
|
|
200
|
+
|
|
201
|
+
| Capture | Envelopes | Operations | Bytes | Apply p50 ms | Replay p50 ms | Conflict p50 ms |
|
|
202
|
+
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
203
|
+
| session | 15 | 15 | 3169 | 0.458 | 0.118 | 0.204 |
|
|
204
|
+
| journal | 15 | 15 | 3169 | 0.294 | 0.092 | 0.161 |
|
|
205
|
+
|
|
206
|
+
Selective maintenance avoids repeated full SQL evaluation. Initialization and high-fan-out maintenance can cost more than rerunning; the table includes both. Offset groups remain rerun in both requested modes.
|
|
207
|
+
|
|
208
|
+
<!--/fact-->
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/db",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.67.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./types/index.d.ts",
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"types": "./types/node.d.ts",
|
|
16
16
|
"default": "./src/drivers/node.js"
|
|
17
17
|
},
|
|
18
|
+
"./postgres": {
|
|
19
|
+
"types": "./types/postgres.d.ts",
|
|
20
|
+
"default": "./src/drivers/postgres.js"
|
|
21
|
+
},
|
|
18
22
|
"./bun": {
|
|
19
23
|
"types": "./types/bun.d.ts",
|
|
20
24
|
"default": "./src/drivers/bun.js"
|
|
@@ -32,7 +36,15 @@
|
|
|
32
36
|
"default": "./src/app.js"
|
|
33
37
|
},
|
|
34
38
|
"./schemas/*": "./schemas/*",
|
|
35
|
-
"./package.json": "./package.json"
|
|
39
|
+
"./package.json": "./package.json",
|
|
40
|
+
"./node-worker": {
|
|
41
|
+
"types": "./types/node-worker.d.ts",
|
|
42
|
+
"default": "./src/drivers/node-worker.js"
|
|
43
|
+
},
|
|
44
|
+
"./node-pool": {
|
|
45
|
+
"types": "./types/node-pool.d.ts",
|
|
46
|
+
"default": "./src/drivers/node-pool.js"
|
|
47
|
+
}
|
|
36
48
|
},
|
|
37
49
|
"files": [
|
|
38
50
|
"types/",
|
|
@@ -41,7 +53,7 @@
|
|
|
41
53
|
"docs/",
|
|
42
54
|
"ARCHITECTURE.md"
|
|
43
55
|
],
|
|
44
|
-
"description": "Document storage for the Jaren suite: a model document declares collections as JSON Schemas with indexes; openStore applies DDL through a dialect and gives transactional, schema-validated reads and writes over SQLite
|
|
56
|
+
"description": "Document storage for the Jaren suite: a model document declares collections as JSON Schemas with indexes; openStore applies DDL through a dialect and gives transactional, schema-validated reads and writes over SQLite (Node, Bun, or an injected wasm handle) or PostgreSQL 16+ through an injected client",
|
|
45
57
|
"author": "joham",
|
|
46
58
|
"repository": {
|
|
47
59
|
"type": "git",
|
|
@@ -62,7 +74,9 @@
|
|
|
62
74
|
"sqlite",
|
|
63
75
|
"document-store",
|
|
64
76
|
"json-schema",
|
|
65
|
-
"database"
|
|
77
|
+
"database",
|
|
78
|
+
"postgres",
|
|
79
|
+
"postgresql"
|
|
66
80
|
],
|
|
67
81
|
"scripts": {
|
|
68
82
|
"build": "npm run build:types",
|
|
@@ -70,9 +84,9 @@
|
|
|
70
84
|
"prepack": "npm run build:types"
|
|
71
85
|
},
|
|
72
86
|
"dependencies": {
|
|
73
|
-
"@jarenjs/core": "^0.
|
|
74
|
-
"@jarenjs/json": "^0.
|
|
75
|
-
"@jarenjs/validate": "^0.
|
|
87
|
+
"@jarenjs/core": "^0.67.0",
|
|
88
|
+
"@jarenjs/json": "^0.67.0",
|
|
89
|
+
"@jarenjs/validate": "^0.67.0"
|
|
76
90
|
},
|
|
77
91
|
"bin": {
|
|
78
92
|
"jaren-db": "./src/cli.js"
|