logisheets-runtime 1.8.0 → 1.10.0

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/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # logisheets-runtime
2
+
3
+ A **headless LogiSheets spreadsheet runtime for Node** — the Node counterpart of
4
+ the browser app. It wires the engine-neutral logic in
5
+ [`logisheets-core`](https://www.npmjs.com/package/logisheets-core) to the Node
6
+ WASM engine, so you can read, manipulate, and write real `.xlsx` files on the
7
+ server with no browser and no UI.
8
+
9
+ A single `SpreadsheetRuntime` owns many open workbooks at once; every operation
10
+ runs against a specific `Workbook` handle. All workbook logic comes from
11
+ `logisheets-core`'s `WorkbookOps` — the runtime only adapts the synchronous Node
12
+ `handle()` entry point into the async `Client` that layer expects.
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install logisheets-runtime
18
+ ```
19
+
20
+ It pulls in `logisheets` (the Node WASM engine) and `logisheets-core`. ESM-only;
21
+ requires a recent Node with WASM support.
22
+
23
+ ## Quick start
24
+
25
+ ```ts
26
+ import {SpreadsheetRuntime} from 'logisheets-runtime'
27
+
28
+ const rt = new SpreadsheetRuntime()
29
+
30
+ // Open workbooks from disk, from bytes, or create empty ones.
31
+ const wb1 = await rt.loadWorkbook('a.xlsx')
32
+ const wb2 = rt.createWorkbook()
33
+
34
+ // Drive edits through the shared core operation layer.
35
+ await wb1.ops.inputCell(0, 0, 0, 'Hello') // sheet 0, cell A1
36
+ await wb1.ops.createSheet(1, 'Summary')
37
+
38
+ // Read evaluated values straight off the handle.
39
+ const v = wb1.getValue(0, 0, 0)
40
+
41
+ // Undo / redo history is per workbook.
42
+ await wb1.undo()
43
+ await wb1.redo()
44
+
45
+ // Release engine resources when done.
46
+ rt.close(wb1)
47
+ rt.closeAll()
48
+ ```
49
+
50
+ ## `SpreadsheetRuntime`
51
+
52
+ | Member | Description |
53
+ |--------|-------------|
54
+ | `createWorkbook()` | Create a new empty workbook |
55
+ | `loadWorkbook(path)` | Load a `.xlsx` from disk (dedups by absolute path) |
56
+ | `loadWorkbookFromBytes(bytes, name, path?)` | Load a `.xlsx` from raw bytes in memory |
57
+ | `workbooks` | All currently open workbooks |
58
+ | `close(wb)` / `closeAll()` | Release engine resources |
59
+
60
+ ## `Workbook`
61
+
62
+ Obtain one from the runtime, then run every operation against it so the target
63
+ workbook is always explicit.
64
+
65
+ | Member | Description |
66
+ |--------|-------------|
67
+ | `id` | The workbook's engine id |
68
+ | `ops` | The shared `WorkbookOps` (cell input, sheets, blocks, styles, …) bound to this workbook |
69
+ | `client` | Raw async `Client` — escape hatch for engine calls not yet on `ops` |
70
+ | `getValue(sheetIdx, row, col)` | Read a single evaluated cell value |
71
+ | `undo()` / `redo()` | Step the history; returns whether anything changed |
72
+ | `cleanHistory()` | Drop history, keeping current state as baseline |
73
+ | `discardChanges()` | Revert every change back to the current baseline |
74
+
75
+ ## Beyond a single process
76
+
77
+ Everything below is exported from the top-level `logisheets-runtime` entry
78
+ (alongside the full re-exported `logisheets-core` surface), so you import it all
79
+ from one place:
80
+
81
+ - **RPC** — a JSON-RPC server for issuing operations against the runtime.
82
+ - **Crafts** — headlessly reconstruct the crafts a workbook depends on.
83
+ - **Watcher** — hot-(re)load workbooks from `wb_*.json` descriptors.
84
+ - **Enterprise** — register with the control panel and pull crafts from the enterprise registry.
85
+
86
+ ## License
87
+
88
+ MIT — part of the [LogiSheets](https://github.com/logisky/LogiSheets) project.
@@ -70,6 +70,26 @@ export interface EnterpriseServerOptions {
70
70
  /** rpcCall → handler (e.g. extractIndicators, compute). */
71
71
  taskHandlers?: Record<string, TaskHandler>;
72
72
  }
73
+ /**
74
+ * A framework-neutral view of an incoming HTTP request. Lets a non-Node host
75
+ * (e.g. a Cloudflare Worker `fetch` handler) drive {@link
76
+ * EnterpriseRuntimeServer.handleRequest} without Node's `IncomingMessage`.
77
+ * `body` is the ALREADY-PARSED JSON body (POST only; the host owns parsing).
78
+ */
79
+ export interface RuntimeHttpRequest {
80
+ method: string;
81
+ /** URL pathname, e.g. `/task`. */
82
+ path: string;
83
+ /** The `Authorization` header value, for the shared-secret gate. */
84
+ authorization?: string;
85
+ body?: unknown;
86
+ }
87
+ /** The framework-neutral result of {@link EnterpriseRuntimeServer.handleRequest};
88
+ * a `body` of `undefined` means "no content" (write just the status). */
89
+ export interface RuntimeHttpResponse {
90
+ status: number;
91
+ body?: unknown;
92
+ }
73
93
  export declare class EnterpriseRuntimeServer {
74
94
  private readonly runtime;
75
95
  private registry?;
@@ -83,6 +103,33 @@ export declare class EnterpriseRuntimeServer {
83
103
  listen(port: number, host?: string): Promise<AddressInfo>;
84
104
  close(): Promise<void>;
85
105
  private authOk;
106
+ /**
107
+ * Transport-agnostic request handler: the `/pin` · `/unpin` · `/task` ·
108
+ * `/status` routing with NO dependency on Node's http types. The Node
109
+ * server ({@link onRequest}) routes through this, and so can any other host
110
+ * — e.g. a Cloudflare Worker, which cannot use {@link listen} (Workers have
111
+ * no listening socket; they're `fetch`-driven):
112
+ *
113
+ * ```ts
114
+ * export default {
115
+ * async fetch(request: Request) {
116
+ * const url = new URL(request.url)
117
+ * const body =
118
+ * request.method === 'POST' ? await request.json() : undefined
119
+ * const {status, body: out} = await server.handleRequest({
120
+ * method: request.method,
121
+ * path: url.pathname,
122
+ * authorization: request.headers.get('authorization') ?? undefined,
123
+ * body,
124
+ * })
125
+ * return out === undefined
126
+ * ? new Response(null, {status})
127
+ * : Response.json(out, {status})
128
+ * },
129
+ * }
130
+ * ```
131
+ */
132
+ handleRequest(req: RuntimeHttpRequest): Promise<RuntimeHttpResponse>;
86
133
  private loadFromUrl;
87
134
  private onRequest;
88
135
  private pin;
@@ -161,10 +161,75 @@ export class EnterpriseRuntimeServer {
161
161
  this.server = undefined;
162
162
  return new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
163
163
  }
164
- authOk(req) {
164
+ authOk(authorization) {
165
165
  if (!this.secret)
166
166
  return true;
167
- return req.headers.authorization === `Bearer ${this.secret}`;
167
+ return authorization === `Bearer ${this.secret}`;
168
+ }
169
+ /**
170
+ * Transport-agnostic request handler: the `/pin` · `/unpin` · `/task` ·
171
+ * `/status` routing with NO dependency on Node's http types. The Node
172
+ * server ({@link onRequest}) routes through this, and so can any other host
173
+ * — e.g. a Cloudflare Worker, which cannot use {@link listen} (Workers have
174
+ * no listening socket; they're `fetch`-driven):
175
+ *
176
+ * ```ts
177
+ * export default {
178
+ * async fetch(request: Request) {
179
+ * const url = new URL(request.url)
180
+ * const body =
181
+ * request.method === 'POST' ? await request.json() : undefined
182
+ * const {status, body: out} = await server.handleRequest({
183
+ * method: request.method,
184
+ * path: url.pathname,
185
+ * authorization: request.headers.get('authorization') ?? undefined,
186
+ * body,
187
+ * })
188
+ * return out === undefined
189
+ * ? new Response(null, {status})
190
+ * : Response.json(out, {status})
191
+ * },
192
+ * }
193
+ * ```
194
+ */
195
+ async handleRequest(req) {
196
+ if (req.method === 'GET' && req.path === '/status') {
197
+ if (!this.authOk(req.authorization))
198
+ return { status: 403, body: { error: 'forbidden' } };
199
+ return {
200
+ status: 200,
201
+ body: {
202
+ pins: [...this.pins.keys()],
203
+ open: this.runtime.workbooks.length,
204
+ },
205
+ };
206
+ }
207
+ if (req.method !== 'POST')
208
+ return { status: 405 };
209
+ if (!this.authOk(req.authorization))
210
+ return { status: 403, body: { error: 'forbidden' } };
211
+ const body = req.body ?? {};
212
+ try {
213
+ if (req.path === '/pin')
214
+ return {
215
+ status: 200,
216
+ body: await this.pin(body),
217
+ };
218
+ if (req.path === '/unpin')
219
+ return { status: 200, body: this.unpin(body) };
220
+ if (req.path === '/task')
221
+ return {
222
+ status: 200,
223
+ body: await this.task(body),
224
+ };
225
+ return { status: 404, body: { error: 'not found' } };
226
+ }
227
+ catch (e) {
228
+ return {
229
+ status: 500,
230
+ body: { error: e instanceof Error ? e.message : String(e) },
231
+ };
232
+ }
168
233
  }
169
234
  async loadFromUrl(url) {
170
235
  const res = await fetch(url);
@@ -176,29 +241,23 @@ export class EnterpriseRuntimeServer {
176
241
  await loadCrafts(wb, this.registry);
177
242
  return wb;
178
243
  }
244
+ // Node http transport: parse the request into a framework-neutral shape,
245
+ // route it through {@link handleRequest}, and write the result back. All
246
+ // routing/auth lives in handleRequest so non-Node hosts share it verbatim.
179
247
  async onRequest(req, res) {
180
248
  try {
181
- const url = req.url ?? '/';
182
- if (req.method === 'GET' && url === '/status') {
183
- if (!this.authOk(req))
184
- return json(res, 403, { error: 'forbidden' });
185
- return json(res, 200, {
186
- pins: [...this.pins.keys()],
187
- open: this.runtime.workbooks.length,
188
- });
189
- }
190
- if (req.method !== 'POST')
191
- return void res.writeHead(405).end();
192
- if (!this.authOk(req))
193
- return json(res, 403, { error: 'forbidden' });
194
- const body = JSON.parse((await readBody(req)) || '{}');
195
- if (url === '/pin')
196
- return json(res, 200, await this.pin(body));
197
- if (url === '/unpin')
198
- return json(res, 200, this.unpin(body));
199
- if (url === '/task')
200
- return json(res, 200, await this.task(body));
201
- return json(res, 404, { error: 'not found' });
249
+ const body = req.method === 'POST'
250
+ ? JSON.parse((await readBody(req)) || '{}')
251
+ : undefined;
252
+ const out = await this.handleRequest({
253
+ method: req.method ?? 'GET',
254
+ path: req.url ?? '/',
255
+ authorization: req.headers.authorization,
256
+ body,
257
+ });
258
+ if (out.body === undefined)
259
+ return void res.writeHead(out.status).end();
260
+ json(res, out.status, out.body);
202
261
  }
203
262
  catch (e) {
204
263
  json(res, 500, { error: e instanceof Error ? e.message : String(e) });
package/dist/index.d.ts CHANGED
@@ -1,6 +1,24 @@
1
1
  import type { Value, Client } from 'logisheets-web';
2
2
  import { WorkbookOps } from 'logisheets-core';
3
3
  export * from 'logisheets-core';
4
+ /** The wasm-bindgen `handle(msg, bookId?)` entry — the single call the whole
5
+ * runtime issues against the engine. */
6
+ export type WasmHandle = (msg: unknown, bookId?: number | null) => unknown;
7
+ /**
8
+ * Inject the wasm `handle` entry. Required on non-Node hosts (Cloudflare
9
+ * Worker, Deno, browser): initialize the web-target glue with an imported
10
+ * module and pass its `handle` here BEFORE creating/loading any workbook, e.g.
11
+ *
12
+ * ```ts
13
+ * import wasmModule from 'logisheets-web/wasm/logisheets_wasm_server_bg.wasm'
14
+ * import {initSync, handle} from 'logisheets-web/wasm/logisheets_wasm_server.js'
15
+ * initSync({module: wasmModule})
16
+ * setWasmHandle(handle)
17
+ * ```
18
+ *
19
+ * Optional on Node — the node-target glue is loaded on first use if unset.
20
+ */
21
+ export declare function setWasmHandle(fn: WasmHandle): void;
4
22
  export * from './rpc.js';
5
23
  export * from './craft.js';
6
24
  export * from './watcher.js';
package/dist/index.js CHANGED
@@ -12,10 +12,42 @@
12
12
  // WorkbookOps consumes, then exposes that ops layer per workbook.
13
13
  import { readFile } from 'node:fs/promises';
14
14
  import { basename, resolve } from 'node:path';
15
- import { handle } from 'logisheets/wasm/logisheets_wasm_server.js';
15
+ import { createRequire } from 'node:module';
16
16
  import { WorkbookOps } from 'logisheets-core';
17
17
  // Re-export the core surface so consumers import everything from one place.
18
18
  export * from 'logisheets-core';
19
+ let handleImpl;
20
+ /**
21
+ * Inject the wasm `handle` entry. Required on non-Node hosts (Cloudflare
22
+ * Worker, Deno, browser): initialize the web-target glue with an imported
23
+ * module and pass its `handle` here BEFORE creating/loading any workbook, e.g.
24
+ *
25
+ * ```ts
26
+ * import wasmModule from 'logisheets-web/wasm/logisheets_wasm_server_bg.wasm'
27
+ * import {initSync, handle} from 'logisheets-web/wasm/logisheets_wasm_server.js'
28
+ * initSync({module: wasmModule})
29
+ * setWasmHandle(handle)
30
+ * ```
31
+ *
32
+ * Optional on Node — the node-target glue is loaded on first use if unset.
33
+ */
34
+ export function setWasmHandle(fn) {
35
+ handleImpl = fn;
36
+ }
37
+ // Node fallback: synchronously require the node-target glue (it auto-initializes
38
+ // the wasm from disk on load). Only reached when no handle was injected; a
39
+ // non-Node host injects first, so `createRequire` never runs there.
40
+ function loadNodeHandle() {
41
+ const require = createRequire(import.meta.url);
42
+ return require('logisheets/wasm/logisheets_wasm_server.js').handle;
43
+ }
44
+ /** The engine entry the runtime calls; resolves the injected handle (or the
45
+ * Node default) on first use. */
46
+ function handle(msg, bookId) {
47
+ if (handleImpl === undefined)
48
+ handleImpl = loadNodeHandle();
49
+ return handleImpl(msg, bookId);
50
+ }
19
51
  // The developer-defined JSON-RPC server (operations run against this runtime).
20
52
  export * from './rpc.js';
21
53
  // Craft loading: reconstruct the crafts a workbook depends on, headlessly.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logisheets-runtime",
3
- "version": "1.8.0",
3
+ "version": "1.10.0",
4
4
  "description": "Headless LogiSheets spreadsheet runtime for Node — logisheets-core wired to the Node WASM engine. The Node counterpart of the browser app.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,11 +18,12 @@
18
18
  "author": "Jeremy He",
19
19
  "license": "MIT",
20
20
  "dependencies": {
21
- "logisheets": "^1.8.0",
22
- "logisheets-core": "^1.8.0"
21
+ "logisheets": "^1.10.0",
22
+ "logisheets-core": "^1.10.0"
23
23
  },
24
24
  "devDependencies": {
25
- "typescript": "^5.5.0",
26
- "vitest": "^2.0.0"
25
+ "@types/node": "^18",
26
+ "typescript": "^6.0.0",
27
+ "vitest": "3.2.6"
27
28
  }
28
29
  }