@milaboratories/pl-model-common 1.29.0 → 1.30.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.
- package/dist/bmodel/block_config.d.ts +1 -0
- package/dist/bmodel/code.d.ts +1 -0
- package/dist/driver_kit.d.ts +1 -1
- package/dist/drivers/index.d.ts +6 -6
- package/dist/drivers/index.js +1 -1
- package/dist/drivers/pframe/driver.cjs.map +1 -1
- package/dist/drivers/pframe/driver.d.ts +13 -5
- package/dist/drivers/pframe/driver.js.map +1 -1
- package/dist/drivers/pframe/index.d.ts +5 -5
- package/dist/drivers/pframe/index.js +1 -1
- package/dist/drivers/pframe/pframe.d.ts +2 -2
- package/dist/drivers/pframe/query/query_common.d.ts +2 -2
- package/dist/drivers/pframe/query/query_data.d.ts +1 -1
- package/dist/drivers/pframe/query/query_spec.d.ts +2 -2
- package/dist/drivers/pframe/spec/ids.d.ts +2 -2
- package/dist/drivers/pframe/spec/index.d.ts +1 -1
- package/dist/drivers/pframe/spec/index.js +1 -1
- package/dist/drivers/pframe/spec/selectors.d.ts +1 -1
- package/dist/drivers/pframe/spec/spec.cjs +14 -1
- package/dist/drivers/pframe/spec/spec.cjs.map +1 -1
- package/dist/drivers/pframe/spec/spec.d.ts +7 -1
- package/dist/drivers/pframe/spec/spec.js +14 -2
- package/dist/drivers/pframe/spec/spec.js.map +1 -1
- package/dist/drivers/pframe/spec_driver.d.ts +36 -6
- package/dist/drivers/pframe/table_calculate.d.ts +2 -2
- package/dist/drivers/pframe/table_common.d.ts +1 -1
- package/dist/drivers/pframe/unique_values.d.ts +2 -2
- package/dist/errors.cjs +48 -0
- package/dist/errors.cjs.map +1 -1
- package/dist/errors.d.ts +25 -1
- package/dist/errors.js +37 -1
- package/dist/errors.js.map +1 -1
- package/dist/flags/block_flags.cjs.map +1 -1
- package/dist/flags/block_flags.d.ts +4 -1
- package/dist/flags/block_flags.js.map +1 -1
- package/dist/flags/flag_utils.d.ts +1 -1
- package/dist/flags/index.d.ts +3 -0
- package/dist/index.cjs +38 -0
- package/dist/index.d.ts +30 -21
- package/dist/index.js +11 -3
- package/dist/pool/query.d.ts +1 -1
- package/dist/pool_entry.cjs +30 -0
- package/dist/pool_entry.cjs.map +1 -0
- package/dist/pool_entry.d.ts +30 -0
- package/dist/pool_entry.js +29 -0
- package/dist/pool_entry.js.map +1 -0
- package/dist/services/index.cjs +6 -0
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.js +6 -0
- package/dist/services/service_capabilities.cjs +51 -0
- package/dist/services/service_capabilities.cjs.map +1 -0
- package/dist/services/service_capabilities.d.ts +33 -0
- package/dist/services/service_capabilities.js +46 -0
- package/dist/services/service_capabilities.js.map +1 -0
- package/dist/services/service_declarations.cjs +17 -0
- package/dist/services/service_declarations.cjs.map +1 -0
- package/dist/services/service_declarations.d.ts +16 -0
- package/dist/services/service_declarations.js +17 -0
- package/dist/services/service_declarations.js.map +1 -0
- package/dist/services/service_injector_factory.cjs +19 -0
- package/dist/services/service_injector_factory.cjs.map +1 -0
- package/dist/services/service_injector_factory.d.ts +8 -0
- package/dist/services/service_injector_factory.js +18 -0
- package/dist/services/service_injector_factory.js.map +1 -0
- package/dist/services/service_injectors.cjs +48 -0
- package/dist/services/service_injectors.cjs.map +1 -0
- package/dist/services/service_injectors.d.ts +23 -0
- package/dist/services/service_injectors.js +46 -0
- package/dist/services/service_injectors.js.map +1 -0
- package/dist/services/service_registry.cjs +52 -0
- package/dist/services/service_registry.cjs.map +1 -0
- package/dist/services/service_registry.d.ts +25 -0
- package/dist/services/service_registry.js +51 -0
- package/dist/services/service_registry.js.map +1 -0
- package/dist/services/service_types.cjs +30 -0
- package/dist/services/service_types.cjs.map +1 -0
- package/dist/services/service_types.d.ts +45 -0
- package/dist/services/service_types.js +28 -0
- package/dist/services/service_types.js.map +1 -0
- package/package.json +3 -3
- package/src/drivers/pframe/driver.ts +11 -1
- package/src/drivers/pframe/spec/spec.ts +16 -2
- package/src/drivers/pframe/spec_driver.ts +37 -5
- package/src/errors.ts +53 -0
- package/src/flags/block_flags.ts +5 -2
- package/src/index.ts +2 -0
- package/src/pool_entry.ts +42 -0
- package/src/services/index.ts +5 -0
- package/src/services/service_capabilities.test.ts +119 -0
- package/src/services/service_capabilities.ts +64 -0
- package/src/services/service_declarations.ts +25 -0
- package/src/services/service_injector_factory.ts +27 -0
- package/src/services/service_injectors.ts +79 -0
- package/src/services/service_registry.test.ts +69 -0
- package/src/services/service_registry.ts +94 -0
- package/src/services/service_types.ts +114 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Branded } from "@milaboratories/helpers";
|
|
2
|
+
import type { PoolEntry } from "../../pool_entry";
|
|
2
3
|
import type {
|
|
3
4
|
AxisSpec,
|
|
4
5
|
AxesSpec,
|
|
@@ -10,6 +11,7 @@ import type {
|
|
|
10
11
|
ColumnValueType,
|
|
11
12
|
} from "./spec";
|
|
12
13
|
import type { PTableColumnId, PTableColumnSpec } from "./table_common";
|
|
14
|
+
import { DataQuery, SpecQuery } from "./query";
|
|
13
15
|
|
|
14
16
|
/** Matches a string value either exactly or by regex pattern */
|
|
15
17
|
export type StringMatcher =
|
|
@@ -144,6 +146,33 @@ export interface DiscoverColumnsResponse {
|
|
|
144
146
|
hits: DiscoverColumnsResponseHit[];
|
|
145
147
|
}
|
|
146
148
|
|
|
149
|
+
/** Request for deleting an entry from a given axes integration */
|
|
150
|
+
export interface DeleteColumnRequest {
|
|
151
|
+
/** Already integrated axes with qualifications */
|
|
152
|
+
axes: ColumnAxesWithQualifications[];
|
|
153
|
+
/** Zero based index of the entry to be deleted */
|
|
154
|
+
delete: number;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Response from delete column */
|
|
158
|
+
export interface DeleteColumnResponse {
|
|
159
|
+
axes: ColumnAxesWithQualifications[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Response from evaluating a query against a PFrame. */
|
|
163
|
+
export type EvaluateQueryResponse = {
|
|
164
|
+
/**
|
|
165
|
+
* The table specification describing the structure of the query result,
|
|
166
|
+
* including all axes and columns that will be present in the output.
|
|
167
|
+
*/
|
|
168
|
+
tableSpec: PTableColumnSpec[];
|
|
169
|
+
/**
|
|
170
|
+
* The data layer query representation with numeric indices,
|
|
171
|
+
* suitable for execution by the data processing engine.
|
|
172
|
+
*/
|
|
173
|
+
dataQuery: DataQuery;
|
|
174
|
+
};
|
|
175
|
+
|
|
147
176
|
/** Handle to a spec-only PFrame (no data, synchronous operations). */
|
|
148
177
|
export type SpecFrameHandle = Branded<string, "SpecFrameHandle">;
|
|
149
178
|
|
|
@@ -155,17 +184,20 @@ export type SpecFrameHandle = Branded<string, "SpecFrameHandle">;
|
|
|
155
184
|
* because the underlying WASM PFrame computes results immediately.
|
|
156
185
|
*/
|
|
157
186
|
export interface PFrameSpecDriver {
|
|
158
|
-
/** Create a spec-only PFrame from column specs. Returns a handle. */
|
|
159
|
-
createSpecFrame(specs: Record<string, PColumnSpec>): SpecFrameHandle
|
|
187
|
+
/** Create a spec-only PFrame from column specs. Returns a pool entry with handle and unref. */
|
|
188
|
+
createSpecFrame(specs: Record<string, PColumnSpec>): PoolEntry<SpecFrameHandle>;
|
|
160
189
|
|
|
161
190
|
/** Discover columns compatible with given axes integration. */
|
|
162
|
-
|
|
191
|
+
discoverColumns(
|
|
163
192
|
handle: SpecFrameHandle,
|
|
164
193
|
request: DiscoverColumnsRequest,
|
|
165
194
|
): DiscoverColumnsResponse;
|
|
166
195
|
|
|
167
|
-
/**
|
|
168
|
-
|
|
196
|
+
/** Delete an entry from a given axes integration */
|
|
197
|
+
deleteColumn(handle: SpecFrameHandle, request: DeleteColumnRequest): DeleteColumnResponse;
|
|
198
|
+
|
|
199
|
+
/** Evaluates a query specification against this PFrame */
|
|
200
|
+
evaluateQuery(handle: SpecFrameHandle, request: SpecQuery): EvaluateQueryResponse;
|
|
169
201
|
|
|
170
202
|
/** Expand index-based parentAxes in AxesSpec to resolved AxisId parents in AxesId. */
|
|
171
203
|
expandAxes(spec: AxesSpec): AxesId;
|
package/src/errors.ts
CHANGED
|
@@ -28,6 +28,59 @@ export function isAggregateError(error: unknown): error is AggregateError {
|
|
|
28
28
|
return error instanceof Error && error.name === "AggregateError";
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
export class ServiceError extends Error {
|
|
32
|
+
name = "ServiceError";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isServiceError(error: unknown): error is ServiceError {
|
|
36
|
+
return (
|
|
37
|
+
error instanceof Error &&
|
|
38
|
+
(error.name === "ServiceError" || error.name.startsWith("ServiceError."))
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class ServiceInvalidIdError extends ServiceError {
|
|
43
|
+
name = "ServiceError.InvalidId";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function isServiceInvalidIdError(error: unknown): error is ServiceInvalidIdError {
|
|
47
|
+
return error instanceof Error && error.name === "ServiceError.InvalidId";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class ServiceAlreadyRegisteredError extends ServiceError {
|
|
51
|
+
name = "ServiceError.AlreadyRegistered";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function isServiceAlreadyRegisteredError(
|
|
55
|
+
error: unknown,
|
|
56
|
+
): error is ServiceAlreadyRegisteredError {
|
|
57
|
+
return error instanceof Error && error.name === "ServiceError.AlreadyRegistered";
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export class ServiceInjectionError extends ServiceError {
|
|
61
|
+
name = "ServiceError.Injection";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function isServiceInjectionError(error: unknown): error is ServiceInjectionError {
|
|
65
|
+
return error instanceof Error && error.name === "ServiceError.Injection";
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class ServiceNotRegisteredError extends ServiceError {
|
|
69
|
+
name = "ServiceError.NotRegistered";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function isServiceNotRegisteredError(error: unknown): error is ServiceNotRegisteredError {
|
|
73
|
+
return error instanceof Error && error.name === "ServiceError.NotRegistered";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class ServiceMethodNotFoundError extends ServiceError {
|
|
77
|
+
name = "ServiceError.MethodNotFound";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isServiceMethodNotFoundError(error: unknown): error is ServiceMethodNotFoundError {
|
|
81
|
+
return error instanceof Error && error.name === "ServiceError.MethodNotFound";
|
|
82
|
+
}
|
|
83
|
+
|
|
31
84
|
export class PFrameError extends Error {
|
|
32
85
|
name = "PFrameError";
|
|
33
86
|
}
|
package/src/flags/block_flags.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ServiceRequireFlags } from "../services";
|
|
1
2
|
import type { ArrayTypeUnion, Assert, Is, IsSubtypeOf } from "./type_utils";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -20,7 +21,7 @@ export type BlockCodeKnownFeatureFlags = {
|
|
|
20
21
|
readonly requiresModelAPIVersion?: number;
|
|
21
22
|
readonly requiresUIAPIVersion?: number;
|
|
22
23
|
readonly requiresCreatePTable?: number;
|
|
23
|
-
};
|
|
24
|
+
} & ServiceRequireFlags;
|
|
24
25
|
|
|
25
26
|
export const AllSupportsFeatureFlags = ["supportsLazyState", "supportsPframeQueryRanking"] as const;
|
|
26
27
|
|
|
@@ -42,9 +43,11 @@ type _KnownFlagsAreValidFlags = Assert<
|
|
|
42
43
|
|
|
43
44
|
// This check ensures that all keys in BlockConfigV3FeatureFlags are covered in the arrays above.
|
|
44
45
|
// It will produce a compile-time error if there's a mismatch.
|
|
46
|
+
// Adding a service to Services automatically satisfies this assertion via ServiceRequireFlags.
|
|
45
47
|
type _AllFlagsAreCovered = Assert<
|
|
46
48
|
Is<
|
|
47
49
|
keyof BlockCodeKnownFeatureFlags,
|
|
48
|
-
ArrayTypeUnion<typeof AllRequiresFeatureFlags, typeof AllSupportsFeatureFlags>
|
|
50
|
+
| ArrayTypeUnion<typeof AllRequiresFeatureFlags, typeof AllSupportsFeatureFlags>
|
|
51
|
+
| keyof ServiceRequireFlags
|
|
49
52
|
>
|
|
50
53
|
>;
|
package/src/index.ts
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface PoolEntry<K extends string = string, R extends {} = {}> extends Disposable {
|
|
2
|
+
/** Resource key, calculated using provided `calculateParamsKey` function */
|
|
3
|
+
readonly key: K;
|
|
4
|
+
|
|
5
|
+
/** Resource itself created by `createNewResource` function */
|
|
6
|
+
readonly resource: R;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Release the reference. Idempotent.
|
|
10
|
+
* Same as `[Symbol.dispose]()` — provided as a named function
|
|
11
|
+
* for use in callbacks (e.g. `addOnDestroy(entry.unref)`).
|
|
12
|
+
*/
|
|
13
|
+
readonly unref: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Wraps a PoolEntry for use with `using`. Auto-calls `unref()` at end of scope
|
|
18
|
+
* unless `keep()` is called to transfer ownership to the caller.
|
|
19
|
+
*/
|
|
20
|
+
export class PoolEntryGuard<K extends string = string, R extends {} = {}> implements Disposable {
|
|
21
|
+
private kept = false;
|
|
22
|
+
|
|
23
|
+
constructor(readonly entry: PoolEntry<K, R>) {}
|
|
24
|
+
|
|
25
|
+
get key(): K {
|
|
26
|
+
return this.entry.key;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get resource(): R {
|
|
30
|
+
return this.entry.resource;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Disarm the guard — caller takes ownership of the entry. */
|
|
34
|
+
keep(): PoolEntry<K, R> {
|
|
35
|
+
this.kept = true;
|
|
36
|
+
return this.entry;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
[Symbol.dispose](): void {
|
|
40
|
+
if (!this.kept) this.entry.unref();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
SERVICE_CAPABILITY_FLAGS,
|
|
4
|
+
registerServiceCapabilities,
|
|
5
|
+
resolveRequiredServices,
|
|
6
|
+
getMethodNames,
|
|
7
|
+
isKnownServiceName,
|
|
8
|
+
} from "./service_capabilities";
|
|
9
|
+
import { Services } from "./service_declarations";
|
|
10
|
+
|
|
11
|
+
describe("SERVICE_CAPABILITY_FLAGS", () => {
|
|
12
|
+
it("should have one flag per service", () => {
|
|
13
|
+
expect(SERVICE_CAPABILITY_FLAGS).toHaveLength(Object.keys(Services).length);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("should derive requires* flag names from Services keys", () => {
|
|
17
|
+
for (const key of Object.keys(Services)) {
|
|
18
|
+
expect(SERVICE_CAPABILITY_FLAGS).toContain(`requires${key}`);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe("registerServiceCapabilities", () => {
|
|
24
|
+
it("should call register once per service with (flag, true)", () => {
|
|
25
|
+
const register = vi.fn();
|
|
26
|
+
registerServiceCapabilities(register);
|
|
27
|
+
expect(register).toHaveBeenCalledTimes(Object.keys(Services).length);
|
|
28
|
+
for (const call of register.mock.calls) {
|
|
29
|
+
expect(call[0]).toMatch(/^requires[A-Z]/);
|
|
30
|
+
expect(call[1]).toBe(true);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("resolveRequiredServices", () => {
|
|
36
|
+
it("should return empty array for undefined flags", () => {
|
|
37
|
+
expect(resolveRequiredServices(undefined)).toEqual([]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should return empty array for flags without requires*", () => {
|
|
41
|
+
expect(resolveRequiredServices({ supportsLazyState: true })).toEqual([]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should return empty array when requires* is false", () => {
|
|
45
|
+
expect(resolveRequiredServices({ requiresPFrameSpec: false })).toEqual([]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("should resolve a single required service", () => {
|
|
49
|
+
const result = resolveRequiredServices({ requiresPFrameSpec: true });
|
|
50
|
+
expect(result).toEqual([Services.PFrameSpec]);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should resolve multiple required services", () => {
|
|
54
|
+
const result = resolveRequiredServices({
|
|
55
|
+
requiresPFrameSpec: true,
|
|
56
|
+
requiresPFrame: true,
|
|
57
|
+
});
|
|
58
|
+
expect(result).toContain(Services.PFrameSpec);
|
|
59
|
+
expect(result).toContain(Services.PFrame);
|
|
60
|
+
expect(result).toHaveLength(2);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should ignore non-boolean requires* values", () => {
|
|
64
|
+
const result = resolveRequiredServices({
|
|
65
|
+
requiresPFrameSpec: true,
|
|
66
|
+
requiresModelAPIVersion: 2,
|
|
67
|
+
});
|
|
68
|
+
expect(result).toEqual([Services.PFrameSpec]);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe("isKnownServiceName", () => {
|
|
73
|
+
it("should return true for registered service names", () => {
|
|
74
|
+
for (const id of Object.values(Services)) {
|
|
75
|
+
expect(isKnownServiceName(id as string)).toBe(true);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should return false for unknown names", () => {
|
|
80
|
+
expect(isKnownServiceName("nonexistent")).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe("getMethodNames", () => {
|
|
85
|
+
it("should return own method names", () => {
|
|
86
|
+
const obj = {
|
|
87
|
+
foo() {},
|
|
88
|
+
bar() {},
|
|
89
|
+
baz: 42,
|
|
90
|
+
};
|
|
91
|
+
const names = getMethodNames(obj as any);
|
|
92
|
+
expect(names).toContain("foo");
|
|
93
|
+
expect(names).toContain("bar");
|
|
94
|
+
expect(names).not.toContain("baz");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("should include prototype methods", () => {
|
|
98
|
+
class Base {
|
|
99
|
+
baseMethod() {}
|
|
100
|
+
}
|
|
101
|
+
class Child extends Base {
|
|
102
|
+
childMethod() {}
|
|
103
|
+
}
|
|
104
|
+
const names = getMethodNames(new Child() as any);
|
|
105
|
+
expect(names).toContain("baseMethod");
|
|
106
|
+
expect(names).toContain("childMethod");
|
|
107
|
+
expect(names).not.toContain("constructor");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should not include getters", () => {
|
|
111
|
+
const obj = Object.create(null, {
|
|
112
|
+
method: { value: () => {}, enumerable: true },
|
|
113
|
+
getter: { get: () => 42, enumerable: true },
|
|
114
|
+
});
|
|
115
|
+
const names = getMethodNames(obj);
|
|
116
|
+
expect(names).toContain("method");
|
|
117
|
+
expect(names).not.toContain("getter");
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -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
|
+
});
|