logisheets-runtime 1.9.0 → 1.11.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/dist/enterprise.d.ts +47 -0
- package/dist/enterprise.js +82 -23
- package/dist/index.d.ts +35 -0
- package/dist/index.js +66 -2
- package/package.json +26 -4
package/dist/enterprise.d.ts
CHANGED
|
@@ -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;
|
package/dist/enterprise.js
CHANGED
|
@@ -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(
|
|
164
|
+
authOk(authorization) {
|
|
165
165
|
if (!this.secret)
|
|
166
166
|
return true;
|
|
167
|
-
return
|
|
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
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
if (
|
|
191
|
-
return void res.writeHead(
|
|
192
|
-
|
|
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';
|
|
@@ -31,6 +49,23 @@ export declare class Workbook {
|
|
|
31
49
|
constructor(bookId: number, path?: string);
|
|
32
50
|
/** Read a single cell's evaluated value. */
|
|
33
51
|
getValue(sheetIdx: number, row: number, col: number): Value;
|
|
52
|
+
/**
|
|
53
|
+
* Serialize the workbook back to .xlsx bytes.
|
|
54
|
+
*
|
|
55
|
+
* `saveWorkbook` is deliberately absent from {@link Client}: it is a
|
|
56
|
+
* whole-file operation rather than one of the per-cell/per-sheet
|
|
57
|
+
* `WorkbookMethods`, so it goes straight through the engine entry here.
|
|
58
|
+
*
|
|
59
|
+
* @param appData opaque per-document JSON the host owns (craft state and
|
|
60
|
+
* friends). It round-trips through the file; pass what the
|
|
61
|
+
* engine's `getAppData` gave you, or nothing.
|
|
62
|
+
*/
|
|
63
|
+
save(appData?: string): Uint8Array;
|
|
64
|
+
/**
|
|
65
|
+
* Serialize and write the workbook to `path`. Defaults to the path it was
|
|
66
|
+
* loaded from, so a load -> edit -> `saveAs()` round-trip needs no argument.
|
|
67
|
+
*/
|
|
68
|
+
saveAs(path?: string, appData?: string): Promise<void>;
|
|
34
69
|
/** Undo the most recent transaction. Returns whether anything was undone. */
|
|
35
70
|
undo(): Promise<boolean>;
|
|
36
71
|
/** Redo the most recently undone transaction. Returns whether anything was redone. */
|
package/dist/index.js
CHANGED
|
@@ -10,12 +10,44 @@
|
|
|
10
10
|
// workbook logic lives in logisheets-core's WorkbookOps; the runtime only
|
|
11
11
|
// adapts the synchronous Node `handle()` entry point into the async Client that
|
|
12
12
|
// WorkbookOps consumes, then exposes that ops layer per workbook.
|
|
13
|
-
import { readFile } from 'node:fs/promises';
|
|
13
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
14
14
|
import { basename, resolve } from 'node:path';
|
|
15
|
-
import {
|
|
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.
|
|
@@ -66,6 +98,38 @@ export class Workbook {
|
|
|
66
98
|
getValue(sheetIdx, row, col) {
|
|
67
99
|
return handle({ method: 'getValue', value: { sheetIdx, row, col } }, this.id);
|
|
68
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Serialize the workbook back to .xlsx bytes.
|
|
103
|
+
*
|
|
104
|
+
* `saveWorkbook` is deliberately absent from {@link Client}: it is a
|
|
105
|
+
* whole-file operation rather than one of the per-cell/per-sheet
|
|
106
|
+
* `WorkbookMethods`, so it goes straight through the engine entry here.
|
|
107
|
+
*
|
|
108
|
+
* @param appData opaque per-document JSON the host owns (craft state and
|
|
109
|
+
* friends). It round-trips through the file; pass what the
|
|
110
|
+
* engine's `getAppData` gave you, or nothing.
|
|
111
|
+
*/
|
|
112
|
+
save(appData = '') {
|
|
113
|
+
const r = handle({ method: 'saveWorkbook', value: { appData } }, this.id);
|
|
114
|
+
if (r.code !== 0) {
|
|
115
|
+
throw new Error(`failed to save workbook (code ${r.code})`);
|
|
116
|
+
}
|
|
117
|
+
// The wasm boundary hands back either a typed array or a plain number
|
|
118
|
+
// array depending on the serializer path; normalize so callers can
|
|
119
|
+
// always treat this as bytes.
|
|
120
|
+
return r.data instanceof Uint8Array ? r.data : Uint8Array.from(r.data);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Serialize and write the workbook to `path`. Defaults to the path it was
|
|
124
|
+
* loaded from, so a load -> edit -> `saveAs()` round-trip needs no argument.
|
|
125
|
+
*/
|
|
126
|
+
async saveAs(path, appData = '') {
|
|
127
|
+
const target = path ?? this.path;
|
|
128
|
+
if (target === undefined) {
|
|
129
|
+
throw new Error('saveAs: no path given and this workbook was not loaded from one');
|
|
130
|
+
}
|
|
131
|
+
await writeFile(resolve(target), this.save(appData));
|
|
132
|
+
}
|
|
69
133
|
/** Undo the most recent transaction. Returns whether anything was undone. */
|
|
70
134
|
async undo() {
|
|
71
135
|
return (await this.client.undo()) === true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "logisheets-runtime",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.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",
|
|
@@ -17,12 +17,34 @@
|
|
|
17
17
|
],
|
|
18
18
|
"author": "Jeremy He",
|
|
19
19
|
"license": "MIT",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"spreadsheet",
|
|
22
|
+
"xlsx",
|
|
23
|
+
"excel",
|
|
24
|
+
"headless",
|
|
25
|
+
"nodejs",
|
|
26
|
+
"server",
|
|
27
|
+
"wasm",
|
|
28
|
+
"rust",
|
|
29
|
+
"formula",
|
|
30
|
+
"workbook"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/logisky/LogiSheets.git",
|
|
35
|
+
"directory": "packages/runtime"
|
|
36
|
+
},
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/logisky/LogiSheets/issues"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/logisky/LogiSheets",
|
|
20
41
|
"dependencies": {
|
|
21
|
-
"logisheets": "^1.
|
|
22
|
-
"logisheets-core": "^1.
|
|
42
|
+
"logisheets": "^1.11.0",
|
|
43
|
+
"logisheets-core": "^1.11.0"
|
|
23
44
|
},
|
|
24
45
|
"devDependencies": {
|
|
25
|
-
"
|
|
46
|
+
"@types/node": "^18",
|
|
47
|
+
"typescript": "^6.0.0",
|
|
26
48
|
"vitest": "3.2.6"
|
|
27
49
|
}
|
|
28
50
|
}
|