@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.
@@ -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/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