vectrojs 0.0.1 → 0.1.1

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +828 -4
  3. package/bin/vectro.mjs +82 -0
  4. package/dist/vectro.wasm +0 -0
  5. package/mcp/server.mjs +288 -0
  6. package/package.json +84 -6
  7. package/src/arrow.mjs +905 -0
  8. package/src/bitpack.mjs +135 -0
  9. package/src/browser.mjs +129 -0
  10. package/src/bytes.mjs +60 -0
  11. package/src/csv-kernel.mjs +205 -0
  12. package/src/csv.mjs +270 -0
  13. package/src/dataset.mjs +1763 -0
  14. package/src/dates.mjs +139 -0
  15. package/src/emitter.mjs +109 -0
  16. package/src/engine.mjs +881 -0
  17. package/src/files-browser.mjs +104 -0
  18. package/src/files-node.mjs +59 -0
  19. package/src/frames.mjs +191 -0
  20. package/src/fuse.mjs +512 -0
  21. package/src/interp.mjs +305 -0
  22. package/src/join.mjs +534 -0
  23. package/src/kernels.mjs +312 -0
  24. package/src/ndjson.mjs +136 -0
  25. package/src/node.mjs +120 -0
  26. package/src/pack.mjs +114 -0
  27. package/src/parallel-sort.mjs +99 -0
  28. package/src/partition-kernels.mjs +388 -0
  29. package/src/partition.mjs +395 -0
  30. package/src/plan.mjs +551 -0
  31. package/src/portable.mjs +21 -0
  32. package/src/rank-kernels.mjs +81 -0
  33. package/src/rows.mjs +91 -0
  34. package/src/serve/catalog.mjs +148 -0
  35. package/src/serve/flight.mjs +270 -0
  36. package/src/serve/formats.mjs +87 -0
  37. package/src/serve/http.mjs +182 -0
  38. package/src/serve/index.mjs +9 -0
  39. package/src/serve/odata.mjs +291 -0
  40. package/src/serve/pg.mjs +588 -0
  41. package/src/serve/pgcatalog.mjs +220 -0
  42. package/src/serve/pgtypes.mjs +199 -0
  43. package/src/sketch.mjs +75 -0
  44. package/src/sort-kernels.mjs +117 -0
  45. package/src/sort.mjs +102 -0
  46. package/src/split-worker-core.mjs +252 -0
  47. package/src/split-worker-node.mjs +8 -0
  48. package/src/split-worker.mjs +7 -0
  49. package/src/split.mjs +741 -0
  50. package/src/sql.mjs +1558 -0
  51. package/src/sqltext.mjs +132 -0
  52. package/src/static-kernels.mjs +164 -0
  53. package/src/stream-worker.mjs +553 -0
  54. package/src/table-kernels.mjs +323 -0
  55. package/src/table.mjs +2247 -0
  56. package/src/textmap.mjs +159 -0
  57. package/src/vectors.mjs +138 -0
  58. package/src/wasm.mjs +254 -0
  59. package/src/window.mjs +160 -0
  60. package/src/worker-browser.mjs +21 -0
  61. package/src/worker-loop.mjs +106 -0
  62. package/src/worker.mjs +22 -0
  63. package/src/zones.mjs +75 -0
@@ -0,0 +1,312 @@
1
+ // Kernels run identically on the main thread and on every worker. Work is
2
+ // split into units that threads claim one at a time (see runUnits).
3
+
4
+ import { csvScan, csvParse } from './csv-kernel.mjs';
5
+ import { countPattern } from './bytes.mjs';
6
+
7
+ export const OP = {
8
+ EXIT: 0, SUM: 1, MIN: 2, MAX: 3, ADD: 4, SCALE: 5, DOT: 6, HEAVY: 7,
9
+ COUNT_GT: 8, SORT_CHUNK: 9, MERGE: 10, COUNT_BYTES: 11, COPY: 12, FUSED: 13, GROUP: 14, COMPACT: 15, CSV_SCAN: 16, CSV_PARSE: 17, GROUP_RANGE: 18, UNITS: 19, READ: 20, TASKS: 21,
10
+ };
11
+
12
+ // Layout of the shared control block (Int32 slots).
13
+ export const C = { SEQ: 0, DONE: 1, OP: 2, N: 3, THREADS: 4, NEXT: 5, WASM: 6, KERNEL: 7, NOUT: 8, NSCAL: 9, ERROR: 10, ARGS: 11 };
14
+ export const MAX_ARGS = 256; // each array arg = [bufferId, byteOffset, length, typeCode]
15
+ export const WASM_MEMORY = 0; // bufferId of the engine's WebAssembly memory
16
+ // SEQ holds this while the main thread rewrites the task (a seqlock): a
17
+ // worker that read the task during the rewrite sees SEQ change and skips it.
18
+ export const SEQ_WRITING = -1;
19
+
20
+ export const TYPES = [
21
+ Float64Array, Float32Array, Int32Array, Uint32Array,
22
+ Int16Array, Uint16Array, Int8Array, Uint8Array,
23
+ ];
24
+
25
+ export const chunkStart = (i, T, n) => Math.floor((i * n) / T);
26
+
27
+ // Work is cut into many small blocks that threads claim one at a time
28
+ // (dynamic scheduling). Fast cores end up doing more blocks than slow ones,
29
+ // and a thread that gets paused by the OS doesn't hold everyone up.
30
+ const BLOCK = 16_384;
31
+ const blockCount = (n, T) => Math.max(1, Math.min(Math.ceil(n / BLOCK), T * 16));
32
+
33
+ // Offsets are read as unsigned so arrays can live anywhere in 4GB of memory.
34
+ // bufferFor(id, bytesNeeded) returns the buffer an array lives in.
35
+ export function decodeArgs(ctrl, bufferFor) {
36
+ const u = new Uint32Array(ctrl.buffer);
37
+ const A = [];
38
+ for (let k = 0; k < MAX_ARGS; k++) {
39
+ const base = C.ARGS + k * 4;
40
+ const id = ctrl[base];
41
+ if (id < 0) { A.push(null); continue; }
42
+ const Type = TYPES[u[base + 3]];
43
+ A.push(new Type(bufferFor(id, u[base + 1] + u[base + 2] * Type.BYTES_PER_ELEMENT), u[base + 1], u[base + 2]));
44
+ }
45
+ return A;
46
+ }
47
+
48
+ // Work is handed out as numbered units (blocks, sort chunks or merge pairs).
49
+ // NEXT holds [12-bit task tag | 20-bit next unit], so a thread that wakes up
50
+ // late and still holds an old task can never claim a unit of a newer one.
51
+ export const TAG_SHIFT = 20;
52
+ export const tagOf = (seq) => seq & 0xfff;
53
+
54
+ export function unitCount(op, T, n, sc) {
55
+ if (op === OP.CSV_SCAN || op === OP.CSV_PARSE || op === OP.READ) return n; // n = number of chunks (READ: of file reads)
56
+ if (op === OP.SORT_CHUNK) return sc[1];
57
+ if (op === OP.GROUP_RANGE) return T; // a range of groups per thread
58
+ if (op === OP.TASKS) return n; // n tasks (e.g. one per partition)
59
+ if (op === OP.MERGE) return Math.ceil(sc[1] / (2 * sc[0]));
60
+ return blockCount(n, T);
61
+ }
62
+ export const maxUnits = (T) => T * 16;
63
+ export const MAX_OUTPUTS = 8; // reductions per fused kernel
64
+
65
+ function claim(ctrl, tag, units) {
66
+ for (;;) {
67
+ const v = Atomics.load(ctrl, C.NEXT);
68
+ if (v >>> TAG_SHIFT !== tag) return -1; // a newer task has started
69
+ const u = v & 0xfffff;
70
+ if (u >= units) return -1; // nothing left
71
+ if (Atomics.compareExchange(ctrl, C.NEXT, v, v + 1) === v) return u;
72
+ }
73
+ }
74
+
75
+ // Claim and run units until none are left. Each unit's result goes in its own
76
+ // slot of res, so reductions combine in the same order on every run.
77
+ // Callers never wait for threads, only for units: a sleeping thread that wakes
78
+ // up after the work is done simply finds nothing to claim.
79
+ // F is this thread's instance of a generated kernel (FUSED, GROUP, COMPACT);
80
+ // self is this thread's index (0 = calling thread). If a unit throws, the
81
+ // error goes to report() and the ERROR flag is set; the unit still counts as
82
+ // done, so the caller never waits forever.
83
+ export function runUnits(op, T, n, A, sc, ctrl, W, res, tag, F, self = 0, report = null) {
84
+ const units = unitCount(op, T, n, sc);
85
+ for (let u; (u = claim(ctrl, tag, units)) >= 0; ) {
86
+ try {
87
+ runUnit(op, u, units, T, n, A, sc, ctrl, W, res, F, self);
88
+ } catch (err) {
89
+ Atomics.store(ctrl, C.ERROR, 1);
90
+ if (report) report(err); else throw err;
91
+ } finally {
92
+ if (Atomics.add(ctrl, C.DONE, 1) + 1 === units) Atomics.notify(ctrl, C.DONE);
93
+ }
94
+ }
95
+ }
96
+
97
+ function runUnit(op, u, units, T, n, A, sc, ctrl, W, res, F, self) {
98
+ if (op === OP.FUSED) {
99
+ runFused(F, u, units, n, A, sc, ctrl, res);
100
+ } else if (op === OP.GROUP) {
101
+ runGroup(F, u, units, n, A, sc, ctrl, self);
102
+ } else if (op === OP.GROUP_RANGE) {
103
+ runGroupRange(F, u, units, n, A, sc, ctrl);
104
+ } else if (op === OP.UNITS) {
105
+ runIndexed(F, u, units, n, sc, ctrl);
106
+ } else if (op === OP.TASKS) {
107
+ runTask(F, u, sc, ctrl, self);
108
+ } else if (op === OP.COMPACT) {
109
+ runCompact(F, u, units, n, A, sc, ctrl);
110
+ } else if (op === OP.CSV_SCAN) {
111
+ res[u] = csvScan(u, A, sc);
112
+ } else if (op === OP.CSV_PARSE) {
113
+ csvParse(u, A, sc, F); // F reports each chunk's text dictionary
114
+ } else if (op === OP.READ) {
115
+ F(A[0], u); // read job u: [fd, file position, bytes, memory offset] (datasets, Node)
116
+ } else if (op === OP.SORT_CHUNK) {
117
+ A[0].subarray(chunkStart(u, units, n), chunkStart(u + 1, units, n)).sort();
118
+ } else if (op === OP.MERGE) {
119
+ mergePair(A[0], A[1], u, sc[1], n, sc[0]);
120
+ } else {
121
+ res[u] = rangeKernel(op, chunkStart(u, units, n), chunkStart(u + 1, units, n), A, sc, W);
122
+ }
123
+ }
124
+
125
+ // A fused kernel takes (ptr per array..., length, scalars...) and returns its
126
+ // reductions (none for an elementwise map).
127
+ function runFused(F, u, units, n, A, sc, ctrl, res) {
128
+ const start = chunkStart(u, units, n);
129
+ const args = [];
130
+ for (const ta of A) if (ta) args.push(ta.byteOffset + start * ta.BYTES_PER_ELEMENT);
131
+ args.push(chunkStart(u + 1, units, n) - start);
132
+ for (let j = 0, ns = ctrl[C.NSCAL]; j < ns; j++) args.push(sc[j]);
133
+ const r = F(...args);
134
+ const R = ctrl[C.NOUT];
135
+ if (R === 1) res[u] = r;
136
+ else for (let j = 0; j < R; j++) res[u * R + j] = r[j];
137
+ }
138
+
139
+ // Group-by kernel: (column ptrs..., key ptr, length, accBase, K, scalars...).
140
+ // Each thread adds into its own accumulators (sc holds base, stride and K
141
+ // after the scalars), so no two threads ever write the same slot.
142
+ function runGroup(F, u, units, n, A, sc, ctrl, self) {
143
+ const start = chunkStart(u, units, n);
144
+ const ns = ctrl[C.NSCAL];
145
+ const args = [];
146
+ for (const ta of A) if (ta) args.push(ta.byteOffset + start * ta.BYTES_PER_ELEMENT);
147
+ args.push(chunkStart(u + 1, units, n) - start, sc[ns] + self * sc[ns + 1], sc[ns + 2]);
148
+ for (let j = 0; j < ns; j++) args.push(sc[j]);
149
+ F(...args);
150
+ }
151
+
152
+ // Group-by over a range of groups: (column ptrs..., key ptr, length, accBase,
153
+ // K, lo, hi, scalars...). Every unit reads all rows but only adds those whose
154
+ // group is in its range [lo, hi), into ONE set of accumulators: no copies per
155
+ // thread (they would be huge with millions of groups) and no two threads
156
+ // writing the same slot.
157
+ function runGroupRange(F, u, units, n, A, sc, ctrl) {
158
+ const ns = ctrl[C.NSCAL], K = sc[ns + 1];
159
+ const args = [];
160
+ for (const ta of A) if (ta) args.push(ta.byteOffset);
161
+ args.push(n, sc[ns], K, Math.floor((u * K) / units), Math.floor(((u + 1) * K) / units));
162
+ for (let j = 0; j < ns; j++) args.push(sc[j]);
163
+ F(...args);
164
+ }
165
+
166
+ // A kernel that is told its rows and its unit: (start, end, unit, scalars...).
167
+ // Pointers travel as scalars; the unit number lets it use its own slots.
168
+ function runIndexed(F, u, units, n, sc, ctrl) {
169
+ const args = [chunkStart(u, units, n), chunkStart(u + 1, units, n), u];
170
+ for (let j = 0, ns = ctrl[C.NSCAL]; j < ns; j++) args.push(sc[j]);
171
+ F(...args);
172
+ }
173
+
174
+ // A kernel run once per task: (task, thread, scalars...). The thread index
175
+ // (0 = calling thread) lets a task use that thread's own scratch memory.
176
+ function runTask(F, u, sc, ctrl, self) {
177
+ const args = [u, self];
178
+ for (let j = 0, ns = ctrl[C.NSCAL]; j < ns; j++) args.push(sc[j]);
179
+ F(...args);
180
+ }
181
+
182
+ // Compact kernel: (src ptrs..., length, dst ptrs..., scalars...). The last
183
+ // array holds each unit's output offset (from a counting pass), so every unit
184
+ // writes straight to its final position.
185
+ function runCompact(F, u, units, n, A, sc, ctrl) {
186
+ const arrays = A.filter(Boolean);
187
+ const offsets = arrays[arrays.length - 1];
188
+ const S = ctrl[C.NOUT];
189
+ const start = chunkStart(u, units, n);
190
+ const args = [];
191
+ for (let k = 0; k < S; k++) args.push(arrays[k].byteOffset + start * arrays[k].BYTES_PER_ELEMENT);
192
+ args.push(chunkStart(u + 1, units, n) - start);
193
+ for (let k = S; k < arrays.length - 1; k++) args.push(arrays[k].byteOffset + offsets[u] * arrays[k].BYTES_PER_ELEMENT);
194
+ for (let j = 0, ns = ctrl[C.NSCAL]; j < ns; j++) args.push(sc[j]);
195
+ F(...args);
196
+ }
197
+
198
+ // Which ops have a WASM SIMD kernel, by element type.
199
+ const WASM_NAMES = {
200
+ [OP.SUM]: { f64: 'sum_f64' },
201
+ [OP.MIN]: { f64: 'min_f64', f32: 'min_f32' },
202
+ [OP.MAX]: { f64: 'max_f64', f32: 'max_f32' },
203
+ [OP.DOT]: { f64: 'dot_f64' },
204
+ [OP.COUNT_GT]: { f64: 'countgt_f64', f32: 'countgt_f32' },
205
+ [OP.ADD]: { f64: 'add_f64', f32: 'add_f32' },
206
+ [OP.SCALE]: { f64: 'scale_f64', f32: 'scale_f32' },
207
+ };
208
+ const elemType = (ta) => (ta instanceof Float64Array ? 'f64' : ta instanceof Float32Array ? 'f32' : null);
209
+
210
+ export function hasWasmKernel(op, A) {
211
+ const t = elemType(A[0]);
212
+ return Boolean(t && WASM_NAMES[op]?.[t] && A.every((x) => !x || elemType(x) === t));
213
+ }
214
+
215
+ // The actual work on [start, end). Also used directly for small inputs.
216
+ // With W (WASM exports) it runs the SIMD kernel on raw memory addresses.
217
+ export function rangeKernel(op, start, end, A, sc, W) {
218
+ if (W) {
219
+ const f = W[WASM_NAMES[op][elemType(A[0])]];
220
+ const len = end - start;
221
+ const at = (ta) => ta.byteOffset + start * ta.BYTES_PER_ELEMENT;
222
+ switch (op) {
223
+ case OP.SUM: case OP.MIN: case OP.MAX: return f(at(A[0]), len);
224
+ case OP.DOT: return f(at(A[0]), at(A[1]), len);
225
+ case OP.COUNT_GT: return f(at(A[0]), len, sc[0]);
226
+ case OP.ADD: f(at(A[0]), at(A[1]), at(A[2]), len); return 0;
227
+ case OP.SCALE: f(at(A[0]), at(A[1]), len, sc[0], sc[1]); return 0;
228
+ }
229
+ }
230
+ switch (op) {
231
+ case OP.SUM: return sum(A[0], start, end);
232
+ case OP.MIN: return min(A[0], start, end);
233
+ case OP.MAX: return max(A[0], start, end);
234
+ case OP.ADD: return add(A[0], A[1], A[2], start, end);
235
+ case OP.SCALE: return scale(A[0], A[1], sc[0], sc[1], start, end);
236
+ case OP.DOT: return dot(A[0], A[1], start, end);
237
+ case OP.HEAVY: return heavy(A[0], start, end);
238
+ case OP.COUNT_GT: return countGreater(A[0], sc[0], start, end);
239
+ case OP.COUNT_BYTES: return countBytes(A[0], A[1], start, end);
240
+ case OP.COPY: A[1].set(A[0].subarray(start, end), start); return 0;
241
+ }
242
+ throw new Error(`unknown op ${op}`);
243
+ }
244
+
245
+ export function sum(a, s, e) {
246
+ let r = 0;
247
+ for (let i = s; i < e; i++) r += a[i];
248
+ return r;
249
+ }
250
+
251
+ export function min(a, s, e) {
252
+ let r = Infinity;
253
+ for (let i = s; i < e; i++) if (a[i] < r) r = a[i];
254
+ return r;
255
+ }
256
+
257
+ export function max(a, s, e) {
258
+ let r = -Infinity;
259
+ for (let i = s; i < e; i++) if (a[i] > r) r = a[i];
260
+ return r;
261
+ }
262
+
263
+ export function add(a, b, out, s, e) {
264
+ for (let i = s; i < e; i++) out[i] = a[i] + b[i];
265
+ return 0;
266
+ }
267
+
268
+ // out = a * k + c
269
+ export function scale(a, out, k, c, s, e) {
270
+ for (let i = s; i < e; i++) out[i] = a[i] * k + c;
271
+ return 0;
272
+ }
273
+
274
+ export function dot(a, b, s, e) {
275
+ let r = 0;
276
+ for (let i = s; i < e; i++) r += a[i] * b[i];
277
+ return r;
278
+ }
279
+
280
+ export function heavy(a, s, e) {
281
+ let r = 0;
282
+ for (let i = s; i < e; i++) {
283
+ const x = a[i];
284
+ r += Math.sqrt(x) * Math.sin(x) + Math.cos(x * 0.5);
285
+ }
286
+ return r;
287
+ }
288
+
289
+ export function countGreater(a, x, s, e) {
290
+ let r = 0;
291
+ for (let i = s; i < e; i++) if (a[i] > x) r++;
292
+ return r;
293
+ }
294
+
295
+ // One merge in a round of merge sort: pair p joins two neighbouring runs,
296
+ // each `width` sorted chunks long, from src into dst.
297
+ function mergePair(src, dst, p, chunks, n, width) {
298
+ const first = p * 2 * width;
299
+ const lo = chunkStart(first, chunks, n);
300
+ const mid = chunkStart(Math.min(first + width, chunks), chunks, n);
301
+ const hi = chunkStart(Math.min(first + 2 * width, chunks), chunks, n);
302
+ let l = lo, r = mid, o = lo;
303
+ while (l < mid && r < hi) dst[o++] = src[l] <= src[r] ? src[l++] : src[r++];
304
+ while (l < mid) dst[o++] = src[l++];
305
+ while (r < hi) dst[o++] = src[r++];
306
+ }
307
+
308
+ // Count occurrences of `pat` that start inside [s, e) (native indexOf finds
309
+ // candidate first bytes, so each thread searches its slice at memchr speed).
310
+ export function countBytes(text, pat, s, e) {
311
+ return countPattern(text, pat, s, e);
312
+ }
package/src/ndjson.mjs ADDED
@@ -0,0 +1,136 @@
1
+ // JSON lines (one flat object per line) straight into columns, without making
2
+ // a JS object per row:
3
+ // {"ts":1700000000000,"price":12.5,"qty":3,"region":"EU"}
4
+ // Only the wanted keys are read. Number columns take numbers (and numeric
5
+ // strings); category columns take strings (and numbers, as their text).
6
+ // null, missing keys and nested values give NaN (numbers) or "" (categories).
7
+ // A line that isn't a JSON object is skipped and counted in `bad`.
8
+ import { ByteDict, parseNumber } from './csv-kernel.mjs';
9
+ import { encode } from './bytes.mjs';
10
+ import { parseDateBytes } from './dates.mjs';
11
+
12
+ const NL = 10, CR = 13, SPACE = 32, TAB = 9, QUOTE = 34, BACKSLASH = 92, COMMA = 44, COLON = 58;
13
+ const LBRACE = 123, RBRACE = 125, LBRACKET = 91, RBRACKET = 93;
14
+ const isSpace = (c) => c === SPACE || c === TAB || c === CR || c === NL;
15
+ const unescapeJSON = (raw) => JSON.parse(`"${raw}"`);
16
+
17
+ // names: column names; kinds: 'number' | 'category' | 'date' for each
18
+ // (dates: ISO text or ms numbers, as ms).
19
+ // Returns { rows, cols: Float64Array[], dicts: (string[] | null)[], bad }.
20
+ export function parseNDJSON(bytes, start, end, names, kinds) {
21
+ const k = names.length;
22
+ const keyBytes = names.map((n) => encode(n));
23
+ let cap = 1;
24
+ for (let i = start; i < end; i++) if (bytes[i] === NL) cap++;
25
+ const cols = names.map(() => new Float64Array(cap));
26
+ const dicts = kinds.map((kind) => (kind === 'category' ? new ByteDict(bytes, unescapeJSON) : null));
27
+ const empty = dicts.map((d) => (d ? -1 : NaN)); // code of "" per category column, made on first use
28
+ const isDate = kinds.map((kind) => kind === 'date');
29
+ const seen = new Uint8Array(k);
30
+ let guess = new Int32Array(64).fill(-1); // column of the j-th key on the previous line
31
+ let rows = 0, bad = 0;
32
+
33
+ // Which column does the key in bytes[s, e) name? (-1: not wanted)
34
+ const columnOf = (s, e, j) => {
35
+ const g = j < guess.length ? guess[j] : -1;
36
+ if (g >= 0 && same(keyBytes[g], s, e)) return g;
37
+ for (let c = 0; c < k; c++) if (same(keyBytes[c], s, e)) { if (j < guess.length) guess[j] = c; return c; }
38
+ return -1;
39
+ };
40
+ const same = (kb, s, e) => {
41
+ if (kb.length !== e - s) return false;
42
+ for (let i = 0; i < kb.length; i++) if (kb[i] !== bytes[s + i]) return false;
43
+ return true;
44
+ };
45
+ const emptyCode = (c) => (empty[c] === -1 ? (empty[c] = dicts[c].code(0, 0, false)) : empty[c]);
46
+
47
+ let i = start;
48
+ while (i < end) {
49
+ // one line: [i, eol)
50
+ let eol = i;
51
+ while (eol < end && bytes[eol] !== NL) eol++;
52
+ let p = i;
53
+ while (p < eol && isSpace(bytes[p])) p++;
54
+ if (p === eol) { i = eol + 1; continue; } // blank line
55
+ const row = rows;
56
+ seen.fill(0);
57
+ let ok = bytes[p] === LBRACE;
58
+ p++;
59
+ let j = 0;
60
+ while (ok) {
61
+ while (p < eol && isSpace(bytes[p])) p++;
62
+ if (bytes[p] === RBRACE) { p++; break; }
63
+ if (j > 0) { if (bytes[p] !== COMMA) { ok = false; break; } p++; while (p < eol && isSpace(bytes[p])) p++; }
64
+ if (bytes[p] !== QUOTE) { ok = false; break; }
65
+ // key
66
+ const ks = ++p;
67
+ let kEsc = false;
68
+ while (p < eol && bytes[p] !== QUOTE) { if (bytes[p] === BACKSLASH) { kEsc = true; p++; } p++; }
69
+ if (p >= eol) { ok = false; break; }
70
+ const c = kEsc ? columnOfText(unescapeJSON(decodeLatin(bytes, ks, p)), names) : columnOf(ks, p, j);
71
+ p++;
72
+ while (p < eol && isSpace(bytes[p])) p++;
73
+ if (bytes[p] !== COLON) { ok = false; break; }
74
+ p++;
75
+ while (p < eol && isSpace(bytes[p])) p++;
76
+ // value
77
+ const b = bytes[p];
78
+ if (b === QUOTE) {
79
+ const vs = ++p;
80
+ let esc = false;
81
+ while (p < eol && bytes[p] !== QUOTE) { if (bytes[p] === BACKSLASH) { esc = true; p++; } p++; }
82
+ if (p >= eol) { ok = false; break; }
83
+ if (c >= 0) {
84
+ if (dicts[c]) cols[c][row] = dicts[c].code(vs, p, esc);
85
+ else if (isDate[c]) cols[c][row] = parseDateBytes(bytes, vs, p);
86
+ else cols[c][row] = esc ? Number(unescapeJSON(decodeLatin(bytes, vs, p))) || NaN : parseNumber(bytes, vs, p);
87
+ seen[c] = 1;
88
+ }
89
+ p++;
90
+ } else if (b === LBRACE || b === LBRACKET) { // nested: skip it
91
+ p = skipNested(bytes, p, eol);
92
+ if (p < 0) { ok = false; break; }
93
+ } else {
94
+ const vs = p;
95
+ while (p < eol && bytes[p] !== COMMA && bytes[p] !== RBRACE && !isSpace(bytes[p])) p++;
96
+ if (c >= 0) {
97
+ const first = bytes[vs];
98
+ if (first === 110) { /* null: leave empty */ }
99
+ else if (dicts[c]) { cols[c][row] = dicts[c].code(vs, p, false); seen[c] = 1; }
100
+ else { cols[c][row] = first === 116 ? 1 : first === 102 ? 0 : parseNumber(bytes, vs, p); seen[c] = 1; }
101
+ }
102
+ }
103
+ j++;
104
+ }
105
+ if (ok) {
106
+ while (p < eol && isSpace(bytes[p])) p++;
107
+ ok = p === eol;
108
+ }
109
+ if (ok) {
110
+ for (let c = 0; c < k; c++) if (!seen[c]) cols[c][row] = dicts[c] ? emptyCode(c) : NaN;
111
+ rows++;
112
+ } else {
113
+ bad++;
114
+ }
115
+ i = eol + 1;
116
+ }
117
+ return { rows, cols: cols.map((col) => col.subarray(0, rows)), dicts: dicts.map((d) => d?.strings ?? null), bad };
118
+ }
119
+
120
+ function columnOfText(text, names) { return names.indexOf(text); }
121
+
122
+ function decodeLatin(bytes, s, e) {
123
+ return new TextDecoder().decode(bytes.subarray(s, e));
124
+ }
125
+
126
+ // Index just past the nested object or array starting at p (-1 if unbalanced).
127
+ function skipNested(bytes, p, eol) {
128
+ let depth = 0;
129
+ for (; p < eol; p++) {
130
+ const c = bytes[p];
131
+ if (c === QUOTE) { for (p++; p < eol && bytes[p] !== QUOTE; p++) if (bytes[p] === BACKSLASH) p++; }
132
+ else if (c === LBRACE || c === LBRACKET) depth++;
133
+ else if (c === RBRACE || c === RBRACKET) { if (--depth === 0) return p + 1; }
134
+ }
135
+ return -1;
136
+ }
package/src/node.mjs ADDED
@@ -0,0 +1,120 @@
1
+ // Node.js entry: the engine with worker threads on all cores, plus file reading.
2
+ // import { Engine } from 'vectrojs'; -> new Engine() (all cores)
3
+ import { Worker, MessageChannel, receiveMessageOnPort } from 'node:worker_threads';
4
+ import { availableParallelism } from 'node:os';
5
+ import { openSync, fstatSync, readSync, closeSync } from 'node:fs';
6
+ import { open } from 'node:fs/promises';
7
+ import { Engine as CoreEngine } from './engine.mjs';
8
+ import { loadBytes, loadBytesAsync } from './csv.mjs';
9
+ import { Dataset, DatasetWriter, writeDataset, csvToDataset, csvToDatasetAsync, openDatasetAsync, writeDatasetAsync, createDatasetAsync, setFiles } from './dataset.mjs';
10
+ import { nodeFiles } from './files-node.mjs';
11
+ setFiles(nodeFiles);
12
+
13
+ export const nodePlatform = {
14
+ cpus: () => availableParallelism(),
15
+ spawn(workerData) {
16
+ const { port1, port2 } = new MessageChannel();
17
+ port1.unref();
18
+ const worker = new Worker(new URL('./worker.mjs', import.meta.url), {
19
+ workerData: { ...workerData, port: port2 },
20
+ transferList: [port2],
21
+ });
22
+ worker.unref();
23
+ return { worker, port: port1 };
24
+ },
25
+ receive: (port) => receiveMessageOnPort(port),
26
+ };
27
+
28
+ export class Engine extends CoreEngine {
29
+ constructor(opts = {}) {
30
+ super({ platform: nodePlatform, ...opts });
31
+ }
32
+
33
+ // Load a CSV file into a table, parsing on all threads.
34
+ readCSV(path, opts = {}) {
35
+ const fd = openSync(path, 'r');
36
+ try {
37
+ const size = fstatSync(fd).size;
38
+ const bytes = this.alloc(Uint8Array, size);
39
+ for (let off = 0; off < size; ) {
40
+ const got = readSync(fd, bytes, off, size - off, off);
41
+ if (got === 0) throw new Error(`vectro: ${path} ended early`);
42
+ off += got;
43
+ }
44
+ const t = loadBytes(this, bytes, opts);
45
+ return opts.compress ? t.compress() : t;
46
+ } finally {
47
+ closeSync(fd);
48
+ }
49
+ }
50
+
51
+ // Same, without blocking: the file is read with async I/O straight into
52
+ // engine memory, and parsing runs on the worker threads.
53
+ async readCSVAsync(path, opts = {}) {
54
+ const file = await open(path, 'r');
55
+ try {
56
+ const { size } = await file.stat();
57
+ const bytes = this.alloc(Uint8Array, size);
58
+ for (let off = 0; off < size; ) {
59
+ const { bytesRead } = await file.read(this.live(bytes), off, size - off, off);
60
+ if (bytesRead === 0) throw new Error(`vectro: ${path} ended early`);
61
+ off += bytesRead;
62
+ }
63
+ const t = await loadBytesAsync(this, bytes, opts);
64
+ return opts.compress ? await t.compressAsync() : t;
65
+ } finally {
66
+ await file.close();
67
+ }
68
+ }
69
+
70
+ // ---- datasets: tables bigger than memory, on disk (see dataset.mjs) ----------
71
+ // Open one for queries (filter, groupBy, join, sql... read it chunk by chunk).
72
+ openDataset(path) { return new Dataset(this, path); }
73
+ // The same from a path, a URL (HTTP range requests) or a Blob; async.
74
+ openDatasetAsync(source) { return openDatasetAsync(this, source); }
75
+ // A writer: append(table or columns) as often as needed, then close().
76
+ createDataset(path, opts) { return new DatasetWriter(this, path, opts); }
77
+ // A table, a query (over a dataset: written chunk by chunk) or columns.
78
+ writeDataset(path, source, opts) { return writeDataset(this, path, source, opts); }
79
+ // A CSV file of any size, read in pieces (async: also from a URL or Blob).
80
+ csvToDataset(csvPath, path, opts) { return csvToDataset(this, csvPath, path, opts); }
81
+ csvToDatasetAsync(csv, path, opts) { return csvToDatasetAsync(this, csv, path, opts); }
82
+ // writeDataset / createDataset without blocking (a writer with appendAsync / closeAsync).
83
+ writeDatasetAsync(path, source, opts) { return writeDatasetAsync(this, path, source, opts); }
84
+ createDatasetAsync(path, opts) { return createDatasetAsync(this, path, opts); }
85
+ }
86
+
87
+ export { canCompileWasm } from './engine.mjs';
88
+ export { Dataset, DatasetWriter, blobFile, urlFile } from './dataset.mjs';
89
+ export { memoryFile } from './files-browser.mjs';
90
+ export { Table } from './table.mjs';
91
+
92
+ // ---- split tables and streams: each worker thread owns its rows --------------
93
+ // import { SplitEngine } from 'vectrojs';
94
+ // const engine = new SplitEngine(); // one worker per core
95
+ // const live = await engine.streamAsync({ columns: { ts: 'number', region: 'category' } });
96
+ import { SplitEngine as BaseSplitEngine } from './split.mjs';
97
+
98
+ export function spawnSplitWorker() {
99
+ const w = new Worker(new URL('./split-worker-node.mjs', import.meta.url));
100
+ w.unref();
101
+ return {
102
+ post: (msg, transfer) => w.postMessage(msg, transfer ?? []),
103
+ onMessage: (fn) => w.on('message', fn),
104
+ onError: (fn) => { w.on('error', fn); w.on('exit', (code) => fn(new Error(`exited with code ${code}`))); },
105
+ terminate: () => w.terminate(),
106
+ };
107
+ }
108
+
109
+ export class SplitEngine extends BaseSplitEngine {
110
+ constructor(opts = {}) {
111
+ super({ workers: availableParallelism(), spawn: spawnSplitWorker, ...opts });
112
+ }
113
+ }
114
+
115
+ export { VectorIndex } from './vectors.mjs';
116
+ export { Emitter } from './emitter.mjs';
117
+ export { BatchEncoder, FrameReader, encodeSchema, encodeEnd } from './frames.mjs';
118
+ export { readArrow, writeArrow, ArrowReader, ArrowEncoder } from './arrow.mjs';
119
+ export { DistinctSketch } from './sketch.mjs';
120
+ export { sql } from './sql.mjs';
package/src/pack.mjs ADDED
@@ -0,0 +1,114 @@
1
+ // Compressed ("packed") number columns. A column whose values are all
2
+ // base + code * mul (whole numbers: counts, codes, IDs, ms on whole seconds)
3
+ // code / div (decimals: prices in cents, rates in basis points)
4
+ // with small whole-number codes is stored as 1, 2 or 4 bytes per row instead
5
+ // of 8 (or 4). Missing values (NaN) take the largest code of the type. Every
6
+ // value is checked to decode back exactly, with the same arithmetic the
7
+ // kernels use (float32 tables: float32), so packing never changes an answer.
8
+ //
9
+ // The fused kernels read packed columns directly (SIMD widen + convert, then
10
+ // scale), so scans read 2-8x fewer bytes. Other operations decode the
11
+ // columns they need first (see Table).
12
+
13
+ export const KINDS = {
14
+ u8: { Type: Uint8Array, bytes: 1, nan: 255, max: 254, min: 0 },
15
+ u16: { Type: Uint16Array, bytes: 2, nan: 65535, max: 65534, min: 0 },
16
+ i32: { Type: Int32Array, bytes: 4, nan: -2147483648, max: 2147483647, min: -2147483647 },
17
+ };
18
+ const SLACK = 16; // bytes after the codes: SIMD loads read a little past the last row
19
+
20
+ const MULS = [1, 1000, 60_000, 3_600_000, 86_400_000];
21
+ const DIVS = [10, 100, 1000, 10_000];
22
+
23
+ // Candidate encodings for a column with these statistics, smallest first.
24
+ // (Decimals: the fewest decimals first; each candidate must still be checked
25
+ // value by value.)
26
+ export function encodingCandidates({ min, max, whole, nan, inf, negZero }, f32 = false) {
27
+ if (min === Infinity || inf || negZero) return [];
28
+ const fullBytes = f32 ? 4 : 8;
29
+ const kindFor = (lo, hi) => {
30
+ for (const [name, k] of Object.entries(KINDS)) if (lo >= k.min && hi <= k.max && k.bytes < fullBytes && !(f32 && name === 'i32' && (Math.abs(lo) > 2 ** 24 || Math.abs(hi) > 2 ** 24))) return name;
31
+ return null;
32
+ };
33
+ const out = [];
34
+ if (whole) {
35
+ for (const mul of MULS) { const kind = kindFor(0, Math.round((max - min) / mul)); if (kind) out.push({ kind, mul, div: 0, base: min, nan }); }
36
+ return out.sort((a, b) => KINDS[a.kind].bytes - KINDS[b.kind].bytes);
37
+ }
38
+ for (const div of DIVS) { const kind = kindFor(Math.round(min * div), Math.round(max * div)); if (kind) out.push({ kind, mul: 0, div, base: 0, nan }); }
39
+ return out;
40
+ }
41
+
42
+ // The encoding to use for values[0..n), or null if none saves memory.
43
+ // enc: { kind, mul, div, base }
44
+ export function chooseEncoding(values, n, f32 = false) {
45
+ if (!n) return null;
46
+ const R = f32 ? Math.fround : (x) => x;
47
+ let min = Infinity, max = -Infinity, nan = false, whole = true;
48
+ for (let i = 0; i < n; i++) {
49
+ const v = values[i];
50
+ if (v !== v) { nan = true; continue; }
51
+ if (v === Infinity || v === -Infinity) return null;
52
+ if (v < min) min = v;
53
+ if (v > max) max = v;
54
+ if (whole && v !== Math.floor(v)) whole = false;
55
+ }
56
+ if (min === Infinity) return null; // only missing values: leave it
57
+ const fullBytes = f32 ? 4 : 8;
58
+ const kindFor = (lo, hi) => {
59
+ for (const [name, k] of Object.entries(KINDS)) if (lo >= k.min && hi <= k.max && k.bytes < fullBytes && !(f32 && name === 'i32' && (Math.abs(lo) > 2 ** 24 || Math.abs(hi) > 2 ** 24))) return name;
60
+ return null;
61
+ };
62
+ const decode = (enc, code) => (enc.div ? R(R(code) / enc.div) : R(R(R(code) * enc.mul) + enc.base));
63
+ const encode = (enc, v) => (enc.div ? Math.round(v * enc.div) : Math.round((v - enc.base) / enc.mul));
64
+ const fits = (enc, sample) => { // every value (or a sample first) decodes back exactly
65
+ const step = sample ? Math.max(1, Math.floor(n / 512)) : 1;
66
+ for (let i = 0; i < n; i += step) {
67
+ const v = values[i];
68
+ if (v !== v) continue;
69
+ const code = encode(enc, v);
70
+ if (!(code >= KINDS[enc.kind].min && code <= KINDS[enc.kind].max) || decode(enc, code) !== v || Object.is(v, -0) !== Object.is(decode(enc, code), -0)) return false;
71
+ }
72
+ return true;
73
+ };
74
+ const candidates = [];
75
+ if (whole) {
76
+ for (const mul of MULS) {
77
+ const lo = 0, hi = Math.round((max - min) / mul);
78
+ const kind = kindFor(lo, hi);
79
+ if (kind) candidates.push({ kind, mul, div: 0, base: min });
80
+ }
81
+ } else {
82
+ for (const div of DIVS) { // the fewest decimals that give every value back
83
+ const kind = kindFor(Math.round(min * div), Math.round(max * div));
84
+ const enc = kind && { kind, mul: 0, div, base: 0 };
85
+ if (enc && fits(enc, true) && fits(enc, false)) return { ...enc, nan };
86
+ }
87
+ return null;
88
+ }
89
+ candidates.sort((a, b) => KINDS[a.kind].bytes - KINDS[b.kind].bytes);
90
+ for (const enc of candidates) if (fits(enc, true) && fits(enc, false)) return { ...enc, nan }; // nan: some codes mean missing
91
+ return null;
92
+ }
93
+
94
+ // Codes for values[0..n) in a new engine array (with room after the end).
95
+ export function packInto(engine, values, n, enc) {
96
+ const k = KINDS[enc.kind];
97
+ const data = engine.alloc(k.Type, n + SLACK / k.bytes);
98
+ const v = engine.live(values);
99
+ for (let i = 0; i < n; i++) {
100
+ const x = v[i];
101
+ data[i] = x !== x ? k.nan : enc.div ? Math.round(x * enc.div) : Math.round((x - enc.base) / enc.mul);
102
+ }
103
+ return data;
104
+ }
105
+
106
+ // Plain JS decode of one code (kernels do this in SIMD).
107
+ export function decodeOne(enc, code, f32 = false) {
108
+ if (code === KINDS[enc.kind].nan) return NaN;
109
+ const R = f32 ? Math.fround : (x) => x;
110
+ return enc.div ? R(R(code) / enc.div) : R(R(R(code) * enc.mul) + enc.base);
111
+ }
112
+
113
+ // Bytes per row stored, and what the column would take unpacked.
114
+ export const packedBytes = (enc, n) => KINDS[enc.kind].bytes * n;