@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/dist/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 |
|
package/dist/README.trouble.md
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,13 @@ export * from './insert/insert.ts';
|
|
|
16
16
|
export * from './insertHistory/insertHistory.ts';
|
|
17
17
|
export * from './rljson.ts';
|
|
18
18
|
export * from './route/route.ts';
|
|
19
|
+
export * from './sync/ack-payload.ts';
|
|
20
|
+
export * from './sync/client-id.ts';
|
|
21
|
+
export * from './sync/conflict.ts';
|
|
22
|
+
export * from './sync/connector-payload.ts';
|
|
23
|
+
export * from './sync/gap-fill.ts';
|
|
24
|
+
export * from './sync/sync-config.ts';
|
|
25
|
+
export * from './sync/sync-events.ts';
|
|
19
26
|
export * from './tools/remove-duplicates.ts';
|
|
20
27
|
export * from './tools/time-id.ts';
|
|
21
28
|
export * from './typedefs.ts';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { TableCfg } from '../content/table-cfg.ts';
|
|
2
2
|
import { RljsonTable } from '../rljson.ts';
|
|
3
3
|
import { RouteRef } from '../route/route.ts';
|
|
4
|
+
import { ClientId } from '../sync/client-id.ts';
|
|
4
5
|
import { Ref } from '../typedefs.ts';
|
|
5
6
|
export type InsertHistoryTimeId = string;
|
|
6
7
|
export type InsertHistoryRow<Str extends string> = {
|
|
@@ -8,8 +9,9 @@ export type InsertHistoryRow<Str extends string> = {
|
|
|
8
9
|
} & {
|
|
9
10
|
timeId: InsertHistoryTimeId;
|
|
10
11
|
route: RouteRef;
|
|
11
|
-
origin?: Ref;
|
|
12
|
+
origin?: ClientId | Ref;
|
|
12
13
|
previous?: InsertHistoryTimeId[];
|
|
14
|
+
clientTimestamp?: number;
|
|
13
15
|
};
|
|
14
16
|
export type InsertHistoryTable<Str extends string> = RljsonTable<InsertHistoryRow<Str>, 'insertHistory'>;
|
|
15
17
|
/**
|
package/dist/rljson.js
CHANGED
|
@@ -1926,6 +1926,12 @@ const createInsertHistoryTableCfg = (tableCfg) => ({
|
|
|
1926
1926
|
type: "jsonArray",
|
|
1927
1927
|
titleLong: "Previous",
|
|
1928
1928
|
titleShort: "Previous"
|
|
1929
|
+
},
|
|
1930
|
+
{
|
|
1931
|
+
key: "clientTimestamp",
|
|
1932
|
+
type: "number",
|
|
1933
|
+
titleLong: "Client Timestamp",
|
|
1934
|
+
titleShort: "Timestamp"
|
|
1929
1935
|
}
|
|
1930
1936
|
],
|
|
1931
1937
|
isHead: false,
|
|
@@ -1972,6 +1978,71 @@ const iterateTables = async (rljson, callback) => {
|
|
|
1972
1978
|
throw errors;
|
|
1973
1979
|
}
|
|
1974
1980
|
};
|
|
1981
|
+
const ackPayloadExample = () => ({
|
|
1982
|
+
r: "1700000000001:EfGh",
|
|
1983
|
+
ok: true,
|
|
1984
|
+
receivedBy: 3,
|
|
1985
|
+
totalClients: 3
|
|
1986
|
+
});
|
|
1987
|
+
const ackPayloadPartialExample = () => ({
|
|
1988
|
+
r: "1700000000001:EfGh",
|
|
1989
|
+
ok: false,
|
|
1990
|
+
receivedBy: 1,
|
|
1991
|
+
totalClients: 3
|
|
1992
|
+
});
|
|
1993
|
+
const clientId = () => {
|
|
1994
|
+
return "client_" + nanoid(12);
|
|
1995
|
+
};
|
|
1996
|
+
const isClientId = (id) => {
|
|
1997
|
+
if (!id.startsWith("client_")) return false;
|
|
1998
|
+
const suffix = id.slice("client_".length);
|
|
1999
|
+
return suffix.length === 12;
|
|
2000
|
+
};
|
|
2001
|
+
const clientIdExample = () => "client_ExAmPlE12345";
|
|
2002
|
+
const connectorPayloadExample = () => ({
|
|
2003
|
+
o: "1700000000000:AbCd",
|
|
2004
|
+
r: "1700000000001:EfGh"
|
|
2005
|
+
});
|
|
2006
|
+
const connectorPayloadFullExample = () => ({
|
|
2007
|
+
o: "1700000000000:AbCd",
|
|
2008
|
+
r: "1700000000001:EfGh",
|
|
2009
|
+
c: "client_ExAmPlE12345",
|
|
2010
|
+
t: 1700000000001,
|
|
2011
|
+
seq: 42,
|
|
2012
|
+
p: ["1700000000000:XyZw"],
|
|
2013
|
+
cksum: "sha256:abc123def456"
|
|
2014
|
+
});
|
|
2015
|
+
const gapFillRequestExample = () => ({
|
|
2016
|
+
route: "/sharedTree",
|
|
2017
|
+
afterSeq: 5,
|
|
2018
|
+
afterTimeId: "1700000000000:AbCd"
|
|
2019
|
+
});
|
|
2020
|
+
const gapFillResponseExample = () => ({
|
|
2021
|
+
route: "/sharedTree",
|
|
2022
|
+
refs: [
|
|
2023
|
+
{ o: "1700000000000:AbCd", r: "1700000000006:MnOp", seq: 6 },
|
|
2024
|
+
{ o: "1700000000000:AbCd", r: "1700000000007:QrSt", seq: 7 }
|
|
2025
|
+
]
|
|
2026
|
+
});
|
|
2027
|
+
const syncConfigDefault = () => ({});
|
|
2028
|
+
const syncConfigFullExample = () => ({
|
|
2029
|
+
causalOrdering: true,
|
|
2030
|
+
requireAck: true,
|
|
2031
|
+
ackTimeoutMs: 5e3,
|
|
2032
|
+
includeClientIdentity: true,
|
|
2033
|
+
maxDedupSetSize: 1e4
|
|
2034
|
+
});
|
|
2035
|
+
const syncConfigCausalOnlyExample = () => ({
|
|
2036
|
+
causalOrdering: true
|
|
2037
|
+
});
|
|
2038
|
+
const syncEvents = (route) => ({
|
|
2039
|
+
ref: route,
|
|
2040
|
+
ack: `${route}:ack`,
|
|
2041
|
+
ackClient: `${route}:ack:client`,
|
|
2042
|
+
gapFillReq: `${route}:gapfill:req`,
|
|
2043
|
+
gapFillRes: `${route}:gapfill:res`,
|
|
2044
|
+
bootstrap: `${route}:bootstrap`
|
|
2045
|
+
});
|
|
1975
2046
|
const removeDuplicates = (rljson) => {
|
|
1976
2047
|
const result = {};
|
|
1977
2048
|
for (const key in rljson) {
|
|
@@ -2971,8 +3042,14 @@ export {
|
|
|
2971
3042
|
InsertValidator,
|
|
2972
3043
|
Route,
|
|
2973
3044
|
Validate,
|
|
3045
|
+
ackPayloadExample,
|
|
3046
|
+
ackPayloadPartialExample,
|
|
2974
3047
|
addColumnsToTableCfg,
|
|
2975
3048
|
bakeryExample,
|
|
3049
|
+
clientId,
|
|
3050
|
+
clientIdExample,
|
|
3051
|
+
connectorPayloadExample,
|
|
3052
|
+
connectorPayloadFullExample,
|
|
2976
3053
|
contentTypes,
|
|
2977
3054
|
createCakeTableCfg,
|
|
2978
3055
|
createEditHistoryTableCfg,
|
|
@@ -2995,8 +3072,11 @@ export {
|
|
|
2995
3072
|
exampleTableCfgTable,
|
|
2996
3073
|
exampleTreesTable,
|
|
2997
3074
|
exampleTypedefs,
|
|
3075
|
+
gapFillRequestExample,
|
|
3076
|
+
gapFillResponseExample,
|
|
2998
3077
|
getTimeIdTimestamp,
|
|
2999
3078
|
getTimeIdUniquePart,
|
|
3079
|
+
isClientId,
|
|
3000
3080
|
isTimeId,
|
|
3001
3081
|
isValidFieldName,
|
|
3002
3082
|
iterateTables,
|
|
@@ -3007,6 +3087,10 @@ export {
|
|
|
3007
3087
|
routeRefSeperator,
|
|
3008
3088
|
routeSliceIdIndicators,
|
|
3009
3089
|
routeSliceIdSeperator,
|
|
3090
|
+
syncConfigCausalOnlyExample,
|
|
3091
|
+
syncConfigDefault,
|
|
3092
|
+
syncConfigFullExample,
|
|
3093
|
+
syncEvents,
|
|
3010
3094
|
throwOnInvalidTableCfg,
|
|
3011
3095
|
timeId,
|
|
3012
3096
|
treeFromObject,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server → Client acknowledgment that all (or some) receivers
|
|
3
|
+
* successfully received and processed a given ref.
|
|
4
|
+
*
|
|
5
|
+
* Sent on the `${route}:ack` event after the server has collected
|
|
6
|
+
* individual client ACKs (or after a timeout).
|
|
7
|
+
*/
|
|
8
|
+
export type AckPayload = {
|
|
9
|
+
/** The ref being acknowledged. */
|
|
10
|
+
r: string;
|
|
11
|
+
/** `true` if all connected clients confirmed receipt; `false` on timeout / partial. */
|
|
12
|
+
ok: boolean;
|
|
13
|
+
/** Number of clients that confirmed receipt. */
|
|
14
|
+
receivedBy?: number;
|
|
15
|
+
/** Total number of receiver clients at the time of broadcast. */
|
|
16
|
+
totalClients?: number;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Returns an example AckPayload where all clients confirmed receipt.
|
|
20
|
+
*/
|
|
21
|
+
export declare const ackPayloadExample: () => AckPayload;
|
|
22
|
+
/**
|
|
23
|
+
* Returns an example AckPayload for a partial / timed-out acknowledgment.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ackPayloadPartialExample: () => AckPayload;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A stable client identity that persists across reconnections.
|
|
3
|
+
*
|
|
4
|
+
* Unlike a Connector's ephemeral `origin` (which changes on every
|
|
5
|
+
* instantiation), a `ClientId` should be generated once and stored
|
|
6
|
+
* (e.g. in local storage) so that it can be reused across sessions.
|
|
7
|
+
*/
|
|
8
|
+
export type ClientId = string;
|
|
9
|
+
/**
|
|
10
|
+
* Generates a new ClientId.
|
|
11
|
+
* A ClientId is a 12-character nanoid, prefixed with `"client_"` for
|
|
12
|
+
* easy visual identification in logs and debugging.
|
|
13
|
+
* @returns A new ClientId string (e.g. `"client_V1StGXR8_Z5j"`)
|
|
14
|
+
*/
|
|
15
|
+
export declare const clientId: () => ClientId;
|
|
16
|
+
/**
|
|
17
|
+
* Checks whether a given string is a valid ClientId.
|
|
18
|
+
* A valid ClientId starts with `"client_"` followed by exactly 12
|
|
19
|
+
* characters.
|
|
20
|
+
* @param id - The string to check
|
|
21
|
+
* @returns `true` if the string matches the ClientId format
|
|
22
|
+
*/
|
|
23
|
+
export declare const isClientId: (id: string) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Returns an example ClientId for documentation and testing.
|
|
26
|
+
*/
|
|
27
|
+
export declare const clientIdExample: () => ClientId;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { InsertHistoryTimeId } from '../insertHistory/insertHistory.ts';
|
|
2
|
+
/**
|
|
3
|
+
* The type of conflict detected in the InsertHistory DAG.
|
|
4
|
+
*
|
|
5
|
+
* - `'dagBranch'` — Two or more InsertHistory rows share the same
|
|
6
|
+
* predecessor, creating divergent branches. This indicates concurrent
|
|
7
|
+
* writes from different clients that have not yet been merged.
|
|
8
|
+
*/
|
|
9
|
+
export type ConflictType = 'dagBranch';
|
|
10
|
+
/**
|
|
11
|
+
* Represents a detected conflict in the InsertHistory DAG.
|
|
12
|
+
*
|
|
13
|
+
* A `Conflict` is emitted when the system detects that the InsertHistory
|
|
14
|
+
* for a table has diverged into multiple branches (multiple "tips" that
|
|
15
|
+
* are not ancestors of each other).
|
|
16
|
+
* Detection only — no resolution: this type signals that a conflict
|
|
17
|
+
* exists. Resolution logic is left to upper layers (application code).
|
|
18
|
+
*/
|
|
19
|
+
export interface Conflict {
|
|
20
|
+
/** The table where the conflict was detected (without InsertHistory suffix). */
|
|
21
|
+
table: string;
|
|
22
|
+
/** The type of conflict. */
|
|
23
|
+
type: ConflictType;
|
|
24
|
+
/** Timestamp (ms since epoch) when the conflict was detected. */
|
|
25
|
+
detectedAt: number;
|
|
26
|
+
/**
|
|
27
|
+
* The InsertHistory tip timeIds that form the branches.
|
|
28
|
+
*
|
|
29
|
+
* A "tip" is a timeId that is not referenced as `previous` by any other
|
|
30
|
+
* InsertHistory row. Multiple tips indicate a DAG fork.
|
|
31
|
+
*
|
|
32
|
+
* Always contains at least two entries when `type === 'dagBranch'`.
|
|
33
|
+
*/
|
|
34
|
+
branches: InsertHistoryTimeId[];
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Callback invoked when a conflict is detected.
|
|
38
|
+
*/
|
|
39
|
+
export type ConflictCallback = (conflict: Conflict) => void;
|