@rljson/rljson 0.0.75 → 0.0.77
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/README.architecture.md +365 -0
- package/README.md +11 -1
- package/README.public.md +397 -4
- package/README.trouble.md +4 -0
- package/dist/README.architecture.md +365 -0
- package/dist/README.md +11 -1
- package/dist/README.public.md +397 -4
- package/dist/README.trouble.md +4 -0
- package/dist/index.d.ts +7 -0
- package/dist/insertHistory/insertHistory.d.ts +3 -1
- package/dist/rljson.js +84 -0
- package/dist/sync/ack-payload.d.ts +25 -0
- package/dist/sync/client-id.d.ts +27 -0
- package/dist/sync/conflict.d.ts +39 -0
- package/dist/sync/connector-payload.d.ts +44 -0
- package/dist/sync/gap-fill.d.ts +41 -0
- package/dist/sync/sync-config.d.ts +70 -0
- package/dist/sync/sync-events.d.ts +32 -0
- package/package.json +8 -8
package/README.public.md
CHANGED
|
@@ -1,7 +1,400 @@
|
|
|
1
|
-
|
|
1
|
+
<!--
|
|
2
|
+
@license
|
|
3
|
+
Copyright (c) 2025 Rljson
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
Use of this source code is governed by terms that can be
|
|
6
|
+
found in the LICENSE file in the root of this package.
|
|
7
|
+
-->
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
# @rljson/rljson
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
Core types, validation, and sync protocol for the RLJSON data format.
|
|
12
|
+
|
|
13
|
+
## Table of Contents <!-- omit in toc -->
|
|
14
|
+
|
|
15
|
+
- [Installation](#installation)
|
|
16
|
+
- [Overview](#overview)
|
|
17
|
+
- [Data Model Types](#data-model-types)
|
|
18
|
+
- [Components](#components)
|
|
19
|
+
- [SliceIds](#sliceids)
|
|
20
|
+
- [Layers](#layers)
|
|
21
|
+
- [Cakes](#cakes)
|
|
22
|
+
- [Buffets](#buffets)
|
|
23
|
+
- [Trees](#trees)
|
|
24
|
+
- [Schema System](#schema-system)
|
|
25
|
+
- [TableCfg](#tablecfg)
|
|
26
|
+
- [Validation](#validation)
|
|
27
|
+
- [Edit Protocol](#edit-protocol)
|
|
28
|
+
- [Insert](#insert)
|
|
29
|
+
- [InsertHistory](#inserthistory)
|
|
30
|
+
- [Edits, MultiEdits, EditHistory](#edits-multiedits-edithistory)
|
|
31
|
+
- [Routing](#routing)
|
|
32
|
+
- [Sync Protocol](#sync-protocol)
|
|
33
|
+
- [ConnectorPayload](#connectorpayload)
|
|
34
|
+
- [AckPayload](#ackpayload)
|
|
35
|
+
- [GapFill](#gapfill)
|
|
36
|
+
- [SyncConfig](#syncconfig)
|
|
37
|
+
- [SyncEventNames](#synceventnames)
|
|
38
|
+
- [ClientId](#clientid)
|
|
39
|
+
- [Utilities](#utilities)
|
|
40
|
+
- [TimeId](#timeid)
|
|
41
|
+
- [RemoveDuplicates](#removeduplicates)
|
|
42
|
+
- [Ecosystem](#ecosystem)
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pnpm add @rljson/rljson
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Overview
|
|
51
|
+
|
|
52
|
+
`@rljson/rljson` is the foundational types package for the RLJSON ecosystem. It
|
|
53
|
+
defines the data format specification — a JSON-based, relational, normalized,
|
|
54
|
+
deeply-hashed exchange format designed for efficient synchronization of large
|
|
55
|
+
datasets.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import {
|
|
59
|
+
Rljson,
|
|
60
|
+
Route,
|
|
61
|
+
ConnectorPayload,
|
|
62
|
+
SyncConfig,
|
|
63
|
+
timeId,
|
|
64
|
+
} from '@rljson/rljson';
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Data Model Types
|
|
68
|
+
|
|
69
|
+
### Components
|
|
70
|
+
|
|
71
|
+
The fundamental data unit. A `ComponentsTable` contains key-value rows with an
|
|
72
|
+
auto-generated `_hash` serving as the primary key.
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { ComponentsTable } from '@rljson/rljson';
|
|
76
|
+
|
|
77
|
+
const ingredients: ComponentsTable = {
|
|
78
|
+
_type: 'components',
|
|
79
|
+
_data: [
|
|
80
|
+
{ id: 'flour', amountUnit: 'g', _hash: 'A5d...' },
|
|
81
|
+
{ id: 'sugar', amountUnit: 'g', _hash: 'B7f...' },
|
|
82
|
+
],
|
|
83
|
+
_hash: 't5o...',
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### SliceIds
|
|
88
|
+
|
|
89
|
+
For efficient management of large layers, slice IDs are separated from their
|
|
90
|
+
data. This allows fetching IDs first and retrieving details on demand.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { SliceIds, SliceIdsTable } from '@rljson/rljson';
|
|
94
|
+
|
|
95
|
+
const slices: SliceIdsTable = {
|
|
96
|
+
_type: 'sliceIds',
|
|
97
|
+
_data: [
|
|
98
|
+
{ add: ['slice0', 'slice1'], remove: [], _hash: 'wyY...' },
|
|
99
|
+
],
|
|
100
|
+
_hash: 'cnG...',
|
|
101
|
+
};
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Derived sets can modify an existing set using `base`, `add`, and `remove`.
|
|
105
|
+
|
|
106
|
+
### Layers
|
|
107
|
+
|
|
108
|
+
Layers assign components to slices.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
import { Layer, LayersTable } from '@rljson/rljson';
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A layer references a `componentsTable` and a `sliceIdsTable`, then maps each
|
|
115
|
+
slice ID to a component hash in its `add` block.
|
|
116
|
+
|
|
117
|
+
### Cakes
|
|
118
|
+
|
|
119
|
+
A `Cake` is a stack of layers sharing the same slice IDs. Each layer assigns
|
|
120
|
+
different component values to the shared slices.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { Cake, CakesTable } from '@rljson/rljson';
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Buffets
|
|
127
|
+
|
|
128
|
+
A `Buffet` is a heterogeneous collection of related items — references to rows
|
|
129
|
+
in any other table (cakes, layers, components, etc.).
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { Buffet, BuffetsTable } from '@rljson/rljson';
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Trees
|
|
136
|
+
|
|
137
|
+
Hierarchical tree structures with content-addressed nodes. Each `Tree` has an
|
|
138
|
+
optional `id` (unique among siblings), an `isParent` flag, a `meta` field
|
|
139
|
+
(`Json | null`), and a `children` array of child hashes (or `null` for leaves).
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import { Tree, TreesTable, treeFromObject } from '@rljson/rljson';
|
|
143
|
+
|
|
144
|
+
// Convert a plain object into hashed tree nodes
|
|
145
|
+
const nodes = treeFromObject({ src: { 'index.ts': 'console.log("hello")' } });
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Schema System
|
|
149
|
+
|
|
150
|
+
### TableCfg
|
|
151
|
+
|
|
152
|
+
`TableCfg` defines the schema for any table — its key, content type, column
|
|
153
|
+
definitions, and metadata flags.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { TableCfg, ColumnCfg, throwOnInvalidTableCfg } from '@rljson/rljson';
|
|
157
|
+
|
|
158
|
+
const cfg: TableCfg = {
|
|
159
|
+
key: 'ingredients',
|
|
160
|
+
type: 'components',
|
|
161
|
+
columns: [
|
|
162
|
+
{ key: '_hash', type: 'string', titleLong: 'Hash', titleShort: 'Hash' },
|
|
163
|
+
{ key: 'name', type: 'string', titleLong: 'Name', titleShort: 'Name' },
|
|
164
|
+
],
|
|
165
|
+
isHead: false,
|
|
166
|
+
isRoot: false,
|
|
167
|
+
isShared: false,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
throwOnInvalidTableCfg(cfg); // throws on invalid config
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Column types: `string`, `number`, `boolean`, `json`, `jsonArray`.
|
|
174
|
+
|
|
175
|
+
Columns can reference other tables via the `ref` property on `ColumnCfgWithRef`.
|
|
176
|
+
|
|
177
|
+
### Validation
|
|
178
|
+
|
|
179
|
+
The `Validate` class coordinates multiple validators to check entire RLJSON
|
|
180
|
+
objects — naming conventions, hash integrity, reference validity, tree
|
|
181
|
+
structure, layer/cake/buffet consistency.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
import { Validate, BaseValidator } from '@rljson/rljson';
|
|
185
|
+
|
|
186
|
+
const validate = new Validate();
|
|
187
|
+
validate.addValidator(new BaseValidator());
|
|
188
|
+
const errors = await validate.run(myRljsonData);
|
|
189
|
+
// errors: { base: { hasErrors: false } }
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The `BaseValidator` checks all core rules. Custom validators can be added by
|
|
193
|
+
implementing the `Validator` interface.
|
|
194
|
+
|
|
195
|
+
## Edit Protocol
|
|
196
|
+
|
|
197
|
+
### Insert
|
|
198
|
+
|
|
199
|
+
An `Insert` describes a data modification operation with a command (`add`),
|
|
200
|
+
a value, a route, and an optional origin.
|
|
201
|
+
|
|
202
|
+
```typescript
|
|
203
|
+
import { Insert, validateInsert } from '@rljson/rljson';
|
|
204
|
+
|
|
205
|
+
const insert: Insert<any> = {
|
|
206
|
+
route: '/ingredients',
|
|
207
|
+
command: 'add',
|
|
208
|
+
value: { name: 'butter', amountUnit: 'g' },
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const errors = validateInsert(insert);
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### InsertHistory
|
|
215
|
+
|
|
216
|
+
`InsertHistoryRow` tracks each insert operation with a unique `timeId`, the
|
|
217
|
+
`route` that was modified, and optional `origin`, `previous` (causal
|
|
218
|
+
predecessors), and `clientTimestamp`.
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { InsertHistoryRow, InsertHistoryTimeId } from '@rljson/rljson';
|
|
222
|
+
|
|
223
|
+
const row: InsertHistoryRow<'ingredients'> = {
|
|
224
|
+
ingredientsRef: 'A5d...',
|
|
225
|
+
timeId: '1700000000000:AbCd',
|
|
226
|
+
route: '/ingredients',
|
|
227
|
+
origin: 'client_ExAmPlE12345',
|
|
228
|
+
previous: ['1699999999999:ZzZz'],
|
|
229
|
+
clientTimestamp: 1700000000000,
|
|
230
|
+
};
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Edits, MultiEdits, EditHistory
|
|
234
|
+
|
|
235
|
+
- **Edit**: A named action with a type and data payload (`EditAction`)
|
|
236
|
+
- **MultiEdit**: Chains edits into a linked list via `previous` ref
|
|
237
|
+
- **EditHistory**: Tracks the full chain of multi-edits with `timeId` timestamps
|
|
238
|
+
|
|
239
|
+
## Routing
|
|
240
|
+
|
|
241
|
+
The `Route` class parses and builds hierarchical data paths used throughout
|
|
242
|
+
the RLJSON ecosystem.
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
import { Route } from '@rljson/rljson';
|
|
246
|
+
|
|
247
|
+
// Parse a flat route string
|
|
248
|
+
const route = Route.fromFlat('/ingredients@A5d.../nutritionalValues');
|
|
249
|
+
|
|
250
|
+
// Navigate route segments
|
|
251
|
+
route.top; // first segment: { tableKey: 'ingredients', ... }
|
|
252
|
+
route.root; // last segment: { tableKey: 'nutritionalValues' }
|
|
253
|
+
route.segment(0); // segment by index
|
|
254
|
+
route.flat; // serialized string
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Routes support references (`@hash`), slice IDs (`(id1,id2)`), and
|
|
258
|
+
InsertHistory refs (`@timestamp:unique`).
|
|
259
|
+
|
|
260
|
+
## Sync Protocol
|
|
261
|
+
|
|
262
|
+
The `sync/` module defines wire-protocol types for the messaging hardening
|
|
263
|
+
system used by `@rljson/db` (Connector) and `@rljson/server`.
|
|
264
|
+
|
|
265
|
+
### ConnectorPayload
|
|
266
|
+
|
|
267
|
+
The payload transmitted between Connector and Server on the wire.
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
import { ConnectorPayload } from '@rljson/rljson';
|
|
271
|
+
|
|
272
|
+
// Minimal (backward-compatible)
|
|
273
|
+
const legacy: ConnectorPayload = { o: 'origin', r: 'ref' };
|
|
274
|
+
|
|
275
|
+
// Fully enriched
|
|
276
|
+
const enriched: ConnectorPayload = {
|
|
277
|
+
o: 'origin', // ephemeral origin (self-echo filter)
|
|
278
|
+
r: 'ref', // the ref being announced
|
|
279
|
+
c: 'client_abc123...', // stable client identity
|
|
280
|
+
t: Date.now(), // client-side timestamp
|
|
281
|
+
seq: 42, // monotonic sequence number
|
|
282
|
+
p: ['prev-timeId'], // causal predecessors
|
|
283
|
+
cksum: 'sha256:...', // content checksum
|
|
284
|
+
};
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### AckPayload
|
|
288
|
+
|
|
289
|
+
Server → Client acknowledgment that all (or some) receivers got a ref.
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
import { AckPayload } from '@rljson/rljson';
|
|
293
|
+
|
|
294
|
+
const ack: AckPayload = {
|
|
295
|
+
r: 'ref',
|
|
296
|
+
ok: true,
|
|
297
|
+
receivedBy: 3,
|
|
298
|
+
totalClients: 3,
|
|
299
|
+
};
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### GapFill
|
|
303
|
+
|
|
304
|
+
Types for requesting and receiving missing refs when a sequence gap is
|
|
305
|
+
detected.
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
import { GapFillRequest, GapFillResponse } from '@rljson/rljson';
|
|
309
|
+
|
|
310
|
+
const request: GapFillRequest = {
|
|
311
|
+
route: '/sharedTree',
|
|
312
|
+
afterSeq: 5,
|
|
313
|
+
};
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
### SyncConfig
|
|
317
|
+
|
|
318
|
+
Feature flags to opt into hardened sync behavior. All flags are optional and
|
|
319
|
+
default to off.
|
|
320
|
+
|
|
321
|
+
```typescript
|
|
322
|
+
import { SyncConfig } from '@rljson/rljson';
|
|
323
|
+
|
|
324
|
+
const config: SyncConfig = {
|
|
325
|
+
causalOrdering: true, // track predecessors + detect gaps
|
|
326
|
+
requireAck: true, // wait for server ACK after send
|
|
327
|
+
ackTimeoutMs: 5_000, // ACK timeout
|
|
328
|
+
includeClientIdentity: true, // attach clientId + timestamp
|
|
329
|
+
maxDedupSetSize: 10_000, // max refs per dedup generation (default: 10 000)
|
|
330
|
+
bootstrapHeartbeatMs: 30_000, // periodic bootstrap heartbeat interval (optional)
|
|
331
|
+
};
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
### SyncEventNames
|
|
335
|
+
|
|
336
|
+
Helper to generate typed, route-specific socket event names.
|
|
337
|
+
|
|
338
|
+
```typescript
|
|
339
|
+
import { syncEvents } from '@rljson/rljson';
|
|
340
|
+
|
|
341
|
+
const events = syncEvents('/sharedTree');
|
|
342
|
+
// events.ref → '/sharedTree'
|
|
343
|
+
// events.ack → '/sharedTree:ack'
|
|
344
|
+
// events.ackClient → '/sharedTree:ack:client'
|
|
345
|
+
// events.gapFillReq → '/sharedTree:gapfill:req'
|
|
346
|
+
// events.gapFillRes → '/sharedTree:gapfill:res'
|
|
347
|
+
// events.bootstrap → '/sharedTree:bootstrap'
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### ClientId
|
|
351
|
+
|
|
352
|
+
A stable client identity that persists across reconnections (unlike the
|
|
353
|
+
ephemeral Connector origin).
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { clientId, isClientId } from '@rljson/rljson';
|
|
357
|
+
|
|
358
|
+
const id = clientId(); // 'client_V1StGXR8_Z5j'
|
|
359
|
+
isClientId(id); // true
|
|
360
|
+
isClientId('not-a-client-id'); // false
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
## Utilities
|
|
364
|
+
|
|
365
|
+
### TimeId
|
|
366
|
+
|
|
367
|
+
A unique, time-based identifier in the format `"timestamp:xxxx"`.
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
import { timeId, isTimeId, getTimeIdTimestamp } from '@rljson/rljson';
|
|
371
|
+
|
|
372
|
+
const id = timeId(); // '1700000000000:AbCd'
|
|
373
|
+
isTimeId(id); // true
|
|
374
|
+
getTimeIdTimestamp(id); // 1700000000000
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### RemoveDuplicates
|
|
378
|
+
|
|
379
|
+
Deduplicates rows (by `_hash`) across all tables in an Rljson object.
|
|
380
|
+
|
|
381
|
+
```typescript
|
|
382
|
+
import { removeDuplicates } from '@rljson/rljson';
|
|
383
|
+
|
|
384
|
+
const deduped = removeDuplicates(myRljsonData);
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Ecosystem
|
|
388
|
+
|
|
389
|
+
`@rljson/rljson` is the foundational layer of the RLJSON ecosystem:
|
|
390
|
+
|
|
391
|
+
| Package | Purpose |
|
|
392
|
+
| ------------------ | ----------------------------------- |
|
|
393
|
+
| `@rljson/rljson` | Core types & validation (this) |
|
|
394
|
+
| `@rljson/hash` | Deep hashing for RLJSON data |
|
|
395
|
+
| `@rljson/json` | JSON type definitions |
|
|
396
|
+
| `@rljson/io` | Data transport (Io, Socket, IoPeer) |
|
|
397
|
+
| `@rljson/bs` | Blob storage (Bs, BsPeer) |
|
|
398
|
+
| `@rljson/db` | Database pipeline (Db, Connector) |
|
|
399
|
+
| `@rljson/server` | Server relay & Client setup |
|
|
400
|
+
| `@rljson/fs-agent` | Filesystem ↔ database sync |
|