logisheets-core 1.1.0 → 1.2.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.
@@ -0,0 +1,4 @@
1
+ export * from './runtime.js';
2
+ export * from './state.js';
3
+ export * from './manifest.js';
4
+ export * from './rpc.js';
@@ -0,0 +1,4 @@
1
+ export * from './runtime.js';
2
+ export * from './state.js';
3
+ export * from './manifest.js';
4
+ export * from './rpc.js';
@@ -0,0 +1,10 @@
1
+ export interface CraftManifest {
2
+ /**
3
+ * The path of the runtime JS file
4
+ */
5
+ readonly rtJs: string;
6
+ /**
7
+ * The path of the html url which is used in web
8
+ */
9
+ readonly html: string;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,17 @@
1
+ export interface JsonRpcRequest {
2
+ jsonrpc: '2.0';
3
+ id?: string | number | null;
4
+ method: string;
5
+ params?: unknown;
6
+ }
7
+ export interface JsonRpcError {
8
+ code: number;
9
+ message: string;
10
+ data?: unknown;
11
+ }
12
+ export interface JsonRpcResponse {
13
+ jsonrpc: '2.0';
14
+ id: string | number | null;
15
+ result?: unknown;
16
+ error?: JsonRpcError;
17
+ }
@@ -0,0 +1,8 @@
1
+ // JSON-RPC 2.0 wire types shared across the craft contract.
2
+ //
3
+ // These describe the envelope a craft's runtime sees in `onRequest` /
4
+ // `onResponse` (see ./runtime.ts). They live in logisheets-core — the shared,
5
+ // host-neutral layer — so both the craft interface here and the Node host's
6
+ // RpcServer (logisheets-runtime) can reference one definition. The host owns
7
+ // the transport (HTTP, dispatch, error mapping); these are only the shapes.
8
+ export {};
@@ -0,0 +1,46 @@
1
+ import type { Result } from 'logisheets-web';
2
+ import type { Violation } from '../validation/index.js';
3
+ import type { JsonRpcRequest, JsonRpcResponse } from './rpc.js';
4
+ /**
5
+ * A craft's persisted state. It is always a JSON object (the craft serializes
6
+ * it to a string for storage in AppData, and the host treats that string as
7
+ * opaque). Craft authors narrow this by supplying their own type argument to
8
+ * {@link CraftRuntime}.
9
+ */
10
+ export type CraftState = Record<string, unknown>;
11
+ /**
12
+ * A lifecycle hook may run engine operations, which are async on every host.
13
+ * So every hook may return its {@link Result} directly or a promise of it; the
14
+ * host always awaits before inspecting the value.
15
+ */
16
+ export type MaybePromise<T> = T | Promise<T>;
17
+ /**
18
+ * The headless logic of a craft, reconstructed by a host from a workbook's
19
+ * persisted craft state.
20
+ *
21
+ * `S` is the craft's own state shape (defaults to a generic JSON object,
22
+ * {@link CraftState}). `W` is the host's workbook handle — logisheets-core is
23
+ * host-neutral and does not know the concrete type, so each host binds it: the
24
+ * Node runtime to its `Workbook`, the browser to its craft workbook wrapper.
25
+ *
26
+ * The hooks fire around a single JSON-RPC exchange in this order:
27
+ *
28
+ * onLoad once, when the workbook is opened — rehydrate from state
29
+ * onRequest inputs of an incoming request are about to be applied
30
+ * onValidate inputs are now in place; check them BEFORE a response is read
31
+ * onResponse the response has been produced and is about to be returned
32
+ */
33
+ export interface CraftRuntime<S extends CraftState = CraftState, W = unknown> {
34
+ onLoad: (s: S, wb: W) => MaybePromise<Result<void>>;
35
+ onRequest: (req: JsonRpcRequest, s: S, wb: W) => MaybePromise<Result<void>>;
36
+ /**
37
+ * Called once an RPC request's inputs have been written into the workbook
38
+ * but BEFORE its response is read back — the gateway's validation
39
+ * checkpoint. Returns the cells that fail their rules (empty when all
40
+ * pass). A host that gets a non-empty result should reject the request and
41
+ * roll the inputs back rather than return a response computed from invalid
42
+ * input. Optional: a craft with no validation needs simply omits it.
43
+ */
44
+ onValidate?: (s: S, wb: W) => MaybePromise<Result<readonly Violation[]>>;
45
+ onResponse: (resp: JsonRpcResponse, s: S, wb: W) => MaybePromise<Result<void>>;
46
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,5 @@
1
+ export declare function setCraftState(craftId: string, json: string): void;
2
+ export declare function getCraftState(craftId: string): string | undefined;
3
+ export declare function clearCraftState(craftId: string): void;
4
+ export declare function getPersistentCraftStates(): Record<string, string>;
5
+ export declare function loadPersistentCraftStates(data: unknown): void;
@@ -0,0 +1,48 @@
1
+ // Host-held store of opaque per-craft JSON state.
2
+ //
3
+ // A craft (running in its iframe) pushes its own serialized state here via the
4
+ // injected `setCraftState(json)` API, keyed by the craft's id (its iframe src
5
+ // path — stable across sessions). On file save the host folds the whole store
6
+ // into the AppData envelope under `craftStates`; on load it is rehydrated, so a
7
+ // craft can read its previous state back via `getCraftState()` the next time
8
+ // its iframe mounts.
9
+ //
10
+ // The host treats each entry as an opaque string and never parses it — the
11
+ // craft owns its own schema. This state lives entirely outside the engine's
12
+ // undo/redo Status (it rides AppData, a side channel on the workbook), so
13
+ // writing it never pollutes edit history.
14
+ const craftStates = new Map();
15
+ // Push a craft's serialized state. Called from the iframe-injected
16
+ // `setCraftState`; `craftId` is captured per-iframe by the host.
17
+ export function setCraftState(craftId, json) {
18
+ if (!craftId)
19
+ return;
20
+ craftStates.set(craftId, json);
21
+ }
22
+ // Read a craft's last-known state (e.g. the one rehydrated from the loaded
23
+ // workbook). Returns undefined if the craft never stored anything.
24
+ export function getCraftState(craftId) {
25
+ return craftStates.get(craftId);
26
+ }
27
+ export function clearCraftState(craftId) {
28
+ craftStates.delete(craftId);
29
+ }
30
+ // Snapshot every craft's state for persistence into the AppData envelope.
31
+ export function getPersistentCraftStates() {
32
+ const out = {};
33
+ for (const [craftId, json] of craftStates)
34
+ out[craftId] = json;
35
+ return out;
36
+ }
37
+ // Rehydrate the store from a previously-persisted snapshot. Replaces the
38
+ // current contents so a freshly-loaded workbook doesn't inherit stale state
39
+ // from a prior one.
40
+ export function loadPersistentCraftStates(data) {
41
+ craftStates.clear();
42
+ if (!data || typeof data !== 'object')
43
+ return;
44
+ for (const [craftId, json] of Object.entries(data)) {
45
+ if (typeof json === 'string')
46
+ craftStates.set(craftId, json);
47
+ }
48
+ }
@@ -362,9 +362,10 @@ export function getPersistentInteractions() {
362
362
  return out;
363
363
  }
364
364
  export function loadPersistentInteractions(data) {
365
- if (!data || typeof data !== 'object')
366
- return;
367
- const d = data;
365
+ // Always clear first, then repopulate from whatever the workbook carried.
366
+ // Passing undefined/null (a workbook with no interaction state) therefore
367
+ // resets to empty rather than leaving the previous workbook's state behind.
368
+ const d = (data && typeof data === 'object' ? data : {});
368
369
  // Radio
369
370
  radioBindings.clear();
370
371
  if (Array.isArray(d.radioBindings)) {
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from './port.js';
2
2
  export * from './ops/index.js';
3
3
  export * from './format/index.js';
4
4
  export * from './craft-interactions/index.js';
5
+ export * from './craft/index.js';
5
6
  export * from './validation/index.js';
6
7
  export * from './field/index.js';
7
8
  export * from './value/index.js';
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ export * from './port.js';
7
7
  export * from './ops/index.js';
8
8
  export * from './format/index.js';
9
9
  export * from './craft-interactions/index.js';
10
+ export * from './craft/index.js';
10
11
  export * from './validation/index.js';
11
12
  export * from './field/index.js';
12
13
  export * from './value/index.js';
@@ -1,4 +1,4 @@
1
- import type { Payload, ActionEffect, SelectedData, Alignment, StPatternType, Value } from 'logisheets-web';
1
+ import type { Payload, ActionEffect, SelectedData, Alignment, StPatternType, Value, SheetCellId } from 'logisheets-web';
2
2
  import type { Client } from '../port.js';
3
3
  import { type ValidationRule, type Violation } from '../validation/index.js';
4
4
  import { type FieldColumn } from '../field/index.js';
@@ -136,7 +136,24 @@ export declare class WorkbookOps {
136
136
  * Lifted verbatim out of the browser's ValidationCell component so the
137
137
  * Node runtime gets the same operation.
138
138
  */
139
- setValidationRule(sheetIdx: number, row: number, col: number, formula: string): Promise<void>;
139
+ setValidationRule(sheetIdx: number, row: number, col: number, formula: string): Promise<SheetCellId>;
140
+ /**
141
+ * Read a set of *installed* validation shadows by their ids and interpret
142
+ * each — the read half of {@link setValidationRule}.
143
+ *
144
+ * The engine evaluates each shadow reactively (with `#PLACEHOLDER` bound to
145
+ * its target cell), so a caller that cached the shadow ids at install time
146
+ * gets the up-to-date verdicts here in a single batch read, with no
147
+ * coordinate resolution. `rule` is carried only into the returned
148
+ * {@link Violation} for reporting; the evaluation is entirely the engine's.
149
+ *
150
+ * Returns one {@link Violation} per failing cell (passing cells and empty
151
+ * shadows contribute nothing), in input order.
152
+ */
153
+ checkValidationShadows(entries: readonly {
154
+ shadow: SheetCellId;
155
+ rule: ValidationRule;
156
+ }[]): Promise<Violation[]>;
140
157
  /**
141
158
  * Evaluate an Excel formula (no leading `=`) in a sheet and return its
142
159
  * Value. Parks the formula in a throwaway ephemeral cell, reads the result
package/dist/ops/index.js CHANGED
@@ -17,7 +17,7 @@
17
17
  // UI side effects (refocus, toasts, event buses, closing dialogs) stay in the
18
18
  // host — only the engine-facing operation lives here.
19
19
  import { makeTransaction } from '../transaction/index.js';
20
- import { checkValidations as checkValidationsPure, } from '../validation/index.js';
20
+ import { checkValidations as checkValidationsPure, interpretValidation, } from '../validation/index.js';
21
21
  import { checkFieldConstraints as checkFieldConstraintsPure, } from '../field/index.js';
22
22
  import { generateFontPayload, generateAlgnmentPayload, generateWrapTextPayload, generateNumFmtPayload, generatePatternFillPayload, generateBorderPayloads, } from '../format/index.js';
23
23
  function isErrorMessage(v) {
@@ -269,6 +269,43 @@ export class WorkbookOps {
269
269
  },
270
270
  },
271
271
  ], false);
272
+ // Return the shadow's stable id so callers can cache it and later read
273
+ // the verdict back by id (see {@link checkValidationShadows}) without
274
+ // re-resolving the cell's coordinates.
275
+ return shadow;
276
+ }
277
+ /**
278
+ * Read a set of *installed* validation shadows by their ids and interpret
279
+ * each — the read half of {@link setValidationRule}.
280
+ *
281
+ * The engine evaluates each shadow reactively (with `#PLACEHOLDER` bound to
282
+ * its target cell), so a caller that cached the shadow ids at install time
283
+ * gets the up-to-date verdicts here in a single batch read, with no
284
+ * coordinate resolution. `rule` is carried only into the returned
285
+ * {@link Violation} for reporting; the evaluation is entirely the engine's.
286
+ *
287
+ * Returns one {@link Violation} per failing cell (passing cells and empty
288
+ * shadows contribute nothing), in input order.
289
+ */
290
+ async checkValidationShadows(entries) {
291
+ if (entries.length === 0)
292
+ return [];
293
+ const infos = await this.client.batchGetCellInfoById({
294
+ ids: entries.map((e) => e.shadow),
295
+ });
296
+ if (isErrorMessage(infos)) {
297
+ throw new Error('Failed to read shadow cells: ' + infos.msg);
298
+ }
299
+ const out = [];
300
+ for (let i = 0; i < entries.length; i++) {
301
+ const info = infos[i];
302
+ if (!info)
303
+ continue;
304
+ const violation = interpretValidation(entries[i].rule, info.value);
305
+ if (violation)
306
+ out.push(violation);
307
+ }
308
+ return out;
272
309
  }
273
310
  /**
274
311
  * Evaluate an Excel formula (no leading `=`) in a sheet and return its
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logisheets-core",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "UI-free LogiSheets logic — runs in the browser or on Node. Depends on logisheets-web only for types; the engine Client is injected by the host.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -1 +0,0 @@
1
- export * from './model.js';
@@ -1 +0,0 @@
1
- export * from './model.js';
@@ -1,10 +0,0 @@
1
- export declare class SelectionModel<T> {
2
- #private;
3
- readonly multi: boolean;
4
- readonly data: readonly T[];
5
- constructor(multi?: boolean, data?: readonly T[]);
6
- get selected(): readonly T[];
7
- toggle(data: T): void;
8
- select(data: T): void;
9
- deSelect(data: T): void;
10
- }
@@ -1,38 +0,0 @@
1
- var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
2
- if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
3
- if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
4
- return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
5
- };
6
- var _SelectionModel_selected;
7
- export class SelectionModel {
8
- constructor(multi = false, data = []) {
9
- this.multi = multi;
10
- this.data = data;
11
- _SelectionModel_selected.set(this, new Map());
12
- data.forEach((d) => {
13
- __classPrivateFieldGet(this, _SelectionModel_selected, "f").set(d, true);
14
- });
15
- }
16
- get selected() {
17
- const selecteds = [];
18
- __classPrivateFieldGet(this, _SelectionModel_selected, "f").forEach((checked, d) => {
19
- if (!checked)
20
- return;
21
- selecteds.push(d);
22
- });
23
- return selecteds;
24
- }
25
- toggle(data) {
26
- if (__classPrivateFieldGet(this, _SelectionModel_selected, "f").has(data))
27
- __classPrivateFieldGet(this, _SelectionModel_selected, "f").set(data, true);
28
- else
29
- __classPrivateFieldGet(this, _SelectionModel_selected, "f").set(data, false);
30
- }
31
- select(data) {
32
- __classPrivateFieldGet(this, _SelectionModel_selected, "f").set(data, true);
33
- }
34
- deSelect(data) {
35
- __classPrivateFieldGet(this, _SelectionModel_selected, "f").set(data, false);
36
- }
37
- }
38
- _SelectionModel_selected = new WeakMap();
@@ -1,4 +0,0 @@
1
- export declare function commonSuffix(a: string, b: string): string;
2
- export declare function commonSuffixLength(a: string, b: string): number;
3
- export declare function commonPrefix(a: string, b: string): string;
4
- export declare function commonPrefixLength(a: string, b: string): number;
@@ -1,28 +0,0 @@
1
- export function commonSuffix(a, b) {
2
- const strs = [];
3
- const aLastIndex = a.length - 1;
4
- const bLastIndex = b.length - 1;
5
- const len = Math.min(a.length, b.length);
6
- for (let i = 0; i < len; i += 1) {
7
- if (a.charCodeAt(aLastIndex - i) !== b.charCodeAt(bLastIndex - i))
8
- break;
9
- strs.push(a[i]);
10
- }
11
- return strs.join('');
12
- }
13
- export function commonSuffixLength(a, b) {
14
- return commonSuffix(a, b).length;
15
- }
16
- export function commonPrefix(a, b) {
17
- const strs = [];
18
- const len = Math.min(a.length, b.length);
19
- for (let i = 0; i < len; i += 1) {
20
- if (a.charCodeAt(i) !== b.charCodeAt(i))
21
- break;
22
- strs.push(a[i]);
23
- }
24
- return strs.join('');
25
- }
26
- export function commonPrefixLength(a, b) {
27
- return commonPrefix(a, b).length;
28
- }
@@ -1,41 +0,0 @@
1
- import type { Value } from 'logisheets-web';
2
- /** The slice of the engine that structured-data ops actually need. */
3
- export interface EnginePort {
4
- /** Apply a cell-input transaction. Returns the engine's status object. */
5
- handleTransaction(tx: GatewayTransaction): {
6
- status: {
7
- type: string;
8
- };
9
- };
10
- /** Read a single cell's evaluated value. */
11
- getValue(sheetIdx: number, row: number, col: number): Value;
12
- }
13
- export interface GatewayTransaction {
14
- payloads: ReadonlyArray<{
15
- type: 'cellInput';
16
- value: {
17
- sheetIdx: number;
18
- row: number;
19
- col: number;
20
- content: string;
21
- };
22
- }>;
23
- undoable: boolean;
24
- temp: boolean;
25
- }
26
- export interface WriteTarget {
27
- sheetIdx: number;
28
- startRow: number;
29
- startCol: number;
30
- }
31
- /**
32
- * Import: write `records` (rows of string cells) into the sheet, anchored at
33
- * (startRow, startCol). One transaction for the whole block. Returns true on
34
- * an `ok` engine status.
35
- */
36
- export declare function writeRecords(port: EnginePort, target: WriteTarget, records: ReadonlyArray<ReadonlyArray<string>>, undoable?: boolean): boolean;
37
- /**
38
- * Export: read a `rows` x `cols` rectangle of evaluated values back out as a
39
- * 2-D array, anchored at (startRow, startCol).
40
- */
41
- export declare function readRecords(port: EnginePort, target: WriteTarget, rows: number, cols: number): Value[][];
@@ -1,50 +0,0 @@
1
- // Structured-data primitives — the engine-facing core of data-gateway.
2
- //
3
- // These functions are the import/export building blocks: write a 2-D block of
4
- // records into the workbook, and read a rectangle of records back out. They
5
- // are PURE LOGIC: they never import a concrete engine. They talk to whatever
6
- // engine the host injects through the minimal `EnginePort` seam below.
7
- //
8
- // - browser: wrap logisheets-engine's worker-backed client as an EnginePort
9
- // - node: wrap logisheets' synchronous handle()/Workbook as an EnginePort
10
- //
11
- // The payload/value shapes are imported as TYPES from logisheets-web, so this
12
- // file still carries no runtime dependency.
13
- /**
14
- * Import: write `records` (rows of string cells) into the sheet, anchored at
15
- * (startRow, startCol). One transaction for the whole block. Returns true on
16
- * an `ok` engine status.
17
- */
18
- export function writeRecords(port, target, records, undoable = true) {
19
- const payloads = [];
20
- records.forEach((row, r) => {
21
- row.forEach((content, c) => {
22
- payloads.push({
23
- type: 'cellInput',
24
- value: {
25
- sheetIdx: target.sheetIdx,
26
- row: target.startRow + r,
27
- col: target.startCol + c,
28
- content,
29
- },
30
- });
31
- });
32
- });
33
- const effect = port.handleTransaction({ payloads, undoable, temp: false });
34
- return effect.status.type === 'ok';
35
- }
36
- /**
37
- * Export: read a `rows` x `cols` rectangle of evaluated values back out as a
38
- * 2-D array, anchored at (startRow, startCol).
39
- */
40
- export function readRecords(port, target, rows, cols) {
41
- const out = [];
42
- for (let r = 0; r < rows; r++) {
43
- const line = [];
44
- for (let c = 0; c < cols; c++) {
45
- line.push(port.getValue(target.sheetIdx, target.startRow + r, target.startCol + c));
46
- }
47
- out.push(line);
48
- }
49
- return out;
50
- }