@heswell/vuu-data-engine-local 4.0.0-alpha.20 → 4.1.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/91.js +282 -0
- package/README.md +222 -25
- package/index.js +3754 -3528
- package/legacy.js +11 -0
- package/package.json +18 -10
- package/react.js +64 -0
- package/src/EngineDataSource.d.ts +13 -3
- package/src/ModuleDataSource.d.ts +3 -3
- package/src/UpdateGenerator.d.ts +12 -0
- package/src/basket/BasketModule.d.ts +12 -2
- package/src/basket/index.d.ts +1 -1
- package/src/core/engine/LocalEngine.d.ts +63 -0
- package/src/core/filter/PermissionFilter.d.ts +2 -2
- package/src/core/module/VuuModule.d.ts +80 -1
- package/src/core/module/edit-hooks.d.ts +135 -0
- package/src/core/table/TableContainer.d.ts +60 -11
- package/src/core/table/defaultTableContainer.d.ts +6 -0
- package/src/data-utils.d.ts +7 -0
- package/src/demo-modules.d.ts +18 -0
- package/src/index.d.ts +6 -3
- package/src/legacy.d.ts +12 -0
- package/src/local-datasource-provider/LocalDatasourceProvider.d.ts +18 -7
- package/src/local-datasource-provider/createDataSourceProvider.d.ts +12 -0
- package/src/module-admin/ModuleAdminModule.d.ts +0 -1
- package/src/module-admin/index.d.ts +2 -2
- package/src/nextFrame.d.ts +1 -0
- package/src/notifications/NotificationModule.d.ts +1 -0
- package/src/notifications/index.d.ts +0 -1
- package/src/react.d.ts +7 -0
- package/src/simul/SimulModule.d.ts +9 -1
- package/src/simul/index.d.ts +1 -1
- package/src/simul/reference-data/instrument-prices.d.ts +2 -2
- package/src/simul/reference-data/instruments-extended.d.ts +1 -1
- package/src/simul/reference-data/instruments.d.ts +2 -3
- package/src/simul/reference-data/orders.d.ts +2 -3
- package/src/simul/reference-data/parent-child-orders.d.ts +15 -4
- package/src/simul/reference-data/prices.d.ts +18 -6
- package/src/test/TestModule.d.ts +2 -3
- package/src/user-admin/UserAdminModule.d.ts +0 -1
- package/src/user-admin/index.d.ts +1 -1
- package/src/basket/reference-data/prices.d.ts +0 -24
- package/src/core/module/ModuleContainer.d.ts +0 -15
- package/src/notifications/simulated/notificationsModule.d.ts +0 -5
package/91.js
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import { JoinTable, Table } from "@heswell/vuu-table";
|
|
2
|
+
import { EventEmitter } from "@vuu-ui/vuu-utils";
|
|
3
|
+
class Table_Table extends EventEmitter {
|
|
4
|
+
engineTable;
|
|
5
|
+
#dataMap;
|
|
6
|
+
#indexOfKey;
|
|
7
|
+
#updateColumnName;
|
|
8
|
+
constructor(schemaOrEngineTable, data = [], dataMap, updateGenerator){
|
|
9
|
+
super();
|
|
10
|
+
this.engineTable = schemaOrEngineTable instanceof Table ? schemaOrEngineTable : new Table(schemaOrEngineTable);
|
|
11
|
+
this.#dataMap = dataMap ?? this.engineTable.columnMap;
|
|
12
|
+
this.#indexOfKey = this.engineTable.indexOfKeyField;
|
|
13
|
+
for (const row of data)this.engineTable.insert(row);
|
|
14
|
+
this.engineTable.addListener({
|
|
15
|
+
onInsert: (_rowIdx, row)=>this.emit("insert", row),
|
|
16
|
+
onUpdate: (_rowIdx, row)=>this.emit("update", row, this.#updateColumnName),
|
|
17
|
+
onDelete: (_rowIdx, row)=>this.emit("delete", String(row[this.#indexOfKey])),
|
|
18
|
+
onClear: ()=>void 0
|
|
19
|
+
});
|
|
20
|
+
updateGenerator?.setTable(this);
|
|
21
|
+
updateGenerator?.setRange({
|
|
22
|
+
from: 0,
|
|
23
|
+
to: 100
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
get data() {
|
|
27
|
+
return this.engineTable.rows;
|
|
28
|
+
}
|
|
29
|
+
get map() {
|
|
30
|
+
return this.#dataMap;
|
|
31
|
+
}
|
|
32
|
+
get schema() {
|
|
33
|
+
return this.engineTable.schema;
|
|
34
|
+
}
|
|
35
|
+
get name() {
|
|
36
|
+
return this.engineTable.name;
|
|
37
|
+
}
|
|
38
|
+
get rowCount() {
|
|
39
|
+
return this.engineTable.rowCount;
|
|
40
|
+
}
|
|
41
|
+
delete(key) {
|
|
42
|
+
if (!this.engineTable.delete(key)) throw Error(`[Table] delete key ${key} not found`);
|
|
43
|
+
}
|
|
44
|
+
insert(row, _emitEvent = true) {
|
|
45
|
+
const ts = Date.now();
|
|
46
|
+
const { vuuCreatedTimestamp, vuuUpdatedTimestamp } = this.#dataMap;
|
|
47
|
+
if (void 0 !== vuuUpdatedTimestamp) row[vuuUpdatedTimestamp] = ts;
|
|
48
|
+
if (void 0 !== vuuCreatedTimestamp) row[vuuCreatedTimestamp] = ts;
|
|
49
|
+
this.engineTable.insert(row);
|
|
50
|
+
}
|
|
51
|
+
findByKey(key) {
|
|
52
|
+
return this.engineTable.getRowAtKey(key, false);
|
|
53
|
+
}
|
|
54
|
+
update(key, columnName, value) {
|
|
55
|
+
const rowIdx = this.engineTable.rowIndexAtKey(key);
|
|
56
|
+
if (-1 !== rowIdx) {
|
|
57
|
+
const newRow = this.engineTable.rows[rowIdx].slice();
|
|
58
|
+
newRow[this.#dataMap[columnName]] = value;
|
|
59
|
+
this.#updateColumnName = columnName;
|
|
60
|
+
try {
|
|
61
|
+
this.engineTable.update(rowIdx, newRow);
|
|
62
|
+
} finally{
|
|
63
|
+
this.#updateColumnName = void 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
updateRow(row) {
|
|
68
|
+
const rowIdx = this.engineTable.rowIndexAtKey(String(row[this.#indexOfKey]));
|
|
69
|
+
if (-1 !== rowIdx) this.engineTable.update(rowIdx, row);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function buildDataColumnMapFromSchema(schema) {
|
|
73
|
+
const map = {};
|
|
74
|
+
schema.columns.forEach((col, index)=>{
|
|
75
|
+
map[col.name] = index;
|
|
76
|
+
});
|
|
77
|
+
return map;
|
|
78
|
+
}
|
|
79
|
+
function buildDataColumnMap(schemas, tableName) {
|
|
80
|
+
return buildDataColumnMapFromSchema(schemas[tableName]);
|
|
81
|
+
}
|
|
82
|
+
const DEFAULT_RANGE_LIMITS = {
|
|
83
|
+
maxRangeEnd: 1000000,
|
|
84
|
+
maxRangeWidth: 1000
|
|
85
|
+
};
|
|
86
|
+
const tableKey = ({ module, table })=>`${module}:${table}`;
|
|
87
|
+
const createTable = (schema, data = [], dataMap = buildDataColumnMapFromSchema(schema), updateGenerator)=>new Table_Table(schema, data, dataMap, updateGenerator);
|
|
88
|
+
const createJoinTable = (joinTable, table1, table2, joinColumn, joinTypeOrOptions = "inner")=>{
|
|
89
|
+
const { joinType = "inner", columns, schema } = "string" == typeof joinTypeOrOptions ? {
|
|
90
|
+
joinType: joinTypeOrOptions
|
|
91
|
+
} : joinTypeOrOptions;
|
|
92
|
+
const engineJoinTable = new JoinTable({
|
|
93
|
+
schema: buildJoinSchema(joinTable, table1.schema, table2.schema, {
|
|
94
|
+
columns,
|
|
95
|
+
schema
|
|
96
|
+
}),
|
|
97
|
+
baseTable: table1.engineTable,
|
|
98
|
+
joinTable: table2.engineTable,
|
|
99
|
+
leftColumn: joinColumn,
|
|
100
|
+
rightColumn: joinColumn,
|
|
101
|
+
joinType
|
|
102
|
+
});
|
|
103
|
+
return new Table_Table(engineJoinTable);
|
|
104
|
+
};
|
|
105
|
+
const buildJoinSchema = (joinTable, schema1, schema2, { columns, schema })=>{
|
|
106
|
+
const name = `${joinTable.module}:${joinTable.table}`;
|
|
107
|
+
const fail = (message)=>{
|
|
108
|
+
throw Error(`[createJoinTable] ${name}: ${message}`);
|
|
109
|
+
};
|
|
110
|
+
if (columns && schema) fail("pass either columns or schema, not both");
|
|
111
|
+
if (schema) {
|
|
112
|
+
const { module, table } = schema.table;
|
|
113
|
+
if (module !== joinTable.module || table !== joinTable.table) fail(`schema is for table ${module}:${table}`);
|
|
114
|
+
if (schema.key !== schema1.key) fail(`schema key ${schema.key} must be the key of ${schema1.table.table} (${schema1.key})`);
|
|
115
|
+
}
|
|
116
|
+
const sourceColumns = new Set([
|
|
117
|
+
...schema1.columns,
|
|
118
|
+
...schema2.columns
|
|
119
|
+
].map((col)=>col.name));
|
|
120
|
+
const columnNames = schema?.columns.map((col)=>col.name) ?? columns ?? Array.from(sourceColumns).sort();
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
for (const columnName of columnNames){
|
|
123
|
+
if (!sourceColumns.has(columnName)) fail(`unknown column ${columnName}, not in ${schema1.table.table} or ${schema2.table.table}`);
|
|
124
|
+
if (seen.has(columnName)) fail(`duplicate column ${columnName}`);
|
|
125
|
+
seen.add(columnName);
|
|
126
|
+
}
|
|
127
|
+
if (!seen.has(schema1.key)) fail(`columns must include the key column ${schema1.key}`);
|
|
128
|
+
const joinColumns = columnNames.map((columnName)=>({
|
|
129
|
+
name: columnName,
|
|
130
|
+
serverDataType: getServerDataType(columnName, schema1, schema2)
|
|
131
|
+
}));
|
|
132
|
+
if (schema) {
|
|
133
|
+
schema.columns.forEach(({ name: columnName, serverDataType }, i)=>{
|
|
134
|
+
if (serverDataType !== joinColumns[i].serverDataType) fail(`column ${columnName} is declared ${serverDataType} but is ${joinColumns[i].serverDataType} in the source table`);
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
...schema,
|
|
138
|
+
rangeLimits: schema.rangeLimits ?? DEFAULT_RANGE_LIMITS
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
key: schema1.key,
|
|
143
|
+
table: joinTable,
|
|
144
|
+
rangeLimits: DEFAULT_RANGE_LIMITS,
|
|
145
|
+
columns: joinColumns
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
class TableContainer {
|
|
149
|
+
#tables = new Map();
|
|
150
|
+
createTable = (schema, data = [], dataMap = buildDataColumnMapFromSchema(schema), updateGenerator)=>{
|
|
151
|
+
const table = createTable(schema, data, dataMap, updateGenerator);
|
|
152
|
+
this.addTable(table);
|
|
153
|
+
return table;
|
|
154
|
+
};
|
|
155
|
+
createJoinTable(joinTable, table1, table2, joinColumn, joinTypeOrOptions = "inner") {
|
|
156
|
+
const newTable = createJoinTable(joinTable, this.getTable(table1), this.getTable(table2), joinColumn, joinTypeOrOptions);
|
|
157
|
+
this.addTable(newTable);
|
|
158
|
+
return newTable;
|
|
159
|
+
}
|
|
160
|
+
addTable(table, as = table.schema.table) {
|
|
161
|
+
this.#tables.set(tableKey(as), table);
|
|
162
|
+
}
|
|
163
|
+
removeTable(table) {
|
|
164
|
+
return this.#tables.delete(tableKey(table));
|
|
165
|
+
}
|
|
166
|
+
hasTable(table) {
|
|
167
|
+
return this.#tables.has(tableKey(table));
|
|
168
|
+
}
|
|
169
|
+
getTable(table) {
|
|
170
|
+
if ("string" == typeof table) {
|
|
171
|
+
const matches = Array.from(this.#tables.keys()).filter((key)=>key.endsWith(`:${table}`));
|
|
172
|
+
if (1 === matches.length) return this.#tables.get(matches[0]);
|
|
173
|
+
if (matches.length > 1) throw Error(`[TableContainer] table name ${table} is ambiguous (${matches.join(", ")}), use {module, table}`);
|
|
174
|
+
throw Error(`[TableContainer] no table ${table}`);
|
|
175
|
+
}
|
|
176
|
+
const found = this.#tables.get(tableKey(table));
|
|
177
|
+
if (found) return found;
|
|
178
|
+
throw Error(`[TableContainer] no table ${tableKey(table)}`);
|
|
179
|
+
}
|
|
180
|
+
get tables() {
|
|
181
|
+
return Array.from(this.#tables.values());
|
|
182
|
+
}
|
|
183
|
+
clear() {
|
|
184
|
+
this.#tables.clear();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
const getServerDataType = (columnName, { columns: cols1, table: t1 }, { columns: cols2, table: t2 })=>{
|
|
188
|
+
const col1 = cols1.find((col)=>col.name === columnName);
|
|
189
|
+
const col2 = cols2.find((col)=>col.name === columnName);
|
|
190
|
+
if (col1 && col2) if (col1.serverDataType === col2.serverDataType) return col1.serverDataType;
|
|
191
|
+
else throw Error(`both tables ${t1.table} and ${t2.table} implement column ${columnName}, but with types differ`);
|
|
192
|
+
if (col1) return col1.serverDataType;
|
|
193
|
+
if (col2) return col2.serverDataType;
|
|
194
|
+
throw Error("how is this possible");
|
|
195
|
+
};
|
|
196
|
+
class LocalEngine {
|
|
197
|
+
tableContainer = new TableContainer();
|
|
198
|
+
#modules = new Map();
|
|
199
|
+
#disposed = false;
|
|
200
|
+
constructor({ modules = [] } = {}){
|
|
201
|
+
for (const module of modules)this.register(module);
|
|
202
|
+
}
|
|
203
|
+
register(module) {
|
|
204
|
+
const existing = this.#modules.get(module.name);
|
|
205
|
+
if (existing === module) return module;
|
|
206
|
+
if (existing) this.unregister(existing.name);
|
|
207
|
+
this.#modules.set(module.name, module);
|
|
208
|
+
for (const [tableName, table] of Object.entries(module.getTables()))this.tableContainer.addTable(table, {
|
|
209
|
+
module: module.name,
|
|
210
|
+
table: tableName
|
|
211
|
+
});
|
|
212
|
+
module.attachEngine(this);
|
|
213
|
+
return module;
|
|
214
|
+
}
|
|
215
|
+
unregister(moduleName) {
|
|
216
|
+
const module = this.#modules.get(moduleName);
|
|
217
|
+
if (module) {
|
|
218
|
+
this.#modules.delete(moduleName);
|
|
219
|
+
for (const tableName of module.getTableList())this.tableContainer.removeTable({
|
|
220
|
+
module: moduleName,
|
|
221
|
+
table: tableName
|
|
222
|
+
});
|
|
223
|
+
module.detachEngine(this);
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
hasModule(moduleName) {
|
|
229
|
+
return this.#modules.has(moduleName);
|
|
230
|
+
}
|
|
231
|
+
getModule(moduleName) {
|
|
232
|
+
const module = this.#modules.get(moduleName);
|
|
233
|
+
if (module) return module;
|
|
234
|
+
throw Error(`[LocalEngine] module ${moduleName} not found`);
|
|
235
|
+
}
|
|
236
|
+
get modules() {
|
|
237
|
+
return Array.from(this.#modules.values());
|
|
238
|
+
}
|
|
239
|
+
get moduleNames() {
|
|
240
|
+
return Array.from(this.#modules.keys());
|
|
241
|
+
}
|
|
242
|
+
getTableList() {
|
|
243
|
+
return this.modules.flatMap((module)=>module.getTableList().map((table)=>({
|
|
244
|
+
module: module.name,
|
|
245
|
+
table
|
|
246
|
+
})));
|
|
247
|
+
}
|
|
248
|
+
getTableSchema({ module, table }) {
|
|
249
|
+
return this.getModule(module).getTableSchema(table);
|
|
250
|
+
}
|
|
251
|
+
createDataSource({ module, table }, viewport, config) {
|
|
252
|
+
return this.getModule(module).createDataSource(table, viewport, config);
|
|
253
|
+
}
|
|
254
|
+
get disposed() {
|
|
255
|
+
return this.#disposed;
|
|
256
|
+
}
|
|
257
|
+
dispose() {
|
|
258
|
+
for (const module of this.modules){
|
|
259
|
+
const owned = module.engine === this;
|
|
260
|
+
this.unregister(module.name);
|
|
261
|
+
if (owned) module.dispose();
|
|
262
|
+
}
|
|
263
|
+
this.tableContainer.clear();
|
|
264
|
+
this.#disposed = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const createLocalEngine = (options)=>new LocalEngine(options);
|
|
268
|
+
let defaultEngine;
|
|
269
|
+
const getDefaultEngine = ()=>{
|
|
270
|
+
if (void 0 === defaultEngine || defaultEngine.disposed) defaultEngine = new LocalEngine();
|
|
271
|
+
return defaultEngine;
|
|
272
|
+
};
|
|
273
|
+
const resetDefaultEngine = ()=>{
|
|
274
|
+
defaultEngine?.dispose();
|
|
275
|
+
defaultEngine = void 0;
|
|
276
|
+
};
|
|
277
|
+
const ensureVuuModule = (module)=>{
|
|
278
|
+
const engine = getDefaultEngine();
|
|
279
|
+
if (!engine.hasModule(module.name)) engine.register(module);
|
|
280
|
+
return module;
|
|
281
|
+
};
|
|
282
|
+
export { LocalEngine, TableContainer, Table_Table as Table, buildDataColumnMap, buildDataColumnMapFromSchema, createJoinTable, createLocalEngine, createTable, ensureVuuModule, getDefaultEngine, resetDefaultEngine };
|
package/README.md
CHANGED
|
@@ -9,17 +9,213 @@ The package runs alongside `@vuu-ui/vuu-data-test` and has the same public API.
|
|
|
9
9
|
meant to replace the `VuuModule` / `TickingArrayDataSource` core in that package. The
|
|
10
10
|
two packages don't depend on each other.
|
|
11
11
|
|
|
12
|
+
## Peer dependencies
|
|
13
|
+
|
|
14
|
+
All `@vuu-ui` packages are peers declared as `^4.0.0-alpha.20`, so the engine uses the host
|
|
15
|
+
application's single copy of each (a second `@vuu-ui/vuu-utils` or `@vuu-ui/core` would mean
|
|
16
|
+
a second set of React contexts). They are `@vuu-ui/vuu-utils` and `@vuu-ui/vuu-filter-parser`
|
|
17
|
+
(runtime), `@vuu-ui/core` (React provider, `/react` entry only), and the type-only `vuu-data-types`,
|
|
18
|
+
`vuu-filter-types`, `vuu-protocol-types` and `vuu-table-types`.
|
|
19
|
+
|
|
12
20
|
## Architecture
|
|
13
21
|
|
|
14
|
-
| Component | Role
|
|
15
|
-
| -------------------------------------- |
|
|
16
|
-
| `Table` | A façade over the engine `Table` that keeps the existing `Table` API. Inserts, updates and deletes go straight to the engine. Every change notifies subscribed viewports.
|
|
17
|
-
| `
|
|
22
|
+
| Component | Role |
|
|
23
|
+
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
24
|
+
| `Table` | A façade over the engine `Table` that keeps the existing `Table` API. Inserts, updates and deletes go straight to the engine. Every change notifies subscribed viewports. |
|
|
25
|
+
| `LocalEngine` | An isolated in-browser Vuu server: the registered `VuuModule`s and a `TableContainer` of their tables. No React or DOM dependency. |
|
|
26
|
+
| `TableContainer` | A registry of tables, including join tables, keyed by `{module, table}`. Each engine has its own. |
|
|
18
27
|
| `EngineDataSource` | A `DataSource` implementation backed by an engine `Viewport`. It handles windowing, sort, filter, groupBy, selection (including select-all), freeze, base filter, link filter and permission filter. Row updates are batched per frame. |
|
|
19
|
-
| `ModuleDataSource` | Extends `EngineDataSource` with the module features: RPC and menu services, edit sessions, and visual links. `TickingArrayDataSource` is a deprecated alias.
|
|
20
|
-
| `VuuModule` | Creates `ModuleDataSource` instances. Row permissions are passed to the engine as a `permissionFilter` predicate.
|
|
21
|
-
| `RuntimeVisualLink` | When the parent's selection changes, sets the child's engine link filter.
|
|
22
|
-
| `UpdateGenerator` / `TickingGenerator` | Simulated ticking data that writes into the engine tables.
|
|
28
|
+
| `ModuleDataSource` | Extends `EngineDataSource` with the module features: RPC and menu services, edit sessions, and visual links. `TickingArrayDataSource` is a deprecated alias. |
|
|
29
|
+
| `VuuModule` | Creates `ModuleDataSource` instances. Row permissions are passed to the engine as a `permissionFilter` predicate. Edit sessions can be customised per table with edit hooks. |
|
|
30
|
+
| `RuntimeVisualLink` | When the parent's selection changes, sets the child's engine link filter. |
|
|
31
|
+
| `UpdateGenerator` / `TickingGenerator` | Simulated ticking data that writes into the engine tables. |
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
Importing the package has no side effects: no modules, tables or timers are created
|
|
36
|
+
until you ask for them. The main entry needs no DOM, so it runs in plain Node or Bun with
|
|
37
|
+
no shims: it uses `requestAnimationFrame` when present and falls back to `setTimeout`
|
|
38
|
+
otherwise. The React bindings (`createDataSourceProvider`, `createLocalVuuServer`,
|
|
39
|
+
`LocalDataSourceProvider`) are in `@heswell/vuu-data-engine-local/react`, because
|
|
40
|
+
`@vuu-ui/core` needs a DOM (`ResizeObserver`) at import time.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
import {
|
|
44
|
+
createLocalEngine,
|
|
45
|
+
registerDemoModules,
|
|
46
|
+
} from "@heswell/vuu-data-engine-local";
|
|
47
|
+
import { createLocalVuuServer } from "@heswell/vuu-data-engine-local/react";
|
|
48
|
+
|
|
49
|
+
// Your own modules, in an isolated engine
|
|
50
|
+
const engine = createLocalEngine({ modules: [new OrdersModule()] });
|
|
51
|
+
const dataSource = engine.createDataSource({
|
|
52
|
+
module: "ORDERS",
|
|
53
|
+
table: "parentOrders",
|
|
54
|
+
});
|
|
55
|
+
await dataSource.subscribe({ range: { from: 0, to: 100 } }, callback);
|
|
56
|
+
engine.dispose(); // disposes its modules and stops their timers
|
|
57
|
+
|
|
58
|
+
// A local Vuu server for a local-mode AuthenticationProvider (new engine per server)
|
|
59
|
+
const server = createLocalVuuServer({
|
|
60
|
+
connectionId: "orders",
|
|
61
|
+
modules: [new OrdersModule()],
|
|
62
|
+
});
|
|
63
|
+
// server.DataSourceProvider, server.engine
|
|
64
|
+
|
|
65
|
+
// The demo SIMUL, BASKET and TEST modules
|
|
66
|
+
const { simulModule } = registerDemoModules(createLocalEngine());
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Engines
|
|
70
|
+
|
|
71
|
+
| API | Description |
|
|
72
|
+
| --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
73
|
+
| `createLocalEngine({ modules? })` | A new, isolated `LocalEngine`. Two engines, or two modules in one engine, can use the same table names. |
|
|
74
|
+
| `engine.register(module)` / `unregister(name)` | Add or remove a module and its tables. A module is attached to the first engine it is registered with. |
|
|
75
|
+
| `engine.getModule(name)`, `hasModule`, `modules` | Look up modules. |
|
|
76
|
+
| `engine.getTableList()` | `VuuTable[]` (`{module, table}`) for every registered module. |
|
|
77
|
+
| `engine.getTableSchema(vuuTable)` | A table's schema. |
|
|
78
|
+
| `engine.createDataSource(vuuTable, vp?, cfg?)` | A `ModuleDataSource` for a table. |
|
|
79
|
+
| `engine.tableContainer` | The engine's `TableContainer`. Tables are keyed by `{module, table}`; a bare table name is accepted only if exactly one module has a table with that name. |
|
|
80
|
+
| `engine.dispose()` | Unregisters every module and disposes those attached to this engine (stopping ticking data and timers). |
|
|
81
|
+
| `createDataSourceProvider(engine)` (`/react`) | A React provider installing the legacy and core data contexts for the engine. |
|
|
82
|
+
| `createLocalVuuServer({ connectionId, modules, engine? })` (`/react`) | A `LocalVuuServer` (`{connectionId, DataSourceProvider, engine}`). Uses a new engine unless one is passed. |
|
|
83
|
+
| `registerDemoModules(engine?)` | Creates the SIMUL, BASKET and TEST demo modules and registers them with `engine` (default: the default engine). |
|
|
84
|
+
| `getDefaultEngine()` / `resetDefaultEngine()` | The shared engine behind the deprecated globals below. |
|
|
85
|
+
|
|
86
|
+
### Edit sessions: hooks and service overrides
|
|
87
|
+
|
|
88
|
+
The built-in edit-session RPC services (`createSessionTable`, `editCell`, `addRow`, `deleteRow`,
|
|
89
|
+
`undoRowChange`, `endEditSession`, …) can be overridden by `rpcName`, per module with
|
|
90
|
+
`moduleServices` or per table with `services`. Precedence: table services, then module
|
|
91
|
+
services, then the built-ins. Services registered for a source table also apply to its
|
|
92
|
+
session tables.
|
|
93
|
+
|
|
94
|
+
Most customisation needs only edit hooks, which mirror vuu-server's. Implement them per
|
|
95
|
+
table with `editHooks` (these win), or for every table as protected methods of the module.
|
|
96
|
+
|
|
97
|
+
| Hook | Default |
|
|
98
|
+
| ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
99
|
+
| `canEditSource(column, ctx)` | `true`: editCell may write directly to a source table (outside a session). |
|
|
100
|
+
| `isEditableColumn(table, column, ctx)` | Any column except the key and the system columns (`vuuCreatedTimestamp`, `vuuUpdatedTimestamp`, `vuuMsg`, `vuuAction`). The key of a row added in the session may be edited. |
|
|
101
|
+
| `validateCellEdit(edit)` | Accept. Return an error message to refuse the edit. |
|
|
102
|
+
| `getStaleColumns(sourceTable, sourceRow, change)` | `defaultGetStaleColumns`: for editCell, the edited columns that changed in the source since the row was copied into the session; for deleteRow, any column. |
|
|
103
|
+
| `canSaveSession(prepared, ctx)` | `true`. Return `false` or a reason to refuse the save. May be async. |
|
|
104
|
+
| `applySessionSave(sessionTable, prepared, ctx)` | `defaultApplySessionSave`: writes the changes to the source table atomically. May be async. |
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
class OrdersModule extends VuuModule<"parentOrders" | "childOrders"> {
|
|
108
|
+
protected editHooks = {
|
|
109
|
+
parentOrders: {
|
|
110
|
+
canSaveSession: ({ changes }) =>
|
|
111
|
+
changes.every(({ cellUpdates }) => Number(cellUpdates.qty) > 0) ||
|
|
112
|
+
"qty must be positive",
|
|
113
|
+
// amend orders through the order service instead of writing the table
|
|
114
|
+
applySessionSave: (_sessionTable, { changes }) =>
|
|
115
|
+
orderService.amend(changes),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
// ...
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`deleteRow` and `deleteSelectedRows` behave as on vuu-server: they are only valid in an edit
|
|
123
|
+
session, and mark the session row `vuuAction = "deleteRow"` whatever the `mode` (`"hard"`,
|
|
124
|
+
vuu-ui's default, or `"soft"`). The row stays in the session until it is saved, when
|
|
125
|
+
`applySessionSave` receives it as a `deleteRow` change, or `undoRowChange` restores it. A
|
|
126
|
+
row added in the session is simply dropped. Outside a session `deleteRow` returns an error;
|
|
127
|
+
`deleteSourceRow` (protected, not registered) keeps the old direct delete for subclasses
|
|
128
|
+
such as SIMUL's `DELETE_ROW`.
|
|
129
|
+
|
|
130
|
+
`endEditSession({ save: true })`:
|
|
131
|
+
|
|
132
|
+
1. Rows already handled by `beforeSessionSave` (with a `vuuMsg`) are skipped.
|
|
133
|
+
2. Changes are prepared. A row added with an existing key fails with `"duplicate key"`; a
|
|
134
|
+
source row that is missing or stale fails with exactly `"stale update"` (vuu-ui's
|
|
135
|
+
`VuuDataSource` matches this string and throws `StaleUpdateError`). Setting
|
|
136
|
+
`params.force` skips the stale check. The affected session rows get a `vuuMsg`
|
|
137
|
+
(`stale:<key>:<columns>`, `<action>:<key>:source row missing`,
|
|
138
|
+
`addRow:<key>:key already exists`).
|
|
139
|
+
3. `canSaveSession`, then `applySessionSave`. A refusal or failure (`false`, an
|
|
140
|
+
`ERROR_RESULT`, or a thrown error) leaves the session open, so the user can retry or
|
|
141
|
+
cancel. Exported messages: `STALE_UPDATE`, `DUPLICATE_KEY`, `SAVE_NOT_PERMITTED`,
|
|
142
|
+
`SAVE_FAILED`.
|
|
143
|
+
4. `afterSessionSave`, then the session rows are reset (deleted rows removed, `vuuAction`
|
|
144
|
+
and `vuuMsg` cleared, snapshots refreshed).
|
|
145
|
+
|
|
146
|
+
### Join tables
|
|
147
|
+
|
|
148
|
+
`createJoinTable(joinTable, table1, table2, joinColumn, joinType = "inner")` (and
|
|
149
|
+
`tableContainer.createJoinTable`) creates a join that is kept up to date as either table
|
|
150
|
+
changes. Pass `"leftOuter"` to keep every `table1` row. For a row with no match, the
|
|
151
|
+
`table2` columns are `null` (not `0`), as on vuu-server, and vuu-ui renders them empty.
|
|
152
|
+
Sort places nulls first when ascending; relational filters (`<`, `>=`, …) never match
|
|
153
|
+
null; and aggregations skip nulls.
|
|
154
|
+
|
|
155
|
+
By default the join table has every column of both tables, in alphabetical order. To
|
|
156
|
+
control the columns, pass an options object as the fifth argument instead of a join type:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// an ordered list of column names, possibly a subset (must include table1's key)
|
|
160
|
+
createJoinTable(joinTable, instruments, prices, "ric", {
|
|
161
|
+
joinType: "leftOuter",
|
|
162
|
+
columns: ["ric", "exchange", "bid", "ask"],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// or a declared TableSchema for the join table, e.g. the schema vuu-server returns
|
|
166
|
+
createJoinTable(instrumentPricesSchema.table, instruments, prices, "ric", {
|
|
167
|
+
joinType: "leftOuter",
|
|
168
|
+
schema: instrumentPricesSchema,
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`columns` and `schema` are mutually exclusive, and `joinType` still defaults to `"inner"`.
|
|
173
|
+
Column types come from the source tables; a column in both tables takes its value from
|
|
174
|
+
`table1`. Unknown or duplicate columns, a missing key column, a declared `serverDataType`
|
|
175
|
+
that differs from the source column, or a `schema` whose `table` or `key` does not match
|
|
176
|
+
(`joinTable`, `table1`'s key) throw an error naming the join table and the problem.
|
|
177
|
+
|
|
178
|
+
### Ranges
|
|
179
|
+
|
|
180
|
+
`subscribe({ range })`, `dataSource.range = …` and `setRange` accept a vuu-utils `Range` or
|
|
181
|
+
a plain `{from, to}`.
|
|
182
|
+
|
|
183
|
+
### Deprecated globals and the `/legacy` entry
|
|
184
|
+
|
|
185
|
+
`tableContainer`, `LocalDataSourceProvider` and `ensureVuuModule` use the default engine
|
|
186
|
+
(`getDefaultEngine()`). `@heswell/vuu-data-engine-local/legacy` re-exports the main and
|
|
187
|
+
`/react` entries and,
|
|
188
|
+
on import, registers the demo modules with the default engine, exporting the instances
|
|
189
|
+
`simulModule`, `basketModule`, `testModule`, `moduleAdminModule`, `userAdminModule` and
|
|
190
|
+
`notificationsModule` as 2.x did. It is the only entry point with side effects
|
|
191
|
+
(`"sideEffects": ["./src/legacy.ts"]`).
|
|
192
|
+
|
|
193
|
+
## Migrating from 3.x
|
|
194
|
+
|
|
195
|
+
These are breaking changes; the next release should be 4.0.0.
|
|
196
|
+
|
|
197
|
+
| 3.x | 4.x |
|
|
198
|
+
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
199
|
+
| `createDataSourceProvider`, `createLocalVuuServer`, `LocalDataSourceProvider` exported by the main entry. | Import them from `@heswell/vuu-data-engine-local/react` (or `/legacy`). The main entry no longer needs a DOM. |
|
|
200
|
+
| `deleteRow` with `mode: "hard"` removed the session row, so the save never saw the delete. | The row is marked `vuuAction = "deleteRow"` in either mode and applied on save (or restored by `undoRowChange`). |
|
|
201
|
+
| `deleteRow` outside an edit session deleted the source row. | Returns an error, as vuu-server does. Use an edit session, or expose the protected `deleteSourceRow` as a service. |
|
|
202
|
+
|
|
203
|
+
## Migrating from 2.x
|
|
204
|
+
|
|
205
|
+
These are breaking changes; the next release should be 3.0.0.
|
|
206
|
+
|
|
207
|
+
| 2.x | 3.x |
|
|
208
|
+
| --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
209
|
+
| Importing the package created and registered SIMUL, BASKET, TEST, MODULE_DISCOVERY, USER_ADMIN and NOTIFICATIONS, and exported the instances. | Nothing is created on import. Call `registerDemoModules()` or create the modules yourself, or import `/legacy`. |
|
|
210
|
+
| `new SomeModule()` registered itself in a global module container. | Register modules with an engine: `createLocalEngine({ modules })`, `engine.register()`, or `createLocalVuuServer`. |
|
|
211
|
+
| `createLocalVuuServer` registered its modules globally, so `LocalDataSourceProvider` saw them. | Each server has its own engine (`server.engine`). Pass `engine: getDefaultEngine()` to share the default engine. |
|
|
212
|
+
| `tableContainer` was global and keyed by bare table name. | Each engine has its own, keyed by `{module, table}`. The global `tableContainer` is a deprecated alias of the default engine's; a bare name throws if two modules have a table with that name. |
|
|
213
|
+
| `ModuleContainer` / `moduleContainer`. | Removed: use `LocalEngine`. |
|
|
214
|
+
| Module and table `services` were added alongside the built-in edit services. | They override built-in services with the same `rpcName`. |
|
|
215
|
+
| editCell could change the key and system columns, and edit missing or deleted rows. | Refused by default (see `isEditableColumn`). Editing a missing row returns an error. |
|
|
216
|
+
| `endEditSession` saved without checking for concurrent source changes or duplicate keys. | Fails with `"stale update"` or `"duplicate key"` (unless `force`). After a successful save the session rows are reset. |
|
|
217
|
+
| Demo prices and notifications ticked from import. | They start when a data source is created, and stop on `engine.dispose()`. |
|
|
218
|
+
| Reference data (`instrumentsTable`, `pricesTable`, `instrumentsData`, …) was created on import. | Lazy getters and factories (`getInstrumentsData()`, `createPricesTable()`, …). |
|
|
23
219
|
|
|
24
220
|
## Differences from `@vuu-ui/vuu-data-test`
|
|
25
221
|
|
|
@@ -38,7 +234,8 @@ npm run showcase:engine # equivalent to: VUU_DATA_ENGINE=local npm run showcas
|
|
|
38
234
|
```
|
|
39
235
|
|
|
40
236
|
This aliases `@vuu-ui/vuu-data-test` to `@heswell/vuu-data-engine-local` in the showcase
|
|
41
|
-
rsbuild config. The examples themselves don't change.
|
|
237
|
+
rsbuild config. The examples themselves don't change. The examples use the eagerly created demo
|
|
238
|
+
modules, so from 3.0 the alias should point to `@heswell/vuu-data-engine-local/legacy`.
|
|
42
239
|
|
|
43
240
|
The showcase uses the published package. To publish it from vuu-websocket:
|
|
44
241
|
|
|
@@ -64,22 +261,22 @@ results for the sort, filter and groupBy scenarios.
|
|
|
64
261
|
|
|
65
262
|
Mean time per operation, 100k rows, Node 24, Apple Silicon:
|
|
66
263
|
|
|
67
|
-
| Scenario
|
|
68
|
-
|
|
|
69
|
-
| create + subscribe
|
|
70
|
-
| sort numeric column
|
|
71
|
-
| sort two columns
|
|
72
|
-
| filter
|
|
73
|
-
| groupBy ccy, set/clear
|
|
74
|
-
| groupBy ccy + exchange, set/clear
|
|
75
|
-
| scroll, 100 range changes
|
|
76
|
-
| 1000 ticks, random rows
|
|
77
|
-
| 1000 ticks, rows in viewport
|
|
78
|
-
| 1000 ticks, sorted column
|
|
79
|
-
| 1000 ticks, filtered column
|
|
80
|
-
| 1000 ticks, grouped
|
|
81
|
-
| 500 deletes + 500 inserts
|
|
82
|
-
| 500 deletes + 500 inserts, sorted
|
|
264
|
+
| Scenario | legacy | engine | speed-up |
|
|
265
|
+
| --------------------------------- | ------- | ------- | -------- |
|
|
266
|
+
| create + subscribe | 24.1 ms | 8.7 ms | 2.8x |
|
|
267
|
+
| sort numeric column | 62.1 ms | 14.1 ms | 4.4x |
|
|
268
|
+
| sort two columns | 64.6 ms | 17.1 ms | 3.8x |
|
|
269
|
+
| filter | 5.7 ms | 1.3 ms | 4.3x |
|
|
270
|
+
| groupBy ccy, set/clear | 1.6 ms | 1.2 ms | 1.3x |
|
|
271
|
+
| groupBy ccy + exchange, set/clear | 2.2 ms | 2.0 ms | 1.1x |
|
|
272
|
+
| scroll, 100 range changes | 0.82 ms | 0.45 ms | 1.8x |
|
|
273
|
+
| 1000 ticks, random rows | 1543 ms | 0.35 ms | ~4400x |
|
|
274
|
+
| 1000 ticks, rows in viewport | 0.63 ms | 0.10 ms | 6.6x |
|
|
275
|
+
| 1000 ticks, sorted column | 3012 ms | 1.9 ms | ~1600x |
|
|
276
|
+
| 1000 ticks, filtered column | 1903 ms | 0.55 ms | ~3400x |
|
|
277
|
+
| 1000 ticks, grouped | 1205 ms | 0.39 ms | ~3100x |
|
|
278
|
+
| 500 deletes + 500 inserts | 1628 ms | 0.60 ms | ~2700x |
|
|
279
|
+
| 500 deletes + 500 inserts, sorted | 2564 ms | 1.57 ms | ~1600x |
|
|
83
280
|
|
|
84
281
|
Notes:
|
|
85
282
|
|