@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.
Files changed (79) hide show
  1. package/ARCHITECTURE.md +412 -56
  2. package/README.md +600 -57
  3. package/docs/HOSTS.md +269 -0
  4. package/docs/JOBS-FORMAT.md +293 -45
  5. package/docs/LIVE-FORMAT.md +169 -20
  6. package/docs/MIGRATION-FORMAT.md +142 -17
  7. package/docs/MODEL-FORMAT.md +752 -64
  8. package/docs/REPLICATION-FORMAT.md +208 -0
  9. package/package.json +21 -7
  10. package/schemas/jaren-model.draft-07.schema.json +224 -162
  11. package/schemas/jaren-model.schema.json +224 -162
  12. package/schemas/jaren-replication-snapshot.draft-07.schema.json +83 -0
  13. package/schemas/jaren-replication-snapshot.schema.json +83 -0
  14. package/schemas/jaren-replication.draft-07.schema.json +82 -0
  15. package/schemas/jaren-replication.schema.json +82 -0
  16. package/src/algebra.js +227 -9
  17. package/src/backup.js +161 -0
  18. package/src/cancellation.js +48 -0
  19. package/src/capture.js +230 -47
  20. package/src/cli.js +165 -59
  21. package/src/cursor.js +417 -0
  22. package/src/dag-job.js +154 -21
  23. package/src/ddl.js +102 -8
  24. package/src/dialect.js +268 -113
  25. package/src/dialects/expression-read.js +158 -0
  26. package/src/dialects/postgres.js +618 -0
  27. package/src/dialects/rtree-ddl.js +129 -0
  28. package/src/dialects/sqlite.js +244 -11
  29. package/src/document-files.js +311 -0
  30. package/src/document-steps.js +422 -0
  31. package/src/documents.js +335 -0
  32. package/src/driver.js +448 -61
  33. package/src/drivers/bun.js +37 -1
  34. package/src/drivers/indexeddb-snapshot.js +149 -0
  35. package/src/drivers/node-pool.js +11 -0
  36. package/src/drivers/node-worker-endpoint.js +105 -0
  37. package/src/drivers/node-worker.js +204 -0
  38. package/src/drivers/node.js +41 -7
  39. package/src/drivers/postgres.js +331 -0
  40. package/src/drivers/wasm-oo1.js +97 -0
  41. package/src/drivers/wasm-session.js +67 -0
  42. package/src/drivers/wasm.js +17 -83
  43. package/src/drivers/worker-pool.js +183 -0
  44. package/src/drivers/worker-protocol.js +79 -0
  45. package/src/drivers/worker-queue.js +60 -0
  46. package/src/emit.js +339 -48
  47. package/src/entity.js +20 -22
  48. package/src/errors.js +430 -19
  49. package/src/expression.js +284 -0
  50. package/src/graph.js +64 -8
  51. package/src/index.js +48 -17
  52. package/src/introspect.js +583 -0
  53. package/src/jobs.js +843 -107
  54. package/src/json-bytes.js +58 -0
  55. package/src/live-join.js +250 -0
  56. package/src/live-nested.js +120 -0
  57. package/src/live.js +18 -4
  58. package/src/logical-rows.js +90 -0
  59. package/src/maintenance.js +175 -0
  60. package/src/migrate.js +248 -181
  61. package/src/model.js +68 -0
  62. package/src/plan.js +1119 -138
  63. package/src/pragmas.js +314 -0
  64. package/src/profile.js +151 -3
  65. package/src/query.js +1634 -323
  66. package/src/replication-format.js +115 -0
  67. package/src/replication.js +332 -0
  68. package/src/residual.js +17 -0
  69. package/src/series.js +12 -4
  70. package/src/store.js +1567 -273
  71. package/src/tracker.js +203 -29
  72. package/src/udf.js +88 -7
  73. package/types/index.d.ts +1158 -27
  74. package/types/node-pool.d.ts +28 -0
  75. package/types/node-worker.d.ts +54 -0
  76. package/types/node.d.ts +69 -2
  77. package/types/postgres.d.ts +46 -0
  78. package/types/typed.d.ts +27 -4
  79. 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.56.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 on Node, Bun, or an injected wasm handle",
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.56.0",
74
- "@jarenjs/json": "^0.56.0",
75
- "@jarenjs/validate": "^0.56.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"