logisheets-runtime 1.3.0 → 1.5.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/craft.d.ts CHANGED
@@ -78,15 +78,38 @@ export declare function validateLoadedCrafts(loaded: readonly LoadedCraft[], wb:
78
78
  * contract as {@link applyCraftRequest}.
79
79
  */
80
80
  export declare function applyCraftResponse(loaded: readonly LoadedCraft[], resp: JsonRpcResponse, wb: Workbook): Promise<string[]>;
81
+ /** Application code: a craft's `onValidate` rejected the request's inputs. */
82
+ export declare const RPC_VALIDATION_FAILED = 1001;
81
83
  /**
82
- * A default {@link CraftRegistry} for local dev and tests. Crafts are
83
- * registered in-process against a fake base url; `importRuntime` just returns
84
- * the pre-registered module so no network or bundler is involved.
84
+ * Run one JSON-RPC exchange against a workbook's loaded crafts and return the
85
+ * reply. This is the generic "run a craft's request/response" primitive a
86
+ * host registers it as an RPC method (or task handler) and never re-implements
87
+ * the exchange. Composes the three hooks in order:
88
+ *
89
+ * 1. {@link applyCraftRequest} — write the request's inputs (`#INVALID_PARAMS` if a craft rejects one),
90
+ * 2. {@link validateLoadedCrafts} — validate them (`#VALIDATION_FAILED` + the violations if any fail),
91
+ * 3. {@link applyCraftResponse} — read the outputs into `result` (`#INTERNAL_ERROR` if a craft errors).
92
+ *
93
+ * The crafts define what the exchange does (via their onRequest/onResponse);
94
+ * this function is entirely craft-agnostic.
95
+ */
96
+ export declare function runCraftExchange(loaded: readonly LoadedCraft[], wb: Workbook, req: JsonRpcRequest): Promise<JsonRpcResponse>;
97
+ /**
98
+ * An in-memory {@link CraftRegistry}: crafts are registered in-process with
99
+ * their already-imported runtime module, so `importRuntime` returns it directly
100
+ * — no network, bundler, or filesystem. This is the general-purpose registry
101
+ * for a host that knows its crafts up front (embedding, local dev, tests);
102
+ * production may instead back the registry with a real HTTP registry.
85
103
  */
86
- export declare class MockCraftRegistry implements CraftRegistry {
104
+ export declare class MemoryCraftRegistry implements CraftRegistry {
87
105
  private readonly entries;
88
- /** Register a craft's manifest and its already-imported runtime module. */
89
- add(craftId: string, manifest: CraftManifest, module: unknown): this;
106
+ /**
107
+ * Register a craft by id with its runtime module (a {@link CraftRuntime},
108
+ * or a module whose default export is one). `manifest` is optional
109
+ * metadata; the default carries a non-empty `rtJs` so {@link loadCrafts}
110
+ * imports the module instead of skipping the craft as runtime-less.
111
+ */
112
+ add(craftId: string, module: unknown, manifest?: CraftManifest): this;
90
113
  getManifest(craftId: string): Promise<CraftManifest | undefined>;
91
114
  importRuntime(craftId: string): Promise<unknown | undefined>;
92
115
  }
package/dist/craft.js CHANGED
@@ -13,8 +13,9 @@
13
13
  // imported module *is* the craft's {@link CraftRuntime},
14
14
  // 4. call the runtime's `onLoad(state, workbook)` so it can rehydrate.
15
15
  //
16
- // The registry is a seam: production wires it to a real HTTP registry, tests
17
- // (and local dev) use {@link MockCraftRegistry}.
16
+ // The registry is a seam: production may wire it to a real HTTP registry;
17
+ // embedding hosts, local dev, and tests use the in-process {@link
18
+ // MemoryCraftRegistry}.
18
19
  /**
19
20
  * The name of the AppData entry the host folds craft/block/interaction state
20
21
  * into. Mirrors the envelope written by the browser host on save.
@@ -164,18 +165,68 @@ export async function applyCraftResponse(loaded, resp, wb) {
164
165
  }
165
166
  return errors;
166
167
  }
168
+ // JSON-RPC 2.0 error codes (kept local so this pure-logic module doesn't pull
169
+ // in the HTTP server that also owns them).
170
+ const RPC_INVALID_PARAMS = -32602;
171
+ const RPC_INTERNAL_ERROR = -32603;
172
+ /** Application code: a craft's `onValidate` rejected the request's inputs. */
173
+ export const RPC_VALIDATION_FAILED = 1001;
174
+ function errorResponse(id, code, message, data) {
175
+ return {
176
+ jsonrpc: '2.0',
177
+ id,
178
+ error: data === undefined ? { code, message } : { code, message, data },
179
+ };
180
+ }
181
+ /**
182
+ * Run one JSON-RPC exchange against a workbook's loaded crafts and return the
183
+ * reply. This is the generic "run a craft's request/response" primitive — a
184
+ * host registers it as an RPC method (or task handler) and never re-implements
185
+ * the exchange. Composes the three hooks in order:
186
+ *
187
+ * 1. {@link applyCraftRequest} — write the request's inputs (`#INVALID_PARAMS` if a craft rejects one),
188
+ * 2. {@link validateLoadedCrafts} — validate them (`#VALIDATION_FAILED` + the violations if any fail),
189
+ * 3. {@link applyCraftResponse} — read the outputs into `result` (`#INTERNAL_ERROR` if a craft errors).
190
+ *
191
+ * The crafts define what the exchange does (via their onRequest/onResponse);
192
+ * this function is entirely craft-agnostic.
193
+ */
194
+ export async function runCraftExchange(loaded, wb, req) {
195
+ const id = req.id ?? null;
196
+ const reqErrors = await applyCraftRequest(loaded, req, wb);
197
+ if (reqErrors.length)
198
+ return errorResponse(id, RPC_INVALID_PARAMS, reqErrors.join('; '));
199
+ const violations = await validateLoadedCrafts(loaded, wb);
200
+ if (violations.length)
201
+ return errorResponse(id, RPC_VALIDATION_FAILED, 'validation failed', violations);
202
+ const resp = { jsonrpc: '2.0', id };
203
+ const respErrors = await applyCraftResponse(loaded, resp, wb);
204
+ if (respErrors.length)
205
+ return errorResponse(id, RPC_INTERNAL_ERROR, respErrors.join('; '));
206
+ return resp;
207
+ }
167
208
  /**
168
- * A default {@link CraftRegistry} for local dev and tests. Crafts are
169
- * registered in-process against a fake base url; `importRuntime` just returns
170
- * the pre-registered module so no network or bundler is involved.
209
+ * An in-memory {@link CraftRegistry}: crafts are registered in-process with
210
+ * their already-imported runtime module, so `importRuntime` returns it directly
211
+ * no network, bundler, or filesystem. This is the general-purpose registry
212
+ * for a host that knows its crafts up front (embedding, local dev, tests);
213
+ * production may instead back the registry with a real HTTP registry.
171
214
  */
172
- export class MockCraftRegistry {
215
+ export class MemoryCraftRegistry {
173
216
  constructor() {
174
217
  this.entries = new Map();
175
218
  }
176
- /** Register a craft's manifest and its already-imported runtime module. */
177
- add(craftId, manifest, module) {
178
- this.entries.set(craftId, { manifest, module });
219
+ /**
220
+ * Register a craft by id with its runtime module (a {@link CraftRuntime},
221
+ * or a module whose default export is one). `manifest` is optional
222
+ * metadata; the default carries a non-empty `rtJs` so {@link loadCrafts}
223
+ * imports the module instead of skipping the craft as runtime-less.
224
+ */
225
+ add(craftId, module, manifest) {
226
+ this.entries.set(craftId, {
227
+ manifest: manifest ?? { rtJs: craftId, html: '' },
228
+ module,
229
+ });
179
230
  return this;
180
231
  }
181
232
  getManifest(craftId) {
@@ -51,6 +51,16 @@ export interface TaskContext {
51
51
  }
52
52
  /** A task handler runs one `rpcCall` against an ephemerally-loaded workbook. */
53
53
  export type TaskHandler = (ctx: TaskContext) => unknown | Promise<unknown>;
54
+ /**
55
+ * The standard `compute` task handler: drive the workbook's loaded crafts
56
+ * through one JSON-RPC exchange (see {@link runCraftExchange}). Generic and
57
+ * craft-agnostic — the crafts loaded from the workbook's AppData define what
58
+ * "compute" does via their `onRequest`/`onResponse`; a runtime serving craft
59
+ * workbooks uses this instead of hand-writing the exchange. `ctx.params` is
60
+ * forwarded verbatim as the request params (e.g. `{inputs}`) and the JSON-RPC
61
+ * response envelope is returned.
62
+ */
63
+ export declare const craftComputeHandler: TaskHandler;
54
64
  export interface EnterpriseServerOptions {
55
65
  runtime: SpreadsheetRuntime;
56
66
  /** Registry to load crafts from. Set after registration via {@link setRegistry}. */
@@ -15,7 +15,7 @@
15
15
  // depend on the merged data-gateway craft + engine cell ops (DATA_GATEWAY_CHANGES).
16
16
  import { createServer, } from 'node:http';
17
17
  import { SpreadsheetRuntime } from './index.js';
18
- import { loadCrafts, } from './craft.js';
18
+ import { loadCrafts, runCraftExchange, } from './craft.js';
19
19
  import { WorkbookWatcher } from './watcher.js';
20
20
  export class ControlPlaneClient {
21
21
  constructor(opts) {
@@ -102,6 +102,24 @@ export class HttpCraftRegistry {
102
102
  }
103
103
  }
104
104
  }
105
+ /**
106
+ * The standard `compute` task handler: drive the workbook's loaded crafts
107
+ * through one JSON-RPC exchange (see {@link runCraftExchange}). Generic and
108
+ * craft-agnostic — the crafts loaded from the workbook's AppData define what
109
+ * "compute" does via their `onRequest`/`onResponse`; a runtime serving craft
110
+ * workbooks uses this instead of hand-writing the exchange. `ctx.params` is
111
+ * forwarded verbatim as the request params (e.g. `{inputs}`) and the JSON-RPC
112
+ * response envelope is returned.
113
+ */
114
+ export const craftComputeHandler = (ctx) => {
115
+ const req = {
116
+ jsonrpc: '2.0',
117
+ id: 1,
118
+ method: 'compute',
119
+ params: ctx.params,
120
+ };
121
+ return runCraftExchange(ctx.crafts, ctx.workbook, req);
122
+ };
105
123
  export class EnterpriseRuntimeServer {
106
124
  constructor(opts) {
107
125
  this.handlers = new Map();
@@ -110,6 +128,9 @@ export class EnterpriseRuntimeServer {
110
128
  this.runtime = opts.runtime;
111
129
  this.registry = opts.registry;
112
130
  this.secret = opts.secret;
131
+ // Ship a generic `compute` by default so a craft-serving runtime needs
132
+ // no bespoke handler; anything in opts.taskHandlers can override it.
133
+ this.handlers.set('compute', craftComputeHandler);
113
134
  for (const [name, h] of Object.entries(opts.taskHandlers ?? {}))
114
135
  this.handlers.set(name, h);
115
136
  }
@@ -211,7 +232,9 @@ export class EnterpriseRuntimeServer {
211
232
  throw new Error(`unknown rpcCall: ${body.rpcCall}`);
212
233
  const wb = await this.loadFromUrl(body.workbookUrl);
213
234
  try {
214
- const crafts = this.registry ? await loadCrafts(wb, this.registry) : [];
235
+ const crafts = this.registry
236
+ ? await loadCrafts(wb, this.registry)
237
+ : [];
215
238
  return await handler({ workbook: wb, crafts, params: body.params });
216
239
  }
217
240
  finally {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logisheets-runtime",
3
- "version": "1.3.0",
3
+ "version": "1.5.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,8 +18,8 @@
18
18
  "author": "Jeremy He",
19
19
  "license": "MIT",
20
20
  "dependencies": {
21
- "logisheets": "^1.3.0",
22
- "logisheets-core": "^1.3.0"
21
+ "logisheets": "^1.5.0",
22
+ "logisheets-core": "^1.5.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "typescript": "^5.5.0",