juxscript 1.1.158 → 1.1.161
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/index.d.ts +4 -0
- package/index.d.ts.map +1 -1
- package/index.js +5 -0
- package/lib/components/checkbox.js +4 -4
- package/lib/components/checkbox.ts +5 -5
- package/lib/components/datepicker.d.ts.map +1 -1
- package/lib/components/datepicker.js +5 -4
- package/lib/components/datepicker.ts +5 -4
- package/lib/components/dialog.d.ts.map +1 -1
- package/lib/components/dialog.js +2 -0
- package/lib/components/dialog.ts +2 -0
- package/lib/components/dropdown.d.ts +1 -0
- package/lib/components/dropdown.d.ts.map +1 -1
- package/lib/components/dropdown.js +5 -6
- package/lib/components/dropdown.ts +6 -9
- package/lib/components/fileupload.d.ts +6 -0
- package/lib/components/fileupload.d.ts.map +1 -1
- package/lib/components/fileupload.js +31 -61
- package/lib/components/fileupload.ts +38 -67
- package/lib/components/input.js +6 -6
- package/lib/components/input.ts +6 -6
- package/lib/components/modal.d.ts.map +1 -1
- package/lib/components/modal.js +2 -0
- package/lib/components/modal.ts +2 -0
- package/lib/components/select.d.ts.map +1 -1
- package/lib/components/select.js +5 -0
- package/lib/components/select.ts +6 -0
- package/lib/components/switch.js +4 -4
- package/lib/components/switch.ts +5 -5
- package/lib/components/tabs.js +4 -4
- package/lib/components/tabs.ts +4 -4
- package/lib/storage/DataFrame.d.ts +59 -0
- package/lib/storage/DataFrame.d.ts.map +1 -0
- package/lib/storage/DataFrame.js +443 -0
- package/lib/storage/DataFrame.ts +472 -0
- package/lib/storage/FileStorage.d.ts +53 -0
- package/lib/storage/FileStorage.d.ts.map +1 -0
- package/lib/storage/FileStorage.js +80 -0
- package/lib/storage/FileStorage.ts +95 -0
- package/lib/storage/IndexedDBDriver.d.ts +75 -0
- package/lib/storage/IndexedDBDriver.d.ts.map +1 -0
- package/lib/storage/IndexedDBDriver.js +177 -0
- package/lib/storage/IndexedDBDriver.ts +226 -0
- package/lib/storage/TabularDriver.d.ts +75 -0
- package/lib/storage/TabularDriver.d.ts.map +1 -0
- package/lib/storage/TabularDriver.js +399 -0
- package/lib/storage/TabularDriver.ts +491 -0
- package/machinery/errors.js +22 -5
- package/package.json +1 -1
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
import { DataFrame } from './DataFrame.js';
|
|
2
|
+
export class TabularDriver {
|
|
3
|
+
constructor(dbName = 'jux-tabular', storeName = 'tables') {
|
|
4
|
+
this._db = null;
|
|
5
|
+
this._dbName = dbName;
|
|
6
|
+
this._storeName = storeName;
|
|
7
|
+
}
|
|
8
|
+
async open() {
|
|
9
|
+
if (this._db)
|
|
10
|
+
return this._db;
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const request = indexedDB.open(this._dbName, 1);
|
|
13
|
+
request.onupgradeneeded = (e) => {
|
|
14
|
+
const db = e.target.result;
|
|
15
|
+
if (!db.objectStoreNames.contains(this._storeName)) {
|
|
16
|
+
const store = db.createObjectStore(this._storeName, { keyPath: 'id' });
|
|
17
|
+
store.createIndex('name', 'name', { unique: false });
|
|
18
|
+
store.createIndex('timestamp', 'timestamp', { unique: false });
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
request.onsuccess = (e) => {
|
|
22
|
+
this._db = e.target.result;
|
|
23
|
+
resolve(this._db);
|
|
24
|
+
};
|
|
25
|
+
request.onerror = () => reject(request.error);
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
/* ═══════════════════════════════════════════════════
|
|
29
|
+
* CSV / TSV PARSING
|
|
30
|
+
* ═══════════════════════════════════════════════════ */
|
|
31
|
+
/**
|
|
32
|
+
* Parse a CSV/TSV string into a DataFrame
|
|
33
|
+
*/
|
|
34
|
+
parseCSV(text, options = {}) {
|
|
35
|
+
const { delimiter = ',', hasHeader = true, maxRows, skipRows = 0, columns: selectCols } = options;
|
|
36
|
+
const lines = this._splitLines(text);
|
|
37
|
+
let startIdx = skipRows;
|
|
38
|
+
let headers;
|
|
39
|
+
if (hasHeader && lines.length > startIdx) {
|
|
40
|
+
headers = this._parseLine(lines[startIdx], delimiter);
|
|
41
|
+
startIdx++;
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const firstLine = this._parseLine(lines[startIdx] || '', delimiter);
|
|
45
|
+
headers = firstLine.map((_, i) => `col_${i}`);
|
|
46
|
+
}
|
|
47
|
+
const rows = [];
|
|
48
|
+
const end = maxRows ? Math.min(startIdx + maxRows, lines.length) : lines.length;
|
|
49
|
+
for (let i = startIdx; i < end; i++) {
|
|
50
|
+
if (!lines[i].trim())
|
|
51
|
+
continue;
|
|
52
|
+
const values = this._parseLine(lines[i], delimiter);
|
|
53
|
+
const row = {};
|
|
54
|
+
headers.forEach((h, j) => {
|
|
55
|
+
row[h] = this._autoType(values[j]);
|
|
56
|
+
});
|
|
57
|
+
rows.push(row);
|
|
58
|
+
}
|
|
59
|
+
let df = new DataFrame(rows);
|
|
60
|
+
if (selectCols) {
|
|
61
|
+
df = df.select(...selectCols);
|
|
62
|
+
}
|
|
63
|
+
return df;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Stream-parse a File into a DataFrame, with progress callback
|
|
67
|
+
*/
|
|
68
|
+
async streamFile(file, options = {}) {
|
|
69
|
+
const { delimiter = file.name.endsWith('.tsv') ? '\t' : ',', hasHeader = true, chunkSize = 64 * 1024, onProgress, maxRows, skipRows = 0, columns: selectCols } = options;
|
|
70
|
+
const totalSize = file.size;
|
|
71
|
+
let bytesRead = 0;
|
|
72
|
+
let headers = null;
|
|
73
|
+
let lineBuffer = '';
|
|
74
|
+
let rowCount = 0;
|
|
75
|
+
let skipped = 0;
|
|
76
|
+
const rows = [];
|
|
77
|
+
const reader = file.stream().getReader();
|
|
78
|
+
const decoder = new TextDecoder();
|
|
79
|
+
while (true) {
|
|
80
|
+
const { done, value } = await reader.read();
|
|
81
|
+
if (done)
|
|
82
|
+
break;
|
|
83
|
+
bytesRead += value.byteLength;
|
|
84
|
+
lineBuffer += decoder.decode(value, { stream: !done });
|
|
85
|
+
const lines = lineBuffer.split('\n');
|
|
86
|
+
lineBuffer = lines.pop() || '';
|
|
87
|
+
for (const line of lines) {
|
|
88
|
+
if (!line.trim())
|
|
89
|
+
continue;
|
|
90
|
+
if (!headers && hasHeader) {
|
|
91
|
+
headers = this._parseLine(line, delimiter);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!headers) {
|
|
95
|
+
const vals = this._parseLine(line, delimiter);
|
|
96
|
+
headers = vals.map((_, i) => `col_${i}`);
|
|
97
|
+
}
|
|
98
|
+
if (skipped < skipRows) {
|
|
99
|
+
skipped++;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (maxRows && rowCount >= maxRows)
|
|
103
|
+
break;
|
|
104
|
+
const values = this._parseLine(line, delimiter);
|
|
105
|
+
const row = {};
|
|
106
|
+
headers.forEach((h, j) => {
|
|
107
|
+
row[h] = this._autoType(values[j]);
|
|
108
|
+
});
|
|
109
|
+
rows.push(row);
|
|
110
|
+
rowCount++;
|
|
111
|
+
}
|
|
112
|
+
if (onProgress) {
|
|
113
|
+
onProgress(bytesRead, totalSize);
|
|
114
|
+
}
|
|
115
|
+
if (maxRows && rowCount >= maxRows)
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
// Handle remaining buffer
|
|
119
|
+
if (lineBuffer.trim() && (!maxRows || rowCount < maxRows)) {
|
|
120
|
+
if (!headers) {
|
|
121
|
+
headers = this._parseLine(lineBuffer, delimiter).map((_, i) => `col_${i}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
const values = this._parseLine(lineBuffer, delimiter);
|
|
125
|
+
const row = {};
|
|
126
|
+
headers.forEach((h, j) => {
|
|
127
|
+
row[h] = this._autoType(values[j]);
|
|
128
|
+
});
|
|
129
|
+
rows.push(row);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (onProgress)
|
|
133
|
+
onProgress(totalSize, totalSize);
|
|
134
|
+
let df = new DataFrame(rows);
|
|
135
|
+
if (selectCols)
|
|
136
|
+
df = df.select(...selectCols);
|
|
137
|
+
return df;
|
|
138
|
+
}
|
|
139
|
+
/* ═══════════════════════════════════════════════════
|
|
140
|
+
* INDEXEDDB PERSISTENCE
|
|
141
|
+
* ═══════════════════════════════════════════════════ */
|
|
142
|
+
/**
|
|
143
|
+
* Store a DataFrame to IndexedDB
|
|
144
|
+
*/
|
|
145
|
+
async store(name, df, metadata) {
|
|
146
|
+
const db = await this.open();
|
|
147
|
+
const table = {
|
|
148
|
+
id: `${name}-${Date.now()}`,
|
|
149
|
+
name,
|
|
150
|
+
columns: df.columns,
|
|
151
|
+
rows: df.toRows().map(row => df.columns.map(c => row[c])),
|
|
152
|
+
rowCount: df.height,
|
|
153
|
+
delimiter: ',',
|
|
154
|
+
timestamp: Date.now(),
|
|
155
|
+
metadata
|
|
156
|
+
};
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
const tx = db.transaction(this._storeName, 'readwrite');
|
|
159
|
+
const store = tx.objectStore(this._storeName);
|
|
160
|
+
const request = store.put(table);
|
|
161
|
+
request.onsuccess = () => resolve(table.id);
|
|
162
|
+
request.onerror = () => reject(request.error);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Load a DataFrame from IndexedDB by ID
|
|
167
|
+
*/
|
|
168
|
+
async load(id) {
|
|
169
|
+
const db = await this.open();
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
const tx = db.transaction(this._storeName, 'readonly');
|
|
172
|
+
const store = tx.objectStore(this._storeName);
|
|
173
|
+
const request = store.get(id);
|
|
174
|
+
request.onsuccess = () => {
|
|
175
|
+
const table = request.result;
|
|
176
|
+
if (!table)
|
|
177
|
+
return resolve(null);
|
|
178
|
+
const rows = table.rows.map(row => {
|
|
179
|
+
const obj = {};
|
|
180
|
+
table.columns.forEach((col, i) => { obj[col] = row[i]; });
|
|
181
|
+
return obj;
|
|
182
|
+
});
|
|
183
|
+
resolve(new DataFrame(rows));
|
|
184
|
+
};
|
|
185
|
+
request.onerror = () => reject(request.error);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Load the most recent table by name
|
|
190
|
+
*/
|
|
191
|
+
async loadByName(name) {
|
|
192
|
+
const db = await this.open();
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
const tx = db.transaction(this._storeName, 'readonly');
|
|
195
|
+
const store = tx.objectStore(this._storeName);
|
|
196
|
+
const index = store.index('name');
|
|
197
|
+
const request = index.getAll(name);
|
|
198
|
+
request.onsuccess = () => {
|
|
199
|
+
const tables = request.result || [];
|
|
200
|
+
if (tables.length === 0)
|
|
201
|
+
return resolve(null);
|
|
202
|
+
tables.sort((a, b) => b.timestamp - a.timestamp);
|
|
203
|
+
const table = tables[0];
|
|
204
|
+
const rows = table.rows.map(row => {
|
|
205
|
+
const obj = {};
|
|
206
|
+
table.columns.forEach((col, i) => { obj[col] = row[i]; });
|
|
207
|
+
return obj;
|
|
208
|
+
});
|
|
209
|
+
resolve(new DataFrame(rows));
|
|
210
|
+
};
|
|
211
|
+
request.onerror = () => reject(request.error);
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* List all stored tables (metadata only)
|
|
216
|
+
*/
|
|
217
|
+
async list() {
|
|
218
|
+
const db = await this.open();
|
|
219
|
+
return new Promise((resolve, reject) => {
|
|
220
|
+
const tx = db.transaction(this._storeName, 'readonly');
|
|
221
|
+
const store = tx.objectStore(this._storeName);
|
|
222
|
+
const request = store.getAll();
|
|
223
|
+
request.onsuccess = () => {
|
|
224
|
+
resolve((request.result || []).map((t) => ({
|
|
225
|
+
id: t.id,
|
|
226
|
+
name: t.name,
|
|
227
|
+
columns: t.columns,
|
|
228
|
+
rowCount: t.rowCount,
|
|
229
|
+
timestamp: t.timestamp
|
|
230
|
+
})));
|
|
231
|
+
};
|
|
232
|
+
request.onerror = () => reject(request.error);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Delete a stored table
|
|
237
|
+
*/
|
|
238
|
+
async delete(id) {
|
|
239
|
+
const db = await this.open();
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
const tx = db.transaction(this._storeName, 'readwrite');
|
|
242
|
+
const store = tx.objectStore(this._storeName);
|
|
243
|
+
const request = store.delete(id);
|
|
244
|
+
request.onsuccess = () => resolve();
|
|
245
|
+
request.onerror = () => reject(request.error);
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
async clear() {
|
|
249
|
+
const db = await this.open();
|
|
250
|
+
return new Promise((resolve, reject) => {
|
|
251
|
+
const tx = db.transaction(this._storeName, 'readwrite');
|
|
252
|
+
const store = tx.objectStore(this._storeName);
|
|
253
|
+
const request = store.clear();
|
|
254
|
+
request.onsuccess = () => resolve();
|
|
255
|
+
request.onerror = () => reject(request.error);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
/* ═══════════════════════════════════════════════════
|
|
259
|
+
* STREAM FROM URL
|
|
260
|
+
* ═══════════════════════════════════════════════════ */
|
|
261
|
+
/**
|
|
262
|
+
* Fetch and stream-parse a remote CSV/TSV file
|
|
263
|
+
*/
|
|
264
|
+
async fetch(url, options = {}) {
|
|
265
|
+
const response = await globalThis.fetch(url);
|
|
266
|
+
if (!response.ok)
|
|
267
|
+
throw new Error(`Failed to fetch: ${response.status}`);
|
|
268
|
+
if (!response.body)
|
|
269
|
+
throw new Error('No response body');
|
|
270
|
+
const contentLength = Number(response.headers.get('content-length')) || null;
|
|
271
|
+
const ext = url.split('.').pop()?.toLowerCase();
|
|
272
|
+
const delimiter = options.delimiter ?? (ext === 'tsv' ? '\t' : ',');
|
|
273
|
+
const { hasHeader = true, chunkSize, onProgress, maxRows, skipRows = 0, columns: selectCols } = options;
|
|
274
|
+
const reader = response.body.getReader();
|
|
275
|
+
const decoder = new TextDecoder();
|
|
276
|
+
let headers = null;
|
|
277
|
+
let lineBuffer = '';
|
|
278
|
+
let rowCount = 0;
|
|
279
|
+
let skipped = 0;
|
|
280
|
+
let bytesRead = 0;
|
|
281
|
+
const rows = [];
|
|
282
|
+
while (true) {
|
|
283
|
+
const { done, value } = await reader.read();
|
|
284
|
+
if (done)
|
|
285
|
+
break;
|
|
286
|
+
bytesRead += value.byteLength;
|
|
287
|
+
lineBuffer += decoder.decode(value, { stream: true });
|
|
288
|
+
const lines = lineBuffer.split('\n');
|
|
289
|
+
lineBuffer = lines.pop() || '';
|
|
290
|
+
for (const line of lines) {
|
|
291
|
+
if (!line.trim())
|
|
292
|
+
continue;
|
|
293
|
+
if (!headers && hasHeader) {
|
|
294
|
+
headers = this._parseLine(line, delimiter);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (!headers) {
|
|
298
|
+
const vals = this._parseLine(line, delimiter);
|
|
299
|
+
headers = vals.map((_, i) => `col_${i}`);
|
|
300
|
+
}
|
|
301
|
+
if (skipped < skipRows) {
|
|
302
|
+
skipped++;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (maxRows && rowCount >= maxRows)
|
|
306
|
+
break;
|
|
307
|
+
const values = this._parseLine(line, delimiter);
|
|
308
|
+
const row = {};
|
|
309
|
+
headers.forEach((h, j) => { row[h] = this._autoType(values[j]); });
|
|
310
|
+
rows.push(row);
|
|
311
|
+
rowCount++;
|
|
312
|
+
}
|
|
313
|
+
if (onProgress)
|
|
314
|
+
onProgress(bytesRead, contentLength);
|
|
315
|
+
if (maxRows && rowCount >= maxRows)
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
if (lineBuffer.trim() && (!maxRows || rowCount < maxRows)) {
|
|
319
|
+
if (headers) {
|
|
320
|
+
const values = this._parseLine(lineBuffer, delimiter);
|
|
321
|
+
const row = {};
|
|
322
|
+
headers.forEach((h, j) => { row[h] = this._autoType(values[j]); });
|
|
323
|
+
rows.push(row);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (onProgress)
|
|
327
|
+
onProgress(bytesRead, bytesRead);
|
|
328
|
+
let df = new DataFrame(rows);
|
|
329
|
+
if (selectCols)
|
|
330
|
+
df = df.select(...selectCols);
|
|
331
|
+
return df;
|
|
332
|
+
}
|
|
333
|
+
/* ═══════════════════════════════════════════════════
|
|
334
|
+
* INTERNAL PARSING HELPERS
|
|
335
|
+
* ═══════════════════════════════════════════════════ */
|
|
336
|
+
_splitLines(text) {
|
|
337
|
+
return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
|
|
338
|
+
}
|
|
339
|
+
_parseLine(line, delimiter) {
|
|
340
|
+
const result = [];
|
|
341
|
+
let current = '';
|
|
342
|
+
let inQuotes = false;
|
|
343
|
+
for (let i = 0; i < line.length; i++) {
|
|
344
|
+
const ch = line[i];
|
|
345
|
+
if (inQuotes) {
|
|
346
|
+
if (ch === '"') {
|
|
347
|
+
if (i + 1 < line.length && line[i + 1] === '"') {
|
|
348
|
+
current += '"';
|
|
349
|
+
i++;
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
inQuotes = false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
current += ch;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
if (ch === '"') {
|
|
361
|
+
inQuotes = true;
|
|
362
|
+
}
|
|
363
|
+
else if (ch === delimiter) {
|
|
364
|
+
result.push(current.trim());
|
|
365
|
+
current = '';
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
current += ch;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
result.push(current.trim());
|
|
373
|
+
return result;
|
|
374
|
+
}
|
|
375
|
+
_autoType(value) {
|
|
376
|
+
if (value === undefined || value === '' || value === null)
|
|
377
|
+
return null;
|
|
378
|
+
if (value === 'true')
|
|
379
|
+
return true;
|
|
380
|
+
if (value === 'false')
|
|
381
|
+
return false;
|
|
382
|
+
const num = Number(value);
|
|
383
|
+
if (!isNaN(num) && value.trim() !== '')
|
|
384
|
+
return num;
|
|
385
|
+
return value;
|
|
386
|
+
}
|
|
387
|
+
close() {
|
|
388
|
+
if (this._db) {
|
|
389
|
+
this._db.close();
|
|
390
|
+
this._db = null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Factory
|
|
396
|
+
*/
|
|
397
|
+
export function tabularDriver(dbName, storeName) {
|
|
398
|
+
return new TabularDriver(dbName, storeName);
|
|
399
|
+
}
|