@milaboratories/pl-model-common 1.29.0 → 1.31.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.
Files changed (105) hide show
  1. package/dist/bmodel/block_config.d.ts +1 -0
  2. package/dist/bmodel/code.d.ts +1 -0
  3. package/dist/driver_kit.d.ts +1 -1
  4. package/dist/drivers/index.d.ts +7 -7
  5. package/dist/drivers/index.js +2 -2
  6. package/dist/drivers/pframe/data_types.cjs +0 -15
  7. package/dist/drivers/pframe/data_types.cjs.map +1 -1
  8. package/dist/drivers/pframe/data_types.d.ts +14 -60
  9. package/dist/drivers/pframe/data_types.js +1 -13
  10. package/dist/drivers/pframe/data_types.js.map +1 -1
  11. package/dist/drivers/pframe/driver.cjs.map +1 -1
  12. package/dist/drivers/pframe/driver.d.ts +13 -5
  13. package/dist/drivers/pframe/driver.js.map +1 -1
  14. package/dist/drivers/pframe/index.d.ts +6 -6
  15. package/dist/drivers/pframe/index.js +2 -2
  16. package/dist/drivers/pframe/pframe.d.ts +2 -2
  17. package/dist/drivers/pframe/query/query_common.d.ts +2 -2
  18. package/dist/drivers/pframe/query/query_data.d.ts +1 -1
  19. package/dist/drivers/pframe/query/query_spec.d.ts +2 -2
  20. package/dist/drivers/pframe/spec/ids.d.ts +2 -2
  21. package/dist/drivers/pframe/spec/index.d.ts +1 -1
  22. package/dist/drivers/pframe/spec/index.js +1 -1
  23. package/dist/drivers/pframe/spec/selectors.d.ts +1 -1
  24. package/dist/drivers/pframe/spec/spec.cjs +14 -1
  25. package/dist/drivers/pframe/spec/spec.cjs.map +1 -1
  26. package/dist/drivers/pframe/spec/spec.d.ts +7 -1
  27. package/dist/drivers/pframe/spec/spec.js +14 -2
  28. package/dist/drivers/pframe/spec/spec.js.map +1 -1
  29. package/dist/drivers/pframe/spec_driver.d.ts +36 -6
  30. package/dist/drivers/pframe/table_calculate.cjs.map +1 -1
  31. package/dist/drivers/pframe/table_calculate.d.ts +5 -6
  32. package/dist/drivers/pframe/table_calculate.js.map +1 -1
  33. package/dist/drivers/pframe/table_common.d.ts +1 -1
  34. package/dist/drivers/pframe/unique_values.d.ts +2 -2
  35. package/dist/errors.cjs +48 -0
  36. package/dist/errors.cjs.map +1 -1
  37. package/dist/errors.d.ts +25 -1
  38. package/dist/errors.js +37 -1
  39. package/dist/errors.js.map +1 -1
  40. package/dist/flags/block_flags.cjs.map +1 -1
  41. package/dist/flags/block_flags.d.ts +4 -1
  42. package/dist/flags/block_flags.js.map +1 -1
  43. package/dist/flags/flag_utils.d.ts +1 -1
  44. package/dist/flags/index.d.ts +3 -0
  45. package/dist/index.cjs +38 -3
  46. package/dist/index.d.ts +31 -22
  47. package/dist/index.js +12 -4
  48. package/dist/pool/query.d.ts +1 -1
  49. package/dist/pool_entry.cjs +30 -0
  50. package/dist/pool_entry.cjs.map +1 -0
  51. package/dist/pool_entry.d.ts +30 -0
  52. package/dist/pool_entry.js +29 -0
  53. package/dist/pool_entry.js.map +1 -0
  54. package/dist/services/index.cjs +6 -0
  55. package/dist/services/index.d.ts +6 -0
  56. package/dist/services/index.js +6 -0
  57. package/dist/services/service_capabilities.cjs +51 -0
  58. package/dist/services/service_capabilities.cjs.map +1 -0
  59. package/dist/services/service_capabilities.d.ts +33 -0
  60. package/dist/services/service_capabilities.js +46 -0
  61. package/dist/services/service_capabilities.js.map +1 -0
  62. package/dist/services/service_declarations.cjs +17 -0
  63. package/dist/services/service_declarations.cjs.map +1 -0
  64. package/dist/services/service_declarations.d.ts +16 -0
  65. package/dist/services/service_declarations.js +17 -0
  66. package/dist/services/service_declarations.js.map +1 -0
  67. package/dist/services/service_injector_factory.cjs +19 -0
  68. package/dist/services/service_injector_factory.cjs.map +1 -0
  69. package/dist/services/service_injector_factory.d.ts +8 -0
  70. package/dist/services/service_injector_factory.js +18 -0
  71. package/dist/services/service_injector_factory.js.map +1 -0
  72. package/dist/services/service_injectors.cjs +48 -0
  73. package/dist/services/service_injectors.cjs.map +1 -0
  74. package/dist/services/service_injectors.d.ts +23 -0
  75. package/dist/services/service_injectors.js +46 -0
  76. package/dist/services/service_injectors.js.map +1 -0
  77. package/dist/services/service_registry.cjs +52 -0
  78. package/dist/services/service_registry.cjs.map +1 -0
  79. package/dist/services/service_registry.d.ts +25 -0
  80. package/dist/services/service_registry.js +51 -0
  81. package/dist/services/service_registry.js.map +1 -0
  82. package/dist/services/service_types.cjs +30 -0
  83. package/dist/services/service_types.cjs.map +1 -0
  84. package/dist/services/service_types.d.ts +45 -0
  85. package/dist/services/service_types.js +28 -0
  86. package/dist/services/service_types.js.map +1 -0
  87. package/package.json +4 -4
  88. package/src/drivers/pframe/data_types.ts +18 -140
  89. package/src/drivers/pframe/driver.ts +11 -1
  90. package/src/drivers/pframe/spec/spec.ts +16 -2
  91. package/src/drivers/pframe/spec_driver.ts +37 -5
  92. package/src/drivers/pframe/table_calculate.ts +3 -4
  93. package/src/errors.ts +53 -0
  94. package/src/flags/block_flags.ts +5 -2
  95. package/src/index.ts +2 -0
  96. package/src/pool_entry.ts +42 -0
  97. package/src/services/index.ts +5 -0
  98. package/src/services/service_capabilities.test.ts +119 -0
  99. package/src/services/service_capabilities.ts +64 -0
  100. package/src/services/service_declarations.ts +25 -0
  101. package/src/services/service_injector_factory.ts +27 -0
  102. package/src/services/service_injectors.ts +79 -0
  103. package/src/services/service_registry.test.ts +69 -0
  104. package/src/services/service_registry.ts +94 -0
  105. package/src/services/service_types.ts +114 -0
@@ -0,0 +1,64 @@
1
+ import type { ServiceName, ServiceRequireFlags } from "./service_types";
2
+ import { Services } from "./service_declarations";
3
+ import type { SupportedRequirement } from "../flags/flag_utils";
4
+
5
+ /**
6
+ * All service-related `requires*` capability flag names, auto-derived from Services.
7
+ * Single source of truth — use this everywhere runtime capabilities are registered.
8
+ */
9
+ export const SERVICE_CAPABILITY_FLAGS: readonly SupportedRequirement[] = Object.keys(Services).map(
10
+ (key) => `requires${key}` as SupportedRequirement,
11
+ );
12
+
13
+ /**
14
+ * Register all service capability flags with the given callback.
15
+ * Works with both `RuntimeCapabilities.addSupportedRequirement`
16
+ * and `MiddleLayer.addRuntimeCapability`.
17
+ */
18
+ export function registerServiceCapabilities(
19
+ register: (flag: SupportedRequirement, value: true) => void,
20
+ ): void {
21
+ for (const flag of SERVICE_CAPABILITY_FLAGS) {
22
+ register(flag, true);
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Resolve which services are required by the given feature flags.
28
+ * Accepts Record<string, unknown> so it works with both BlockCodeKnownFeatureFlags
29
+ * (from middle layer) and Zod-parsed records (from preload).
30
+ */
31
+ export function resolveRequiredServices(flags: Record<string, unknown> | undefined): ServiceName[] {
32
+ if (!flags) return [];
33
+ return (Object.keys(Services) as (keyof typeof Services)[])
34
+ .filter((key) => flags[`requires${key}`] === true)
35
+ .map((key) => Services[key]);
36
+ }
37
+
38
+ export type KnownServiceName = (typeof Services)[keyof typeof Services] & string;
39
+
40
+ export function isKnownServiceName(name: string): name is KnownServiceName {
41
+ return Object.values(Services).some((v) => v === name);
42
+ }
43
+
44
+ /** All service require flags set to true, auto-generated from Services.
45
+ * Used to distinguish service-related feature flags from non-service flags. */
46
+ export const SERVICE_FEATURE_FLAGS: { readonly [K in keyof ServiceRequireFlags]-?: true } =
47
+ Object.fromEntries(Object.keys(Services).map((key) => [`requires${key}`, true])) as any;
48
+
49
+ /** Introspect method names on an instance (including prototype chain).
50
+ * Uses Object.getOwnPropertyDescriptor to avoid triggering getters. */
51
+ export function getMethodNames<T extends object>(instance: T): string[] {
52
+ const methods = new Set<string>();
53
+ let proto: object | null = instance;
54
+ while (proto && proto !== Object.prototype) {
55
+ for (const key of Object.getOwnPropertyNames(proto)) {
56
+ const descriptor = Object.getOwnPropertyDescriptor(proto, key);
57
+ if (key !== "constructor" && typeof descriptor?.value === "function") {
58
+ methods.add(key);
59
+ }
60
+ }
61
+ proto = Object.getPrototypeOf(proto);
62
+ }
63
+ return [...methods];
64
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Service declarations — add new services here.
3
+ *
4
+ * After adding a service, fix type errors in these files:
5
+ *
6
+ * Model side:
7
+ * - lib/node/pl-middle-layer/src/js_render/service_injectors.ts — VM bridge for workflow scripts
8
+ * - lib/node/pl-middle-layer/src/middle_layer/middle_layer.ts — ModelServiceRegistry factory
9
+ *
10
+ * UI side:
11
+ * - lib/model/common/src/services/service_injector_factory.ts — driver method wrappers (node services only)
12
+ * - sdk/ui-vue/src/internal/createAppV3.ts — UiServiceRegistry factory
13
+ *
14
+ * Optional:
15
+ * - sdk/model/src/services/block_service_flags.ts — only if default-required by all blocks
16
+ */
17
+
18
+ import type { PFrameDriver, PFrameModelDriver } from "../drivers/pframe/driver";
19
+ import type { PFrameSpecDriver } from "../drivers/pframe/spec_driver";
20
+ import { service } from "./service_types";
21
+
22
+ export const Services = {
23
+ PFrameSpec: service<PFrameSpecDriver, PFrameSpecDriver>()({ type: "wasm", name: "pframeSpec" }),
24
+ PFrame: service<PFrameModelDriver, PFrameDriver>()({ type: "node", name: "pframe" }),
25
+ };
@@ -0,0 +1,27 @@
1
+ /**
2
+ * UI service injector factory.
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.
7
+ */
8
+
9
+ import type { DriverKit } from "../driver_kit";
10
+ import type { UiServiceInjectorMap } from "./service_injectors";
11
+
12
+ export function createUiServiceInjectors(driverKit: DriverKit): UiServiceInjectorMap {
13
+ const { pFrameDriver } = driverKit;
14
+ return {
15
+ PFrame: {
16
+ findColumns: (handle, request) => pFrameDriver.findColumns(handle, request),
17
+ getColumnSpec: (handle, columnId) => pFrameDriver.getColumnSpec(handle, columnId),
18
+ listColumns: (handle) => pFrameDriver.listColumns(handle),
19
+ calculateTableData: (handle, request, range) =>
20
+ pFrameDriver.calculateTableData(handle, request, range),
21
+ getUniqueValues: (handle, request) => pFrameDriver.getUniqueValues(handle, request),
22
+ getShape: (handle) => pFrameDriver.getShape(handle),
23
+ getSpec: (handle) => pFrameDriver.getSpec(handle),
24
+ getData: (handle, columnIndices, range) => pFrameDriver.getData(handle, columnIndices, range),
25
+ },
26
+ };
27
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * UI service injectors — auto-derived types, method map, and service info builder.
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.
6
+ */
7
+
8
+ import type { InferServiceKind, InferServiceUi, ServiceBrand, ServiceName } from "./service_types";
9
+ import type { DriverKit } from "../driver_kit";
10
+ import { Services } from "./service_declarations";
11
+ import { getMethodNames, resolveRequiredServices } from "./service_capabilities";
12
+ import { createUiServiceInjectors } from "./service_injector_factory";
13
+
14
+ export { createUiServiceInjectors } from "./service_injector_factory";
15
+
16
+ type NodeServiceKeys = {
17
+ [K in keyof typeof Services]: InferServiceKind<ServiceBrand<(typeof Services)[K]>> extends "node"
18
+ ? K
19
+ : never;
20
+ }[keyof typeof Services];
21
+
22
+ /** Auto-derived map of node service keys to their UI-side driver interfaces. */
23
+ export type UiServiceInjectorMap = {
24
+ [K in NodeServiceKeys]: InferServiceUi<ServiceBrand<(typeof Services)[K]>>;
25
+ };
26
+
27
+ let cachedKit: DriverKit | undefined;
28
+ let cachedInjectors: UiServiceInjectorMap | undefined;
29
+
30
+ function getOrCreateInjectors(driverKit: DriverKit): UiServiceInjectorMap {
31
+ if (!cachedInjectors || cachedKit !== driverKit) {
32
+ cachedKit = driverKit;
33
+ cachedInjectors = createUiServiceInjectors(driverKit);
34
+ }
35
+ return cachedInjectors;
36
+ }
37
+
38
+ /** Resolve the injector for a given service ID. Caches injectors per DriverKit reference. */
39
+ export function resolveUiInjector(
40
+ driverKit: DriverKit,
41
+ serviceId: ServiceName,
42
+ ): UiServiceInjectorMap[keyof UiServiceInjectorMap] | null {
43
+ const injectors = getOrCreateInjectors(driverKit);
44
+ const key = Object.keys(Services).find(
45
+ (k) => Services[k as keyof typeof Services] === serviceId,
46
+ ) as NodeServiceKeys | undefined;
47
+ if (!key) return null;
48
+ return injectors[key];
49
+ }
50
+
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);
62
+ 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
+ }
68
+ return result;
69
+ })();
70
+
71
+ /** Build service info for a block from its feature flags. */
72
+ export function buildServiceInfo(
73
+ featureFlags: Record<string, unknown>,
74
+ ): Record<ServiceName, string[]> {
75
+ const serviceIds = resolveRequiredServices(featureFlags);
76
+ return Object.fromEntries(
77
+ serviceIds.map((id) => [id, SERVICE_METHOD_MAP[id as string] ?? []]),
78
+ ) as Record<ServiceName, string[]>;
79
+ }
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { ModelServiceRegistry, UiServiceRegistry } from "./service_registry";
3
+ import { service } from "./service_types";
4
+
5
+ // Create test services with distinct types
6
+ const TestServices = {
7
+ Alpha: service<{ alphaModel: () => void }, { alphaUi: () => void }>()({
8
+ type: "wasm",
9
+ name: "alpha",
10
+ }),
11
+ Beta: service<{ betaModel: () => void }, { betaUi: () => void }>()({
12
+ type: "node",
13
+ name: "beta",
14
+ }),
15
+ };
16
+
17
+ describe("ModelServiceRegistry", () => {
18
+ it("should lazily instantiate service on first get()", () => {
19
+ const factory = vi.fn(() => ({ alphaModel: () => {} }));
20
+ const registry = new ModelServiceRegistry(TestServices, {
21
+ Alpha: factory,
22
+ Beta: null,
23
+ });
24
+ expect(factory).not.toHaveBeenCalled();
25
+ registry.get(TestServices.Alpha);
26
+ expect(factory).toHaveBeenCalledTimes(1);
27
+ });
28
+
29
+ it("should cache instance on subsequent get() calls", () => {
30
+ const factory = vi.fn(() => ({ alphaModel: () => {} }));
31
+ const registry = new ModelServiceRegistry(TestServices, {
32
+ Alpha: factory,
33
+ Beta: null,
34
+ });
35
+ const first = registry.get(TestServices.Alpha);
36
+ const second = registry.get(TestServices.Alpha);
37
+ expect(first).toBe(second);
38
+ expect(factory).toHaveBeenCalledTimes(1);
39
+ });
40
+
41
+ it("should return null for services with null factory", () => {
42
+ const registry = new ModelServiceRegistry(TestServices, {
43
+ Alpha: () => ({ alphaModel: () => {} }),
44
+ Beta: null,
45
+ });
46
+ expect(registry.get(TestServices.Beta)).toBeNull();
47
+ });
48
+
49
+ it("should throw for unknown service IDs", () => {
50
+ const registry = new ModelServiceRegistry(TestServices, {
51
+ Alpha: () => ({ alphaModel: () => {} }),
52
+ Beta: null,
53
+ });
54
+ expect(() => registry.get("unknown" as any)).toThrow(/not registered/);
55
+ });
56
+ });
57
+
58
+ describe("UiServiceRegistry", () => {
59
+ it("should lazily instantiate UI service", () => {
60
+ const factory = vi.fn(() => ({ alphaUi: () => {} }));
61
+ const registry = new UiServiceRegistry(TestServices, {
62
+ Alpha: factory,
63
+ Beta: null,
64
+ });
65
+ expect(factory).not.toHaveBeenCalled();
66
+ registry.get(TestServices.Alpha);
67
+ expect(factory).toHaveBeenCalledTimes(1);
68
+ });
69
+ });
@@ -0,0 +1,94 @@
1
+ import type {
2
+ ServiceTypesLike,
3
+ InferServiceModel,
4
+ InferServiceUi,
5
+ ServiceName,
6
+ ModelServiceFactoryMap,
7
+ UiServiceFactoryMap,
8
+ } from "./service_types";
9
+ import { ServiceNotRegisteredError } from "../errors";
10
+ import type { Services } from "./service_declarations";
11
+
12
+ type RegistryName = "ModelServiceRegistry" | "UiServiceRegistry";
13
+ type ServiceFactory = () => Record<string, Function>;
14
+
15
+ class ServiceRegistryBase {
16
+ private readonly registryName: RegistryName;
17
+ private readonly knownServices = new Set<ServiceName>();
18
+ private readonly factories = new Map<ServiceName, ServiceFactory>();
19
+ private readonly instances = new Map<ServiceName, Record<string, Function>>();
20
+
21
+ protected constructor(
22
+ registryName: RegistryName,
23
+ serviceMap: Record<string, ServiceName>,
24
+ factories: Record<string, (() => unknown) | null>,
25
+ ) {
26
+ this.registryName = registryName;
27
+ for (const [key, factory] of Object.entries(factories)) {
28
+ const serviceId = serviceMap[key];
29
+ this.knownServices.add(serviceId);
30
+ if (factory !== null) {
31
+ this.factories.set(serviceId, factory as ServiceFactory);
32
+ }
33
+ }
34
+ }
35
+
36
+ async dispose(): Promise<void> {
37
+ for (const instance of this.instances.values()) {
38
+ if (Symbol.asyncDispose in instance) {
39
+ await (instance as AsyncDisposable)[Symbol.asyncDispose]();
40
+ } else if (Symbol.dispose in instance) {
41
+ (instance as Disposable)[Symbol.dispose]();
42
+ }
43
+ }
44
+ this.instances.clear();
45
+ }
46
+
47
+ protected getById(serviceId: ServiceName): Record<string, Function> | null {
48
+ if (!this.knownServices.has(serviceId)) {
49
+ throw new ServiceNotRegisteredError(
50
+ `Service "${serviceId}" is not registered in ${this.registryName}. Add it to the factory map.`,
51
+ );
52
+ }
53
+ if (this.instances.has(serviceId)) return this.instances.get(serviceId)!;
54
+
55
+ const factory = this.factories.get(serviceId);
56
+ if (!factory) return null;
57
+
58
+ const instance = factory();
59
+ this.instances.set(serviceId, instance);
60
+ return instance;
61
+ }
62
+ }
63
+
64
+ export class ModelServiceRegistry<
65
+ SMap extends Record<string, ServiceName> = typeof Services,
66
+ > extends ServiceRegistryBase {
67
+ constructor(serviceMap: SMap, factories: ModelServiceFactoryMap<SMap>) {
68
+ super("ModelServiceRegistry", serviceMap, factories);
69
+ }
70
+
71
+ get<S extends ServiceTypesLike>(
72
+ id: ServiceName<S>,
73
+ ):
74
+ | ([unknown] extends [InferServiceModel<S>] ? Record<string, Function> : InferServiceModel<S>)
75
+ | null;
76
+ get(id: ServiceName): Record<string, Function> | null {
77
+ return this.getById(id);
78
+ }
79
+ }
80
+
81
+ export class UiServiceRegistry<
82
+ SMap extends Record<string, ServiceName> = typeof Services,
83
+ > extends ServiceRegistryBase {
84
+ constructor(serviceMap: SMap, factories: UiServiceFactoryMap<SMap>) {
85
+ super("UiServiceRegistry", serviceMap, factories);
86
+ }
87
+
88
+ get<S extends ServiceTypesLike>(
89
+ id: ServiceName<S>,
90
+ ): ([unknown] extends [InferServiceUi<S>] ? Record<string, Function> : InferServiceUi<S>) | null;
91
+ get(id: ServiceName): Record<string, Function> | null {
92
+ return this.getById(id);
93
+ }
94
+ }
@@ -0,0 +1,114 @@
1
+ import type { Branded } from "../branding";
2
+ import { ServiceAlreadyRegisteredError, ServiceInvalidIdError } from "../errors";
3
+ import type { Services } from "./service_declarations";
4
+
5
+ export type ServiceTypesLike<
6
+ Model = unknown,
7
+ Ui = unknown,
8
+ Kind extends ServiceType = ServiceType,
9
+ > = {
10
+ readonly __types?: { model: Model; ui: Ui; kind: Kind };
11
+ };
12
+
13
+ export type InferServiceModel<S extends ServiceTypesLike> =
14
+ S extends ServiceTypesLike<infer M, unknown, ServiceType> ? M : unknown;
15
+
16
+ export type InferServiceUi<S extends ServiceTypesLike> =
17
+ S extends ServiceTypesLike<unknown, infer U, ServiceType> ? U : unknown;
18
+
19
+ export type InferServiceKind<S extends ServiceTypesLike> =
20
+ S extends ServiceTypesLike<unknown, unknown, infer K> ? K : ServiceType;
21
+
22
+ export type ServiceName<S extends ServiceTypesLike = ServiceTypesLike> = Branded<string, S>;
23
+
24
+ export type ServiceType = "node" | "wasm";
25
+
26
+ const SERVICE_ID_PATTERN = /^[a-zA-Z][a-zA-Z0-9]*$/;
27
+
28
+ export const { service, isNodeService } = (() => {
29
+ const typeMap = new Map<string, ServiceType>();
30
+ return {
31
+ service<Model, Ui>() {
32
+ return <K extends ServiceType, N extends string>(options: {
33
+ readonly type: K;
34
+ readonly name: N;
35
+ }): Branded<N, ServiceTypesLike<Model, Ui, K>> => {
36
+ const { name, type } = options;
37
+ if (!SERVICE_ID_PATTERN.test(name)) {
38
+ throw new ServiceInvalidIdError(
39
+ `Invalid service ID "${name}": must match ${SERVICE_ID_PATTERN}`,
40
+ );
41
+ }
42
+ if (typeMap.has(name)) {
43
+ throw new ServiceAlreadyRegisteredError(`Service "${name}" already registered`);
44
+ }
45
+ typeMap.set(name, type);
46
+ return name as Branded<N, ServiceTypesLike<Model, Ui, K>>;
47
+ };
48
+ },
49
+ isNodeService(id: ServiceName): boolean {
50
+ return typeMap.get(id) === "node";
51
+ },
52
+ };
53
+ })();
54
+
55
+ export function serviceFnKey(serviceId: string, method = ""): string {
56
+ return `service:${serviceId}:${method}`;
57
+ }
58
+
59
+ export type ServiceBrand<T> =
60
+ T extends Branded<string, infer S extends ServiceTypesLike> ? S : never;
61
+
62
+ export type ModelServiceFactoryMap<SMap extends Record<string, ServiceName>> = {
63
+ [K in keyof SMap]: (() => InferServiceModel<ServiceBrand<SMap[K]>>) | null;
64
+ };
65
+
66
+ export type UiServiceFactoryMap<SMap extends Record<string, ServiceName>> = {
67
+ [K in keyof SMap]: (() => InferServiceUi<ServiceBrand<SMap[K]>>) | null;
68
+ };
69
+
70
+ /** Contract between any service provider and any service consumer. */
71
+ export interface ServiceDispatch {
72
+ getServiceNames(): ServiceName[];
73
+ getServiceMethods(serviceId: ServiceName): string[];
74
+ callServiceMethod(serviceId: ServiceName, method: string, ...args: unknown[]): unknown;
75
+ }
76
+
77
+ // Auto-derived types from the Services const in service_declarations.ts.
78
+ // Adding a service to Services automatically updates all of these.
79
+
80
+ type SMap = typeof Services;
81
+
82
+ type ExtractServiceName<T> = T extends Branded<infer N extends string, any> ? N : never;
83
+
84
+ /** Model-side service interfaces keyed by service name literal. */
85
+ export type ModelServices = {
86
+ [K in keyof SMap as ExtractServiceName<SMap[K]>]: InferServiceModel<ServiceBrand<SMap[K]>>;
87
+ };
88
+
89
+ /** UI-side service interfaces keyed by service name literal. */
90
+ export type UiServices = {
91
+ [K in keyof SMap as ExtractServiceName<SMap[K]>]: InferServiceUi<ServiceBrand<SMap[K]>>;
92
+ };
93
+
94
+ /** Map from Services keys to their unbranded string name literals. */
95
+ export type ServiceNameLiterals = {
96
+ [K in keyof SMap]: ExtractServiceName<SMap[K]>;
97
+ };
98
+
99
+ /** Auto-derived requires* feature flags from Services keys. */
100
+ export type ServiceRequireFlags = {
101
+ [K in keyof SMap as `requires${K & string}`]?: boolean;
102
+ };
103
+
104
+ type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (
105
+ k: infer I,
106
+ ) => void
107
+ ? I
108
+ : never;
109
+
110
+ export type RequireServices<T extends ServiceName> = UnionToIntersection<
111
+ T extends Branded<infer N extends string, infer S extends ServiceTypesLike>
112
+ ? Record<N, InferServiceModel<S>>
113
+ : never
114
+ >;