@platforma-sdk/ui-vue 1.61.2 → 1.62.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.
@@ -165,6 +165,11 @@ function createMockApiV3<
165
165
  async dispose(): Promise<ResultOrError<void>> {
166
166
  return { value: undefined };
167
167
  },
168
+ serviceDispatch: {
169
+ getServiceNames: () => [],
170
+ getServiceMethods: () => [],
171
+ callServiceMethod: () => undefined,
172
+ },
168
173
  //
169
174
  blobDriver: undefined as any,
170
175
  //
@@ -521,6 +526,96 @@ describe("createAppV3", { timeout: 20_000 }, () => {
521
526
  app.closedRef = true;
522
527
  });
523
528
 
529
+ it("should default stable to true on plugin withStatus outputs when stable is absent", async () => {
530
+ type F = PluginFactory<PluginData, undefined, { pFrame: OutputWithStatus<string | undefined> }>;
531
+ const pluginId = "graphMaker" as PluginHandle<F>;
532
+ const pluginOutputName = pluginOutputKey(pluginId, "pFrame");
533
+
534
+ const outputsWithoutStable = {
535
+ ...defaultOutputs(),
536
+ [pluginOutputName]: { ok: true, value: undefined } as OutputWithStatus<string | undefined>,
537
+ } as Outputs;
538
+
539
+ const state = new BlockStateV3Mock<Data, Outputs>(defaultData(), outputsWithoutStable, "/", {
540
+ [pluginId]: defaultPluginData(),
541
+ });
542
+
543
+ const blockModelInfo: BlockModelInfo = {
544
+ outputs: {
545
+ doubled: { withStatus: false },
546
+ [pluginOutputName]: { withStatus: true },
547
+ },
548
+ pluginIds: [pluginId],
549
+ featureFlags: {},
550
+ };
551
+
552
+ const platforma = createMockApiV3<Data, Args, Outputs>(state, blockModelInfo);
553
+ const initialState = await platforma.loadBlockState();
554
+ if ("error" in initialState) throw initialState.error;
555
+
556
+ const { pluginAccess } = createAppV3<Data, Args, Outputs>(initialState.value!, platforma, {
557
+ debug: false,
558
+ debounceSpan: 10,
559
+ });
560
+
561
+ const pluginState = pluginAccess.getOrCreatePluginState(pluginId);
562
+
563
+ const pFrame = pluginState.model.outputs["pFrame"] as Extract<
564
+ OutputWithStatus<string | undefined>,
565
+ { ok: true }
566
+ >;
567
+ expect(pFrame).toBeDefined();
568
+ expect(pFrame.ok).toBe(true);
569
+ expect(pFrame.stable).toBe(true);
570
+ expect(pFrame.value).toBeUndefined();
571
+ });
572
+
573
+ it("should preserve stable=false on plugin withStatus outputs when explicitly set", async () => {
574
+ type F = PluginFactory<PluginData, undefined, { pFrame: OutputWithStatus<string | undefined> }>;
575
+ const pluginId = "graphMaker" as PluginHandle<F>;
576
+ const pluginOutputName = pluginOutputKey(pluginId, "pFrame");
577
+
578
+ const outputsUnstable = {
579
+ ...defaultOutputs(),
580
+ [pluginOutputName]: { ok: true, value: undefined, stable: false } as OutputWithStatus<
581
+ string | undefined
582
+ >,
583
+ } as Outputs;
584
+
585
+ const state = new BlockStateV3Mock<Data, Outputs>(defaultData(), outputsUnstable, "/", {
586
+ [pluginId]: defaultPluginData(),
587
+ });
588
+
589
+ const blockModelInfo: BlockModelInfo = {
590
+ outputs: {
591
+ doubled: { withStatus: false },
592
+ [pluginOutputName]: { withStatus: true },
593
+ },
594
+ pluginIds: [pluginId],
595
+ featureFlags: {},
596
+ };
597
+
598
+ const platforma = createMockApiV3<Data, Args, Outputs>(state, blockModelInfo);
599
+ const initialState = await platforma.loadBlockState();
600
+ if ("error" in initialState) throw initialState.error;
601
+
602
+ const { pluginAccess } = createAppV3<Data, Args, Outputs>(initialState.value!, platforma, {
603
+ debug: false,
604
+ debounceSpan: 10,
605
+ });
606
+
607
+ const pluginState = pluginAccess.getOrCreatePluginState(pluginId);
608
+
609
+ const pFrame = pluginState.model.outputs["pFrame"] as Extract<
610
+ OutputWithStatus<string | undefined>,
611
+ { ok: true }
612
+ >;
613
+ expect(pFrame).toBeDefined();
614
+ expect(pFrame.ok).toBe(true);
615
+ expect(pFrame.stable).toBe(false);
616
+ expect(pFrame.value).toBeUndefined();
617
+ });
618
+
524
619
  it("should navigate to href", async () => {
525
620
  const state = createDefaultState();
526
621
  const platforma = createMockApiV3<Data, Args, Outputs>(state, defaultBlockModelInfo());
@@ -12,6 +12,7 @@ import type {
12
12
  PluginHandle,
13
13
  InferFactoryData,
14
14
  InferFactoryOutputs,
15
+ InferFactoryUiServices,
15
16
  PluginFactoryLike,
16
17
  } from "@platforma-sdk/model";
17
18
  import {
@@ -21,9 +22,13 @@ import {
21
22
  getPluginData,
22
23
  isPluginOutputKey,
23
24
  pluginOutputPrefix,
25
+ createNodeServiceProxy,
26
+ buildServices,
24
27
  } from "@platforma-sdk/model";
28
+ import { type UiServices as AllUiServices } from "@milaboratories/pl-model-common";
29
+ import { createUiServiceRegistry } from "./service_factories";
25
30
  import type { Ref } from "vue";
26
- import { reactive, computed, ref } from "vue";
31
+ import { reactive, computed, ref, markRaw } from "vue";
27
32
  import type { OutputValues, OutputErrors, AppSettings } from "../types";
28
33
  import { parseQuery } from "../urls";
29
34
  import { ensureOutputHasStableFlag, MultiError } from "../utils";
@@ -35,10 +40,11 @@ import type { PluginState, PluginAccess } from "../usePlugin";
35
40
  export const patchPoolingDelay = 150;
36
41
 
37
42
  /** Internal per-plugin state with reconciliation support. */
38
- interface InternalPluginState<Data = unknown, Outputs = unknown> extends PluginState<
39
- Data,
40
- Outputs
41
- > {
43
+ interface InternalPluginState<
44
+ Data = unknown,
45
+ Outputs = unknown,
46
+ Services = Record<string, unknown>,
47
+ > extends PluginState<Data, Outputs, Services> {
42
48
  readonly ignoreUpdates: (fn: () => void) => void;
43
49
  }
44
50
 
@@ -75,9 +81,10 @@ export function createAppV3<
75
81
  Outputs extends BlockOutputsBase = BlockOutputsBase,
76
82
  Href extends `/${string}` = `/${string}`,
77
83
  Plugins extends Record<string, unknown> = Record<string, unknown>,
84
+ UiServices extends Partial<AllUiServices> = Partial<AllUiServices>,
78
85
  >(
79
86
  state: ValueWithUTag<BlockStateV3<Data, Outputs, Href>>,
80
- platforma: PlatformaExtended<PlatformaV3<Data, Args, Outputs, Href, Plugins>>,
87
+ platforma: PlatformaExtended<PlatformaV3<Data, Args, Outputs, Href, Plugins, UiServices>>,
81
88
  settings: AppSettings,
82
89
  ) {
83
90
  const debug = (msg: string, ...rest: unknown[]) => {
@@ -223,12 +230,11 @@ export function createAppV3<
223
230
  (async () => {
224
231
  window.addEventListener("beforeunload", () => {
225
232
  closedRef.value = true;
226
- platforma
227
- .dispose()
228
- .then(unwrapResult)
229
- .catch((err) => {
233
+ Promise.allSettled([uiRegistry.dispose(), platforma.dispose().then(unwrapResult)]).catch(
234
+ (err) => {
230
235
  error("error in dispose", err);
231
- });
236
+ },
237
+ );
232
238
  });
233
239
 
234
240
  while (!closedRef.value) {
@@ -328,6 +334,10 @@ export function createAppV3<
328
334
  },
329
335
  };
330
336
 
337
+ const proxy = createNodeServiceProxy(platforma.serviceDispatch);
338
+ const uiRegistry = createUiServiceRegistry({ proxy });
339
+ const services = buildServices<UiServices>(platforma.serviceDispatch, uiRegistry);
340
+
331
341
  /** Creates a lazily-cached per-plugin reactive state. */
332
342
  const createPluginState = <F extends PluginFactoryLike>(
333
343
  handle: PluginHandle<F>,
@@ -342,7 +352,9 @@ export function createAppV3<
342
352
  if (!key.startsWith(prefix)) continue;
343
353
  const outputKey = key.slice(prefix.length);
344
354
  if (platforma.blockModelInfo.outputs[key]?.withStatus) {
345
- result[outputKey] = outputWithStatus ?? undefined;
355
+ result[outputKey] = outputWithStatus
356
+ ? ensureOutputHasStableFlag(outputWithStatus)
357
+ : undefined;
346
358
  } else {
347
359
  result[outputKey] =
348
360
  outputWithStatus.ok && outputWithStatus.value !== undefined
@@ -385,6 +397,7 @@ export function createAppV3<
385
397
 
386
398
  return {
387
399
  model: pluginModel,
400
+ services: markRaw(services),
388
401
  ignoreUpdates,
389
402
  };
390
403
  };
@@ -394,11 +407,19 @@ export function createAppV3<
394
407
  getOrCreatePluginState<F extends PluginFactoryLike>(handle: PluginHandle<F>) {
395
408
  const existing = pluginStates.get(handle);
396
409
  if (existing) {
397
- return existing as unknown as PluginState<InferFactoryData<F>, InferFactoryOutputs<F>>;
410
+ return existing as unknown as PluginState<
411
+ InferFactoryData<F>,
412
+ InferFactoryOutputs<F>,
413
+ InferFactoryUiServices<F>
414
+ >;
398
415
  }
399
416
  const state = createPluginState(handle);
400
417
  pluginStates.set(handle, state);
401
- return state;
418
+ return state as unknown as PluginState<
419
+ InferFactoryData<F>,
420
+ InferFactoryOutputs<F>,
421
+ InferFactoryUiServices<F>
422
+ >;
402
423
  },
403
424
  };
404
425
 
@@ -410,6 +431,7 @@ export function createAppV3<
410
431
  closedRef,
411
432
  snapshot,
412
433
  plugins,
434
+ services: markRaw(services),
413
435
  queryParams: computed(() => parseQuery<Href>(snapshot.value.navigationState.href as Href)),
414
436
  href: computed(() => snapshot.value.navigationState.href),
415
437
  hasErrors: computed(() =>
@@ -433,4 +455,5 @@ export type BaseAppV3<
433
455
  Outputs extends BlockOutputsBase = BlockOutputsBase,
434
456
  Href extends `/${string}` = `/${string}`,
435
457
  Plugins extends Record<string, unknown> = Record<string, unknown>,
436
- > = ReturnType<typeof createAppV3<Data, Args, Outputs, Href, Plugins>>["app"];
458
+ UiServices extends Partial<AllUiServices> = Partial<AllUiServices>,
459
+ > = ReturnType<typeof createAppV3<Data, Args, Outputs, Href, Plugins, UiServices>>["app"];
@@ -0,0 +1,23 @@
1
+ /**
2
+ * UI service factories — add a factory for each new service here.
3
+ *
4
+ * Each entry maps a Services key to a factory function that creates the
5
+ * UI-side driver instance:
6
+ * - WASM services: instantiated directly (e.g. SpecDriver)
7
+ * - Node services: proxied via IPC using NodeServiceProxy
8
+ */
9
+
10
+ import { Services, UiServiceRegistry } from "@milaboratories/pl-model-common";
11
+ import { SpecDriver } from "@milaboratories/pf-spec-driver";
12
+ import type { NodeServiceProxy } from "@platforma-sdk/model";
13
+
14
+ export type UiServiceOptions = {
15
+ proxy: NodeServiceProxy;
16
+ };
17
+
18
+ export function createUiServiceRegistry(options: UiServiceOptions) {
19
+ return new UiServiceRegistry(Services, {
20
+ PFrameSpec: () => new SpecDriver(),
21
+ PFrame: () => options.proxy(Services.PFrame),
22
+ });
23
+ }
package/src/usePlugin.ts CHANGED
@@ -4,11 +4,16 @@ import type {
4
4
  PluginHandle,
5
5
  InferFactoryData,
6
6
  InferFactoryOutputs,
7
+ InferFactoryUiServices,
7
8
  PluginFactoryLike,
8
9
  } from "@platforma-sdk/model";
9
10
 
10
11
  /** Per-plugin reactive model exposed to consumers via usePlugin(). */
11
- export interface PluginState<Data = unknown, Outputs = unknown> {
12
+ export interface PluginState<
13
+ Data = unknown,
14
+ Outputs = unknown,
15
+ Services = Record<string, unknown>,
16
+ > {
12
17
  readonly model: Reactive<{
13
18
  data: Data;
14
19
  outputs: Outputs extends Record<string, unknown>
@@ -18,13 +23,14 @@ export interface PluginState<Data = unknown, Outputs = unknown> {
18
23
  ? { [K in keyof Outputs]?: Error }
19
24
  : Record<string, Error | undefined>;
20
25
  }>;
26
+ readonly services: Services;
21
27
  }
22
28
 
23
29
  /** Internal interface for plugin access — provided via Vue injection to usePlugin(). */
24
30
  export interface PluginAccess {
25
31
  getOrCreatePluginState<F extends PluginFactoryLike>(
26
32
  handle: PluginHandle<F>,
27
- ): PluginState<InferFactoryData<F>, InferFactoryOutputs<F>>;
33
+ ): PluginState<InferFactoryData<F>, InferFactoryOutputs<F>, InferFactoryUiServices<F>>;
28
34
  }
29
35
 
30
36
  /**