Skip to content

Commit 050b00e

Browse files
committed
wip
1 parent 181cf10 commit 050b00e

4 files changed

Lines changed: 132 additions & 16 deletions

File tree

apps/dashboard/public/rayforce.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/dashboard/public/rayforce.umd.js

Lines changed: 52 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,25 @@
208208

209209
class RayError extends RayObject {
210210
get isError() { return true; }
211-
get message() { return this.toString(); }
211+
212+
// Get error message directly from WASM (no allocation, always works)
213+
get message() {
214+
return this._sdk._getErrorMessage(this._ptr) || 'Unknown error';
215+
}
216+
217+
// Get structured error info as a plain JS object (may fail if OOM)
218+
get info() {
219+
try {
220+
const infoPtr = this._sdk._getErrorInfo(this._ptr);
221+
if (infoPtr === 0) return { code: 'unknown', message: this.message };
222+
const infoObj = this._sdk._wrapPtr(infoPtr);
223+
if (!infoObj || infoObj.isNull) return { code: 'unknown', message: this.message };
224+
return infoObj.toJS();
225+
} catch (e) {
226+
return { code: 'unknown', message: this.message };
227+
}
228+
}
229+
212230
toJS() { throw new Error(this.message); }
213231
}
214232

@@ -402,13 +420,33 @@
402420
const names = this.columnNames();
403421
const count = this.rowCount;
404422
const rows = [];
423+
424+
// Cache column references to avoid repeated lookups
425+
const cols = names.map(name => this.col(name));
426+
405427
for (let i = 0; i < count; i++) {
406428
const row = {};
407-
for (const name of names) {
408-
row[name] = this.col(name).at(i);
409-
if (typeof row[name] === 'bigint') {
410-
const n = Number(row[name]);
411-
row[name] = Number.isSafeInteger(n) ? n : row[name];
429+
for (let c = 0; c < names.length; c++) {
430+
const col = cols[c];
431+
if (!col || col.isNull) {
432+
row[names[c]] = null;
433+
continue;
434+
}
435+
436+
try {
437+
let val = col.at(i);
438+
// If at() returns a RayObject (e.g. from List), convert to JS
439+
if (val instanceof RayObject) {
440+
val = val.toJS();
441+
}
442+
// Handle BigInt values
443+
if (typeof val === 'bigint') {
444+
const n = Number(val);
445+
val = Number.isSafeInteger(n) ? n : val;
446+
}
447+
row[names[c]] = val;
448+
} catch (e) {
449+
row[names[c]] = null;
412450
}
413451
}
414452
rows.push(row);
@@ -578,6 +616,8 @@
578616
this._isObjVector = w.cwrap('is_obj_vector', 'number', ['number']);
579617
this._isObjNull = w.cwrap('is_obj_null', 'number', ['number']);
580618
this._isObjError = w.cwrap('is_obj_error', 'number', ['number']);
619+
this._getErrorInfo = w.cwrap('get_error_info', 'number', ['number']);
620+
this._getErrorMessage = w.cwrap('get_error_message', 'string', ['number']);
581621
this._getObjRc = w.cwrap('get_obj_rc', 'number', ['number']);
582622

583623
this._getDataPtr = w.cwrap('get_data_ptr', 'number', ['number']);
@@ -843,9 +883,13 @@
843883
}
844884

845885
set(name, value) {
886+
// Create symbol object (binary_set expects -TYPE_SYMBOL object pointer)
846887
const sym = this.symbol(name);
847888
const val = value instanceof RayObject ? value : this._toRayObject(value);
889+
// binary_set internally clones the value, so we just pass the pointer
848890
this._globalSet(sym._ptr, val._ptr);
891+
// Drop the symbol wrapper after use
892+
sym.drop();
849893
}
850894

851895
get(name) { return this.eval(name); }
@@ -863,7 +907,8 @@
863907
try {
864908
this._wasm.stringToUTF8(content, stringOnHeap, lengthBytes);
865909
// Pass pointer and length (excluding null terminator)
866-
return new Table(this, this._readCSV(stringOnHeap, lengthBytes - 1));
910+
// Use _wrapPtr to properly detect errors vs tables
911+
return this._wrapPtr(this._readCSV(stringOnHeap, lengthBytes - 1));
867912
} finally {
868913
this._wasm._free(stringOnHeap);
869914
}
6.6 KB
Binary file not shown.

apps/dashboard/src/lib/rayforce.ts

Lines changed: 79 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,22 @@ interface EmscriptenModule {
179179
ccall: (name: string, returnType: string, argTypes: string[], args: unknown[]) => unknown;
180180
}
181181

182+
/** Structured error information from Rayforce */
183+
interface ErrorInfo {
184+
code: string;
185+
message?: string;
186+
expected?: string;
187+
got?: string;
188+
need?: number;
189+
have?: number;
190+
index?: number;
191+
bound?: number;
192+
name?: string;
193+
type?: string;
194+
limit?: number;
195+
raw?: string;
196+
}
197+
182198
interface RayObject {
183199
ptr: number;
184200
type: number;
@@ -192,6 +208,11 @@ interface RayObject {
192208
drop(): void;
193209
}
194210

211+
interface RayError extends RayObject {
212+
info: ErrorInfo;
213+
message: string;
214+
}
215+
195216
interface Vector extends RayObject {
196217
typedArray: ArrayBufferView;
197218
at(idx: number): unknown;
@@ -747,26 +768,40 @@ export class RayforceClient {
747768
throw new Error('SDK not loaded');
748769
}
749770

750-
logInfo('Rayforce', `Loading CSV into table '${name}' (${content.length} bytes)`);
771+
logInfo('Rayforce', `Loading CSV into table '${name}' (${content.length} bytes, ${(content.length / (1024 * 1024)).toFixed(1)} MB)`);
751772

752773
// 1. Parse CSV into Table object
753774
// Note: read_csv returns a Table object associated with the WASM heap
754775
const table = this.sdk.read_csv(content);
776+
777+
logDebug('Rayforce', `read_csv returned: type=${table.type}, isNull=${table.isNull}, isError=${table.isError}, ptr=${table.ptr}`);
755778

756779
if (table.isError) {
757-
const err = table.toString();
780+
// Use structured error info for clean display
781+
const errObj = table as unknown as RayError;
782+
const errMsg = errObj.message || table.toString();
783+
logError('Rayforce', `CSV parse error: ${errMsg}`);
758784
table.drop();
759-
throw new Error(`CSV parse error: ${err}`);
785+
throw new Error(errMsg);
786+
}
787+
788+
if (table.isNull) {
789+
logError('Rayforce', `read_csv returned null`);
790+
throw new Error('CSV parse returned null');
760791
}
761792

793+
// Get table info before setting
794+
const rowCount = (table as Table).rowCount;
795+
const columns = (table as Table).columnNames();
796+
logInfo('Rayforce', `Parsed table: ${rowCount} rows, ${columns.length} columns (${columns.join(', ')})`);
797+
762798
// 2. Register table as a variable
763799
// This allows SQL/queries to reference it by name
764800
this.sdk.set(name, table);
801+
logDebug('Rayforce', `Called set('${name}', table)`);
765802

766803
// 3. Drop our reference (the variable 'name' now holds a reference)
767-
// Actually, set() might copy or increment ref count.
768-
// Usually SDK.set takes ownership or we should drop our handle if we don't need it.
769-
// Based on typical Rayforce pattern, we should drop the local handle.
804+
// binary_set clones the value, so dropping our reference is safe
770805
table.drop();
771806

772807
logInfo('Rayforce', `CSV loaded successfully as '${name}'`);
@@ -1299,7 +1334,10 @@ export class RayforceClient {
12991334
}
13001335

13011336
if (obj.isError) {
1302-
return { type: 'error', data: obj.toString() };
1337+
// Use structured error info for clean display
1338+
const errObj = obj as unknown as RayError;
1339+
const errMsg = errObj.message || obj.toString();
1340+
return { type: 'error', data: errMsg };
13031341
}
13041342

13051343
const type = Math.abs(obj.type);
@@ -1309,10 +1347,43 @@ export class RayforceClient {
13091347
const table = obj as Table;
13101348
const columns = table.columnNames();
13111349
const rowCount = table.rowCount;
1350+
1351+
logDebug('Rayforce', `wrapRayObject TABLE: columns=${columns.length}, rowCount=${rowCount}`);
1352+
1353+
// For display, only convert first N rows (large tables would be too slow)
1354+
const MAX_DISPLAY_ROWS = 1000;
1355+
let rows: Record<string, unknown>[] = [];
1356+
1357+
if (rowCount <= MAX_DISPLAY_ROWS) {
1358+
// Small table - convert all rows
1359+
try {
1360+
const startTime = performance.now();
1361+
rows = table.toRows();
1362+
logDebug('Rayforce', `toRows() returned ${rows.length} rows in ${(performance.now() - startTime).toFixed(1)}ms`);
1363+
} catch (err) {
1364+
logError('Rayforce', `toRows() failed: ${err}`);
1365+
}
1366+
} else {
1367+
// Large table - only get first N rows for display
1368+
logDebug('Rayforce', `Large table (${rowCount} rows), showing first ${MAX_DISPLAY_ROWS} for display`);
1369+
try {
1370+
const startTime = performance.now();
1371+
for (let i = 0; i < MAX_DISPLAY_ROWS && i < rowCount; i++) {
1372+
const rowObj = table.row(i);
1373+
if (rowObj && !rowObj.isNull) {
1374+
rows.push(rowObj.toJS() as Record<string, unknown>);
1375+
}
1376+
}
1377+
logDebug('Rayforce', `Extracted ${rows.length} display rows in ${(performance.now() - startTime).toFixed(1)}ms`);
1378+
} catch (err) {
1379+
logError('Rayforce', `Row extraction failed: ${err}`);
1380+
}
1381+
}
13121382

13131383
return {
13141384
type: 'table',
13151385
rayObject: obj,
1386+
data: rows,
13161387
columns,
13171388
rowCount,
13181389
// Zero-copy column access
@@ -1321,7 +1392,7 @@ export class RayforceClient {
13211392
if (!col) return null;
13221393
return col.typedArray || null;
13231394
},
1324-
// Lazy JS conversion
1395+
// Full conversion - use with caution for large tables
13251396
toJS: () => table.toRows(),
13261397
};
13271398
}

0 commit comments

Comments
 (0)