@rljson/rljson 0.0.74 → 0.0.76
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/content/tree.d.ts +2 -1
- package/dist/index.d.ts +6 -0
- package/dist/insertHistory/insertHistory.d.ts +3 -1
- package/dist/rljson.js +98 -1
- package/dist/sync/ack-payload.d.ts +25 -0
- package/dist/sync/client-id.d.ts +27 -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 +11 -11
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/content/tree.d.ts
CHANGED
|
@@ -47,6 +47,7 @@ export declare const exampleTreesTable: () => TreesTable;
|
|
|
47
47
|
/**
|
|
48
48
|
* Converts a plain object into a tree structure
|
|
49
49
|
* @param obj - The plain object to convert
|
|
50
|
+
* @param skipRootCreation - If true, skips creating an automatic root node (default: false)
|
|
50
51
|
* @returns An array of Tree nodes representing the tree structure
|
|
51
52
|
*/
|
|
52
|
-
export declare const treeFromObject: (obj: any) => TreeWithHash[];
|
|
53
|
+
export declare const treeFromObject: (obj: any, skipRootCreation?: boolean) => TreeWithHash[];
|
package/dist/index.d.ts
CHANGED
|
@@ -16,6 +16,12 @@ 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/connector-payload.ts';
|
|
22
|
+
export * from './sync/gap-fill.ts';
|
|
23
|
+
export * from './sync/sync-config.ts';
|
|
24
|
+
export * from './sync/sync-events.ts';
|
|
19
25
|
export * from './tools/remove-duplicates.ts';
|
|
20
26
|
export * from './tools/time-id.ts';
|
|
21
27
|
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
|
@@ -643,7 +643,7 @@ const createTreesTableCfg = (treesTableKey) => ({
|
|
|
643
643
|
isShared: true
|
|
644
644
|
});
|
|
645
645
|
const exampleTreesTable = () => bakeryExample().recipesTreeTable;
|
|
646
|
-
const treeFromObject = (obj) => {
|
|
646
|
+
const treeFromObject = (obj, skipRootCreation = false) => {
|
|
647
647
|
const result = [];
|
|
648
648
|
const processedIds = /* @__PURE__ */ new Set();
|
|
649
649
|
const idToHashMap = /* @__PURE__ */ new Map();
|
|
@@ -725,13 +725,26 @@ const treeFromObject = (obj) => {
|
|
|
725
725
|
result.push(hashedNode);
|
|
726
726
|
}
|
|
727
727
|
};
|
|
728
|
+
const topLevelIds = [];
|
|
728
729
|
if (obj !== null && typeof obj === "object" && !Array.isArray(obj)) {
|
|
729
730
|
for (const key in obj) {
|
|
730
731
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
732
|
+
topLevelIds.push(key);
|
|
731
733
|
processNode(obj[key], key);
|
|
732
734
|
}
|
|
733
735
|
}
|
|
734
736
|
}
|
|
737
|
+
if (!skipRootCreation && topLevelIds.length > 0) {
|
|
738
|
+
const rootNode = {
|
|
739
|
+
id: "root",
|
|
740
|
+
isParent: true,
|
|
741
|
+
meta: null,
|
|
742
|
+
children: topLevelIds.map((id) => idToHashMap.get(id))
|
|
743
|
+
};
|
|
744
|
+
const hashedRootNode = hip(rootNode);
|
|
745
|
+
idToHashMap.set("root", hashedRootNode._hash);
|
|
746
|
+
result.push(hashedRootNode);
|
|
747
|
+
}
|
|
735
748
|
return result;
|
|
736
749
|
};
|
|
737
750
|
class Example {
|
|
@@ -1913,6 +1926,12 @@ const createInsertHistoryTableCfg = (tableCfg) => ({
|
|
|
1913
1926
|
type: "jsonArray",
|
|
1914
1927
|
titleLong: "Previous",
|
|
1915
1928
|
titleShort: "Previous"
|
|
1929
|
+
},
|
|
1930
|
+
{
|
|
1931
|
+
key: "clientTimestamp",
|
|
1932
|
+
type: "number",
|
|
1933
|
+
titleLong: "Client Timestamp",
|
|
1934
|
+
titleShort: "Timestamp"
|
|
1916
1935
|
}
|
|
1917
1936
|
],
|
|
1918
1937
|
isHead: false,
|
|
@@ -1959,6 +1978,71 @@ const iterateTables = async (rljson, callback) => {
|
|
|
1959
1978
|
throw errors;
|
|
1960
1979
|
}
|
|
1961
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
|
+
});
|
|
1962
2046
|
const removeDuplicates = (rljson) => {
|
|
1963
2047
|
const result = {};
|
|
1964
2048
|
for (const key in rljson) {
|
|
@@ -2958,8 +3042,14 @@ export {
|
|
|
2958
3042
|
InsertValidator,
|
|
2959
3043
|
Route,
|
|
2960
3044
|
Validate,
|
|
3045
|
+
ackPayloadExample,
|
|
3046
|
+
ackPayloadPartialExample,
|
|
2961
3047
|
addColumnsToTableCfg,
|
|
2962
3048
|
bakeryExample,
|
|
3049
|
+
clientId,
|
|
3050
|
+
clientIdExample,
|
|
3051
|
+
connectorPayloadExample,
|
|
3052
|
+
connectorPayloadFullExample,
|
|
2963
3053
|
contentTypes,
|
|
2964
3054
|
createCakeTableCfg,
|
|
2965
3055
|
createEditHistoryTableCfg,
|
|
@@ -2982,8 +3072,11 @@ export {
|
|
|
2982
3072
|
exampleTableCfgTable,
|
|
2983
3073
|
exampleTreesTable,
|
|
2984
3074
|
exampleTypedefs,
|
|
3075
|
+
gapFillRequestExample,
|
|
3076
|
+
gapFillResponseExample,
|
|
2985
3077
|
getTimeIdTimestamp,
|
|
2986
3078
|
getTimeIdUniquePart,
|
|
3079
|
+
isClientId,
|
|
2987
3080
|
isTimeId,
|
|
2988
3081
|
isValidFieldName,
|
|
2989
3082
|
iterateTables,
|
|
@@ -2994,6 +3087,10 @@ export {
|
|
|
2994
3087
|
routeRefSeperator,
|
|
2995
3088
|
routeSliceIdIndicators,
|
|
2996
3089
|
routeSliceIdSeperator,
|
|
3090
|
+
syncConfigCausalOnlyExample,
|
|
3091
|
+
syncConfigDefault,
|
|
3092
|
+
syncConfigFullExample,
|
|
3093
|
+
syncEvents,
|
|
2997
3094
|
throwOnInvalidTableCfg,
|
|
2998
3095
|
timeId,
|
|
2999
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;
|