@heswell/vuu-table 1.0.0
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.md +40 -0
- package/index.js +350 -0
- package/package.json +25 -0
- package/src/JoinTable.d.ts +54 -0
- package/src/Table.d.ts +72 -0
- package/src/index.d.ts +3 -0
- package/src/types.d.ts +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# `@heswell/vuu-table`
|
|
2
|
+
|
|
3
|
+
Runtime-agnostic Vuu table store. It runs unchanged in the browser or on a
|
|
4
|
+
server (Bun, Node), and is the data layer under `@heswell/vuu-viewport`.
|
|
5
|
+
|
|
6
|
+
| Export | Use |
|
|
7
|
+
| ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
8
|
+
| `Table` | Keyed row store built from a Vuu `TableSchema`: insert, upsert, update, delete, with row created/updated timestamps. Notifies listeners of every change. |
|
|
9
|
+
| `JoinTable` | A materialized join of two tables (left outer by default, or `joinType: "inner"`). Joins can be chained. |
|
|
10
|
+
| `RowSource`, `TableListener`, `VuuDataRow`, ... | The types an engine uses to consume a table. |
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { Table } from "@heswell/vuu-table";
|
|
14
|
+
|
|
15
|
+
const prices = new Table({
|
|
16
|
+
table: { module: "SIMUL", table: "prices" },
|
|
17
|
+
key: "ric",
|
|
18
|
+
columns: [
|
|
19
|
+
{ name: "ric", serverDataType: "string" },
|
|
20
|
+
{ name: "bid", serverDataType: "double" },
|
|
21
|
+
],
|
|
22
|
+
});
|
|
23
|
+
prices.upsert(["VOD.L", 101.5]);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Peer dependencies: `@vuu-ui/vuu-data-types` and `@vuu-ui/vuu-protocol-types`
|
|
27
|
+
(type-only). See
|
|
28
|
+
[docs/design/data-engine.md](https://github.com/heswell/vuu-websocket/blob/main/docs/design/data-engine.md)
|
|
29
|
+
for the design.
|
|
30
|
+
|
|
31
|
+
## Publishing
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm run build:packages -- --package @heswell/vuu-table
|
|
35
|
+
npm run pub -- --package=@heswell/vuu-table --dry-run
|
|
36
|
+
npm run pub -- --package=@heswell/vuu-table [--tag alpha]
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The build is written to `dist/vuu-table`. Publish `@heswell/vuu-table` before
|
|
40
|
+
`@heswell/vuu-viewport`, which depends on it.
|
package/index.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
function buildColumnMap(columns) {
|
|
2
|
+
const map = {};
|
|
3
|
+
for(let i = 0; i < columns.length; i++){
|
|
4
|
+
const column = columns[i];
|
|
5
|
+
if ("string" == typeof column) map[column] = i;
|
|
6
|
+
else if ("number" == typeof column.key) map[column.name] = column.key;
|
|
7
|
+
else map[column.name] = i;
|
|
8
|
+
}
|
|
9
|
+
return map;
|
|
10
|
+
}
|
|
11
|
+
const NO_LISTENERS = [];
|
|
12
|
+
class Table {
|
|
13
|
+
columnMap;
|
|
14
|
+
indexOfKeyField;
|
|
15
|
+
schema;
|
|
16
|
+
#index = new Map();
|
|
17
|
+
#listeners = NO_LISTENERS;
|
|
18
|
+
#nextSeq = 0;
|
|
19
|
+
#rows = [];
|
|
20
|
+
#seq = [];
|
|
21
|
+
#indexOfCreatedTs;
|
|
22
|
+
#indexOfUpdatedTs;
|
|
23
|
+
constructor(schemaOrDefinition){
|
|
24
|
+
const schema = "schema" in schemaOrDefinition ? schemaOrDefinition.schema : schemaOrDefinition;
|
|
25
|
+
this.schema = schema;
|
|
26
|
+
this.columnMap = buildColumnMap(schema.columns);
|
|
27
|
+
this.indexOfKeyField = this.columnMap[schema.key];
|
|
28
|
+
if (void 0 === this.indexOfKeyField) throw Error(`[Table] key column ${schema.key} not found in schema for ${schema.table.table}`);
|
|
29
|
+
this.#indexOfCreatedTs = this.columnMap.vuuCreatedTimestamp;
|
|
30
|
+
this.#indexOfUpdatedTs = this.columnMap.vuuUpdatedTimestamp;
|
|
31
|
+
}
|
|
32
|
+
get columns() {
|
|
33
|
+
return this.schema.columns;
|
|
34
|
+
}
|
|
35
|
+
get name() {
|
|
36
|
+
return this.schema.table.table;
|
|
37
|
+
}
|
|
38
|
+
get primaryKey() {
|
|
39
|
+
return this.schema.key;
|
|
40
|
+
}
|
|
41
|
+
get rowCount() {
|
|
42
|
+
return this.#rows.length;
|
|
43
|
+
}
|
|
44
|
+
get rows() {
|
|
45
|
+
return this.#rows;
|
|
46
|
+
}
|
|
47
|
+
get keys() {
|
|
48
|
+
return this.#index.keys();
|
|
49
|
+
}
|
|
50
|
+
addListener(listener) {
|
|
51
|
+
this.#listeners = this.#listeners === NO_LISTENERS ? [
|
|
52
|
+
listener
|
|
53
|
+
] : this.#listeners.concat(listener);
|
|
54
|
+
}
|
|
55
|
+
removeListener(listener) {
|
|
56
|
+
const listeners = this.#listeners.filter((l)=>l !== listener);
|
|
57
|
+
this.#listeners = 0 === listeners.length ? NO_LISTENERS : listeners;
|
|
58
|
+
}
|
|
59
|
+
get listenerCount() {
|
|
60
|
+
return this.#listeners.length;
|
|
61
|
+
}
|
|
62
|
+
seqAt(rowIdx) {
|
|
63
|
+
return this.#seq[rowIdx];
|
|
64
|
+
}
|
|
65
|
+
get seq() {
|
|
66
|
+
return this.#seq;
|
|
67
|
+
}
|
|
68
|
+
rowAt(rowIdx) {
|
|
69
|
+
return this.#rows[rowIdx];
|
|
70
|
+
}
|
|
71
|
+
rowIndexAtKey = (key)=>this.#index.get(key) ?? -1;
|
|
72
|
+
hasKey(key) {
|
|
73
|
+
return this.#index.has(key);
|
|
74
|
+
}
|
|
75
|
+
getRowAtKey(key, throwIfMissing = true) {
|
|
76
|
+
const rowIdx = this.#index.get(key);
|
|
77
|
+
if (void 0 !== rowIdx) return this.#rows[rowIdx];
|
|
78
|
+
if (throwIfMissing) throw Error(`Table getRowAtKey, no row at key ${key}`);
|
|
79
|
+
}
|
|
80
|
+
insert(row, _emitEvent = true) {
|
|
81
|
+
const key = String(row[this.indexOfKeyField]);
|
|
82
|
+
const existingIdx = this.#index.get(key);
|
|
83
|
+
if (void 0 !== existingIdx) {
|
|
84
|
+
this.update(existingIdx, row);
|
|
85
|
+
return existingIdx;
|
|
86
|
+
}
|
|
87
|
+
const createdTs = this.#indexOfCreatedTs;
|
|
88
|
+
if (void 0 !== createdTs && !row[createdTs]) {
|
|
89
|
+
const ts = Date.now();
|
|
90
|
+
row[createdTs] = ts;
|
|
91
|
+
if (void 0 !== this.#indexOfUpdatedTs) row[this.#indexOfUpdatedTs] = ts;
|
|
92
|
+
}
|
|
93
|
+
const rowIdx = this.#rows.length;
|
|
94
|
+
this.#rows.push(row);
|
|
95
|
+
this.#seq.push(this.#nextSeq++);
|
|
96
|
+
this.#index.set(key, rowIdx);
|
|
97
|
+
const listeners = this.#listeners;
|
|
98
|
+
for(let i = 0; i < listeners.length; i++)listeners[i].onInsert?.(rowIdx, row);
|
|
99
|
+
return rowIdx;
|
|
100
|
+
}
|
|
101
|
+
update(rowIdx, row, _columnName) {
|
|
102
|
+
const previous = this.#rows[rowIdx];
|
|
103
|
+
if (void 0 === previous) throw Error(`[Table] update, no row at index ${rowIdx}`);
|
|
104
|
+
if (void 0 !== this.#indexOfUpdatedTs) row[this.#indexOfUpdatedTs] = Date.now();
|
|
105
|
+
this.#rows[rowIdx] = row;
|
|
106
|
+
const listeners = this.#listeners;
|
|
107
|
+
for(let i = 0; i < listeners.length; i++)listeners[i].onUpdate?.(rowIdx, row, previous);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
updateByKey(key, values) {
|
|
111
|
+
const rowIdx = this.#index.get(key);
|
|
112
|
+
if (void 0 === rowIdx) throw Error(`[Table] updateByKey, no row at key ${key}`);
|
|
113
|
+
const row = this.#rows[rowIdx].slice();
|
|
114
|
+
for(const columnName in values){
|
|
115
|
+
const colIdx = this.columnMap[columnName];
|
|
116
|
+
if (void 0 !== colIdx) row[colIdx] = values[columnName];
|
|
117
|
+
}
|
|
118
|
+
return this.update(rowIdx, row);
|
|
119
|
+
}
|
|
120
|
+
upsert(row, emitEvent = true) {
|
|
121
|
+
const key = String(row[this.indexOfKeyField]);
|
|
122
|
+
const rowIdx = this.#index.get(key);
|
|
123
|
+
if (void 0 === rowIdx) this.insert(row, emitEvent);
|
|
124
|
+
else this.update(rowIdx, row);
|
|
125
|
+
}
|
|
126
|
+
delete(key) {
|
|
127
|
+
const rowIdx = this.#index.get(key);
|
|
128
|
+
if (void 0 === rowIdx) return false;
|
|
129
|
+
const rows = this.#rows;
|
|
130
|
+
const seq = this.#seq;
|
|
131
|
+
const row = rows[rowIdx];
|
|
132
|
+
const lastIdx = rows.length - 1;
|
|
133
|
+
let movedFromIdx = -1;
|
|
134
|
+
this.#index.delete(key);
|
|
135
|
+
if (rowIdx !== lastIdx) {
|
|
136
|
+
const lastRow = rows[lastIdx];
|
|
137
|
+
rows[rowIdx] = lastRow;
|
|
138
|
+
seq[rowIdx] = seq[lastIdx];
|
|
139
|
+
this.#index.set(String(lastRow[this.indexOfKeyField]), rowIdx);
|
|
140
|
+
movedFromIdx = lastIdx;
|
|
141
|
+
}
|
|
142
|
+
rows.pop();
|
|
143
|
+
seq.pop();
|
|
144
|
+
const listeners = this.#listeners;
|
|
145
|
+
for(let i = 0; i < listeners.length; i++)listeners[i].onDelete?.(rowIdx, row, movedFromIdx);
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
clear() {
|
|
149
|
+
this.#rows.length = 0;
|
|
150
|
+
this.#seq.length = 0;
|
|
151
|
+
this.#index.clear();
|
|
152
|
+
const listeners = this.#listeners;
|
|
153
|
+
for(let i = 0; i < listeners.length; i++)listeners[i].onClear?.();
|
|
154
|
+
}
|
|
155
|
+
pullRowAsArray(key, columns) {
|
|
156
|
+
const row = this.getRowAtKey(key, true);
|
|
157
|
+
return columns.map((column)=>row[this.columnMap[column.name]]);
|
|
158
|
+
}
|
|
159
|
+
getUniqueValuesForColumn(column, pattern) {
|
|
160
|
+
const colIdx = this.columnMap[column];
|
|
161
|
+
if (void 0 === colIdx) throw Error(`[Table] getUniqueValuesForColumn no column ${column}`);
|
|
162
|
+
const set = new Set();
|
|
163
|
+
const lowercasePattern = pattern?.toLowerCase();
|
|
164
|
+
for (const row of this.#rows){
|
|
165
|
+
const value = row[colIdx];
|
|
166
|
+
if (null == value) continue;
|
|
167
|
+
const strValue = String(value);
|
|
168
|
+
if (void 0 === lowercasePattern || strValue.toLowerCase().startsWith(lowercasePattern)) set.add(strValue);
|
|
169
|
+
}
|
|
170
|
+
return Array.from(set).sort();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const NO_KEYS = new Set();
|
|
174
|
+
class JoinTable extends Table {
|
|
175
|
+
baseTable;
|
|
176
|
+
joinTable;
|
|
177
|
+
joinType;
|
|
178
|
+
#inner;
|
|
179
|
+
#leftIdx;
|
|
180
|
+
#rightIdx;
|
|
181
|
+
#rightIsKey;
|
|
182
|
+
#sources;
|
|
183
|
+
#sourceIndices;
|
|
184
|
+
#baseKeysByJoinValue = new Map();
|
|
185
|
+
#rightKeyByJoinValue = new Map();
|
|
186
|
+
#baseListener;
|
|
187
|
+
#rightListener;
|
|
188
|
+
constructor({ schema, baseTable, joinTable, leftColumn, rightColumn, joinType = "leftOuter" }){
|
|
189
|
+
super(schema);
|
|
190
|
+
this.baseTable = baseTable;
|
|
191
|
+
this.joinTable = joinTable;
|
|
192
|
+
this.joinType = joinType;
|
|
193
|
+
this.#inner = "inner" === joinType;
|
|
194
|
+
const leftIdx = baseTable.columnMap[leftColumn];
|
|
195
|
+
const rightIdx = joinTable.columnMap[rightColumn];
|
|
196
|
+
if (void 0 === leftIdx) throw Error(`[JoinTable] no column ${leftColumn} in ${baseTable.name}`);
|
|
197
|
+
if (void 0 === rightIdx) throw Error(`[JoinTable] no column ${rightColumn} in ${joinTable.name}`);
|
|
198
|
+
this.#leftIdx = leftIdx;
|
|
199
|
+
this.#rightIdx = rightIdx;
|
|
200
|
+
this.#rightIsKey = rightIdx === joinTable.indexOfKeyField;
|
|
201
|
+
this.#sources = [];
|
|
202
|
+
this.#sourceIndices = [];
|
|
203
|
+
for (const { name } of schema.columns){
|
|
204
|
+
const baseIdx = baseTable.columnMap[name];
|
|
205
|
+
if (void 0 !== baseIdx) {
|
|
206
|
+
this.#sources.push(0);
|
|
207
|
+
this.#sourceIndices.push(baseIdx);
|
|
208
|
+
} else {
|
|
209
|
+
const joinIdx = joinTable.columnMap[name];
|
|
210
|
+
this.#sources.push(1);
|
|
211
|
+
this.#sourceIndices.push(joinIdx ?? -1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (!this.#rightIsKey) for (const row of joinTable.rows)this.indexRightRow(row);
|
|
215
|
+
const baseKeyIdx = baseTable.indexOfKeyField;
|
|
216
|
+
for (const row of baseTable.rows){
|
|
217
|
+
this.indexBaseRow(row);
|
|
218
|
+
this.sync(String(row[baseKeyIdx]), row);
|
|
219
|
+
}
|
|
220
|
+
this.#baseListener = {
|
|
221
|
+
onInsert: (_, row)=>{
|
|
222
|
+
this.indexBaseRow(row);
|
|
223
|
+
this.sync(String(row[baseKeyIdx]), row);
|
|
224
|
+
},
|
|
225
|
+
onUpdate: (_, row, previous)=>{
|
|
226
|
+
const key = String(row[baseKeyIdx]);
|
|
227
|
+
if (previous !== row) {
|
|
228
|
+
const prevJoinValue = previous[this.#leftIdx];
|
|
229
|
+
if (prevJoinValue !== row[this.#leftIdx]) {
|
|
230
|
+
this.unindexBaseRow(key, prevJoinValue);
|
|
231
|
+
this.indexBaseRow(row);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
this.sync(key, row);
|
|
235
|
+
},
|
|
236
|
+
onDelete: (_, row)=>{
|
|
237
|
+
const key = String(row[baseKeyIdx]);
|
|
238
|
+
this.unindexBaseRow(key, row[this.#leftIdx]);
|
|
239
|
+
if (-1 !== this.rowIndexAtKey(key)) super.delete(key);
|
|
240
|
+
},
|
|
241
|
+
onClear: ()=>{
|
|
242
|
+
this.#baseKeysByJoinValue.clear();
|
|
243
|
+
super.clear();
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
this.#rightListener = {
|
|
247
|
+
onInsert: (_, row)=>{
|
|
248
|
+
if (!this.#rightIsKey) this.indexRightRow(row);
|
|
249
|
+
this.refreshForJoinValue(row[this.#rightIdx]);
|
|
250
|
+
},
|
|
251
|
+
onUpdate: (_, row, previous)=>{
|
|
252
|
+
const joinValue = row[this.#rightIdx];
|
|
253
|
+
if (previous !== row && previous[this.#rightIdx] !== joinValue) {
|
|
254
|
+
if (!this.#rightIsKey) {
|
|
255
|
+
this.unindexRightRow(previous);
|
|
256
|
+
this.indexRightRow(row);
|
|
257
|
+
}
|
|
258
|
+
this.refreshForJoinValue(previous[this.#rightIdx]);
|
|
259
|
+
}
|
|
260
|
+
this.refreshForJoinValue(joinValue);
|
|
261
|
+
},
|
|
262
|
+
onDelete: (_, row)=>{
|
|
263
|
+
if (!this.#rightIsKey) this.unindexRightRow(row);
|
|
264
|
+
this.refreshForJoinValue(row[this.#rightIdx]);
|
|
265
|
+
},
|
|
266
|
+
onClear: ()=>{
|
|
267
|
+
this.#rightKeyByJoinValue.clear();
|
|
268
|
+
if (this.#inner) super.clear();
|
|
269
|
+
else for (const row of this.baseTable.rows)this.sync(String(row[baseKeyIdx]), row);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
baseTable.addListener(this.#baseListener);
|
|
273
|
+
joinTable.addListener(this.#rightListener);
|
|
274
|
+
}
|
|
275
|
+
destroy() {
|
|
276
|
+
this.baseTable.removeListener(this.#baseListener);
|
|
277
|
+
this.joinTable.removeListener(this.#rightListener);
|
|
278
|
+
}
|
|
279
|
+
findRightRow(joinValue) {
|
|
280
|
+
if (null == joinValue) return;
|
|
281
|
+
if (this.#rightIsKey) return this.joinTable.getRowAtKey(String(joinValue), false);
|
|
282
|
+
{
|
|
283
|
+
const rightKey = this.#rightKeyByJoinValue.get(joinValue);
|
|
284
|
+
return void 0 === rightKey ? void 0 : this.joinTable.getRowAtKey(rightKey, false);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
joinRow(baseRow, rightRow) {
|
|
288
|
+
const sources = this.#sources;
|
|
289
|
+
const indices = this.#sourceIndices;
|
|
290
|
+
const len = sources.length;
|
|
291
|
+
const row = new Array(len);
|
|
292
|
+
for(let i = 0; i < len; i++){
|
|
293
|
+
const idx = indices[i];
|
|
294
|
+
if (0 === sources[i]) row[i] = baseRow[idx];
|
|
295
|
+
else row[i] = void 0 === rightRow || -1 === idx ? null : rightRow[idx];
|
|
296
|
+
}
|
|
297
|
+
return row;
|
|
298
|
+
}
|
|
299
|
+
sync(key, baseRow) {
|
|
300
|
+
const rightRow = this.findRightRow(baseRow[this.#leftIdx]);
|
|
301
|
+
const rowIdx = this.rowIndexAtKey(key);
|
|
302
|
+
if (void 0 === rightRow && this.#inner) {
|
|
303
|
+
if (-1 !== rowIdx) super.delete(key);
|
|
304
|
+
} else if (-1 === rowIdx) super.insert(this.joinRow(baseRow, rightRow));
|
|
305
|
+
else super.update(rowIdx, this.joinRow(baseRow, rightRow));
|
|
306
|
+
}
|
|
307
|
+
refreshForJoinValue(joinValue) {
|
|
308
|
+
const keys = this.#baseKeysByJoinValue.get(joinValue) ?? NO_KEYS;
|
|
309
|
+
for (const key of keys){
|
|
310
|
+
const baseRow = this.baseTable.getRowAtKey(key, false);
|
|
311
|
+
if (baseRow) this.sync(key, baseRow);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
indexBaseRow(row) {
|
|
315
|
+
const joinValue = row[this.#leftIdx];
|
|
316
|
+
const key = String(row[this.baseTable.indexOfKeyField]);
|
|
317
|
+
let keys = this.#baseKeysByJoinValue.get(joinValue);
|
|
318
|
+
if (void 0 === keys) this.#baseKeysByJoinValue.set(joinValue, keys = new Set());
|
|
319
|
+
keys.add(key);
|
|
320
|
+
}
|
|
321
|
+
unindexBaseRow(key, joinValue) {
|
|
322
|
+
const keys = this.#baseKeysByJoinValue.get(joinValue);
|
|
323
|
+
if (keys) {
|
|
324
|
+
keys.delete(key);
|
|
325
|
+
if (0 === keys.size) this.#baseKeysByJoinValue.delete(joinValue);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
indexRightRow(row) {
|
|
329
|
+
const joinValue = row[this.#rightIdx];
|
|
330
|
+
if (!this.#rightKeyByJoinValue.has(joinValue)) this.#rightKeyByJoinValue.set(joinValue, String(row[this.joinTable.indexOfKeyField]));
|
|
331
|
+
}
|
|
332
|
+
unindexRightRow(row) {
|
|
333
|
+
const joinValue = row[this.#rightIdx];
|
|
334
|
+
const key = String(row[this.joinTable.indexOfKeyField]);
|
|
335
|
+
if (this.#rightKeyByJoinValue.get(joinValue) === key) {
|
|
336
|
+
this.#rightKeyByJoinValue.delete(joinValue);
|
|
337
|
+
for (const r of this.joinTable.rows)if (r[this.#rightIdx] === joinValue && String(r[this.joinTable.indexOfKeyField]) !== key) {
|
|
338
|
+
this.#rightKeyByJoinValue.set(joinValue, String(r[this.joinTable.indexOfKeyField]));
|
|
339
|
+
break;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
insert(_row) {
|
|
344
|
+
throw Error(`[JoinTable] ${this.name} is read only`);
|
|
345
|
+
}
|
|
346
|
+
upsert(_row) {
|
|
347
|
+
throw Error(`[JoinTable] ${this.name} is read only`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
export { JoinTable, Table, buildColumnMap };
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@heswell/vuu-table",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Runtime agnostic (browser or server) Vuu table store, including join tables",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.js",
|
|
7
|
+
"module": "./index.js",
|
|
8
|
+
"types": "./src/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.d.ts",
|
|
12
|
+
"import": "./index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"author": "heswell",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"peerDependencies": {
|
|
22
|
+
"@vuu-ui/vuu-data-types": "^3.4.0",
|
|
23
|
+
"@vuu-ui/vuu-protocol-types": "^3.4.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Table } from "./Table.js";
|
|
2
|
+
import type { TableSchema, VuuDataRow } from "./types.js";
|
|
3
|
+
export interface JoinTableConfig {
|
|
4
|
+
/**
|
|
5
|
+
* schema of the join table. Key must be the key of the base table.
|
|
6
|
+
* Column order defines the layout of joined rows.
|
|
7
|
+
*/
|
|
8
|
+
schema: TableSchema;
|
|
9
|
+
baseTable: Table;
|
|
10
|
+
joinTable: Table;
|
|
11
|
+
/** column in base table holding the join value */
|
|
12
|
+
leftColumn: string;
|
|
13
|
+
/** column in join (right) table matched against leftColumn */
|
|
14
|
+
rightColumn: string;
|
|
15
|
+
/**
|
|
16
|
+
* leftOuter (default): every base row is present, right columns are null
|
|
17
|
+
* when there is no matching right row.
|
|
18
|
+
* inner: base rows are present only while a matching right row exists.
|
|
19
|
+
*/
|
|
20
|
+
joinType?: JoinType;
|
|
21
|
+
}
|
|
22
|
+
export type JoinType = "leftOuter" | "inner";
|
|
23
|
+
/**
|
|
24
|
+
* Materialized left outer (or inner) join of two tables. Joined rows are maintained
|
|
25
|
+
* incrementally as either source table changes, so a JoinTable is itself a
|
|
26
|
+
* regular Table that viewports can sort, filter and group with no join cost
|
|
27
|
+
* at query time.
|
|
28
|
+
*
|
|
29
|
+
* Base table columns take precedence when a column name exists in both
|
|
30
|
+
* tables.
|
|
31
|
+
*/
|
|
32
|
+
export declare class JoinTable extends Table {
|
|
33
|
+
#private;
|
|
34
|
+
readonly baseTable: Table;
|
|
35
|
+
readonly joinTable: Table;
|
|
36
|
+
readonly joinType: JoinType;
|
|
37
|
+
constructor({ schema, baseTable, joinTable, leftColumn, rightColumn, joinType, }: JoinTableConfig);
|
|
38
|
+
destroy(): void;
|
|
39
|
+
private findRightRow;
|
|
40
|
+
private joinRow;
|
|
41
|
+
/**
|
|
42
|
+
* Bring the joined row for a base row up to date: insert, update or, for an
|
|
43
|
+
* inner join without a matching right row, delete it.
|
|
44
|
+
*/
|
|
45
|
+
private sync;
|
|
46
|
+
private refreshForJoinValue;
|
|
47
|
+
private indexBaseRow;
|
|
48
|
+
private unindexBaseRow;
|
|
49
|
+
private indexRightRow;
|
|
50
|
+
private unindexRightRow;
|
|
51
|
+
/** Join tables are read-only, rows are derived from the source tables */
|
|
52
|
+
insert(_row: VuuDataRow): number;
|
|
53
|
+
upsert(_row: VuuDataRow): void;
|
|
54
|
+
}
|
package/src/Table.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ColumnMap, RowSource, TableListener, TableSchema, VuuDataRow, VuuRowDataItemType } from "./types.js";
|
|
2
|
+
export type ColumnDescriptor = string | {
|
|
3
|
+
name: string;
|
|
4
|
+
key?: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function buildColumnMap(columns: readonly ColumnDescriptor[]): ColumnMap;
|
|
7
|
+
export interface TableDefinition {
|
|
8
|
+
schema: TableSchema;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* In-memory, runtime agnostic, keyed row store.
|
|
12
|
+
*
|
|
13
|
+
* - rows are stored densely in `rows`, index lookups by key are O(1)
|
|
14
|
+
* - delete is O(1), using swap-remove (last row moves into the vacated slot)
|
|
15
|
+
* - every row carries an insertion sequence number, providing a stable
|
|
16
|
+
* natural order regardless of physical position
|
|
17
|
+
* - listeners are notified synchronously of every change
|
|
18
|
+
*/
|
|
19
|
+
export declare class Table implements RowSource {
|
|
20
|
+
#private;
|
|
21
|
+
readonly columnMap: ColumnMap;
|
|
22
|
+
readonly indexOfKeyField: number;
|
|
23
|
+
readonly schema: TableSchema;
|
|
24
|
+
constructor(schemaOrDefinition: TableSchema | TableDefinition);
|
|
25
|
+
get columns(): readonly import("@vuu-ui/vuu-data-types").SchemaColumn[];
|
|
26
|
+
get name(): string;
|
|
27
|
+
get primaryKey(): string;
|
|
28
|
+
get rowCount(): number;
|
|
29
|
+
/**
|
|
30
|
+
* Dense array of rows. Treat as read only, use the Table api to mutate.
|
|
31
|
+
*/
|
|
32
|
+
get rows(): VuuDataRow[];
|
|
33
|
+
get keys(): IterableIterator<string>;
|
|
34
|
+
addListener(listener: TableListener): void;
|
|
35
|
+
removeListener(listener: TableListener): void;
|
|
36
|
+
get listenerCount(): number;
|
|
37
|
+
seqAt(rowIdx: number): number;
|
|
38
|
+
/**
|
|
39
|
+
* Insertion sequence numbers, parallel to rows. Treat as read only.
|
|
40
|
+
*/
|
|
41
|
+
get seq(): readonly number[];
|
|
42
|
+
rowAt(rowIdx: number): VuuDataRow;
|
|
43
|
+
rowIndexAtKey: (key: string) => number;
|
|
44
|
+
hasKey(key: string): boolean;
|
|
45
|
+
getRowAtKey(key: string, throwIfMissing?: true): VuuDataRow;
|
|
46
|
+
getRowAtKey(key: string, throwIfMissing: false): VuuDataRow | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Insert a new row. If a row with the same key exists, it is replaced
|
|
49
|
+
* (treated as an update).
|
|
50
|
+
*
|
|
51
|
+
* @param _emitEvent retained for api compatibility. Listeners are always
|
|
52
|
+
* notified, a listener that missed an insert would be inconsistent.
|
|
53
|
+
*/
|
|
54
|
+
insert(row: VuuDataRow, _emitEvent?: boolean): number;
|
|
55
|
+
/**
|
|
56
|
+
* Replace the row at rowIdx.
|
|
57
|
+
*/
|
|
58
|
+
update(rowIdx: number, row: VuuDataRow, _columnName?: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Apply a partial update to the row with given key. Values are keyed by
|
|
61
|
+
* column name. A new row array is created so listeners can detect
|
|
62
|
+
* which columns have changed.
|
|
63
|
+
*/
|
|
64
|
+
updateByKey(key: string, values: Record<string, VuuRowDataItemType>): boolean;
|
|
65
|
+
upsert(row: VuuDataRow, emitEvent?: boolean): void;
|
|
66
|
+
delete(key: string): boolean;
|
|
67
|
+
clear(): void;
|
|
68
|
+
pullRowAsArray(key: string, columns: readonly {
|
|
69
|
+
name: string;
|
|
70
|
+
}[]): VuuRowDataItemType[];
|
|
71
|
+
getUniqueValuesForColumn(column: string, pattern?: string): string[];
|
|
72
|
+
}
|
package/src/index.d.ts
ADDED
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { TableSchema } from "@vuu-ui/vuu-data-types";
|
|
2
|
+
import type { VuuDataRow, VuuRowDataItemType } from "@vuu-ui/vuu-protocol-types";
|
|
3
|
+
export type { TableSchema, VuuDataRow, VuuRowDataItemType };
|
|
4
|
+
/**
|
|
5
|
+
* Tables may hold bigint values (e.g. long columns from vuu-ui test data),
|
|
6
|
+
* although rows are typed with the protocol value types. Viewport engines
|
|
7
|
+
* sort, filter, aggregate and join on bigint values and convert them to a
|
|
8
|
+
* protocol value on output. Hosts can cast such rows to VuuDataRow.
|
|
9
|
+
*/
|
|
10
|
+
export type VuuDataRowWithBigint = (VuuRowDataItemType | bigint)[];
|
|
11
|
+
/**
|
|
12
|
+
* Map of column name to index position within a row array.
|
|
13
|
+
*/
|
|
14
|
+
export type ColumnMap = Record<string, number>;
|
|
15
|
+
/**
|
|
16
|
+
* Receives notification of every change to a table. Listeners are invoked
|
|
17
|
+
* synchronously, in registration order. Rows are stored densely, a delete
|
|
18
|
+
* moves the last row into the vacated slot (swap-remove) so listeners that
|
|
19
|
+
* track row indices must handle `movedFromIdx`.
|
|
20
|
+
*/
|
|
21
|
+
export interface TableListener {
|
|
22
|
+
onInsert?(rowIdx: number, row: VuuDataRow): void;
|
|
23
|
+
/**
|
|
24
|
+
* `previous` is the row that was replaced. It may be the same array
|
|
25
|
+
* instance as `row` if the caller mutated the row in place, in which
|
|
26
|
+
* case it is not possible to determine which columns have changed.
|
|
27
|
+
*/
|
|
28
|
+
onUpdate?(rowIdx: number, row: VuuDataRow, previous: VuuDataRow): void;
|
|
29
|
+
/**
|
|
30
|
+
* The row previously at `rowIdx` has been removed. If `movedFromIdx` is
|
|
31
|
+
* not -1, the row previously at `movedFromIdx` (always the last row) has
|
|
32
|
+
* been moved into `rowIdx`.
|
|
33
|
+
*/
|
|
34
|
+
onDelete?(rowIdx: number, row: VuuDataRow, movedFromIdx: number): void;
|
|
35
|
+
onClear?(): void;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Minimal read-only view of a table, sufficient for a viewport engine.
|
|
39
|
+
*/
|
|
40
|
+
export interface RowSource {
|
|
41
|
+
readonly columnMap: ColumnMap;
|
|
42
|
+
readonly indexOfKeyField: number;
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly rowCount: number;
|
|
45
|
+
readonly rows: readonly VuuDataRow[];
|
|
46
|
+
readonly schema: TableSchema;
|
|
47
|
+
/**
|
|
48
|
+
* Monotonic insertion sequence for the row at rowIdx. Used to provide a
|
|
49
|
+
* stable 'natural' (insertion) order independent of physical position.
|
|
50
|
+
*/
|
|
51
|
+
seqAt(rowIdx: number): number;
|
|
52
|
+
/** insertion sequence numbers, parallel to rows */
|
|
53
|
+
readonly seq: readonly number[];
|
|
54
|
+
rowIndexAtKey(key: string): number;
|
|
55
|
+
addListener(listener: TableListener): void;
|
|
56
|
+
removeListener(listener: TableListener): void;
|
|
57
|
+
}
|