@svgrid/enterprise 3.1.1 → 3.1.2
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/dist/SvSheet.svelte +7 -1
- package/dist/cdn/svgrid-enterprise.svelte-external.js +10534 -10332
- package/dist/designer/assets/{index-DkhD2kIe.js → index-V_cMXdxt.js} +311 -311
- package/dist/designer/assets/validate-YxSrBF1H.js +1 -0
- package/dist/designer/index.html +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/node/studio.js +16 -4
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/server-row-model.d.ts +9 -0
- package/dist/server/server-row-model.js +120 -3
- package/dist/server/worker-data-source.d.ts +62 -0
- package/dist/server/worker-data-source.js +133 -0
- package/dist/studio/emit-project.js +8 -3
- package/dist/sveltekit/in-memory.d.ts +21 -1
- package/dist/sveltekit/in-memory.js +153 -105
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/version.d.ts +14 -1
- package/dist/version.js +14 -1
- package/dist/wc/sv-sheet-element.js +14 -9
- package/dist/wc/{validate-BqDthkNr.js → validate-JAuMsk6Z.js} +36 -36
- package/dist/worker/columnar.d.ts +42 -0
- package/dist/worker/columnar.js +410 -0
- package/dist/worker/index.d.ts +12 -0
- package/dist/worker/index.js +12 -0
- package/dist/worker/infer-schema.d.ts +21 -0
- package/dist/worker/infer-schema.js +34 -0
- package/dist/worker/protocol.d.ts +73 -0
- package/dist/worker/protocol.js +14 -0
- package/dist/worker/serve.d.ts +44 -0
- package/dist/worker/serve.js +100 -0
- package/package.json +7 -3
- package/dist/designer/assets/validate-C3B8QKsM.js +0 -1
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { planQuery } from '../sveltekit/query-plan.js';
|
|
2
|
+
import { asString, createInMemoryDataSource, matchesPredicate, sortRows } from '../sveltekit/in-memory.js';
|
|
3
|
+
const RESULT_CACHE_SIZE = 8;
|
|
4
|
+
const collator = new Intl.Collator();
|
|
5
|
+
export function createColumnarDataSource(initial, schema) {
|
|
6
|
+
const reference = createInMemoryDataSource(initial, schema, { cacheResults: true });
|
|
7
|
+
let store = reference.rows();
|
|
8
|
+
let columns = new Map();
|
|
9
|
+
const cache = new Map();
|
|
10
|
+
const fields = schema.fields.map((f) => f.field);
|
|
11
|
+
// Index of the next schema field `warm()` builds.
|
|
12
|
+
let warmed = 0;
|
|
13
|
+
function invalidate() {
|
|
14
|
+
store = reference.rows();
|
|
15
|
+
columns = new Map();
|
|
16
|
+
cache.clear();
|
|
17
|
+
warmed = 0;
|
|
18
|
+
}
|
|
19
|
+
// ---- columns ------------------------------------------------------------
|
|
20
|
+
function column(field) {
|
|
21
|
+
let col = columns.get(field);
|
|
22
|
+
if (col)
|
|
23
|
+
return col;
|
|
24
|
+
const n = store.length;
|
|
25
|
+
const codes = new Uint32Array(n);
|
|
26
|
+
const values = [];
|
|
27
|
+
const index = new Map();
|
|
28
|
+
for (let i = 0; i < n; i += 1) {
|
|
29
|
+
const v = store[i][field];
|
|
30
|
+
let code = index.get(v);
|
|
31
|
+
if (code === undefined) {
|
|
32
|
+
code = values.length;
|
|
33
|
+
index.set(v, code);
|
|
34
|
+
values.push(v);
|
|
35
|
+
}
|
|
36
|
+
codes[i] = code;
|
|
37
|
+
}
|
|
38
|
+
col = { codes, values };
|
|
39
|
+
columns.set(field, col);
|
|
40
|
+
return col;
|
|
41
|
+
}
|
|
42
|
+
function ranked(col) {
|
|
43
|
+
if (col.ranks !== undefined)
|
|
44
|
+
return col.ranks;
|
|
45
|
+
const { values } = col;
|
|
46
|
+
let numbers = 0;
|
|
47
|
+
let finite = 0;
|
|
48
|
+
for (const v of values) {
|
|
49
|
+
if (typeof v === 'number') {
|
|
50
|
+
numbers += 1;
|
|
51
|
+
if (Number.isFinite(v))
|
|
52
|
+
finite += 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// The reference compares two numbers by subtraction and anything else as
|
|
56
|
+
// text. All finite numbers, or no numbers at all, is a total order a
|
|
57
|
+
// ranking can reproduce; a mix (or NaN / Infinity) is not.
|
|
58
|
+
if (numbers > 0 && (finite !== values.length)) {
|
|
59
|
+
col.ranks = null;
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const byValue = values.map((_, code) => code);
|
|
63
|
+
const text = numbers === 0 ? values.map(asString) : null;
|
|
64
|
+
const cmp = text
|
|
65
|
+
? (a, b) => collator.compare(text[a], text[b])
|
|
66
|
+
: (a, b) => values[a] - values[b];
|
|
67
|
+
byValue.sort(cmp);
|
|
68
|
+
const ranks = new Uint32Array(values.length);
|
|
69
|
+
let rank = 0;
|
|
70
|
+
for (let i = 0; i < byValue.length; i += 1) {
|
|
71
|
+
// Values the comparison calls equal share a rank, so the stable sort
|
|
72
|
+
// keeps them in row order, as the reference's index tiebreak does.
|
|
73
|
+
if (i > 0 && cmp(byValue[i - 1], byValue[i]) !== 0)
|
|
74
|
+
rank += 1;
|
|
75
|
+
ranks[byValue[i]] = rank;
|
|
76
|
+
}
|
|
77
|
+
col.ranks = ranks;
|
|
78
|
+
col.rankCount = values.length ? rank + 1 : 0;
|
|
79
|
+
return ranks;
|
|
80
|
+
}
|
|
81
|
+
function keyed(col) {
|
|
82
|
+
if (!col.keyOf) {
|
|
83
|
+
const keyOf = new Uint32Array(col.values.length);
|
|
84
|
+
const keys = [];
|
|
85
|
+
const index = new Map();
|
|
86
|
+
col.values.forEach((v, code) => {
|
|
87
|
+
const key = asString(v);
|
|
88
|
+
let k = index.get(key);
|
|
89
|
+
if (k === undefined) {
|
|
90
|
+
k = keys.length;
|
|
91
|
+
index.set(key, k);
|
|
92
|
+
keys.push(key);
|
|
93
|
+
}
|
|
94
|
+
keyOf[code] = k;
|
|
95
|
+
});
|
|
96
|
+
col.keyOf = keyOf;
|
|
97
|
+
col.keys = keys;
|
|
98
|
+
}
|
|
99
|
+
return { keyOf: col.keyOf, keys: col.keys };
|
|
100
|
+
}
|
|
101
|
+
function numbers(col) {
|
|
102
|
+
if (!col.nums) {
|
|
103
|
+
const nums = new Float64Array(col.values.length);
|
|
104
|
+
col.values.forEach((v, code) => {
|
|
105
|
+
const n = Number(v);
|
|
106
|
+
nums[code] = Number.isFinite(n) ? n : NaN;
|
|
107
|
+
});
|
|
108
|
+
col.nums = nums;
|
|
109
|
+
}
|
|
110
|
+
return col.nums;
|
|
111
|
+
}
|
|
112
|
+
// ---- filtering ----------------------------------------------------------
|
|
113
|
+
/** Row indexes passing `where` and the search, in store order. */
|
|
114
|
+
function select(where, search) {
|
|
115
|
+
const n = store.length;
|
|
116
|
+
const tests = [];
|
|
117
|
+
for (const p of where) {
|
|
118
|
+
const col = column(p.field);
|
|
119
|
+
const pass = new Uint8Array(col.values.length);
|
|
120
|
+
col.values.forEach((v, code) => {
|
|
121
|
+
pass[code] = matchesPredicate({ [p.field]: v }, p) ? 1 : 0;
|
|
122
|
+
});
|
|
123
|
+
tests.push({ codes: col.codes, pass });
|
|
124
|
+
}
|
|
125
|
+
const searched = [];
|
|
126
|
+
if (search) {
|
|
127
|
+
const term = search.term.toLowerCase();
|
|
128
|
+
for (const field of search.fields) {
|
|
129
|
+
const col = column(field);
|
|
130
|
+
const pass = new Uint8Array(col.values.length);
|
|
131
|
+
col.values.forEach((v, code) => {
|
|
132
|
+
pass[code] = asString(v).toLowerCase().includes(term) ? 1 : 0;
|
|
133
|
+
});
|
|
134
|
+
searched.push({ codes: col.codes, pass });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (tests.length === 0 && !search) {
|
|
138
|
+
const all = new Uint32Array(n);
|
|
139
|
+
for (let i = 0; i < n; i += 1)
|
|
140
|
+
all[i] = i;
|
|
141
|
+
return all;
|
|
142
|
+
}
|
|
143
|
+
const out = new Uint32Array(n);
|
|
144
|
+
let m = 0;
|
|
145
|
+
rows: for (let i = 0; i < n; i += 1) {
|
|
146
|
+
if (search) {
|
|
147
|
+
let hit = false;
|
|
148
|
+
for (const s of searched) {
|
|
149
|
+
if (s.pass[s.codes[i]]) {
|
|
150
|
+
hit = true;
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!hit)
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
for (const t of tests)
|
|
158
|
+
if (!t.pass[t.codes[i]])
|
|
159
|
+
continue rows;
|
|
160
|
+
out[m++] = i;
|
|
161
|
+
}
|
|
162
|
+
return out.slice(0, m);
|
|
163
|
+
}
|
|
164
|
+
// ---- sorting ------------------------------------------------------------
|
|
165
|
+
/** Stable counting sort of `idx` by one column; LSD over several keys. */
|
|
166
|
+
function sortIndexes(idx, orderBy) {
|
|
167
|
+
const keys = [];
|
|
168
|
+
for (const o of orderBy) {
|
|
169
|
+
const col = column(o.field);
|
|
170
|
+
const ranks = ranked(col);
|
|
171
|
+
if (!ranks)
|
|
172
|
+
return null;
|
|
173
|
+
keys.push({ codes: col.codes, ranks, count: col.rankCount, desc: o.desc });
|
|
174
|
+
}
|
|
175
|
+
let cur = idx;
|
|
176
|
+
let next = new Uint32Array(idx.length);
|
|
177
|
+
// Least significant key first: each pass is stable, so earlier keys win.
|
|
178
|
+
for (let k = keys.length - 1; k >= 0; k -= 1) {
|
|
179
|
+
const { codes, ranks, count, desc } = keys[k];
|
|
180
|
+
const counts = new Uint32Array(count + 1);
|
|
181
|
+
for (let i = 0; i < cur.length; i += 1) {
|
|
182
|
+
const r = ranks[codes[cur[i]]];
|
|
183
|
+
counts[(desc ? count - 1 - r : r) + 1] += 1;
|
|
184
|
+
}
|
|
185
|
+
for (let r = 1; r <= count; r += 1)
|
|
186
|
+
counts[r] += counts[r - 1];
|
|
187
|
+
for (let i = 0; i < cur.length; i += 1) {
|
|
188
|
+
const row = cur[i];
|
|
189
|
+
const r = ranks[codes[row]];
|
|
190
|
+
next[counts[desc ? count - 1 - r : r]++] = row;
|
|
191
|
+
}
|
|
192
|
+
const swap = cur === idx ? new Uint32Array(idx.length) : cur;
|
|
193
|
+
cur = next;
|
|
194
|
+
next = swap;
|
|
195
|
+
}
|
|
196
|
+
return cur;
|
|
197
|
+
}
|
|
198
|
+
function accumulate(idx,
|
|
199
|
+
/** The group of the i-th row of `idx`, or null for one group. */
|
|
200
|
+
rowGroup, groupCount, aggregations) {
|
|
201
|
+
const out = new Map();
|
|
202
|
+
for (const agg of aggregations) {
|
|
203
|
+
if (out.has(agg.field))
|
|
204
|
+
continue;
|
|
205
|
+
const col = column(agg.field);
|
|
206
|
+
const codes = col.codes;
|
|
207
|
+
const nums = numbers(col);
|
|
208
|
+
const acc = {
|
|
209
|
+
count: new Float64Array(groupCount),
|
|
210
|
+
sum: new Float64Array(groupCount),
|
|
211
|
+
min: new Float64Array(groupCount).fill(Infinity),
|
|
212
|
+
max: new Float64Array(groupCount).fill(-Infinity),
|
|
213
|
+
n: new Float64Array(groupCount),
|
|
214
|
+
};
|
|
215
|
+
for (let i = 0; i < idx.length; i += 1) {
|
|
216
|
+
const row = idx[i];
|
|
217
|
+
const g = rowGroup ? rowGroup[i] : 0;
|
|
218
|
+
acc.count[g] += 1;
|
|
219
|
+
const v = nums[codes[row]];
|
|
220
|
+
if (v !== v)
|
|
221
|
+
continue; // NaN: not a finite number
|
|
222
|
+
acc.n[g] += 1;
|
|
223
|
+
acc.sum[g] += v;
|
|
224
|
+
if (v < acc.min[g])
|
|
225
|
+
acc.min[g] = v;
|
|
226
|
+
if (v > acc.max[g])
|
|
227
|
+
acc.max[g] = v;
|
|
228
|
+
}
|
|
229
|
+
out.set(agg.field, acc);
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
/** The reference's `aggregate`, read off the accumulators. */
|
|
234
|
+
function valueOf(acc, g, fn) {
|
|
235
|
+
if (fn === 'count')
|
|
236
|
+
return acc.count[g];
|
|
237
|
+
if (acc.n[g] === 0)
|
|
238
|
+
return null;
|
|
239
|
+
switch (fn) {
|
|
240
|
+
case 'sum':
|
|
241
|
+
return acc.sum[g];
|
|
242
|
+
case 'avg':
|
|
243
|
+
return acc.sum[g] / acc.n[g];
|
|
244
|
+
case 'min':
|
|
245
|
+
return acc.min[g];
|
|
246
|
+
case 'max':
|
|
247
|
+
return acc.max[g];
|
|
248
|
+
default:
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function totals(idx, plan) {
|
|
253
|
+
const aggs = plan.aggregations ?? [];
|
|
254
|
+
const acc = accumulate(idx, null, 1, aggs);
|
|
255
|
+
const out = {};
|
|
256
|
+
for (const agg of aggs)
|
|
257
|
+
out[agg.field] = valueOf(acc.get(agg.field), 0, agg.fn);
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
// ---- answering ----------------------------------------------------------
|
|
261
|
+
function supported(plan) {
|
|
262
|
+
return !plan.expression && !plan.pivotBy?.length;
|
|
263
|
+
}
|
|
264
|
+
function prepare(plan) {
|
|
265
|
+
const key = JSON.stringify({ ...plan, limit: 0, offset: 0 });
|
|
266
|
+
const hit = cache.get(key);
|
|
267
|
+
if (hit) {
|
|
268
|
+
cache.delete(key);
|
|
269
|
+
cache.set(key, hit);
|
|
270
|
+
return hit;
|
|
271
|
+
}
|
|
272
|
+
const prepared = run(plan);
|
|
273
|
+
if (!prepared)
|
|
274
|
+
return null;
|
|
275
|
+
cache.set(key, prepared);
|
|
276
|
+
if (cache.size > RESULT_CACHE_SIZE)
|
|
277
|
+
cache.delete(cache.keys().next().value);
|
|
278
|
+
return prepared;
|
|
279
|
+
}
|
|
280
|
+
function run(plan) {
|
|
281
|
+
const idx = select(plan.where, plan.search);
|
|
282
|
+
const out = {};
|
|
283
|
+
if (plan.grandTotal) {
|
|
284
|
+
const filtersOnly = plan.where.slice(0, plan.where.length - (plan.pathPredicates ?? 0));
|
|
285
|
+
const all = filtersOnly.length === plan.where.length ? idx : select(filtersOnly, plan.search);
|
|
286
|
+
out.grandTotal = totals(all, plan);
|
|
287
|
+
}
|
|
288
|
+
if (!plan.groupBy) {
|
|
289
|
+
const order = plan.orderBy.length ? sortIndexes(idx, plan.orderBy) : idx;
|
|
290
|
+
if (!order)
|
|
291
|
+
return null;
|
|
292
|
+
out.order = order;
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
295
|
+
// Groups in order of first appearance, as the reference's Map buckets are.
|
|
296
|
+
const groupField = plan.groupBy;
|
|
297
|
+
const gcol = column(groupField);
|
|
298
|
+
const { keyOf, keys } = keyed(gcol);
|
|
299
|
+
const slot = new Int32Array(keys.length).fill(-1);
|
|
300
|
+
const groupKeys = [];
|
|
301
|
+
const gcodes = gcol.codes;
|
|
302
|
+
const rowGroup = new Uint32Array(idx.length);
|
|
303
|
+
for (let i = 0; i < idx.length; i += 1) {
|
|
304
|
+
const k = keyOf[gcodes[idx[i]]];
|
|
305
|
+
let g = slot[k];
|
|
306
|
+
if (g < 0) {
|
|
307
|
+
g = groupKeys.length;
|
|
308
|
+
slot[k] = g;
|
|
309
|
+
groupKeys.push(k);
|
|
310
|
+
}
|
|
311
|
+
rowGroup[i] = g;
|
|
312
|
+
}
|
|
313
|
+
const groupCount = groupKeys.length;
|
|
314
|
+
const acc = accumulate(idx, rowGroup, groupCount, plan.aggregations ?? []);
|
|
315
|
+
const childCounts = new Float64Array(groupCount);
|
|
316
|
+
if (plan.childGroupBy) {
|
|
317
|
+
const ccol = column(plan.childGroupBy);
|
|
318
|
+
const { keyOf: childKeyOf, keys: childKeys } = keyed(ccol);
|
|
319
|
+
const seen = new Set();
|
|
320
|
+
for (let i = 0; i < idx.length; i += 1) {
|
|
321
|
+
const g = rowGroup[i];
|
|
322
|
+
const c = childKeyOf[ccol.codes[idx[i]]];
|
|
323
|
+
const pair = g * childKeys.length + c;
|
|
324
|
+
if (!seen.has(pair)) {
|
|
325
|
+
seen.add(pair);
|
|
326
|
+
childCounts[g] += 1;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
for (let i = 0; i < idx.length; i += 1)
|
|
332
|
+
childCounts[rowGroup[i]] += 1;
|
|
333
|
+
}
|
|
334
|
+
const groups = groupKeys.map((k, g) => {
|
|
335
|
+
const row = { [groupField]: keys[k] };
|
|
336
|
+
for (const agg of plan.aggregations ?? [])
|
|
337
|
+
row[agg.field] = valueOf(acc.get(agg.field), g, agg.fn);
|
|
338
|
+
row.childCount = childCounts[g];
|
|
339
|
+
return row;
|
|
340
|
+
});
|
|
341
|
+
const produced = new Set([groupField, ...(plan.aggregations ?? []).map((a) => a.field)]);
|
|
342
|
+
const groupOrder = plan.orderBy.filter((o) => produced.has(o.field));
|
|
343
|
+
out.groups = groupOrder.length
|
|
344
|
+
? sortRows(groups, groupOrder)
|
|
345
|
+
: sortRows(groups, [{ field: groupField, desc: false }]);
|
|
346
|
+
return out;
|
|
347
|
+
}
|
|
348
|
+
const counts = { columnar: 0, reference: 0 };
|
|
349
|
+
return {
|
|
350
|
+
async getRows(request) {
|
|
351
|
+
const plan = planQuery(schema, request);
|
|
352
|
+
const prepared = supported(plan) ? prepare(plan) : null;
|
|
353
|
+
if (!prepared) {
|
|
354
|
+
counts.reference += 1;
|
|
355
|
+
return reference.getRows(request);
|
|
356
|
+
}
|
|
357
|
+
counts.columnar += 1;
|
|
358
|
+
const from = plan.offset;
|
|
359
|
+
const to = plan.offset + plan.limit;
|
|
360
|
+
const total = prepared.groups ? prepared.groups.length : prepared.order.length;
|
|
361
|
+
const rows = prepared.groups
|
|
362
|
+
? prepared.groups.slice(from, to)
|
|
363
|
+
: Array.from(prepared.order.subarray(from, Math.min(to, total)), (i) => store[i]);
|
|
364
|
+
return {
|
|
365
|
+
rows,
|
|
366
|
+
rowCount: total,
|
|
367
|
+
appliedExpression: false,
|
|
368
|
+
...(prepared.grandTotal !== undefined ? { grandTotal: prepared.grandTotal } : {}),
|
|
369
|
+
};
|
|
370
|
+
},
|
|
371
|
+
async createRow(input) {
|
|
372
|
+
const row = await reference.createRow(input);
|
|
373
|
+
invalidate();
|
|
374
|
+
return row;
|
|
375
|
+
},
|
|
376
|
+
async updateRow(id, patch) {
|
|
377
|
+
const row = await reference.updateRow(id, patch);
|
|
378
|
+
invalidate();
|
|
379
|
+
return row;
|
|
380
|
+
},
|
|
381
|
+
async deleteRow(id) {
|
|
382
|
+
await reference.deleteRow(id);
|
|
383
|
+
invalidate();
|
|
384
|
+
},
|
|
385
|
+
async updateWhere(filterModel, patch, selection) {
|
|
386
|
+
const changed = await reference.updateWhere(filterModel, patch, selection);
|
|
387
|
+
invalidate();
|
|
388
|
+
return changed;
|
|
389
|
+
},
|
|
390
|
+
getAggregate: (request) => reference.getAggregate(request),
|
|
391
|
+
rows: () => store,
|
|
392
|
+
stats: () => ({ ...counts }),
|
|
393
|
+
warm() {
|
|
394
|
+
while (warmed < fields.length) {
|
|
395
|
+
const field = fields[warmed++];
|
|
396
|
+
const existing = columns.get(field);
|
|
397
|
+
if (existing?.ranks !== undefined && existing.keyOf && existing.nums)
|
|
398
|
+
continue;
|
|
399
|
+
// Everything a sort, a grouping and an aggregate read, so none of
|
|
400
|
+
// them pays for it on the first click.
|
|
401
|
+
const col = column(field);
|
|
402
|
+
ranked(col);
|
|
403
|
+
keyed(col);
|
|
404
|
+
numbers(col);
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
},
|
|
409
|
+
};
|
|
410
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@svgrid/enterprise/worker` - what runs INSIDE the worker behind
|
|
3
|
+
* `createWorkerDataSource`. Import it from your worker file and nowhere else:
|
|
4
|
+
* it has no Svelte and no DOM in its import graph, so the worker bundle stays
|
|
5
|
+
* the in-memory source and little more.
|
|
6
|
+
*
|
|
7
|
+
* The main-thread half, `createWorkerDataSource`, is on the package root and
|
|
8
|
+
* on `@svgrid/enterprise/server`.
|
|
9
|
+
*/
|
|
10
|
+
export { serveWorkerDataSource, type ServeWorkerDataSourceOptions, type WorkerScopeLike, } from './serve.js';
|
|
11
|
+
export { inferSchemaFromRows, type InferSchemaOptions } from './infer-schema.js';
|
|
12
|
+
export { createColumnarDataSource, type ColumnarDataSource } from './columnar.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@svgrid/enterprise/worker` - what runs INSIDE the worker behind
|
|
3
|
+
* `createWorkerDataSource`. Import it from your worker file and nowhere else:
|
|
4
|
+
* it has no Svelte and no DOM in its import graph, so the worker bundle stays
|
|
5
|
+
* the in-memory source and little more.
|
|
6
|
+
*
|
|
7
|
+
* The main-thread half, `createWorkerDataSource`, is on the package root and
|
|
8
|
+
* on `@svgrid/enterprise/server`.
|
|
9
|
+
*/
|
|
10
|
+
export { serveWorkerDataSource, } from './serve.js';
|
|
11
|
+
export { inferSchemaFromRows } from './infer-schema.js';
|
|
12
|
+
export { createColumnarDataSource } from './columnar.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An `EntitySchema` read off the rows themselves, for a worker data source
|
|
3
|
+
* that was handed plain objects. The in-memory source only filters, sorts and
|
|
4
|
+
* groups on fields its schema declares, and the type decides how a request's
|
|
5
|
+
* string values are compared: `'35'` is a number for a number field and text
|
|
6
|
+
* for a text one.
|
|
7
|
+
*/
|
|
8
|
+
import type { RowData } from '@svgrid/grid';
|
|
9
|
+
import type { EntitySchema } from '../schema.js';
|
|
10
|
+
export type InferSchemaOptions = {
|
|
11
|
+
/** The row id field. Default: `id` when the rows have one, else the first field. */
|
|
12
|
+
idField?: string;
|
|
13
|
+
/** The schema's name. Default `rows`. */
|
|
14
|
+
name?: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A field is `number` or `boolean` when every non-empty sampled value is one,
|
|
18
|
+
* and `text` otherwise. Dates come out as `text`: send them as ISO strings,
|
|
19
|
+
* which sort and compare correctly as text, or as epoch numbers.
|
|
20
|
+
*/
|
|
21
|
+
export declare function inferSchemaFromRows<TData extends RowData>(rows: ReadonlyArray<TData>, options?: InferSchemaOptions): EntitySchema<TData>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** Rows read to decide each field's type. */
|
|
2
|
+
const SAMPLE_SIZE = 200;
|
|
3
|
+
/**
|
|
4
|
+
* A field is `number` or `boolean` when every non-empty sampled value is one,
|
|
5
|
+
* and `text` otherwise. Dates come out as `text`: send them as ISO strings,
|
|
6
|
+
* which sort and compare correctly as text, or as epoch numbers.
|
|
7
|
+
*/
|
|
8
|
+
export function inferSchemaFromRows(rows, options = {}) {
|
|
9
|
+
const sample = rows.slice(0, SAMPLE_SIZE);
|
|
10
|
+
const seen = new Map();
|
|
11
|
+
for (const row of sample) {
|
|
12
|
+
for (const [field, value] of Object.entries(row)) {
|
|
13
|
+
let kinds = seen.get(field);
|
|
14
|
+
if (!kinds)
|
|
15
|
+
seen.set(field, (kinds = new Set()));
|
|
16
|
+
if (value != null && value !== '')
|
|
17
|
+
kinds.add(typeof value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const fields = [...seen].map(([field, kinds]) => {
|
|
21
|
+
let type = 'text';
|
|
22
|
+
if (kinds.size === 1 && kinds.has('number'))
|
|
23
|
+
type = 'number';
|
|
24
|
+
else if (kinds.size === 1 && kinds.has('boolean'))
|
|
25
|
+
type = 'boolean';
|
|
26
|
+
return { field: field, type };
|
|
27
|
+
});
|
|
28
|
+
const idField = options.idField ?? (seen.has('id') ? 'id' : fields[0]?.field) ?? 'id';
|
|
29
|
+
return {
|
|
30
|
+
name: options.name ?? 'rows',
|
|
31
|
+
idField: idField,
|
|
32
|
+
fields,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The messages between `createWorkerDataSource` (main thread) and
|
|
3
|
+
* `serveWorkerDataSource` (inside the worker). Internal: both ends ship in
|
|
4
|
+
* the same package version, so the shape can change without a migration.
|
|
5
|
+
*
|
|
6
|
+
* Every message carries `channel`, so a worker that also answers messages of
|
|
7
|
+
* its own can host the data source beside them; each end ignores anything
|
|
8
|
+
* without it.
|
|
9
|
+
*/
|
|
10
|
+
import type { EntitySchema } from '../schema.js';
|
|
11
|
+
export declare const CHANNEL: "svgrid-worker-data-source";
|
|
12
|
+
/** The data source methods the client may call across the boundary. */
|
|
13
|
+
export type WorkerMethod = 'getRows' | 'createRow' | 'updateRow' | 'deleteRow' | 'updateWhere' | 'getAggregate';
|
|
14
|
+
/** Main thread to worker. */
|
|
15
|
+
export type ToWorker =
|
|
16
|
+
/** Replace the dataset. Answered with a `result` carrying the row count. */
|
|
17
|
+
{
|
|
18
|
+
channel: typeof CHANNEL;
|
|
19
|
+
kind: 'load';
|
|
20
|
+
id: number;
|
|
21
|
+
rows: ReadonlyArray<unknown>;
|
|
22
|
+
schema?: EntitySchema;
|
|
23
|
+
idField?: string;
|
|
24
|
+
} | {
|
|
25
|
+
channel: typeof CHANNEL;
|
|
26
|
+
kind: 'call';
|
|
27
|
+
id: number;
|
|
28
|
+
method: WorkerMethod;
|
|
29
|
+
args: unknown[];
|
|
30
|
+
}
|
|
31
|
+
/** Drop a queued call: its block was evicted, or the sort or filter moved on. */
|
|
32
|
+
| {
|
|
33
|
+
channel: typeof CHANNEL;
|
|
34
|
+
kind: 'cancel';
|
|
35
|
+
id: number;
|
|
36
|
+
};
|
|
37
|
+
/** An error as it crosses the boundary: an `Error` does not survive cloning intact everywhere. */
|
|
38
|
+
export type WorkerError = {
|
|
39
|
+
name: string;
|
|
40
|
+
message: string;
|
|
41
|
+
};
|
|
42
|
+
/** Worker to main thread. */
|
|
43
|
+
export type FromWorker =
|
|
44
|
+
/** The worker holds data and answers calls. Sent after every load. */
|
|
45
|
+
{
|
|
46
|
+
channel: typeof CHANNEL;
|
|
47
|
+
kind: 'ready';
|
|
48
|
+
rowCount: number;
|
|
49
|
+
}
|
|
50
|
+
/** The worker's own `rows` loader threw: nothing will ever be answered. */
|
|
51
|
+
| {
|
|
52
|
+
channel: typeof CHANNEL;
|
|
53
|
+
kind: 'failed';
|
|
54
|
+
error: WorkerError;
|
|
55
|
+
} | {
|
|
56
|
+
channel: typeof CHANNEL;
|
|
57
|
+
kind: 'result';
|
|
58
|
+
id: number;
|
|
59
|
+
ok: true;
|
|
60
|
+
value: unknown;
|
|
61
|
+
} | {
|
|
62
|
+
channel: typeof CHANNEL;
|
|
63
|
+
kind: 'result';
|
|
64
|
+
id: number;
|
|
65
|
+
ok: false;
|
|
66
|
+
error: WorkerError;
|
|
67
|
+
};
|
|
68
|
+
export declare function toWorkerError(error: unknown): WorkerError;
|
|
69
|
+
export declare function fromWorkerError(error: WorkerError): Error;
|
|
70
|
+
export declare function isChannelMessage(data: unknown): data is {
|
|
71
|
+
channel: typeof CHANNEL;
|
|
72
|
+
kind: string;
|
|
73
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const CHANNEL = 'svgrid-worker-data-source';
|
|
2
|
+
export function toWorkerError(error) {
|
|
3
|
+
if (error instanceof Error)
|
|
4
|
+
return { name: error.name, message: error.message };
|
|
5
|
+
return { name: 'Error', message: String(error) };
|
|
6
|
+
}
|
|
7
|
+
export function fromWorkerError(error) {
|
|
8
|
+
const out = new Error(error.message);
|
|
9
|
+
out.name = error.name;
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
export function isChannelMessage(data) {
|
|
13
|
+
return typeof data === 'object' && data !== null && data.channel === CHANNEL;
|
|
14
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The worker half of `createWorkerDataSource`: holds the rows, answers the
|
|
3
|
+
* grid's requests with the columnar engine (`createColumnarDataSource`, which
|
|
4
|
+
* answers exactly as the in-memory reference source does), and sends back only
|
|
5
|
+
* the rows each request asks for. While no request is waiting, it builds the
|
|
6
|
+
* next column's index, so the first sort on any column finds it ready.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* // grid.worker.ts
|
|
10
|
+
* import { serveWorkerDataSource } from '@svgrid/enterprise/worker'
|
|
11
|
+
* serveWorkerDataSource()
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Calls run one at a time, each in its own task, so a `cancel` that arrives
|
|
15
|
+
* while earlier calls are running still reaches the queue before the call it
|
|
16
|
+
* names: a fast scroll skips the blocks it scrolled past instead of computing
|
|
17
|
+
* every one of them.
|
|
18
|
+
*/
|
|
19
|
+
import type { RowData } from '@svgrid/grid';
|
|
20
|
+
import type { EntitySchema } from '../schema.js';
|
|
21
|
+
/** The part of a worker's global scope (or a `MessagePort`) this needs. */
|
|
22
|
+
export type WorkerScopeLike = {
|
|
23
|
+
postMessage(message: unknown): void;
|
|
24
|
+
addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
|
|
25
|
+
removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
|
|
26
|
+
};
|
|
27
|
+
export type ServeWorkerDataSourceOptions<TData extends RowData> = {
|
|
28
|
+
/**
|
|
29
|
+
* The rows, loaded inside the worker: an array, or a function that fetches
|
|
30
|
+
* or builds one. Loading here means a large dataset never passes through
|
|
31
|
+
* the main thread. Omit it to wait for the rows the main thread sends
|
|
32
|
+
* (`createWorkerDataSource(worker, { rows })`).
|
|
33
|
+
*/
|
|
34
|
+
rows?: ReadonlyArray<TData> | (() => ReadonlyArray<TData> | Promise<ReadonlyArray<TData>>);
|
|
35
|
+
/** Which fields can be filtered, sorted and grouped, and their types. Default: read off the rows. */
|
|
36
|
+
schema?: EntitySchema<TData>;
|
|
37
|
+
/** The row id field, when the schema is inferred. See `inferSchemaFromRows`. */
|
|
38
|
+
idField?: string;
|
|
39
|
+
/** Where to listen. Default: the worker's global scope. */
|
|
40
|
+
scope?: WorkerScopeLike;
|
|
41
|
+
};
|
|
42
|
+
export declare function serveWorkerDataSource<TData extends RowData>(options?: ServeWorkerDataSourceOptions<TData>): {
|
|
43
|
+
dispose(): void;
|
|
44
|
+
};
|