UNPKG

ethercalc

Version:

Multi-User Spreadsheet Server — TypeScript rewrite (Cloudflare fullstack)

83 lines (79 loc) 3.25 kB
/** * Cross-sheet formula resolution helpers. * * SocialCalc formulas like `'other'!A1` need the referenced sheet loaded * into `SocialCalc.Formula.SheetCache` before recalc — otherwise the * formula evaluates to `#NAME?`. The DO fetches sibling snapshots via the * standard `/_do/snapshot` route; these helpers handle the orchestration * (pure / testable) so `room.ts` stays focused on I/O. */ /** * Pull the `sheet:` MIME part out of a full SocialCalc spreadsheet save. * `SocialCalc.Formula.AddSheetToCache` wants only the sheet-section body * (lines starting with `version:1.5` up to the next MIME boundary or EOF). * * If the input is a bare sheet save (no MIME envelope — e.g. what * `AddSheetToCache` itself produces, or what the migration tool stores), * returns it unchanged. If no recognizable sheet body is present, returns * the input untouched so the caller can still attempt `ParseSheetSave` — * it'll fail gracefully and SocialCalc will treat the ref as unresolvable. */ export function extractSheetSave(save: string): string { const match = /^version:1\.5$[\s\S]*?(?=^--SocialCalcSpreadsheetControlSave|$(?![\r\n]))/m.exec(save); if (match) return match[0]; return save; } /** * Shape of a spreadsheet wrapper we can orchestrate recalc against. * Kept as a structural type so the helper doesn't force a direct * dependency on `@ethercalc/socialcalc-headless`. */ export interface CrossSheetSpreadsheet { findCrossSheetRefs(): readonly string[]; addSiblingSheet(name: string, save: string): void; recalc(): void; } /** * Walk the spreadsheet's cross-sheet refs, fetch each sibling's save via * the caller-supplied `fetchSibling`, and inject into the spreadsheet's * formula cache. Calls `recalc()` once at the end iff any sibling was * added. Returns the number of siblings added (for metrics / tests). * * `ownName` is the current room's name (from the `?name=` query param); * it's used to skip self-references which would be re-entrant DO calls. * Pass `undefined` when the caller doesn't know the name (e.g. unit * tests). */ export async function hydrateCrossSheetRefs( ss: CrossSheetSpreadsheet, fetchSibling: (name: string) => Promise<string | null>, ownName?: string, ): Promise<number> { const refs = ss.findCrossSheetRefs(); // Stryker disable next-line ConditionalExpression : early-out optimization // that's functionally redundant — an empty `refs` makes the for-loop a // no-op, so dropping the guard produces identical output. if (refs.length === 0) return 0; let added = 0; for (const ref of refs) { if (ownName && ref === ownName) continue; let save: string | null; try { save = await fetchSibling(ref); } catch { // Sibling unreachable (e.g. workers recursion limit). Skip. // Stryker disable next-line BlockStatement : dropping the `continue` // falls through to `if (!save) continue;` below, and `save` is still // undefined because the assignment threw — so the net effect is the // same next-iteration skip. continue; } if (!save) continue; ss.addSiblingSheet(ref, extractSheetSave(save)); added++; } if (added > 0) { ss.recalc(); } return added; }