@rljson/io 0.0.68 → 0.0.70

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.
@@ -18,7 +18,7 @@ found in the LICENSE file in the root of this package.
18
18
 
19
19
  The central abstraction that defines all database operations:
20
20
 
21
- ```
21
+ ```text
22
22
  ┌─────────────────────────────────────────────┐
23
23
  │ Io Interface │
24
24
  ├─────────────────────────────────────────────┤
@@ -42,7 +42,7 @@ The central abstraction that defines all database operations:
42
42
 
43
43
  In-memory implementation using JavaScript objects.
44
44
 
45
- ```
45
+ ```text
46
46
  ┌─────────────────────┐
47
47
  │ IoMem │
48
48
  ├─────────────────────┤
@@ -61,6 +61,21 @@ In-memory implementation using JavaScript objects.
61
61
  - Uses `@rljson/hash` for data hashing and identity
62
62
  - `IsReady` pattern for initialization tracking
63
63
 
64
+ **Performance internals** (no observable API change):
65
+
66
+ - Rows are indexed per table by content hash — write dedup is O(1) and
67
+ `readRows` with a string `_hash` in the where clause resolves through
68
+ the index instead of scanning the table
69
+ - Table data is kept hash-sorted incrementally (binary-searched insert)
70
+ instead of re-sorting the whole table per write
71
+ - Table and global hashes are updated lazily: writes mark tables dirty,
72
+ hashes are recomputed before they become observable (`dump`,
73
+ `dumpTable`, `createOrExtendTable`, re-`init`) — deterministic hashes
74
+ make the refreshed state identical to eager updates
75
+ - The latest table configuration and column keys are cached per table
76
+ (updated on create/extend), so validation does not re-scan all
77
+ configurations per read/write
78
+
64
79
  **Data Structure:**
65
80
 
66
81
  ```typescript
@@ -76,7 +91,7 @@ _mem = {
76
91
 
77
92
  Remote database connection over sockets (Socket.IO compatible).
78
93
 
79
- ```
94
+ ```text
80
95
  ┌──────────────┐ Socket ┌──────────────┐
81
96
  │ IoPeer │◄───────────────────────►│ IoPeerBridge │
82
97
  │ (Client) │ Events/Acks │ (Server) │
@@ -107,7 +122,7 @@ Remote database connection over sockets (Socket.IO compatible).
107
122
 
108
123
  Server-side handler that bridges socket events to Io operations.
109
124
 
110
- ```
125
+ ```text
111
126
  ┌─────────────────────────────────────┐
112
127
  │ IoPeerBridge │
113
128
  ├─────────────────────────────────────┤
@@ -132,7 +147,7 @@ Server-side handler that bridges socket events to Io operations.
132
147
 
133
148
  Aggregates multiple Io instances with priority-based cascading.
134
149
 
135
- ```
150
+ ```text
136
151
  ┌────────────────────────────────────────────┐
137
152
  │ IoMulti │
138
153
  ├────────────────────────────────────────────┤
@@ -182,7 +197,7 @@ IoMultiIo {
182
197
 
183
198
  Provides name mapping between different table name formats.
184
199
 
185
- ```
200
+ ```text
186
201
  ┌──────────────────────────────────────┐
187
202
  │ IoDbNameMapping │
188
203
  ├──────────────────────────────────────┤
@@ -202,7 +217,7 @@ Provides name mapping between different table name formats.
202
217
 
203
218
  Server implementation that combines Socket.IO with Io backends.
204
219
 
205
- ```
220
+ ```text
206
221
  ┌─────────────────────────────────────┐
207
222
  │ IoServer │
208
223
  ├─────────────────────────────────────┤
@@ -230,7 +245,7 @@ Server implementation that combines Socket.IO with Io backends.
230
245
 
231
246
  ### Write Operation
232
247
 
233
- ```
248
+ ```text
234
249
  Client Code
235
250
 
236
251
 
@@ -248,7 +263,7 @@ io.write(data)
248
263
 
249
264
  ### Read Operation (IoMulti Cascade)
250
265
 
251
- ```
266
+ ```text
252
267
  io.readRows({table: 'users', where: {id: 1}})
253
268
 
254
269
 
@@ -293,7 +308,7 @@ return rljson; // Only after loop completes
293
308
 
294
309
  ### Event-Based Protocol
295
310
 
296
- ```
311
+ ```text
297
312
  Client (IoPeer) Server (IoPeerBridge)
298
313
  │ │
299
314
  ├─── emit('readRows', request) ────►│
@@ -18,7 +18,7 @@ found in the LICENSE file in the root of this package.
18
18
 
19
19
  The central abstraction that defines all database operations:
20
20
 
21
- ```
21
+ ```text
22
22
  ┌─────────────────────────────────────────────┐
23
23
  │ Io Interface │
24
24
  ├─────────────────────────────────────────────┤
@@ -42,7 +42,7 @@ The central abstraction that defines all database operations:
42
42
 
43
43
  In-memory implementation using JavaScript objects.
44
44
 
45
- ```
45
+ ```text
46
46
  ┌─────────────────────┐
47
47
  │ IoMem │
48
48
  ├─────────────────────┤
@@ -61,6 +61,21 @@ In-memory implementation using JavaScript objects.
61
61
  - Uses `@rljson/hash` for data hashing and identity
62
62
  - `IsReady` pattern for initialization tracking
63
63
 
64
+ **Performance internals** (no observable API change):
65
+
66
+ - Rows are indexed per table by content hash — write dedup is O(1) and
67
+ `readRows` with a string `_hash` in the where clause resolves through
68
+ the index instead of scanning the table
69
+ - Table data is kept hash-sorted incrementally (binary-searched insert)
70
+ instead of re-sorting the whole table per write
71
+ - Table and global hashes are updated lazily: writes mark tables dirty,
72
+ hashes are recomputed before they become observable (`dump`,
73
+ `dumpTable`, `createOrExtendTable`, re-`init`) — deterministic hashes
74
+ make the refreshed state identical to eager updates
75
+ - The latest table configuration and column keys are cached per table
76
+ (updated on create/extend), so validation does not re-scan all
77
+ configurations per read/write
78
+
64
79
  **Data Structure:**
65
80
 
66
81
  ```typescript
@@ -76,7 +91,7 @@ _mem = {
76
91
 
77
92
  Remote database connection over sockets (Socket.IO compatible).
78
93
 
79
- ```
94
+ ```text
80
95
  ┌──────────────┐ Socket ┌──────────────┐
81
96
  │ IoPeer │◄───────────────────────►│ IoPeerBridge │
82
97
  │ (Client) │ Events/Acks │ (Server) │
@@ -107,7 +122,7 @@ Remote database connection over sockets (Socket.IO compatible).
107
122
 
108
123
  Server-side handler that bridges socket events to Io operations.
109
124
 
110
- ```
125
+ ```text
111
126
  ┌─────────────────────────────────────┐
112
127
  │ IoPeerBridge │
113
128
  ├─────────────────────────────────────┤
@@ -132,7 +147,7 @@ Server-side handler that bridges socket events to Io operations.
132
147
 
133
148
  Aggregates multiple Io instances with priority-based cascading.
134
149
 
135
- ```
150
+ ```text
136
151
  ┌────────────────────────────────────────────┐
137
152
  │ IoMulti │
138
153
  ├────────────────────────────────────────────┤
@@ -182,7 +197,7 @@ IoMultiIo {
182
197
 
183
198
  Provides name mapping between different table name formats.
184
199
 
185
- ```
200
+ ```text
186
201
  ┌──────────────────────────────────────┐
187
202
  │ IoDbNameMapping │
188
203
  ├──────────────────────────────────────┤
@@ -202,7 +217,7 @@ Provides name mapping between different table name formats.
202
217
 
203
218
  Server implementation that combines Socket.IO with Io backends.
204
219
 
205
- ```
220
+ ```text
206
221
  ┌─────────────────────────────────────┐
207
222
  │ IoServer │
208
223
  ├─────────────────────────────────────┤
@@ -230,7 +245,7 @@ Server implementation that combines Socket.IO with Io backends.
230
245
 
231
246
  ### Write Operation
232
247
 
233
- ```
248
+ ```text
234
249
  Client Code
235
250
 
236
251
 
@@ -248,7 +263,7 @@ io.write(data)
248
263
 
249
264
  ### Read Operation (IoMulti Cascade)
250
265
 
251
- ```
266
+ ```text
252
267
  io.readRows({table: 'users', where: {id: 1}})
253
268
 
254
269
 
@@ -293,7 +308,7 @@ return rljson; // Only after loop completes
293
308
 
294
309
  ### Event-Based Protocol
295
310
 
296
- ```
311
+ ```text
297
312
  Client (IoPeer) Server (IoPeerBridge)
298
313
  │ │
299
314
  ├─── emit('readRows', request) ────►│
package/dist/io-mem.d.ts CHANGED
@@ -36,6 +36,79 @@ export declare class IoMem implements Io {
36
36
  private _isReady;
37
37
  private _isOpen;
38
38
  private _mem;
39
+ /**
40
+ * Latest table configuration per table (the one with the most
41
+ * columns). Kept in sync by _createTable/_extendTable so that reads
42
+ * and writes need no repeated scan over all configurations.
43
+ */
44
+ private readonly _latestCfgs;
45
+ /** Column key sets per table, derived from _latestCfgs */
46
+ private readonly _columnKeys;
47
+ /**
48
+ * Per-table index of rows by content hash. Rows are only ever added,
49
+ * never removed, so the index cannot go stale.
50
+ */
51
+ private readonly _rowIndex;
52
+ /**
53
+ * Tables whose table hash (and thus the global hash) is outdated
54
+ * after writes. Hashes are refreshed lazily before they become
55
+ * observable (dump, dumpTable, create/extend) instead of after every
56
+ * single write — row hashes themselves are always up to date.
57
+ */
58
+ private readonly _dirtyTableHashes;
59
+ /**
60
+ * Recomputes the hashes of all dirty tables and the global hash.
61
+ * Produces exactly the state an eager per-write update would have
62
+ * produced (hashes are deterministic over the same data).
63
+ */
64
+ private _refreshHashes;
65
+ /**
66
+ * Returns the row index of a table, building it lazily from the
67
+ * table data on first access.
68
+ * @param table - The table to index
69
+ */
70
+ private _rowIndexFor;
71
+ /**
72
+ * Inserts a row into hash-sorted table data at its sorted position —
73
+ * preserves the order sortTableDataAndUpdateHash establishes without
74
+ * a full re-sort.
75
+ * @param data - The hash-sorted table data
76
+ * @param row - The row to insert
77
+ */
78
+ private static _insertSortedByHash;
79
+ /**
80
+ * The row filter predicate of readRows. Extracted so that the indexed
81
+ * fast path and the full scan share identical semantics.
82
+ * @param row - The row to check
83
+ * @param where - The where clause
84
+ */
85
+ private static _rowMatchesWhere;
86
+ /**
87
+ * Returns the latest table configuration, filling the cache lazily
88
+ * from IoTools (which picks the config with the most columns).
89
+ * @param table - The table to get the configuration for
90
+ */
91
+ private _latestCfg;
92
+ /**
93
+ * Updates the cached latest configuration of a table
94
+ * @param cfg - The new latest configuration
95
+ */
96
+ private _setLatestCfg;
97
+ /**
98
+ * Throws when one of the given columns does not exist in the table.
99
+ * Mirrors IoTools.throwWhenColumnDoesNotExist but uses the cached
100
+ * configuration.
101
+ * @param table - The table to check
102
+ * @param columns - The columns to check
103
+ */
104
+ private _throwWhenColumnDoesNotExist;
105
+ /**
106
+ * Throws when the data does not match the table configurations.
107
+ * Mirrors IoTools.throwWhenTableDataDoesNotMatchCfg but uses the
108
+ * cached configurations.
109
+ * @param data - The data to validate
110
+ */
111
+ private _throwWhenTableDataDoesNotMatchCfg;
39
112
  private _init;
40
113
  private _initTableCfgs;
41
114
  private _updateGlobalHash;
@@ -48,5 +121,5 @@ export declare class IoMem implements Io {
48
121
  private _contentType;
49
122
  private _write;
50
123
  private _readRows;
51
- _removeNullValues(rljson: Rljson): void;
124
+ _removeNullValues(rljson: Rljson): boolean;
52
125
  }
package/dist/io.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { hip, hsh } from "@rljson/hash";
2
2
  import { IsReady } from "@rljson/is-ready";
3
- import { copy, equals, merge } from "@rljson/json";
3
+ import { equals, copy, merge } from "@rljson/json";
4
4
  import { iterateTables, throwOnInvalidTableCfg, validateRljsonAgainstTableCfg, iterateTablesSync } from "@rljson/rljson";
5
5
  class IoDbNameMapping {
6
6
  // The primary key column is always named '_hash'
@@ -443,8 +443,182 @@ class IoMem {
443
443
  _isReady = new IsReady();
444
444
  _isOpen = false;
445
445
  _mem = hip({});
446
+ /**
447
+ * Latest table configuration per table (the one with the most
448
+ * columns). Kept in sync by _createTable/_extendTable so that reads
449
+ * and writes need no repeated scan over all configurations.
450
+ */
451
+ _latestCfgs = /* @__PURE__ */ new Map();
452
+ /** Column key sets per table, derived from _latestCfgs */
453
+ _columnKeys = /* @__PURE__ */ new Map();
454
+ /**
455
+ * Per-table index of rows by content hash. Rows are only ever added,
456
+ * never removed, so the index cannot go stale.
457
+ */
458
+ _rowIndex = /* @__PURE__ */ new Map();
459
+ /**
460
+ * Tables whose table hash (and thus the global hash) is outdated
461
+ * after writes. Hashes are refreshed lazily before they become
462
+ * observable (dump, dumpTable, create/extend) instead of after every
463
+ * single write — row hashes themselves are always up to date.
464
+ */
465
+ _dirtyTableHashes = /* @__PURE__ */ new Set();
466
+ // ...........................................................................
467
+ /**
468
+ * Recomputes the hashes of all dirty tables and the global hash.
469
+ * Produces exactly the state an eager per-write update would have
470
+ * produced (hashes are deterministic over the same data).
471
+ */
472
+ _refreshHashes() {
473
+ if (this._dirtyTableHashes.size === 0) {
474
+ return;
475
+ }
476
+ for (const tableKey of this._dirtyTableHashes) {
477
+ const table = this._mem[tableKey];
478
+ table._hash = "";
479
+ hip(table, {
480
+ updateExistingHashes: false,
481
+ throwOnWrongHashes: false
482
+ });
483
+ }
484
+ this._dirtyTableHashes.clear();
485
+ this._updateGlobalHash();
486
+ }
487
+ // ...........................................................................
488
+ /**
489
+ * Returns the row index of a table, building it lazily from the
490
+ * table data on first access.
491
+ * @param table - The table to index
492
+ */
493
+ _rowIndexFor(table) {
494
+ let index = this._rowIndex.get(table);
495
+ if (!index) {
496
+ index = /* @__PURE__ */ new Map();
497
+ const tableData = this._mem[table]._data;
498
+ for (const row of tableData) {
499
+ index.set(row._hash, row);
500
+ }
501
+ this._rowIndex.set(table, index);
502
+ }
503
+ return index;
504
+ }
505
+ // ...........................................................................
506
+ /**
507
+ * Inserts a row into hash-sorted table data at its sorted position —
508
+ * preserves the order sortTableDataAndUpdateHash establishes without
509
+ * a full re-sort.
510
+ * @param data - The hash-sorted table data
511
+ * @param row - The row to insert
512
+ */
513
+ static _insertSortedByHash(data, row) {
514
+ const hash = row._hash;
515
+ let lo = 0;
516
+ let hi = data.length;
517
+ while (lo < hi) {
518
+ const mid = lo + hi >> 1;
519
+ if (data[mid]._hash < hash) {
520
+ lo = mid + 1;
521
+ } else {
522
+ hi = mid;
523
+ }
524
+ }
525
+ data.splice(lo, 0, row);
526
+ }
527
+ // ...........................................................................
528
+ /**
529
+ * The row filter predicate of readRows. Extracted so that the indexed
530
+ * fast path and the full scan share identical semantics.
531
+ * @param row - The row to check
532
+ * @param where - The where clause
533
+ */
534
+ static _rowMatchesWhere(row, where) {
535
+ for (const column in where) {
536
+ const a = row[column];
537
+ const b = where[column];
538
+ if (b === null && a === void 0) {
539
+ return true;
540
+ }
541
+ if (!equals(a, b)) {
542
+ return false;
543
+ }
544
+ }
545
+ return true;
546
+ }
547
+ // ...........................................................................
548
+ /**
549
+ * Returns the latest table configuration, filling the cache lazily
550
+ * from IoTools (which picks the config with the most columns).
551
+ * @param table - The table to get the configuration for
552
+ */
553
+ async _latestCfg(table) {
554
+ let cfg = this._latestCfgs.get(table);
555
+ if (!cfg) {
556
+ cfg = await this._ioTools.tableCfg(table);
557
+ this._latestCfgs.set(table, cfg);
558
+ }
559
+ return cfg;
560
+ }
561
+ /**
562
+ * Updates the cached latest configuration of a table
563
+ * @param cfg - The new latest configuration
564
+ */
565
+ _setLatestCfg(cfg) {
566
+ this._latestCfgs.set(cfg.key, cfg);
567
+ this._columnKeys.set(cfg.key, new Set(cfg.columns.map((c) => c.key)));
568
+ }
569
+ /**
570
+ * Throws when one of the given columns does not exist in the table.
571
+ * Mirrors IoTools.throwWhenColumnDoesNotExist but uses the cached
572
+ * configuration.
573
+ * @param table - The table to check
574
+ * @param columns - The columns to check
575
+ */
576
+ async _throwWhenColumnDoesNotExist(table, columns) {
577
+ let columnKeys = this._columnKeys.get(table);
578
+ if (!columnKeys) {
579
+ const cfg = await this._latestCfg(table);
580
+ columnKeys = new Set(cfg.columns.map((c) => c.key));
581
+ this._columnKeys.set(table, columnKeys);
582
+ }
583
+ const missingColumns = columns.filter((column) => !columnKeys.has(column));
584
+ if (missingColumns.length > 0) {
585
+ throw new Error(
586
+ `The following columns do not exist in table "${table}": ${missingColumns.join(
587
+ ", "
588
+ )}.`
589
+ );
590
+ }
591
+ }
592
+ /**
593
+ * Throws when the data does not match the table configurations.
594
+ * Mirrors IoTools.throwWhenTableDataDoesNotMatchCfg but uses the
595
+ * cached configurations.
596
+ * @param data - The data to validate
597
+ */
598
+ async _throwWhenTableDataDoesNotMatchCfg(data) {
599
+ const errors = [];
600
+ for (const tableKey of Object.keys(data)) {
601
+ const table = data[tableKey];
602
+ if (typeof table !== "object" || !Array.isArray(table?._data)) continue;
603
+ if (table._type === "tableCfgs") continue;
604
+ const tableCfg = await this._latestCfg(tableKey);
605
+ errors.push(...validateRljsonAgainstTableCfg(table._data, tableCfg));
606
+ }
607
+ if (errors.length > 0) {
608
+ throw new Error(
609
+ `Table data does not match the configuration.
610
+
611
+ Errors:
612
+ ${errors.map((e) => `- ${e}`).join("\n")}`
613
+ );
614
+ }
615
+ }
446
616
  // ...........................................................................
447
617
  async _init() {
618
+ this._refreshHashes();
619
+ this._rowIndex.clear();
620
+ this._latestCfgs.clear();
621
+ this._columnKeys.clear();
448
622
  this._ioTools = new IoTools(this);
449
623
  this._initTableCfgs();
450
624
  this._updateGlobalHash();
@@ -476,6 +650,7 @@ class IoMem {
476
650
  }
477
651
  // ...........................................................................
478
652
  async _createOrExtendTable(request) {
653
+ this._refreshHashes();
479
654
  const tableCfg = request.tableCfg;
480
655
  await this._ioTools.throwWhenTableIsNotCompatible(tableCfg);
481
656
  const { key } = tableCfg;
@@ -492,6 +667,8 @@ class IoMem {
492
667
  newConfig = hsh(newConfig);
493
668
  this._mem.tableCfgs._data.push(newConfig);
494
669
  this._ioTools.sortTableDataAndUpdateHash(this._mem.tableCfgs);
670
+ this._setLatestCfg(newConfig);
671
+ this._rowIndex.get("tableCfgs")?.set(newConfig._hash, newConfig);
495
672
  const table = {
496
673
  _type: newConfig.type,
497
674
  _data: [],
@@ -509,6 +686,8 @@ class IoMem {
509
686
  newConfig = hsh(newConfig);
510
687
  this._mem.tableCfgs._data.push(newConfig);
511
688
  this._ioTools.sortTableDataAndUpdateHash(this._mem.tableCfgs);
689
+ this._setLatestCfg(newConfig);
690
+ this._rowIndex.get("tableCfgs")?.set(newConfig._hash, newConfig);
512
691
  const table = this._mem[newConfig.key];
513
692
  table._tableCfg = newConfig._hash;
514
693
  this._updateTableHash(newConfig.key);
@@ -516,11 +695,13 @@ class IoMem {
516
695
  }
517
696
  // ...........................................................................
518
697
  async _dump() {
698
+ this._refreshHashes();
519
699
  return copy(this._mem);
520
700
  }
521
701
  // ...........................................................................
522
702
  async _dumpTable(request) {
523
703
  await this._ioTools.throwWhenTableDoesNotExist(request.table);
704
+ this._refreshHashes();
524
705
  const table = this._mem[request.table];
525
706
  return {
526
707
  [request.table]: copy(table)
@@ -534,49 +715,48 @@ class IoMem {
534
715
  // ...........................................................................
535
716
  async _write(request) {
536
717
  const addedData = hsh(request.data);
537
- this._removeNullValues(addedData);
718
+ const removedNullValues = this._removeNullValues(addedData);
538
719
  const tables = Object.keys(addedData);
539
- hsh(addedData);
720
+ if (removedNullValues) {
721
+ hsh(addedData);
722
+ }
540
723
  await this._ioTools.throwWhenTablesDoNotExist(request.data);
541
- await this._ioTools.throwWhenTableDataDoesNotMatchCfg(request.data);
724
+ await this._throwWhenTableDataDoesNotMatchCfg(request.data);
542
725
  for (const table of tables) {
543
726
  if (table.startsWith("_")) {
544
727
  continue;
545
728
  }
546
729
  const oldTable = this._mem[table];
547
730
  const newTable = addedData[table];
731
+ const rowIndex = this._rowIndexFor(table);
548
732
  for (const item of newTable._data) {
549
733
  const hash = item._hash;
550
- const exists = oldTable._data.find((i) => i._hash === hash);
551
- if (!exists) {
552
- oldTable._data.push(item);
734
+ if (!rowIndex.has(hash)) {
735
+ rowIndex.set(hash, item);
736
+ IoMem._insertSortedByHash(oldTable._data, item);
553
737
  }
554
738
  }
555
- this._ioTools.sortTableDataAndUpdateHash(oldTable);
739
+ this._dirtyTableHashes.add(table);
556
740
  }
557
- this._updateGlobalHash();
558
741
  }
559
742
  // ...........................................................................
560
743
  async _readRows(request) {
561
744
  await this._ioTools.throwWhenTableDoesNotExist(request.table);
562
- await this._ioTools.throwWhenColumnDoesNotExist(
745
+ await this._throwWhenColumnDoesNotExist(
563
746
  request.table,
564
747
  Object.keys(request.where)
565
748
  );
566
749
  const table = this._mem[request.table];
567
- const tableDataFiltered = table._data.filter((row) => {
568
- for (const column in request.where) {
569
- const a = row[column];
570
- const b = request.where[column];
571
- if (b === null && a === void 0) {
572
- return true;
573
- }
574
- if (!equals(a, b)) {
575
- return false;
576
- }
577
- }
578
- return true;
579
- });
750
+ const whereHash = request.where["_hash"];
751
+ let tableDataFiltered;
752
+ if (typeof whereHash === "string") {
753
+ const row = this._rowIndexFor(request.table).get(whereHash);
754
+ tableDataFiltered = row && IoMem._rowMatchesWhere(row, request.where) ? [row] : [];
755
+ } else {
756
+ tableDataFiltered = table._data.filter(
757
+ (row) => IoMem._rowMatchesWhere(row, request.where)
758
+ );
759
+ }
580
760
  const tableFiltered = {
581
761
  _type: table._type,
582
762
  _data: tableDataFiltered
@@ -588,16 +768,19 @@ class IoMem {
588
768
  return result;
589
769
  }
590
770
  _removeNullValues(rljson) {
771
+ let removedAny = false;
591
772
  iterateTablesSync(rljson, (table) => {
592
773
  const data = rljson[table]._data;
593
774
  for (const row of data) {
594
775
  for (const key in row) {
595
776
  if (row[key] === null) {
596
777
  delete row[key];
778
+ removedAny = true;
597
779
  }
598
780
  }
599
781
  }
600
782
  });
783
+ return removedAny;
601
784
  }
602
785
  }
603
786
  class PeerSocketMock {