@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.
- package/README.architecture.md +25 -10
- package/dist/README.architecture.md +25 -10
- package/dist/io-mem.d.ts +74 -1
- package/dist/io.js +206 -23
- package/dist/io.js.map +1 -1
- package/package.json +3 -3
package/README.architecture.md
CHANGED
|
@@ -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):
|
|
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 {
|
|
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
|
-
|
|
720
|
+
if (removedNullValues) {
|
|
721
|
+
hsh(addedData);
|
|
722
|
+
}
|
|
540
723
|
await this._ioTools.throwWhenTablesDoNotExist(request.data);
|
|
541
|
-
await this.
|
|
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
|
-
|
|
551
|
-
|
|
552
|
-
oldTable._data
|
|
734
|
+
if (!rowIndex.has(hash)) {
|
|
735
|
+
rowIndex.set(hash, item);
|
|
736
|
+
IoMem._insertSortedByHash(oldTable._data, item);
|
|
553
737
|
}
|
|
554
738
|
}
|
|
555
|
-
this.
|
|
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.
|
|
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
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
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 {
|