@milaboratories/pl-model-common 1.35.0 → 1.36.1

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.
Files changed (71) hide show
  1. package/dist/dialog/index.d.ts +34 -0
  2. package/dist/dialog/index.d.ts.map +1 -0
  3. package/dist/drivers/index.d.ts +1 -1
  4. package/dist/drivers/pframe/data_types.cjs +1 -0
  5. package/dist/drivers/pframe/data_types.cjs.map +1 -1
  6. package/dist/drivers/pframe/data_types.d.ts +27 -1
  7. package/dist/drivers/pframe/data_types.d.ts.map +1 -1
  8. package/dist/drivers/pframe/data_types.js +1 -1
  9. package/dist/drivers/pframe/data_types.js.map +1 -1
  10. package/dist/drivers/pframe/driver.cjs.map +1 -1
  11. package/dist/drivers/pframe/driver.d.ts +6 -1
  12. package/dist/drivers/pframe/driver.d.ts.map +1 -1
  13. package/dist/drivers/pframe/driver.js.map +1 -1
  14. package/dist/drivers/pframe/index.d.ts +1 -1
  15. package/dist/drivers/pframe/query/query_common.d.ts +14 -28
  16. package/dist/drivers/pframe/query/query_common.d.ts.map +1 -1
  17. package/dist/drivers/pframe/query/query_data.d.ts +3 -3
  18. package/dist/drivers/pframe/query/query_data.d.ts.map +1 -1
  19. package/dist/drivers/pframe/query/query_spec.d.ts +9 -25
  20. package/dist/drivers/pframe/query/query_spec.d.ts.map +1 -1
  21. package/dist/drivers/pframe/query/utils.cjs +19 -14
  22. package/dist/drivers/pframe/query/utils.cjs.map +1 -1
  23. package/dist/drivers/pframe/query/utils.js +19 -14
  24. package/dist/drivers/pframe/query/utils.js.map +1 -1
  25. package/dist/index.cjs +10 -3
  26. package/dist/index.d.ts +6 -5
  27. package/dist/index.js +5 -5
  28. package/dist/services/index.cjs +1 -1
  29. package/dist/services/index.d.ts +3 -3
  30. package/dist/services/index.js +1 -1
  31. package/dist/services/{service_injector_factory.js → node_service_handlers.cjs} +6 -5
  32. package/dist/services/node_service_handlers.cjs.map +1 -0
  33. package/dist/services/node_service_handlers.d.ts +8 -0
  34. package/dist/services/node_service_handlers.d.ts.map +1 -0
  35. package/dist/services/{service_injector_factory.cjs → node_service_handlers.js} +6 -5
  36. package/dist/services/node_service_handlers.js.map +1 -0
  37. package/dist/services/service_declarations.cjs +48 -2
  38. package/dist/services/service_declarations.cjs.map +1 -1
  39. package/dist/services/service_declarations.d.ts +2 -0
  40. package/dist/services/service_declarations.d.ts.map +1 -1
  41. package/dist/services/service_declarations.js +48 -2
  42. package/dist/services/service_declarations.js.map +1 -1
  43. package/dist/services/service_injectors.cjs +29 -25
  44. package/dist/services/service_injectors.cjs.map +1 -1
  45. package/dist/services/service_injectors.d.ts +21 -10
  46. package/dist/services/service_injectors.d.ts.map +1 -1
  47. package/dist/services/service_injectors.js +29 -26
  48. package/dist/services/service_injectors.js.map +1 -1
  49. package/dist/services/service_types.cjs +26 -2
  50. package/dist/services/service_types.cjs.map +1 -1
  51. package/dist/services/service_types.d.ts +6 -4
  52. package/dist/services/service_types.d.ts.map +1 -1
  53. package/dist/services/service_types.js +22 -3
  54. package/dist/services/service_types.js.map +1 -1
  55. package/package.json +3 -3
  56. package/src/dialog/index.ts +33 -0
  57. package/src/drivers/pframe/data_types.ts +30 -1
  58. package/src/drivers/pframe/driver.ts +17 -2
  59. package/src/drivers/pframe/query/query_common.ts +13 -5
  60. package/src/drivers/pframe/query/query_data.ts +2 -3
  61. package/src/drivers/pframe/query/query_spec.ts +9 -33
  62. package/src/drivers/pframe/query/utils.ts +26 -16
  63. package/src/index.ts +1 -0
  64. package/src/services/{service_injector_factory.ts → node_service_handlers.ts} +13 -6
  65. package/src/services/service_declarations.ts +60 -3
  66. package/src/services/service_injectors.ts +50 -34
  67. package/src/services/service_types.ts +38 -4
  68. package/dist/services/service_injector_factory.cjs.map +0 -1
  69. package/dist/services/service_injector_factory.d.ts +0 -8
  70. package/dist/services/service_injector_factory.d.ts.map +0 -1
  71. package/dist/services/service_injector_factory.js.map +0 -1
@@ -48,7 +48,7 @@ function isBitSet(bitVector: Uint8Array, offset: number): boolean {
48
48
  return (bitVector[chunkIndex] & mask) > 0;
49
49
  }
50
50
 
51
- function isValueNA(vector: PTableVector, row: number): boolean {
51
+ export function isValueNA(vector: PTableVector, row: number): boolean {
52
52
  if (vector.isNA) return isBitSet(vector.isNA, row);
53
53
 
54
54
  // Check for legacy magic values to support old desktop versions
@@ -236,3 +236,32 @@ export type PTableShape = {
236
236
  /** Number of rows */
237
237
  rows: number;
238
238
  };
239
+
240
+ /** Supported formats for PTable file download. */
241
+ export type PTableDownloadFormat = "csv" | "tsv";
242
+
243
+ /** Compression applied to the written file. */
244
+ export type PTableDownloadCompression = "none" | "gzip";
245
+
246
+ /** Options for downloading PTable data to a file. */
247
+ export interface WritePTableToFsOptions {
248
+ path: string;
249
+ format: PTableDownloadFormat;
250
+ columnIndices: number[];
251
+ range?: TableRange;
252
+ chunkSize?: number;
253
+ includeHeader?: boolean;
254
+ bom?: boolean;
255
+ compression?: {
256
+ type: "gzip";
257
+ level?: number;
258
+ };
259
+ signal?: AbortSignal;
260
+ }
261
+
262
+ /** Result of a PTable file download. */
263
+ export interface WritePTableToFsResult {
264
+ path: string;
265
+ rowsWritten: number;
266
+ bytesWritten: number;
267
+ }
@@ -5,7 +5,13 @@ import type { PColumn } from "./spec/spec";
5
5
  import type { PColumnValues, DataInfo } from "./data_info";
6
6
  import type { PTableDef, PTableDefV2 } from "./table_calculate";
7
7
  import type { AddParameterToAllMethods } from "./type_util";
8
- import type { PTableShape, PTableVector, TableRange } from "./data_types";
8
+ import type {
9
+ WritePTableToFsOptions,
10
+ WritePTableToFsResult,
11
+ PTableShape,
12
+ PTableVector,
13
+ TableRange,
14
+ } from "./data_types";
9
15
  import type { FindColumnsRequest, FindColumnsResponse } from "./find_columns";
10
16
  import type { PObjectId } from "../../pool";
11
17
  import type { PColumnIdAndSpec, PColumnSpec } from "./spec/spec";
@@ -88,6 +94,15 @@ export interface PFrameDriver {
88
94
  columnIndices: number[],
89
95
  range?: TableRange,
90
96
  ): Promise<PTableVector[]>;
97
+
98
+ /**
99
+ * Stream the table to a file at the given path. Caller is responsible
100
+ * for producing the destination path (e.g. via the `Dialog` service).
101
+ */
102
+ writePTableToFs(
103
+ handle: PTableHandle,
104
+ options: WritePTableToFsOptions,
105
+ ): Promise<WritePTableToFsResult>;
91
106
  }
92
107
 
93
108
  //
@@ -102,4 +117,4 @@ type ExpectedPFrameDriverType = ExpectedPFrameDriverTypeF & ExpectedPFrameDriver
102
117
  type TypeEqualityGuard<A, B> = Exclude<A, B> | Exclude<B, A>;
103
118
  function assert<_T extends never>() {}
104
119
 
105
- assert<TypeEqualityGuard<PFrameDriver, ExpectedPFrameDriverType>>();
120
+ assert<TypeEqualityGuard<Omit<PFrameDriver, "writePTableToFs">, ExpectedPFrameDriverType>>();
@@ -632,23 +632,31 @@ export interface QueryOuterJoin<JE extends QueryJoinEntry<unknown>> {
632
632
  * @template A - Axis selector type
633
633
  *
634
634
  * @example
635
- * // Slice 3D data [sample, gene, condition] to 1D [gene]
635
+ * // Spec layer: axisSelector is a SingleAxisSelector.
636
636
  * {
637
637
  * type: 'sliceAxes',
638
638
  * input: fullDataQuery,
639
639
  * axisFilters: [
640
- * { type: 'constant', axisSelector: { type: 'axis', id: 'sample' }, constant: 'Sample1' },
641
- * { type: 'constant', axisSelector: { type: 'axis', id: 'condition' }, constant: 'Treatment' }
640
+ * { axisSelector: { name: 'sample' }, constant: 'Sample1' },
641
+ * { axisSelector: { name: 'condition' }, constant: 'Treatment' }
642
642
  * ]
643
643
  * }
644
+ *
645
+ * @example
646
+ * // Data layer: axisSelector is the axis index.
647
+ * {
648
+ * type: 'sliceAxes',
649
+ * input: fullDataQuery,
650
+ * axisFilters: [{ axisSelector: 0, constant: 'Sample1' }]
651
+ * }
644
652
  */
645
- export interface QuerySliceAxes<Q, A extends QueryAxisSelector<unknown>> {
653
+ export interface QuerySliceAxes<Q, A> {
646
654
  type: "sliceAxes";
647
655
  /** Input query to slice */
648
656
  input: Q;
649
657
  /** List of axis filters to apply (at least one required) */
650
658
  axisFilters: {
651
- /** Selector identifying which axis to filter */
659
+ /** Axis to filter. `SingleAxisSelector` at the spec layer; axis index at the data layer. */
652
660
  axisSelector: A;
653
661
  /** The constant value to filter the axis to */
654
662
  constant: string | number;
@@ -15,7 +15,6 @@ import type {
15
15
  ExprStringEquals,
16
16
  ExprStringRegex,
17
17
  ExprNumericUnary,
18
- QueryAxisSelector,
19
18
  QueryColumn,
20
19
  QuerySparseToDenseColumn,
21
20
  QueryFilter,
@@ -40,7 +39,7 @@ type ColumnIdAndTypeSpec = {
40
39
  /** Unique identifier of the column */
41
40
  id: PObjectId;
42
41
  /** Type specification defining axes and column types */
43
- typeSpec: TypeSpec;
42
+ spec: TypeSpec;
44
43
  };
45
44
 
46
45
  /**
@@ -89,7 +88,7 @@ export type DataQueryLinkerJoinLinker = {
89
88
  /** @see QueryLinkerJoin */
90
89
  export type DataQueryLinkerJoin = QueryLinkerJoin<DataQueryLinkerJoinLinker, DataQueryJoinEntry>;
91
90
  /** @see QuerySliceAxes */
92
- export type DataQuerySliceAxes = QuerySliceAxes<DataQuery, QueryAxisSelector<number>>;
91
+ export type DataQuerySliceAxes = QuerySliceAxes<DataQuery, number>;
93
92
  /** @see QuerySort */
94
93
  export type DataQuerySort = QuerySort<DataQuery, DataQueryExpression>;
95
94
  /** @see QueryFilter */
@@ -15,7 +15,6 @@ import type {
15
15
  ExprStringEquals,
16
16
  ExprStringRegex,
17
17
  ExprNumericUnary,
18
- QueryAxisSelector,
19
18
  QueryColumn,
20
19
  QuerySparseToDenseColumn,
21
20
  QueryFilter,
@@ -28,43 +27,23 @@ import type {
28
27
  QuerySymmetricJoin,
29
28
  InferBooleanExpressionUnion,
30
29
  } from "./query_common";
31
- import type { Domain, PColumnIdAndSpec, PColumnSpec, SingleAxisSelector } from "../spec";
30
+ import type { Domain, PColumnIdAndSpec, SingleAxisSelector } from "../spec";
32
31
 
33
32
  /**
34
- * Column identifier with specification.
35
- *
36
- * Pairs a column ID with its full PColumnSpec.
37
- * Used in spec layer to carry column specification alongside references.
38
- */
39
- type ColumnIdAndSpec = {
40
- /** Unique identifier of the column */
41
- id: PObjectId;
42
- /** Full column specification including axes and value type */
43
- spec: PColumnSpec;
44
- };
45
-
46
- /**
47
- * Join entry for spec layer queries.
48
- *
49
- * Extends the base join entry with axis qualifications.
50
- * Qualifications specify additional domain constraints for each axis,
51
- * enabling more precise control over how columns are joined.
33
+ * Join entry for spec-layer queries — the base join entry extended with
34
+ * per-axis domain constraints. Absent `qualifications` is equivalent to `[]`.
52
35
  *
53
36
  * @example
54
- * // Join entry with axis qualifications
55
37
  * {
56
38
  * entry: querySpec,
57
- * qualifications: [
58
- * { axis: { name: 'sample' }, contextDomain: { ... } }
59
- * ]
39
+ * qualifications: [{ axis: { name: 'sample' }, contextDomain: { ... } }]
60
40
  * }
61
41
  */
62
42
  export type SpecQueryJoinEntry<C = PObjectId> = QueryJoinEntry<SpecQuery<C>> & {
63
- /** Axis qualifications with additional domain constraints */
64
- qualifications: {
65
- /** Axis selector identifying which axis to qualify */
43
+ qualifications?: {
44
+ /** Axis to qualify. */
66
45
  axis: SingleAxisSelector;
67
- /** Additional domain constraints for this axis */
46
+ /** Additional domain constraints for this axis. */
68
47
  contextDomain: Domain;
69
48
  }[];
70
49
  };
@@ -72,7 +51,7 @@ export type SpecQueryJoinEntry<C = PObjectId> = QueryJoinEntry<SpecQuery<C>> & {
72
51
  /** @see QueryColumn */
73
52
  export type SpecQueryColumn<C = PObjectId> = QueryColumn<C>;
74
53
  /** @see QueryInlineColumn */
75
- export type SpecQueryInlineColumn = QueryInlineColumn<ColumnIdAndSpec>;
54
+ export type SpecQueryInlineColumn = QueryInlineColumn<PColumnIdAndSpec>;
76
55
  /** @see QuerySparseToDenseColumn */
77
56
  export type SpecQuerySparseToDenseColumn<C = PObjectId> = QuerySparseToDenseColumn<
78
57
  C,
@@ -98,10 +77,7 @@ export type SpecQueryLinkerJoin<C = PObjectId> = QueryLinkerJoin<
98
77
  SpecQueryJoinEntry<C>
99
78
  >;
100
79
  /** @see QuerySliceAxes */
101
- export type SpecQuerySliceAxes<C = PObjectId> = QuerySliceAxes<
102
- SpecQuery<C>,
103
- QueryAxisSelector<SingleAxisSelector>
104
- >;
80
+ export type SpecQuerySliceAxes<C = PObjectId> = QuerySliceAxes<SpecQuery<C>, SingleAxisSelector>;
105
81
  /** @see QuerySort */
106
82
  export type SpecQuerySort<C = PObjectId> = QuerySort<SpecQuery<C>, SpecQueryExpression>;
107
83
  /** @see QueryFilter */
@@ -150,14 +150,21 @@ export function sortSpecQuery(query: SpecQuery): SpecQuery {
150
150
  return node;
151
151
  }
152
152
  },
153
- joinEntry: (entry) => ({
154
- ...entry,
155
- qualifications: entry.qualifications.toSorted((a, b) => {
156
- const ak = canonicalizeJson(a.axis);
157
- const bk = canonicalizeJson(b.axis);
158
- return ak < bk ? -1 : ak === bk ? 0 : 1;
159
- }),
160
- }),
153
+ joinEntry: (entry) => {
154
+ // Preserve absence of `qualifications` (absent == `[]`) so canonical
155
+ // output is stable across both input forms.
156
+ if (entry.qualifications === undefined || entry.qualifications.length === 0) {
157
+ return entry;
158
+ }
159
+ return {
160
+ ...entry,
161
+ qualifications: entry.qualifications.toSorted((a, b) => {
162
+ const ak = canonicalizeJson(a.axis);
163
+ const bk = canonicalizeJson(b.axis);
164
+ return ak < bk ? -1 : ak === bk ? 0 : 1;
165
+ }),
166
+ };
167
+ },
161
168
  });
162
169
  }
163
170
 
@@ -173,9 +180,9 @@ function cmpQuerySpec(lhs: SpecQuery, rhs: SpecQuery): number {
173
180
  ? 0
174
181
  : 1;
175
182
  case "inlineColumn":
176
- return lhs.spec.id < (rhs as typeof lhs).spec.id
183
+ return lhs.spec.columnId < (rhs as typeof lhs).spec.columnId
177
184
  ? -1
178
- : lhs.spec.id === (rhs as typeof lhs).spec.id
185
+ : lhs.spec.columnId === (rhs as typeof lhs).spec.columnId
179
186
  ? 0
180
187
  : 1;
181
188
  case "sparseToDenseColumn":
@@ -237,13 +244,16 @@ function cmpQuerySpec(lhs: SpecQuery, rhs: SpecQuery): number {
237
244
  function cmpQueryJoinEntrySpec(lhs: SpecQueryJoinEntry, rhs: SpecQueryJoinEntry): number {
238
245
  const cmp = cmpQuerySpec(lhs.entry, rhs.entry);
239
246
  if (cmp !== 0) return cmp;
240
- if (lhs.qualifications.length !== rhs.qualifications.length) {
241
- return lhs.qualifications.length - rhs.qualifications.length;
247
+ // Absent `qualifications` is equivalent to an empty list.
248
+ const lhsQ = lhs.qualifications ?? [];
249
+ const rhsQ = rhs.qualifications ?? [];
250
+ if (lhsQ.length !== rhsQ.length) {
251
+ return lhsQ.length - rhsQ.length;
242
252
  }
243
- for (let i = 0; i < lhs.qualifications.length; i++) {
244
- const lhsQ = canonicalizeJson(lhs.qualifications[i]);
245
- const rhsQ = canonicalizeJson(rhs.qualifications[i]);
246
- if (lhsQ !== rhsQ) return lhsQ < rhsQ ? -1 : 1;
253
+ for (let i = 0; i < lhsQ.length; i++) {
254
+ const l = canonicalizeJson(lhsQ[i]);
255
+ const r = canonicalizeJson(rhsQ[i]);
256
+ if (l !== r) return l < r ? -1 : 1;
247
257
  }
248
258
  return 0;
249
259
  }
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from "./bmodel";
4
4
  export * from "./branding";
5
5
  export * from "./common_types";
6
6
  export * from "./driver_kit";
7
+ export * from "./dialog";
7
8
  export * from "./drivers";
8
9
  export * from "./errors";
9
10
  export * from "./flags";
@@ -1,15 +1,21 @@
1
1
  /**
2
- * UI service injector factory.
2
+ * Worker-side dispatch table for `node`-kind services.
3
3
  *
4
- * When adding a new node service, add its method wrappers here.
5
- * Everything else (SERVICE_METHOD_MAP, buildServiceInfo, UiServiceInjectorMap)
6
- * auto-derives from this factory + Services in service_declarations.ts.
4
+ * Each entry maps a method name to a closure that calls the real driver
5
+ * inside the worker process. Used by `callServiceMethod` in the worker
6
+ * (see `platforma-desktop-app/packages/worker/src/workerApi.ts`) to route
7
+ * incoming IPC calls to concrete driver methods.
8
+ *
9
+ * NOT used for:
10
+ * - `wasm` services — instantiated locally on each consumer (no IPC).
11
+ * - `main` services — dispatched in the Electron main process via its
12
+ * own router, never reaches the worker.
7
13
  */
8
14
 
9
15
  import type { DriverKit } from "../driver_kit";
10
- import type { UiServiceInjectorMap } from "./service_injectors";
16
+ import type { NodeServiceHandlerMap } from "./service_injectors";
11
17
 
12
- export function createUiServiceInjectors(driverKit: DriverKit): UiServiceInjectorMap {
18
+ export function createNodeServiceHandlers(driverKit: DriverKit): NodeServiceHandlerMap {
13
19
  const { pFrameDriver } = driverKit;
14
20
  return {
15
21
  PFrame: {
@@ -22,6 +28,7 @@ export function createUiServiceInjectors(driverKit: DriverKit): UiServiceInjecto
22
28
  getShape: (handle) => pFrameDriver.getShape(handle),
23
29
  getSpec: (handle) => pFrameDriver.getSpec(handle),
24
30
  getData: (handle, columnIndices, range) => pFrameDriver.getData(handle, columnIndices, range),
31
+ writePTableToFs: (handle, options) => pFrameDriver.writePTableToFs(handle, options),
25
32
  },
26
33
  };
27
34
  }
@@ -1,6 +1,11 @@
1
1
  /**
2
2
  * Service declarations — add new services here.
3
3
  *
4
+ * `modelMethods` / `uiMethods` lists are the single source of truth for
5
+ * method names exposed on each side. They drive SERVICE_METHOD_MAP (UI
6
+ * ServiceProxy forwarders) and MODEL_SERVICE_METHOD_MAP (workflow VM
7
+ * bridge), so node/main/wasm split no longer needs introspection stubs.
8
+ *
4
9
  * After adding a service, fix type errors in these files:
5
10
  *
6
11
  * Model side:
@@ -8,18 +13,70 @@
8
13
  * - lib/node/pl-middle-layer/src/middle_layer/middle_layer.ts — ModelServiceRegistry factory
9
14
  *
10
15
  * UI side:
11
- * - lib/model/common/src/services/service_injector_factory.ts — driver method wrappers (node services only)
16
+ * - lib/model/common/src/services/node_service_handlers.ts — driver method wrappers (node services only)
12
17
  * - sdk/ui-vue/src/internal/createAppV3.ts — UiServiceRegistry factory
13
18
  *
19
+ * Electron main side (only for `main` services):
20
+ * - platforma-desktop-app/packages/main/src/services/mainServices.ts — main-process dispatcher
21
+ *
14
22
  * Optional:
15
23
  * - sdk/model/src/services/block_service_flags.ts — only if default-required by all blocks
16
24
  */
17
25
 
26
+ import type { DialogService } from "../dialog";
18
27
  import type { PFrameDriver, PFrameModelDriver } from "../drivers/pframe/driver";
19
28
  import type { PFrameSpecDriver } from "../drivers/pframe/spec_driver";
20
29
  import { service } from "./service_types";
21
30
 
22
31
  export const Services = {
23
- PFrameSpec: service<PFrameSpecDriver, PFrameSpecDriver>()({ type: "wasm", name: "pframeSpec" }),
24
- PFrame: service<PFrameModelDriver, PFrameDriver>()({ type: "node", name: "pframe" }),
32
+ PFrameSpec: service<PFrameSpecDriver, PFrameSpecDriver>()({
33
+ type: "wasm",
34
+ name: "pframeSpec",
35
+ modelMethods: [
36
+ "createSpecFrame",
37
+ "listColumns",
38
+ "discoverColumns",
39
+ "deleteColumn",
40
+ "evaluateQuery",
41
+ "buildQuery",
42
+ "expandAxes",
43
+ "collapseAxes",
44
+ "findAxis",
45
+ "findTableColumn",
46
+ ] as const,
47
+ uiMethods: [
48
+ "createSpecFrame",
49
+ "listColumns",
50
+ "discoverColumns",
51
+ "deleteColumn",
52
+ "evaluateQuery",
53
+ "buildQuery",
54
+ "expandAxes",
55
+ "collapseAxes",
56
+ "findAxis",
57
+ "findTableColumn",
58
+ ] as const,
59
+ }),
60
+ PFrame: service<PFrameModelDriver, PFrameDriver>()({
61
+ type: "node",
62
+ name: "pframe",
63
+ modelMethods: ["createPFrame", "createPTable", "createPTableV2"] as const,
64
+ uiMethods: [
65
+ "findColumns",
66
+ "getColumnSpec",
67
+ "listColumns",
68
+ "calculateTableData",
69
+ "getUniqueValues",
70
+ "getShape",
71
+ "getSpec",
72
+ "getData",
73
+ "writePTableToFs",
74
+ ] as const,
75
+ }),
76
+ Dialog: service<Record<string, never>, DialogService>()({
77
+ type: "main",
78
+ name: "dialog",
79
+ modelMethods: [] as const,
80
+ uiMethods: ["showSaveDialog"] as const,
81
+ }),
25
82
  };
@@ -1,17 +1,21 @@
1
1
  /**
2
- * UI service injectors — auto-derived types, method map, and service info builder.
2
+ * Service dispatch glue — auto-derived types, method maps, and service info builder.
3
3
  *
4
- * The factory (createUiServiceInjectors) lives in service_injector_factory.ts —
5
- * that's the only file to edit when adding a new node service.
4
+ * The worker-side handler factory (createNodeServiceHandlers) lives in
5
+ * node_service_handlers.ts — that's the only file to edit when adding a
6
+ * new node service. Method-name registries for UI and model sides are
7
+ * derived directly from the `uiMethods` / `modelMethods` declarations in
8
+ * service_declarations.ts — no factory introspection, no stubs.
6
9
  */
7
10
 
8
11
  import type { InferServiceKind, InferServiceUi, ServiceBrand, ServiceName } from "./service_types";
9
12
  import type { DriverKit } from "../driver_kit";
10
13
  import { Services } from "./service_declarations";
11
- import { getMethodNames, resolveRequiredServices } from "./service_capabilities";
12
- import { createUiServiceInjectors } from "./service_injector_factory";
14
+ import { getServiceModelMethods, getServiceUiMethods } from "./service_types";
15
+ import { resolveRequiredServices } from "./service_capabilities";
16
+ import { createNodeServiceHandlers } from "./node_service_handlers";
13
17
 
14
- export { createUiServiceInjectors } from "./service_injector_factory";
18
+ export { createNodeServiceHandlers } from "./node_service_handlers";
15
19
 
16
20
  type NodeServiceKeys = {
17
21
  [K in keyof typeof Services]: InferServiceKind<ServiceBrand<(typeof Services)[K]>> extends "node"
@@ -19,54 +23,66 @@ type NodeServiceKeys = {
19
23
  : never;
20
24
  }[keyof typeof Services];
21
25
 
22
- /** Auto-derived map of node service keys to their UI-side driver interfaces. */
23
- export type UiServiceInjectorMap = {
26
+ /**
27
+ * Auto-derived map of node service keys to their UI-facing interfaces.
28
+ * Fulfilled by real driver wrappers in the worker. Main services are
29
+ * dispatched by the Electron main-process router and never reach this
30
+ * map; wasm services are instantiated locally on each consumer.
31
+ */
32
+ export type NodeServiceHandlerMap = {
24
33
  [K in NodeServiceKeys]: InferServiceUi<ServiceBrand<(typeof Services)[K]>>;
25
34
  };
26
35
 
27
36
  let cachedKit: DriverKit | undefined;
28
- let cachedInjectors: UiServiceInjectorMap | undefined;
37
+ let cachedHandlers: NodeServiceHandlerMap | undefined;
29
38
 
30
- function getOrCreateInjectors(driverKit: DriverKit): UiServiceInjectorMap {
31
- if (!cachedInjectors || cachedKit !== driverKit) {
39
+ function getOrCreateHandlers(driverKit: DriverKit): NodeServiceHandlerMap {
40
+ if (!cachedHandlers || cachedKit !== driverKit) {
32
41
  cachedKit = driverKit;
33
- cachedInjectors = createUiServiceInjectors(driverKit);
42
+ cachedHandlers = createNodeServiceHandlers(driverKit);
34
43
  }
35
- return cachedInjectors;
44
+ return cachedHandlers;
36
45
  }
37
46
 
38
- /** Resolve the injector for a given service ID. Caches injectors per DriverKit reference. */
39
- export function resolveUiInjector(
47
+ /**
48
+ * Resolve the worker-side handler object for a given service ID. Caches
49
+ * per DriverKit reference. Called from the worker's IPC dispatch path.
50
+ */
51
+ export function resolveNodeServiceHandler(
40
52
  driverKit: DriverKit,
41
53
  serviceId: ServiceName,
42
- ): UiServiceInjectorMap[keyof UiServiceInjectorMap] | null {
43
- const injectors = getOrCreateInjectors(driverKit);
54
+ ): NodeServiceHandlerMap[keyof NodeServiceHandlerMap] | null {
55
+ const handlers = getOrCreateHandlers(driverKit);
44
56
  const key = Object.keys(Services).find(
45
57
  (k) => Services[k as keyof typeof Services] === serviceId,
46
58
  ) as NodeServiceKeys | undefined;
47
59
  if (!key) return null;
48
- return injectors[key];
60
+ return handlers[key];
49
61
  }
50
62
 
51
- /**
52
- * Static map of ServiceName → method names, auto-derived from the injector shape.
53
- * Computed once at module load. The stub DriverKit is never called — it only
54
- * provides a target so the closures in createUiServiceInjectors have own-property
55
- * keys that getMethodNames can introspect.
56
- */
57
- export const SERVICE_METHOD_MAP: Readonly<Record<string, string[]>> = (() => {
58
- const stubKit = new Proxy({} as DriverKit, {
59
- get: () => new Proxy({}, { get: () => () => {} }),
60
- });
61
- const injectors = createUiServiceInjectors(stubKit);
63
+ function buildMethodMap(
64
+ pick: (id: ServiceName) => readonly string[],
65
+ ): Readonly<Record<string, string[]>> {
62
66
  const result: Record<string, string[]> = {};
63
- for (const key of Object.keys(Services) as (keyof typeof Services)[]) {
64
- const serviceId = Services[key];
65
- const injector = injectors[key as NodeServiceKeys];
66
- result[serviceId] = injector ? getMethodNames(injector) : [];
67
+ for (const id of Object.values(Services)) {
68
+ result[id as string] = [...pick(id)];
67
69
  }
68
70
  return result;
69
- })();
71
+ }
72
+
73
+ /**
74
+ * UI-side method names per service. Drives ServiceProxy forwarder
75
+ * construction in the renderer.
76
+ */
77
+ export const SERVICE_METHOD_MAP: Readonly<Record<string, string[]>> =
78
+ buildMethodMap(getServiceUiMethods);
79
+
80
+ /**
81
+ * Model-side method names per service. Drives the workflow VM bridge
82
+ * (see pl-middle-layer js_render).
83
+ */
84
+ export const MODEL_SERVICE_METHOD_MAP: Readonly<Record<string, string[]>> =
85
+ buildMethodMap(getServiceModelMethods);
70
86
 
71
87
  /** Build service info for a block from its feature flags. */
72
88
  export function buildServiceInfo(
@@ -22,19 +22,36 @@ export type InferServiceKind<S extends ServiceTypesLike> =
22
22
 
23
23
  export type ServiceName<S extends ServiceTypesLike = ServiceTypesLike> = Branded<string, S>;
24
24
 
25
- export type ServiceType = "node" | "wasm";
25
+ export type ServiceType = "node" | "wasm" | "main";
26
26
 
27
27
  const SERVICE_ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9]*$/;
28
28
 
29
- export const { service, isNodeService } = (() => {
29
+ export const {
30
+ service,
31
+ isNodeService,
32
+ isWasmService,
33
+ isMainService,
34
+ getServiceKind,
35
+ getServiceModelMethods,
36
+ getServiceUiMethods,
37
+ } = (() => {
30
38
  const typeMap = new Map<string, ServiceType>();
39
+ const modelMethodsMap = new Map<string, readonly string[]>();
40
+ const uiMethodsMap = new Map<string, readonly string[]>();
31
41
  return {
32
42
  service<Model, Ui>() {
33
- return <K extends ServiceType, N extends string>(options: {
43
+ return <
44
+ K extends ServiceType,
45
+ N extends string,
46
+ MM extends readonly (keyof Model & string)[],
47
+ UM extends readonly (keyof Ui & string)[],
48
+ >(options: {
34
49
  readonly type: K;
35
50
  readonly name: N;
51
+ readonly modelMethods: MM;
52
+ readonly uiMethods: UM;
36
53
  }): Branded<N, ServiceTypesLike<Model, Ui, K>> => {
37
- const { name, type } = options;
54
+ const { name, type, modelMethods, uiMethods } = options;
38
55
  if (!SERVICE_ID_PATTERN.test(name)) {
39
56
  throw new ServiceInvalidIdError(
40
57
  `Invalid service ID "${name}": must match ${SERVICE_ID_PATTERN}`,
@@ -44,12 +61,29 @@ export const { service, isNodeService } = (() => {
44
61
  throw new ServiceAlreadyRegisteredError(`Service "${name}" already registered`);
45
62
  }
46
63
  typeMap.set(name, type);
64
+ modelMethodsMap.set(name, modelMethods);
65
+ uiMethodsMap.set(name, uiMethods);
47
66
  return name as Branded<N, ServiceTypesLike<Model, Ui, K>>;
48
67
  };
49
68
  },
50
69
  isNodeService(id: ServiceName): boolean {
51
70
  return typeMap.get(id) === "node";
52
71
  },
72
+ isWasmService(id: ServiceName): boolean {
73
+ return typeMap.get(id) === "wasm";
74
+ },
75
+ isMainService(id: ServiceName): boolean {
76
+ return typeMap.get(id) === "main";
77
+ },
78
+ getServiceKind(id: ServiceName): ServiceType | undefined {
79
+ return typeMap.get(id);
80
+ },
81
+ getServiceModelMethods(id: ServiceName): readonly string[] {
82
+ return modelMethodsMap.get(id) ?? [];
83
+ },
84
+ getServiceUiMethods(id: ServiceName): readonly string[] {
85
+ return uiMethodsMap.get(id) ?? [];
86
+ },
53
87
  };
54
88
  })();
55
89
 
@@ -1 +0,0 @@
1
- {"version":3,"file":"service_injector_factory.cjs","names":[],"sources":["../../src/services/service_injector_factory.ts"],"sourcesContent":["/**\n * UI service injector factory.\n *\n * When adding a new node service, add its method wrappers here.\n * Everything else (SERVICE_METHOD_MAP, buildServiceInfo, UiServiceInjectorMap)\n * auto-derives from this factory + Services in service_declarations.ts.\n */\n\nimport type { DriverKit } from \"../driver_kit\";\nimport type { UiServiceInjectorMap } from \"./service_injectors\";\n\nexport function createUiServiceInjectors(driverKit: DriverKit): UiServiceInjectorMap {\n const { pFrameDriver } = driverKit;\n return {\n PFrame: {\n findColumns: (handle, request) => pFrameDriver.findColumns(handle, request),\n getColumnSpec: (handle, columnId) => pFrameDriver.getColumnSpec(handle, columnId),\n listColumns: (handle) => pFrameDriver.listColumns(handle),\n calculateTableData: (handle, request, range) =>\n pFrameDriver.calculateTableData(handle, request, range),\n getUniqueValues: (handle, request) => pFrameDriver.getUniqueValues(handle, request),\n getShape: (handle) => pFrameDriver.getShape(handle),\n getSpec: (handle) => pFrameDriver.getSpec(handle),\n getData: (handle, columnIndices, range) => pFrameDriver.getData(handle, columnIndices, range),\n },\n };\n}\n"],"mappings":";AAWA,SAAgB,yBAAyB,WAA4C;CACnF,MAAM,EAAE,iBAAiB;AACzB,QAAO,EACL,QAAQ;EACN,cAAc,QAAQ,YAAY,aAAa,YAAY,QAAQ,QAAQ;EAC3E,gBAAgB,QAAQ,aAAa,aAAa,cAAc,QAAQ,SAAS;EACjF,cAAc,WAAW,aAAa,YAAY,OAAO;EACzD,qBAAqB,QAAQ,SAAS,UACpC,aAAa,mBAAmB,QAAQ,SAAS,MAAM;EACzD,kBAAkB,QAAQ,YAAY,aAAa,gBAAgB,QAAQ,QAAQ;EACnF,WAAW,WAAW,aAAa,SAAS,OAAO;EACnD,UAAU,WAAW,aAAa,QAAQ,OAAO;EACjD,UAAU,QAAQ,eAAe,UAAU,aAAa,QAAQ,QAAQ,eAAe,MAAM;EAC9F,EACF"}
@@ -1,8 +0,0 @@
1
- import { DriverKit } from "../driver_kit.js";
2
- import { UiServiceInjectorMap } from "./service_injectors.js";
3
-
4
- //#region src/services/service_injector_factory.d.ts
5
- declare function createUiServiceInjectors(driverKit: DriverKit): UiServiceInjectorMap;
6
- //#endregion
7
- export { createUiServiceInjectors };
8
- //# sourceMappingURL=service_injector_factory.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"service_injector_factory.d.ts","names":[],"sources":["../../src/services/service_injector_factory.ts"],"mappings":";;;;iBAWgB,wBAAA,CAAyB,SAAA,EAAW,SAAA,GAAY,oBAAA"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"service_injector_factory.js","names":[],"sources":["../../src/services/service_injector_factory.ts"],"sourcesContent":["/**\n * UI service injector factory.\n *\n * When adding a new node service, add its method wrappers here.\n * Everything else (SERVICE_METHOD_MAP, buildServiceInfo, UiServiceInjectorMap)\n * auto-derives from this factory + Services in service_declarations.ts.\n */\n\nimport type { DriverKit } from \"../driver_kit\";\nimport type { UiServiceInjectorMap } from \"./service_injectors\";\n\nexport function createUiServiceInjectors(driverKit: DriverKit): UiServiceInjectorMap {\n const { pFrameDriver } = driverKit;\n return {\n PFrame: {\n findColumns: (handle, request) => pFrameDriver.findColumns(handle, request),\n getColumnSpec: (handle, columnId) => pFrameDriver.getColumnSpec(handle, columnId),\n listColumns: (handle) => pFrameDriver.listColumns(handle),\n calculateTableData: (handle, request, range) =>\n pFrameDriver.calculateTableData(handle, request, range),\n getUniqueValues: (handle, request) => pFrameDriver.getUniqueValues(handle, request),\n getShape: (handle) => pFrameDriver.getShape(handle),\n getSpec: (handle) => pFrameDriver.getSpec(handle),\n getData: (handle, columnIndices, range) => pFrameDriver.getData(handle, columnIndices, range),\n },\n };\n}\n"],"mappings":";AAWA,SAAgB,yBAAyB,WAA4C;CACnF,MAAM,EAAE,iBAAiB;AACzB,QAAO,EACL,QAAQ;EACN,cAAc,QAAQ,YAAY,aAAa,YAAY,QAAQ,QAAQ;EAC3E,gBAAgB,QAAQ,aAAa,aAAa,cAAc,QAAQ,SAAS;EACjF,cAAc,WAAW,aAAa,YAAY,OAAO;EACzD,qBAAqB,QAAQ,SAAS,UACpC,aAAa,mBAAmB,QAAQ,SAAS,MAAM;EACzD,kBAAkB,QAAQ,YAAY,aAAa,gBAAgB,QAAQ,QAAQ;EACnF,WAAW,WAAW,aAAa,SAAS,OAAO;EACnD,UAAU,WAAW,aAAa,QAAQ,OAAO;EACjD,UAAU,QAAQ,eAAe,UAAU,aAAa,QAAQ,QAAQ,eAAe,MAAM;EAC9F,EACF"}