@milaboratories/pl-middle-layer 1.53.3 → 1.54.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 (32) hide show
  1. package/dist/js_render/computable_context.cjs +92 -85
  2. package/dist/js_render/computable_context.cjs.map +1 -1
  3. package/dist/js_render/computable_context.js +92 -84
  4. package/dist/js_render/computable_context.js.map +1 -1
  5. package/dist/js_render/context.cjs.map +1 -1
  6. package/dist/js_render/context.js.map +1 -1
  7. package/dist/js_render/service_injectors.cjs +49 -0
  8. package/dist/js_render/service_injectors.cjs.map +1 -0
  9. package/dist/js_render/service_injectors.js +48 -0
  10. package/dist/js_render/service_injectors.js.map +1 -0
  11. package/dist/middle_layer/middle_layer.cjs +10 -0
  12. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  13. package/dist/middle_layer/middle_layer.d.ts +4 -0
  14. package/dist/middle_layer/middle_layer.js +10 -0
  15. package/dist/middle_layer/middle_layer.js.map +1 -1
  16. package/dist/pool/driver.cjs +1 -0
  17. package/dist/pool/driver.cjs.map +1 -1
  18. package/dist/pool/driver.js +1 -0
  19. package/dist/pool/driver.js.map +1 -1
  20. package/dist/service_factories.cjs +22 -0
  21. package/dist/service_factories.cjs.map +1 -0
  22. package/dist/service_factories.js +21 -0
  23. package/dist/service_factories.js.map +1 -0
  24. package/package.json +20 -20
  25. package/src/js_render/computable_context.ts +105 -171
  26. package/src/js_render/context.ts +4 -4
  27. package/src/js_render/service_injectors.ts +153 -0
  28. package/src/middle_layer/middle_layer.ts +18 -0
  29. package/src/pool/driver.ts +1 -1
  30. package/src/service_factories.ts +22 -0
  31. package/dist/js_render/spec_driver.cjs +0 -2
  32. package/dist/js_render/spec_driver.js +0 -3
@@ -34,6 +34,7 @@ import {
34
34
  mapValueInVOE,
35
35
  } from "@platforma-sdk/model";
36
36
  import { notEmpty } from "@milaboratories/ts-helpers";
37
+ import { PoolEntryGuard } from "@milaboratories/pl-model-common";
37
38
  import { randomUUID } from "node:crypto";
38
39
  import type { Optional } from "utility-types";
39
40
  import type { BlockContextAny } from "../middle_layer/block_ctx";
@@ -44,18 +45,15 @@ import type { ResultPool } from "../pool/result_pool";
44
45
  import type { JsExecutionContext } from "./context";
45
46
  import type { VmFunctionImplementation } from "quickjs-emscripten";
46
47
  import { Scope, type QuickJSHandle } from "quickjs-emscripten";
47
- import type {
48
- AxesId,
49
- AxesSpec,
50
- DiscoverColumnsRequest,
51
- DiscoverColumnsResponse,
52
- PColumnSpec,
53
- PTableColumnId,
54
- PTableColumnSpec,
55
- SingleAxisSelector,
56
- SpecFrameHandle,
48
+ import {
49
+ resolveRequiredServices,
50
+ serviceFnKey,
51
+ Services,
52
+ ModelServiceRegistry,
53
+ ServiceInjectionError,
54
+ ServiceMethodNotFoundError,
57
55
  } from "@milaboratories/pl-model-common";
58
- import { SpecDriver } from "./spec_driver";
56
+ import { getServiceInjectors } from "./service_injectors";
59
57
 
60
58
  function bytesToBase64(data: Uint8Array | undefined): string | undefined {
61
59
  return data !== undefined ? Buffer.from(data).toString("base64") : undefined;
@@ -69,18 +67,18 @@ export class ComputableContextHelper implements JsRenderInternal.GlobalCfgRender
69
67
 
70
68
  private computableCtx: ComputableCtx | undefined;
71
69
  private readonly accessors = new Map<string, PlTreeNodeAccessor | undefined>();
72
- private readonly specDriver = new SpecDriver();
73
-
74
70
  private _meta: Map<string, Block> | undefined;
75
71
  private get meta(): Map<string, Block> {
76
72
  if (this._meta === undefined) {
77
- if (this.computableCtx === undefined)
78
- throw new Error("blockMeta can't be resolved in this context");
79
- this._meta = this.blockCtx.blockMeta(this.computableCtx);
73
+ this._meta = this.blockCtx.blockMeta(this.requireComputableCtx);
80
74
  }
81
75
  return this._meta;
82
76
  }
83
77
 
78
+ public get serviceRegistry(): ModelServiceRegistry {
79
+ return this.env.serviceRegistry;
80
+ }
81
+
84
82
  constructor(
85
83
  private readonly parent: JsExecutionContext,
86
84
  private readonly blockCtx: BlockContextAny,
@@ -96,21 +94,29 @@ export class ComputableContextHelper implements JsRenderInternal.GlobalCfgRender
96
94
  this.accessors.clear();
97
95
  }
98
96
 
97
+ private get requireComputableCtx(): ComputableCtx {
98
+ if (this.computableCtx === undefined)
99
+ throw new Error("computableCtx not available (called from future mapper?)");
100
+ return this.computableCtx;
101
+ }
102
+
103
+ public addOnDestroy(callback: () => void): void {
104
+ this.requireComputableCtx.addOnDestroy(callback);
105
+ }
106
+
99
107
  //
100
108
  // Methods for injected ctx object
101
109
  //
102
110
 
103
111
  getAccessorHandleByName(name: string): string | undefined {
104
- if (this.computableCtx === undefined)
105
- throw new Error("Accessors can't be used in this context");
112
+ const cCtx = this.requireComputableCtx;
106
113
  const wellKnownAccessor = (name: string, ctxKey: "staging" | "prod"): string | undefined => {
107
114
  if (!this.accessors.has(name)) {
108
115
  const lambda = this.blockCtx[ctxKey];
109
116
  if (lambda === undefined) throw new Error("Staging context not available");
110
- const entry = lambda(this.computableCtx!);
117
+ const entry = lambda(cCtx);
111
118
  if (!entry) this.accessors.set(name, undefined);
112
- else
113
- this.accessors.set(name, this.computableCtx!.accessor(entry).node({ ignoreError: true }));
119
+ else this.accessors.set(name, cCtx.accessor(entry).node({ ignoreError: true }));
114
120
  }
115
121
  return this.accessors.get(name) ? name : undefined;
116
122
  };
@@ -336,14 +342,10 @@ export class ComputableContextHelper implements JsRenderInternal.GlobalCfgRender
336
342
  private _resultPool: ResultPool | undefined = undefined;
337
343
  private get resultPool(): ResultPool {
338
344
  if (this._resultPool === undefined) {
339
- if (this.computableCtx === undefined)
340
- throw new Error(
341
- "can't use result pool in this context (most porbably called from the future mapper)",
342
- );
343
345
  this._resultPool = notEmpty(
344
346
  this.blockCtx.getResultsPool,
345
347
  "getResultsPool",
346
- )(this.computableCtx);
348
+ )(this.requireComputableCtx);
347
349
  }
348
350
  return this._resultPool;
349
351
  }
@@ -355,7 +357,9 @@ export class ComputableContextHelper implements JsRenderInternal.GlobalCfgRender
355
357
  public getDataFromResultPool(): ResultCollection<PObject<string>> {
356
358
  const collection = this.resultPool.getData();
357
359
  if (collection.instabilityMarker !== undefined)
358
- this.computableCtx!.markUnstable(`incomplete_result_pool:${collection.instabilityMarker}`);
360
+ this.requireComputableCtx.markUnstable(
361
+ `incomplete_result_pool:${collection.instabilityMarker}`,
362
+ );
359
363
  return {
360
364
  isComplete: collection.isComplete,
361
365
  entries: collection.entries.map((e) => ({
@@ -370,7 +374,9 @@ export class ComputableContextHelper implements JsRenderInternal.GlobalCfgRender
370
374
  > {
371
375
  const collection = this.resultPool.getDataWithErrors();
372
376
  if (collection.instabilityMarker !== undefined)
373
- this.computableCtx!.markUnstable(`incomplete_result_pool:${collection.instabilityMarker}`);
377
+ this.requireComputableCtx.markUnstable(
378
+ `incomplete_result_pool:${collection.instabilityMarker}`,
379
+ );
374
380
  return {
375
381
  isComplete: collection.isComplete,
376
382
  entries: collection.entries.map((e) => ({
@@ -387,7 +393,9 @@ export class ComputableContextHelper implements JsRenderInternal.GlobalCfgRender
387
393
  public getSpecsFromResultPool(): ResultCollection<PObjectSpec> {
388
394
  const specs = this.resultPool.getSpecs();
389
395
  if (specs.instabilityMarker !== undefined)
390
- this.computableCtx!.markUnstable(`specs_from_pool_incomplete:${specs.instabilityMarker}`);
396
+ this.requireComputableCtx.markUnstable(
397
+ `specs_from_pool_incomplete:${specs.instabilityMarker}`,
398
+ );
391
399
  return specs;
392
400
  }
393
401
 
@@ -408,79 +416,37 @@ export class ComputableContextHelper implements JsRenderInternal.GlobalCfgRender
408
416
  public createPFrame(
409
417
  def: PFrameDef<PColumn<string | PColumnValues | DataInfo<string>>>,
410
418
  ): PFrameHandle {
411
- if (this.computableCtx === undefined)
412
- throw new Error(
413
- "can't instantiate PFrames from this context (most porbably called from the future mapper)",
414
- );
415
- const { key, unref } = this.env.driverKit.pFrameDriver.createPFrame(
416
- def.map((c) => mapPObjectData(c, (d) => this.transformInputPData(d))),
419
+ using guard = new PoolEntryGuard(
420
+ this.env.driverKit.pFrameDriver.createPFrame(
421
+ def.map((c) => mapPObjectData(c, (d) => this.transformInputPData(d))),
422
+ ),
417
423
  );
418
- this.computableCtx.addOnDestroy(unref);
419
- return key;
424
+ this.requireComputableCtx.addOnDestroy(guard.entry.unref);
425
+ return guard.keep().key;
420
426
  }
421
427
 
422
428
  public createPTable(
423
429
  def: PTableDef<PColumn<string | PColumnValues | DataInfo<string>>>,
424
430
  ): PTableHandle {
425
- if (this.computableCtx === undefined)
426
- throw new Error(
427
- "can't instantiate PTable from this context (most porbably called from the future mapper)",
428
- );
429
- const { key, unref } = this.env.driverKit.pFrameDriver.createPTable(
430
- mapPTableDef(def, (c) => mapPObjectData(c, (d) => this.transformInputPData(d))),
431
+ using guard = new PoolEntryGuard(
432
+ this.env.driverKit.pFrameDriver.createPTable(
433
+ mapPTableDef(def, (c) => mapPObjectData(c, (d) => this.transformInputPData(d))),
434
+ ),
431
435
  );
432
- this.computableCtx.addOnDestroy(unref);
433
- return key;
436
+ this.requireComputableCtx.addOnDestroy(guard.entry.unref);
437
+ return guard.keep().key;
434
438
  }
439
+
435
440
  public createPTableV2(
436
441
  def: PTableDefV2<PColumn<string | PColumnValues | DataInfo<string>>>,
437
442
  ): PTableHandle {
438
- if (this.computableCtx === undefined)
439
- throw new Error(
440
- "can't instantiate PTable from this context (most porbably called from the future mapper)",
441
- );
442
- const { key, unref } = this.env.driverKit.pFrameDriver.createPTableV2(
443
- mapPTableDefV2(def, (c) => mapPObjectData(c, (d) => this.transformInputPData(d))),
443
+ using guard = new PoolEntryGuard(
444
+ this.env.driverKit.pFrameDriver.createPTableV2(
445
+ mapPTableDefV2(def, (c) => mapPObjectData(c, (d) => this.transformInputPData(d))),
446
+ ),
444
447
  );
445
- this.computableCtx.addOnDestroy(unref);
446
- return key;
447
- }
448
-
449
- //
450
- // Spec Frames
451
- //
452
-
453
- public createSpecFrame(specs: Record<string, PColumnSpec>): SpecFrameHandle {
454
- const handle = this.specDriver.createSpecFrame(specs);
455
- this.computableCtx?.addOnDestroy(() => this.specDriver.disposeSpecFrame(handle));
456
- return handle;
457
- }
458
-
459
- public specFrameDiscoverColumns(
460
- handle: SpecFrameHandle,
461
- request: DiscoverColumnsRequest,
462
- ): DiscoverColumnsResponse {
463
- return this.specDriver.specFrameDiscoverColumns(handle as SpecFrameHandle, request);
464
- }
465
-
466
- public disposeSpecFrame(handle: SpecFrameHandle): void {
467
- this.specDriver.disposeSpecFrame(handle as SpecFrameHandle);
468
- }
469
-
470
- public expandAxes(spec: AxesSpec): AxesId {
471
- return this.specDriver.expandAxes(spec);
472
- }
473
-
474
- public collapseAxes(ids: AxesId): AxesSpec {
475
- return this.specDriver.collapseAxes(ids);
476
- }
477
-
478
- public findAxis(spec: AxesSpec, selector: SingleAxisSelector): number {
479
- return this.specDriver.findAxis(spec, selector);
480
- }
481
-
482
- public findTableColumn(tableSpec: PTableColumnSpec[], selector: PTableColumnId): number {
483
- return this.specDriver.findTableColumn(tableSpec, selector);
448
+ this.requireComputableCtx.addOnDestroy(guard.entry.unref);
449
+ return guard.keep().key;
484
450
  }
485
451
 
486
452
  /**
@@ -597,49 +563,32 @@ export class ComputableContextHelper implements JsRenderInternal.GlobalCfgRender
597
563
  if (checkBlockFlag(this.featureFlags, "supportsLazyState")) {
598
564
  // injecting lazy state functions
599
565
  exportCtxFunction("args", () => {
600
- if (this.computableCtx === undefined)
601
- throw new Error(
602
- `Add dummy call to ctx.args outside the future lambda. Can't be directly used in this context.`,
603
- );
604
- const args = this.blockCtx.args(this.computableCtx);
566
+ const cCtx = this.requireComputableCtx;
567
+ const args = this.blockCtx.args(cCtx);
605
568
  return args === undefined ? vm.undefined : vm.newString(args);
606
569
  });
607
570
  exportCtxFunction("blockStorage", () => {
608
- if (this.computableCtx === undefined)
609
- throw new Error(
610
- `Add dummy call to ctx.blockStorage outside the future lambda. Can't be directly used in this context.`,
611
- );
612
- return vm.newString(this.blockCtx.blockStorage(this.computableCtx) ?? "{}");
571
+ return vm.newString(this.blockCtx.blockStorage(this.requireComputableCtx) ?? "{}");
613
572
  });
614
573
  exportCtxFunction("data", () => {
615
- if (this.computableCtx === undefined)
616
- throw new Error(
617
- `Add dummy call to ctx.data outside the future lambda. Can't be directly used in this context.`,
618
- );
619
- return vm.newString(this.blockCtx.data(this.computableCtx) ?? "{}");
574
+ return vm.newString(this.blockCtx.data(this.requireComputableCtx) ?? "{}");
620
575
  });
621
576
  exportCtxFunction("activeArgs", () => {
622
- if (this.computableCtx === undefined)
623
- throw new Error(
624
- `Add dummy call to ctx.activeArgs outside the future lambda. Can't be directly used in this context.`,
625
- );
626
- const res = this.blockCtx.activeArgs(this.computableCtx);
577
+ const cCtx = this.requireComputableCtx;
578
+ const res = this.blockCtx.activeArgs(cCtx);
627
579
  return res === undefined ? vm.undefined : vm.newString(res);
628
580
  });
629
581
  // For v1/v2 blocks, also inject uiState (extracted from state.uiState)
630
582
  if (isLegacyBlock) {
631
583
  exportCtxFunction("uiState", () => {
632
- if (this.computableCtx === undefined)
633
- throw new Error(
634
- `Add dummy call to ctx.uiState outside the future lambda. Can't be directly used in this context.`,
635
- );
636
- return vm.newString(extractUiState(this.blockCtx.data(this.computableCtx)));
584
+ return vm.newString(extractUiState(this.blockCtx.data(this.requireComputableCtx)));
637
585
  });
638
586
  }
639
587
  } else {
640
- const args = this.blockCtx.args(this.computableCtx!);
641
- const activeArgs = this.blockCtx.activeArgs(this.computableCtx!);
642
- const data = this.blockCtx.data(this.computableCtx!);
588
+ const cCtx = this.requireComputableCtx;
589
+ const args = this.blockCtx.args(cCtx);
590
+ const activeArgs = this.blockCtx.activeArgs(cCtx);
591
+ const data = this.blockCtx.data(cCtx);
643
592
  if (args !== undefined) {
644
593
  vm.setProp(configCtx, "args", localScope.manage(vm.newString(args)));
645
594
  }
@@ -888,10 +837,6 @@ export class ComputableContextHelper implements JsRenderInternal.GlobalCfgRender
888
837
  );
889
838
  });
890
839
 
891
- //
892
- // PFrames / PTables
893
- //
894
-
895
840
  exportCtxFunction("createPFrame", (def) => {
896
841
  return parent.exportSingleValue(
897
842
  this.createPFrame(
@@ -920,63 +865,52 @@ export class ComputableContextHelper implements JsRenderInternal.GlobalCfgRender
920
865
  });
921
866
 
922
867
  //
923
- // Spec Frames
868
+ // Services
924
869
  //
925
870
 
926
- exportCtxFunction("createSpecFrame", (specs) => {
927
- return parent.exportSingleValue(
928
- this.createSpecFrame(parent.importObjectViaJson(specs) as Record<string, PColumnSpec>),
929
- undefined,
930
- );
931
- });
932
-
933
- exportCtxFunction("specFrameDiscoverColumns", (handle, request) => {
934
- return parent.exportObjectViaJson(
935
- this.specFrameDiscoverColumns(
936
- vm.getString(handle) as SpecFrameHandle,
937
- parent.importObjectViaJson(request) as DiscoverColumnsRequest,
938
- ),
939
- undefined,
940
- );
941
- });
942
-
943
- exportCtxFunction("disposeSpecFrame", (handle) => {
944
- this.disposeSpecFrame(vm.getString(handle) as SpecFrameHandle);
945
- });
871
+ const requiredServiceNames = new Set(resolveRequiredServices(this.featureFlags) as string[]);
872
+ const serviceFunctions = new Map<string, VmFunctionImplementation<QuickJSHandle>>();
873
+ const injectors = getServiceInjectors();
946
874
 
947
- exportCtxFunction("expandAxes", (spec) => {
948
- return parent.exportObjectViaJson(
949
- this.expandAxes(parent.importObjectViaJson(spec) as AxesSpec),
950
- undefined,
951
- );
952
- });
875
+ for (const [key, serviceId] of Object.entries(Services)) {
876
+ if (!requiredServiceNames.has(serviceId)) continue;
877
+ const injector = injectors[key as keyof typeof injectors];
878
+ try {
879
+ const methods = injector({ host: this, vm: parent });
880
+ for (const [method, fn] of Object.entries(methods)) {
881
+ serviceFunctions.set(serviceFnKey(serviceId, method), fn);
882
+ }
883
+ } catch (e) {
884
+ throw new ServiceInjectionError(`Failed to inject service "${serviceId}"`, { cause: e });
885
+ }
886
+ }
953
887
 
954
- exportCtxFunction("collapseAxes", (ids) => {
955
- return parent.exportObjectViaJson(
956
- this.collapseAxes(parent.importObjectViaJson(ids) as AxesId),
957
- undefined,
958
- );
888
+ exportCtxFunction("getServiceNames", () => {
889
+ return parent.exportObjectViaJson([...requiredServiceNames]);
959
890
  });
960
891
 
961
- exportCtxFunction("findAxis", (spec, selector) => {
962
- return parent.exportSingleValue(
963
- this.findAxis(
964
- parent.importObjectViaJson(spec) as AxesSpec,
965
- parent.importObjectViaJson(selector) as SingleAxisSelector,
966
- ),
967
- undefined,
968
- );
892
+ exportCtxFunction("getServiceMethods", (serviceIdHandle) => {
893
+ const serviceId = vm.getString(serviceIdHandle);
894
+ const pfx = serviceFnKey(serviceId);
895
+ const methods = [...serviceFunctions.keys()]
896
+ .filter((k) => k.startsWith(pfx))
897
+ .map((k) => k.slice(pfx.length));
898
+ return parent.exportObjectViaJson(methods, undefined);
969
899
  });
970
900
 
971
- exportCtxFunction("findTableColumn", (tableSpec, selector) => {
972
- return parent.exportSingleValue(
973
- this.findTableColumn(
974
- parent.importObjectViaJson(tableSpec) as PTableColumnSpec[],
975
- parent.importObjectViaJson(selector) as PTableColumnId,
976
- ),
977
- undefined,
978
- );
979
- });
901
+ exportCtxFunction(
902
+ "callServiceMethod",
903
+ function (this: QuickJSHandle, serviceIdHandle, methodHandle, ...argHandles) {
904
+ const serviceId = vm.getString(serviceIdHandle);
905
+ const method = vm.getString(methodHandle);
906
+ const fn = serviceFunctions.get(serviceFnKey(serviceId, method));
907
+ if (!fn)
908
+ throw new ServiceMethodNotFoundError(
909
+ `Method "${method}" not found on service "${serviceId}"`,
910
+ );
911
+ return fn.call(this, ...argHandles);
912
+ },
913
+ );
980
914
 
981
915
  //
982
916
  // Computable
@@ -200,7 +200,7 @@ export class JsExecutionContext {
200
200
 
201
201
  public exportSingleValue(
202
202
  obj: boolean | number | string | null | ArrayBuffer | undefined,
203
- scope: Scope | undefined,
203
+ scope?: Scope,
204
204
  ): QuickJSHandle {
205
205
  const result = this.tryExportSingleValue(obj, scope);
206
206
  if (result === undefined) {
@@ -211,7 +211,7 @@ export class JsExecutionContext {
211
211
  return result;
212
212
  }
213
213
 
214
- public tryExportSingleValue(obj: unknown, scope: Scope | undefined): QuickJSHandle | undefined {
214
+ public tryExportSingleValue(obj: unknown, scope?: Scope): QuickJSHandle | undefined {
215
215
  let handle: QuickJSHandle;
216
216
  let manage = false;
217
217
  switch (typeof obj) {
@@ -244,13 +244,13 @@ export class JsExecutionContext {
244
244
  return manage && scope != undefined ? scope.manage(handle) : handle;
245
245
  }
246
246
 
247
- public exportObjectUniversal(obj: unknown, scope: Scope | undefined): QuickJSHandle {
247
+ public exportObjectUniversal(obj: unknown, scope?: Scope): QuickJSHandle {
248
248
  const simpleHandle = this.tryExportSingleValue(obj, scope);
249
249
  if (simpleHandle !== undefined) return simpleHandle;
250
250
  return this.exportObjectViaJson(obj, scope);
251
251
  }
252
252
 
253
- public exportObjectViaJson(obj: unknown, scope: Scope | undefined): QuickJSHandle {
253
+ public exportObjectViaJson(obj: unknown, scope?: Scope): QuickJSHandle {
254
254
  const t0 = performance.now();
255
255
  const json = JSON.stringify(obj);
256
256
  this.stats.serInBytes += json.length;
@@ -0,0 +1,153 @@
1
+ import type { QuickJSHandle, VmFunctionImplementation } from "quickjs-emscripten";
2
+ import type { InferServiceModel, ServiceBrand } from "@milaboratories/pl-model-common";
3
+ import { Services, ServiceNotRegisteredError } from "@milaboratories/pl-model-common";
4
+ import type {
5
+ AxesId,
6
+ AxesSpec,
7
+ DataInfo,
8
+ PColumn,
9
+ PColumnSpec,
10
+ PColumnValues,
11
+ PTableColumnId,
12
+ PTableColumnSpec,
13
+ SingleAxisSelector,
14
+ DeleteColumnRequest,
15
+ DiscoverColumnsRequest,
16
+ PFrameDef,
17
+ SpecQuery,
18
+ PTableDef,
19
+ PTableDefV2,
20
+ SpecFrameHandle,
21
+ } from "@milaboratories/pl-model-common";
22
+ import { PoolEntryGuard } from "@milaboratories/pl-model-common";
23
+ import type { JsExecutionContext } from "./context";
24
+ import type { ComputableContextHelper } from "./computable_context";
25
+
26
+ type VmMethod = VmFunctionImplementation<QuickJSHandle>;
27
+
28
+ export type ServiceInjectorContext = {
29
+ host: ComputableContextHelper;
30
+ vm: JsExecutionContext;
31
+ };
32
+
33
+ // Each injector returns a record of method name -> VM function implementation.
34
+ // The framework automatically registers them with serviceFnKey(serviceId, methodName).
35
+ export type ServiceInjector<Methods extends string = string> = (
36
+ ctx: ServiceInjectorContext,
37
+ ) => Record<Methods, VmMethod>;
38
+
39
+ // Type-safe injector for a specific service — must return all methods from the model interface.
40
+ type ServiceInjectorFor<S extends keyof typeof Services> = ServiceInjector<
41
+ string & keyof InferServiceModel<ServiceBrand<(typeof Services)[S]>>
42
+ >;
43
+
44
+ // Complete, type-checked injector map.
45
+ // Adding a service to Services without an entry here is a compile-time error.
46
+ // Missing a method from the interface is also a compile-time error.
47
+ type ServiceInjectorMap = { [K in keyof typeof Services]: ServiceInjectorFor<K> };
48
+
49
+ export function getServiceInjectors(): ServiceInjectorMap {
50
+ return {
51
+ PFrameSpec: ({ host, vm }: ServiceInjectorContext) => {
52
+ const driver = host.serviceRegistry.get(Services.PFrameSpec);
53
+ if (!driver)
54
+ throw new ServiceNotRegisteredError(
55
+ `Service "${Services.PFrameSpec}" has no factory in ModelServiceRegistry. Provide a non-null factory.`,
56
+ );
57
+
58
+ return {
59
+ createSpecFrame: (specs: QuickJSHandle) => {
60
+ using guard = new PoolEntryGuard(
61
+ driver.createSpecFrame(vm.importObjectViaJson(specs) as Record<string, PColumnSpec>),
62
+ );
63
+ host.addOnDestroy(guard.entry.unref);
64
+ const entry = guard.keep();
65
+ // TODO: add [Symbol.dispose] once QuickJS supports ES2024 explicit resource management
66
+ const obj = vm.vm.newObject();
67
+ vm.vm.newString(entry.key).consume((k) => vm.vm.setProp(obj, "key", k));
68
+ vm.vm
69
+ .newFunction("unref", () => {
70
+ entry.unref();
71
+ })
72
+ .consume((fn) => vm.vm.setProp(obj, "unref", fn));
73
+ return obj;
74
+ },
75
+
76
+ discoverColumns: (handle: QuickJSHandle, request: QuickJSHandle) =>
77
+ vm.exportObjectViaJson(
78
+ driver.discoverColumns(
79
+ vm.vm.getString(handle) as SpecFrameHandle,
80
+ vm.importObjectViaJson(request) as DiscoverColumnsRequest,
81
+ ),
82
+ ),
83
+
84
+ deleteColumn: (handle: QuickJSHandle, request: QuickJSHandle) =>
85
+ vm.exportObjectViaJson(
86
+ driver.deleteColumn(
87
+ vm.vm.getString(handle) as SpecFrameHandle,
88
+ vm.importObjectViaJson(request) as DeleteColumnRequest,
89
+ ),
90
+ ),
91
+
92
+ evaluateQuery: (handle: QuickJSHandle, request: QuickJSHandle) =>
93
+ vm.exportObjectViaJson(
94
+ driver.evaluateQuery(
95
+ vm.vm.getString(handle) as SpecFrameHandle,
96
+ vm.importObjectViaJson(request) as SpecQuery,
97
+ ),
98
+ ),
99
+
100
+ expandAxes: (spec: QuickJSHandle) =>
101
+ vm.exportObjectViaJson(driver.expandAxes(vm.importObjectViaJson(spec) as AxesSpec)),
102
+
103
+ collapseAxes: (ids: QuickJSHandle) =>
104
+ vm.exportObjectViaJson(driver.collapseAxes(vm.importObjectViaJson(ids) as AxesId)),
105
+
106
+ findAxis: (spec: QuickJSHandle, selector: QuickJSHandle) =>
107
+ vm.exportSingleValue(
108
+ driver.findAxis(
109
+ vm.importObjectViaJson(spec) as AxesSpec,
110
+ vm.importObjectViaJson(selector) as SingleAxisSelector,
111
+ ),
112
+ ),
113
+
114
+ findTableColumn: (tableSpec: QuickJSHandle, selector: QuickJSHandle) =>
115
+ vm.exportSingleValue(
116
+ driver.findTableColumn(
117
+ vm.importObjectViaJson(tableSpec) as PTableColumnSpec[],
118
+ vm.importObjectViaJson(selector) as PTableColumnId,
119
+ ),
120
+ ),
121
+ };
122
+ },
123
+
124
+ PFrame: ({ host, vm }: ServiceInjectorContext) => ({
125
+ createPFrame: (def: QuickJSHandle) =>
126
+ vm.exportSingleValue(
127
+ host.createPFrame(
128
+ vm.importObjectViaJson(def) as PFrameDef<
129
+ PColumn<string | PColumnValues | DataInfo<string>>
130
+ >,
131
+ ),
132
+ ),
133
+
134
+ createPTable: (def: QuickJSHandle) =>
135
+ vm.exportSingleValue(
136
+ host.createPTable(
137
+ vm.importObjectViaJson(def) as PTableDef<
138
+ PColumn<string | PColumnValues | DataInfo<string>>
139
+ >,
140
+ ),
141
+ ),
142
+
143
+ createPTableV2: (def: QuickJSHandle) =>
144
+ vm.exportSingleValue(
145
+ host.createPTableV2(
146
+ vm.importObjectViaJson(def) as PTableDefV2<
147
+ PColumn<string | PColumnValues | DataInfo<string>>
148
+ >,
149
+ ),
150
+ ),
151
+ }),
152
+ };
153
+ }
@@ -32,6 +32,11 @@ import type { MiddleLayerDriverKit } from "./driver_kit";
32
32
  import { initDriverKit } from "./driver_kit";
33
33
  import type { BlockCodeFeatureFlags, DriverKit, SupportedRequirement } from "@platforma-sdk/model";
34
34
  import { RuntimeCapabilities } from "@platforma-sdk/model";
35
+ import {
36
+ type ModelServiceRegistry,
37
+ registerServiceCapabilities,
38
+ } from "@milaboratories/pl-model-common";
39
+ import { createModelServiceRegistry } from "../service_factories";
35
40
  import type { DownloadUrlDriver } from "@milaboratories/pl-drivers";
36
41
  import { V2RegistryProvider } from "../block_registry";
37
42
  import type { Dispatcher } from "undici";
@@ -54,6 +59,7 @@ export interface MiddleLayerEnvironment {
54
59
  readonly blockUpdateWatcher: BlockUpdateWatcher;
55
60
  readonly quickJs: QuickJSWASMModule;
56
61
  readonly driverKit: MiddleLayerDriverKit;
62
+ readonly serviceRegistry: ModelServiceRegistry;
57
63
  readonly projectHelper: ProjectHelper;
58
64
  }
59
65
 
@@ -115,6 +121,11 @@ export class MiddleLayer {
115
121
  return this.env.driverKit;
116
122
  }
117
123
 
124
+ /** Returns the service registry for service introspection. */
125
+ public get serviceRegistry(): ModelServiceRegistry {
126
+ return this.env.serviceRegistry;
127
+ }
128
+
118
129
  //
119
130
  // Project List Manipulation
120
131
  //
@@ -335,8 +346,13 @@ export class MiddleLayer {
335
346
  runtimeCapabilities.addSupportedRequirement("requiresModelAPIVersion", 1);
336
347
  runtimeCapabilities.addSupportedRequirement("requiresModelAPIVersion", 2);
337
348
  runtimeCapabilities.addSupportedRequirement("requiresCreatePTable", 2);
349
+ registerServiceCapabilities((flag, value) =>
350
+ runtimeCapabilities.addSupportedRequirement(flag, value),
351
+ );
338
352
  // runtime capabilities of the desktop are to be added by the desktop app / test framework
339
353
 
354
+ const serviceRegistry = createModelServiceRegistry({ logger });
355
+
340
356
  const env: MiddleLayerEnvironment = {
341
357
  pl,
342
358
  blockEventDispatcher: new BlockEventDispatcher(),
@@ -354,9 +370,11 @@ export class MiddleLayer {
354
370
  preferredUpdateChannel: ops.preferredUpdateChannel,
355
371
  }),
356
372
  runtimeCapabilities,
373
+ serviceRegistry,
357
374
  quickJs,
358
375
  projectHelper: new ProjectHelper(quickJs, logger),
359
376
  dispose: async () => {
377
+ await serviceRegistry.dispose();
360
378
  await retryHttpDispatcher.destroy();
361
379
  await driverKit.dispose();
362
380
  },
@@ -219,7 +219,6 @@ class BlobStore extends PFrameInternal.BaseObjectStore {
219
219
  type: "Ok",
220
220
  size: blob.size,
221
221
  range: translatedRange,
222
- // eslint-disable-next-line n/no-unsupported-features/node-builtins
223
222
  data: Readable.fromWeb(data),
224
223
  });
225
224
  },
@@ -268,6 +267,7 @@ class RemoteBlobProviderImpl implements RemoteBlobProvider<PlTreeEntry> {
268
267
 
269
268
  async [Symbol.asyncDispose](): Promise<void> {
270
269
  await this.server.stop();
270
+ await this.pool[Symbol.asyncDispose]();
271
271
  }
272
272
  }
273
273