@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
|
@@ -34,6 +34,24 @@ This document describes the architecture of the Rljson format.
|
|
|
34
34
|
- [Layer](#layer)
|
|
35
35
|
- [Cake](#cake)
|
|
36
36
|
- [Buffet](#buffet)
|
|
37
|
+
- [Trees](#trees)
|
|
38
|
+
- [Schema System](#schema-system)
|
|
39
|
+
- [TableCfg](#tablecfg)
|
|
40
|
+
- [Validation](#validation)
|
|
41
|
+
- [Edit Protocol](#edit-protocol)
|
|
42
|
+
- [Insert](#insert)
|
|
43
|
+
- [InsertHistory](#inserthistory)
|
|
44
|
+
- [Edits \& MultiEdits](#edits--multiedits)
|
|
45
|
+
- [EditHistory](#edithistory)
|
|
46
|
+
- [Routing](#routing)
|
|
47
|
+
- [Sync Protocol](#sync-protocol)
|
|
48
|
+
- [ConnectorPayload](#connectorpayload)
|
|
49
|
+
- [AckPayload](#ackpayload)
|
|
50
|
+
- [GapFill](#gapfill)
|
|
51
|
+
- [SyncConfig](#syncconfig)
|
|
52
|
+
- [SyncEventNames](#synceventnames)
|
|
53
|
+
- [ClientId](#clientid)
|
|
54
|
+
- [Package Structure](#package-structure)
|
|
37
55
|
|
|
38
56
|
## What is Rljson?
|
|
39
57
|
|
|
@@ -368,3 +386,350 @@ such as cakes, layers, or components:
|
|
|
368
386
|
}
|
|
369
387
|
}
|
|
370
388
|
```
|
|
389
|
+
|
|
390
|
+
### Trees
|
|
391
|
+
|
|
392
|
+
Trees represent hierarchical, content-addressed data structures. Each
|
|
393
|
+
`Tree` node has an optional `id` (unique among siblings), a boolean `isParent`
|
|
394
|
+
flag, a `meta` field (`Json | null`) for arbitrary metadata, and a `children`
|
|
395
|
+
array (`Array<TreeRef> | null`) containing hashes of child nodes.
|
|
396
|
+
|
|
397
|
+
Trees are stored as **separate rows with parent-child relationships**, not as
|
|
398
|
+
a single denormalized structure. Parent nodes reference children via hash:
|
|
399
|
+
|
|
400
|
+
```json
|
|
401
|
+
{
|
|
402
|
+
"projectTree": {
|
|
403
|
+
"_type": "trees",
|
|
404
|
+
"_data": [
|
|
405
|
+
{
|
|
406
|
+
"id": "src",
|
|
407
|
+
"isParent": true,
|
|
408
|
+
"meta": null,
|
|
409
|
+
"children": ["def456", "ghi789"],
|
|
410
|
+
"_hash": "abc123"
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"id": "index.ts",
|
|
414
|
+
"isParent": false,
|
|
415
|
+
"meta": { "type": "file" },
|
|
416
|
+
"children": null,
|
|
417
|
+
"_hash": "def456"
|
|
418
|
+
},
|
|
419
|
+
{
|
|
420
|
+
"id": "utils.ts",
|
|
421
|
+
"isParent": false,
|
|
422
|
+
"meta": { "type": "file" },
|
|
423
|
+
"children": null,
|
|
424
|
+
"_hash": "ghi789"
|
|
425
|
+
}
|
|
426
|
+
]
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
The helper function `treeFromObject()` converts a plain JavaScript object into
|
|
432
|
+
an array of hashed `Tree` entries.
|
|
433
|
+
|
|
434
|
+
## Schema System
|
|
435
|
+
|
|
436
|
+
### TableCfg
|
|
437
|
+
|
|
438
|
+
Every table in RLJSON can have an associated `TableCfg` that describes its
|
|
439
|
+
schema — the table key, content type, column definitions, and metadata flags:
|
|
440
|
+
|
|
441
|
+
```json
|
|
442
|
+
{
|
|
443
|
+
"tableCfgs": {
|
|
444
|
+
"_type": "tableCfgs",
|
|
445
|
+
"_data": [
|
|
446
|
+
{
|
|
447
|
+
"key": "ingredients",
|
|
448
|
+
"type": "components",
|
|
449
|
+
"columns": [
|
|
450
|
+
{ "key": "_hash", "type": "string", "titleLong": "Hash", "titleShort": "Hash" },
|
|
451
|
+
{ "key": "name", "type": "string", "titleLong": "Name", "titleShort": "Name" },
|
|
452
|
+
{ "key": "amount", "type": "number", "titleLong": "Amount", "titleShort": "Amt" }
|
|
453
|
+
],
|
|
454
|
+
"isHead": false,
|
|
455
|
+
"isRoot": false,
|
|
456
|
+
"isShared": false
|
|
457
|
+
}
|
|
458
|
+
]
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
Supported column types: `string`, `number`, `boolean`, `json`, `jsonArray`.
|
|
464
|
+
|
|
465
|
+
Columns can reference other tables via a `ref` property, enabling foreign-key
|
|
466
|
+
relationships:
|
|
467
|
+
|
|
468
|
+
```json
|
|
469
|
+
{
|
|
470
|
+
"key": "ingredientsRef",
|
|
471
|
+
"type": "string",
|
|
472
|
+
"titleLong": "Ingredient",
|
|
473
|
+
"titleShort": "Ing",
|
|
474
|
+
"ref": { "tableKey": "ingredients", "type": "components" }
|
|
475
|
+
}
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
`createInsertHistoryTableCfg()` auto-generates the schema for InsertHistory
|
|
479
|
+
tables from any base `TableCfg`.
|
|
480
|
+
|
|
481
|
+
### Validation
|
|
482
|
+
|
|
483
|
+
The `Validate` class performs comprehensive structural validation of RLJSON
|
|
484
|
+
objects:
|
|
485
|
+
|
|
486
|
+
- **Naming**: Table keys, column keys must be valid identifiers
|
|
487
|
+
- **Hashes**: All `_hash` values must match the actual content
|
|
488
|
+
- **References**: All `*Ref` columns must point to existing rows
|
|
489
|
+
- **Trees**: No cycles, all child hashes must resolve, correct node types
|
|
490
|
+
- **Layers**: Component and SliceId tables must exist
|
|
491
|
+
- **Cakes**: Layer tables must exist and share the same slice structure
|
|
492
|
+
- **Buffets**: Referenced items must exist in their declared tables
|
|
493
|
+
|
|
494
|
+
The `BaseValidator` provides an extensible validator framework — multiple
|
|
495
|
+
validators can run in parallel and merge their results.
|
|
496
|
+
|
|
497
|
+
## Edit Protocol
|
|
498
|
+
|
|
499
|
+
### Insert
|
|
500
|
+
|
|
501
|
+
An `Insert` describes a data modification operation:
|
|
502
|
+
|
|
503
|
+
```json
|
|
504
|
+
{
|
|
505
|
+
"route": "/ingredients",
|
|
506
|
+
"command": "add",
|
|
507
|
+
"value": { "name": "butter", "amountUnit": "g" }
|
|
508
|
+
}
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
The `InsertValidator` class validates insert operations — checking that the
|
|
512
|
+
route is valid, the command is supported, and the value is present.
|
|
513
|
+
|
|
514
|
+
### InsertHistory
|
|
515
|
+
|
|
516
|
+
`InsertHistory` tracks every insert operation as an append-only log.
|
|
517
|
+
Each `InsertHistoryRow` contains:
|
|
518
|
+
|
|
519
|
+
| Field | Type | Description |
|
|
520
|
+
| ----------------- | ----------------------- | ----------------------------------------------- |
|
|
521
|
+
| `timeId` | `InsertHistoryTimeId` | Unique row identifier (timestamp + random) |
|
|
522
|
+
| `<table>Ref` | `string` | Hash of the data that was inserted |
|
|
523
|
+
| `route` | `RouteRef` | Route that was modified |
|
|
524
|
+
| `origin` | `ClientId \| Ref` | Who performed the insert (optional) |
|
|
525
|
+
| `previous` | `InsertHistoryTimeId[]` | Causal predecessors — supports merge (optional) |
|
|
526
|
+
| `clientTimestamp` | `number` | Client-side wall-clock timestamp (optional) |
|
|
527
|
+
|
|
528
|
+
The `previous` field enables **causal ordering**: each insert can declare
|
|
529
|
+
which prior inserts it depends on. When multiple clients insert concurrently,
|
|
530
|
+
the result is a DAG (directed acyclic graph) rather than a linear chain,
|
|
531
|
+
supporting merge semantics.
|
|
532
|
+
|
|
533
|
+
### Edits & MultiEdits
|
|
534
|
+
|
|
535
|
+
- **Edit**: A named action with a type and data payload
|
|
536
|
+
- **MultiEdit**: Chains edits into a linked list via `previousRef`, enabling
|
|
537
|
+
undo/redo and conflict resolution
|
|
538
|
+
- **EditHistory**: An append-only log of multi-edits with timestamps
|
|
539
|
+
|
|
540
|
+
### EditHistory
|
|
541
|
+
|
|
542
|
+
`EditHistory` links multi-edits together with `timeId` and `dataRef`:
|
|
543
|
+
|
|
544
|
+
```json
|
|
545
|
+
{
|
|
546
|
+
"editHistory": {
|
|
547
|
+
"_type": "editHistory",
|
|
548
|
+
"_data": [
|
|
549
|
+
{
|
|
550
|
+
"timeId": "1700000000000:AbCd",
|
|
551
|
+
"multiEditRef": "xyz...",
|
|
552
|
+
"dataRef": "abc...",
|
|
553
|
+
"previous": ["1699999999999:ZzZz"]
|
|
554
|
+
}
|
|
555
|
+
]
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
## Routing
|
|
561
|
+
|
|
562
|
+
The `Route` class is the addressing system for all data paths in RLJSON.
|
|
563
|
+
A route identifies a specific location in the data hierarchy.
|
|
564
|
+
|
|
565
|
+
**Flat string format**: `/tableKey@ref/childTableKey`
|
|
566
|
+
|
|
567
|
+
Components of a route segment:
|
|
568
|
+
|
|
569
|
+
| Part | Syntax | Example |
|
|
570
|
+
| ------------- | --------------- | ------------------ |
|
|
571
|
+
| Table key | plain name | `ingredients` |
|
|
572
|
+
| Ref | `@hash` | `@A5d...` |
|
|
573
|
+
| InsertHistory | `@timestamp:id` | `@1700000000:AbCd` |
|
|
574
|
+
| Slice IDs | `(id1,id2)` | `(flour,sugar)` |
|
|
575
|
+
|
|
576
|
+
```javascript
|
|
577
|
+
// Parsing
|
|
578
|
+
const route = Route.fromFlat('/ingredients@A5d.../nutritionalValues');
|
|
579
|
+
|
|
580
|
+
// Navigation
|
|
581
|
+
route.top; // { tableKey: 'ingredients', ingredientsRef: 'A5d...' }
|
|
582
|
+
route.root; // last segment: { tableKey: 'nutritionalValues' }
|
|
583
|
+
route.segment(0); // { tableKey: 'ingredients', ingredientsRef: 'A5d...' }
|
|
584
|
+
route.segment(1); // { tableKey: 'nutritionalValues' }
|
|
585
|
+
route.flat; // '/ingredients@A5d.../nutritionalValues'
|
|
586
|
+
route.isRoot; // false (more than one segment)
|
|
587
|
+
|
|
588
|
+
// Navigating up/down
|
|
589
|
+
route.upper(); // Route with only '/ingredients@A5d...'
|
|
590
|
+
route.deeper(); // Route with only '/nutritionalValues'
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Routes are used by the Connector, Db, and Server to identify which data
|
|
594
|
+
a message refers to.
|
|
595
|
+
|
|
596
|
+
## Sync Protocol
|
|
597
|
+
|
|
598
|
+
The `sync/` module defines wire-protocol types for the messaging hardening
|
|
599
|
+
system. These types are consumed by `@rljson/db` (Connector) and
|
|
600
|
+
`@rljson/server` (Server relay).
|
|
601
|
+
|
|
602
|
+
All fields beyond `o` (origin) and `r` (ref) are optional — existing code
|
|
603
|
+
that sends `{ o, r }` continues to work unchanged. New features activate
|
|
604
|
+
only when the corresponding `SyncConfig` flags are set.
|
|
605
|
+
|
|
606
|
+
### ConnectorPayload
|
|
607
|
+
|
|
608
|
+
The payload transmitted on the wire between Connector and Server:
|
|
609
|
+
|
|
610
|
+
| Field | Type | Concept | Purpose |
|
|
611
|
+
| ------- | ----------------------- | ----------------- | ------------------------------------- |
|
|
612
|
+
| `r` | `string` | existing | The ref being announced |
|
|
613
|
+
| `o` | `string` | existing | Ephemeral origin for self-echo filter |
|
|
614
|
+
| `c` | `ClientId` | Client identity | Stable client id across reconnections |
|
|
615
|
+
| `t` | `number` | Client identity | Client-side wall-clock timestamp (ms) |
|
|
616
|
+
| `seq` | `number` | Predecessor chain | Monotonic counter per (client, route) |
|
|
617
|
+
| `p` | `InsertHistoryTimeId[]` | Predecessor chain | Causal predecessor timeIds |
|
|
618
|
+
| `cksum` | `string` | Acknowledgment | Content checksum for ACK verification |
|
|
619
|
+
|
|
620
|
+
### AckPayload
|
|
621
|
+
|
|
622
|
+
Server → Client acknowledgment that all (or some) receivers successfully
|
|
623
|
+
received and processed a ref:
|
|
624
|
+
|
|
625
|
+
| Field | Type | Description |
|
|
626
|
+
| -------------- | --------- | ----------------------------------- |
|
|
627
|
+
| `r` | `string` | The ref being acknowledged |
|
|
628
|
+
| `ok` | `boolean` | All clients confirmed? |
|
|
629
|
+
| `receivedBy` | `number` | Count of confirming clients |
|
|
630
|
+
| `totalClients` | `number` | Total receiver clients at broadcast |
|
|
631
|
+
|
|
632
|
+
### GapFill
|
|
633
|
+
|
|
634
|
+
When a Connector detects a gap in sequence numbers from a peer, it requests
|
|
635
|
+
the missing refs from the server:
|
|
636
|
+
|
|
637
|
+
- **`GapFillRequest`**: Client → Server — route + `afterSeq` (+ optional
|
|
638
|
+
`afterTimeId`)
|
|
639
|
+
- **`GapFillResponse`**: Server → Client — ordered list of missing
|
|
640
|
+
`ConnectorPayload` entries
|
|
641
|
+
|
|
642
|
+
### SyncConfig
|
|
643
|
+
|
|
644
|
+
Feature flags for hardened sync behavior:
|
|
645
|
+
|
|
646
|
+
| Flag | Concept | Effect when enabled |
|
|
647
|
+
| ----------------------- | ----------------- | ------------------------------------- |
|
|
648
|
+
| `causalOrdering` | Predecessor chain | Attach `seq` + `p` to payloads; |
|
|
649
|
+
| | | detect gaps; request gap-fill |
|
|
650
|
+
| `requireAck` | Acknowledgment | Wait for server ACK after send |
|
|
651
|
+
| `ackTimeoutMs` | Acknowledgment | Timeout before treating ACK as failed |
|
|
652
|
+
| `includeClientIdentity` | Client identity | Attach `c` + `t` to payloads |
|
|
653
|
+
| `maxDedupSetSize` | Memory bounds | Max refs per dedup generation |
|
|
654
|
+
| | | (default 10 000; two-generation |
|
|
655
|
+
| | | eviction caps memory usage) |
|
|
656
|
+
| `bootstrapHeartbeatMs` | Bootstrap | Periodic heartbeat interval (ms); |
|
|
657
|
+
| | | server sends latest ref to all |
|
|
658
|
+
| | | clients at this interval |
|
|
659
|
+
|
|
660
|
+
### SyncEventNames
|
|
661
|
+
|
|
662
|
+
Helper to generate typed socket event names from a route:
|
|
663
|
+
|
|
664
|
+
| Event Name | Direction | Purpose |
|
|
665
|
+
| ---------------------- | --------------- | --------------------------------- |
|
|
666
|
+
| `${route}` | bidirectional | Ref broadcast (existing) |
|
|
667
|
+
| `${route}:ack` | server → client | Delivery acknowledgment |
|
|
668
|
+
| `${route}:ack:client` | client → server | Individual client ACK |
|
|
669
|
+
| `${route}:gapfill:req` | client → server | Request missing refs |
|
|
670
|
+
| `${route}:gapfill:res` | server → client | Supply missing refs |
|
|
671
|
+
| `${route}:bootstrap` | server → client | Latest ref on connect / heartbeat |
|
|
672
|
+
|
|
673
|
+
### ClientId
|
|
674
|
+
|
|
675
|
+
A stable client identity that persists across reconnections. Unlike the
|
|
676
|
+
ephemeral Connector `origin` (which changes on every instantiation), a
|
|
677
|
+
`ClientId` should be generated once and stored for reuse.
|
|
678
|
+
|
|
679
|
+
Format: `"client_"` + 12-character nanoid (e.g. `"client_V1StGXR8_Z5j"`).
|
|
680
|
+
|
|
681
|
+
## Package Structure
|
|
682
|
+
|
|
683
|
+
```
|
|
684
|
+
src/
|
|
685
|
+
├── index.ts # Re-exports everything
|
|
686
|
+
├── rljson.ts # Core Rljson type, RljsonTable, iterators
|
|
687
|
+
├── rljson-indexed.ts # Hash-indexed Rljson for O(1) lookups
|
|
688
|
+
├── typedefs.ts # Base types: Ref, SliceId, TableKey, ContentType
|
|
689
|
+
├── example.ts # Example class with valid/invalid data
|
|
690
|
+
│
|
|
691
|
+
├── content/ # Data model types
|
|
692
|
+
│ ├── buffet.ts # Buffet, BuffetsTable
|
|
693
|
+
│ ├── cake.ts # Cake, CakesTable
|
|
694
|
+
│ ├── components.ts # ComponentsTable
|
|
695
|
+
│ ├── layer.ts # Layer, LayersTable
|
|
696
|
+
│ ├── revision.ts # Revision, RevisionsTable
|
|
697
|
+
│ ├── slice-ids.ts # SliceIds, SliceIdsTable
|
|
698
|
+
│ ├── table-cfg.ts # TableCfg, ColumnCfg, validation
|
|
699
|
+
│ └── tree.ts # TreeNode, TreesTable, treeFromObject
|
|
700
|
+
│
|
|
701
|
+
├── edit/ # Edit protocol
|
|
702
|
+
│ ├── edit.ts # Edit, EditsTable
|
|
703
|
+
│ ├── edit-history.ts # EditHistory, EditHistoryTable
|
|
704
|
+
│ └── multi-edit.ts # MultiEdit, MultiEditsTable
|
|
705
|
+
│
|
|
706
|
+
├── insert/ # Insert operations
|
|
707
|
+
│ ├── insert.ts # Insert type, InsertCommand
|
|
708
|
+
│ └── insert-validator.ts # InsertValidator, validateInsert
|
|
709
|
+
│
|
|
710
|
+
├── insertHistory/ # Insert tracking
|
|
711
|
+
│ └── insertHistory.ts # InsertHistoryRow, InsertHistoryTable
|
|
712
|
+
│
|
|
713
|
+
├── route/ # Data path addressing
|
|
714
|
+
│ └── route.ts # Route class, RouteSegment, RouteRef
|
|
715
|
+
│
|
|
716
|
+
├── sync/ # Sync protocol types
|
|
717
|
+
│ ├── ack-payload.ts # AckPayload
|
|
718
|
+
│ ├── client-id.ts # ClientId, clientId(), isClientId()
|
|
719
|
+
│ ├── connector-payload.ts # ConnectorPayload
|
|
720
|
+
│ ├── gap-fill.ts # GapFillRequest, GapFillResponse
|
|
721
|
+
│ ├── sync-config.ts # SyncConfig
|
|
722
|
+
│ └── sync-events.ts # SyncEventNames, syncEvents()
|
|
723
|
+
│
|
|
724
|
+
├── tools/ # Utilities
|
|
725
|
+
│ ├── object-depth.ts # objectDepth() (internal)
|
|
726
|
+
│ ├── remove-duplicates.ts # removeDuplicates()
|
|
727
|
+
│ └── time-id.ts # TimeId, timeId(), isTimeId()
|
|
728
|
+
│
|
|
729
|
+
├── validate/ # Validation framework
|
|
730
|
+
│ ├── base-validator.ts # BaseValidator, ValidatorInterface
|
|
731
|
+
│ └── validate.ts # Validate class (full structural validator)
|
|
732
|
+
│
|
|
733
|
+
└── example/ # Example data
|
|
734
|
+
└── bakery-example.ts # bakeryExample() — canonical test dataset
|
|
735
|
+
```
|
package/dist/README.md
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# @rljson/rljson
|
|
2
2
|
|
|
3
|
-
The RLJSON data format specification
|
|
3
|
+
The RLJSON data format specification — core types, validation, and sync protocol
|
|
4
|
+
for the RLJSON ecosystem.
|
|
5
|
+
|
|
6
|
+
This package provides:
|
|
7
|
+
|
|
8
|
+
- **Data model types**: Components, SliceIds, Layers, Cakes, Buffets, Trees
|
|
9
|
+
- **Schema system**: TableCfg, column definitions, structural validation
|
|
10
|
+
- **Edit protocol**: Edits, MultiEdits, EditHistory, InsertHistory
|
|
11
|
+
- **Routing**: Route class for addressing data paths
|
|
12
|
+
- **Sync protocol**: ConnectorPayload, AckPayload, GapFill, SyncConfig
|
|
13
|
+
- **Utilities**: TimeId generation, hashing integration, deduplication
|
|
4
14
|
|
|
5
15
|
## Users
|
|
6
16
|
|