@highstate/contract 0.7.2 → 0.7.4

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.
@@ -0,0 +1,361 @@
1
+ /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment */
4
+ /* eslint-disable @typescript-eslint/no-unsafe-return */
5
+
6
+ import type { Entity } from "./entity"
7
+ import type { ArgumentValue, ArgumentValueSchema, Meta } from "./types"
8
+ import {
9
+ type Static,
10
+ type TObject,
11
+ type TOptional,
12
+ type TSchema,
13
+ OptionalKind,
14
+ } from "@sinclair/typebox"
15
+ import { isNonNullish, mapValues, pickBy } from "remeda"
16
+ import { Ajv } from "ajv"
17
+ import { boundaryInput, type InstanceInput } from "./instance"
18
+ import { type OptionalUndefinedFields, type OptionalEmptyRecords } from "./utils"
19
+ import { registerInstance } from "./evaluation"
20
+
21
+ const ajv = new Ajv()
22
+
23
+ // Argument
24
+ export type ComponentArgument = {
25
+ schema: ArgumentValueSchema
26
+ required: boolean
27
+ meta: Meta
28
+ }
29
+
30
+ type ComponentArgumentFullOptions = Meta & {
31
+ schema: ArgumentValueSchema
32
+ required?: boolean
33
+ }
34
+
35
+ export type ComponentArgumentOptions = ArgumentValueSchema | ComponentArgumentFullOptions
36
+
37
+ export type ComponentArgumentOptionsToSchema<T extends ComponentArgumentOptions> = T extends TSchema
38
+ ? T
39
+ : T["required"] extends false
40
+ ? TOptional<T["schema"]>
41
+ : T["schema"]
42
+
43
+ type ComponentArgumentMapToValue<T extends Record<string, ComponentArgumentOptions>> = {
44
+ [K in keyof T]: Static<ComponentArgumentOptionsToSchema<T[K]>>
45
+ }
46
+
47
+ // Input
48
+ export type ComponentInput = {
49
+ type: string
50
+ required: boolean
51
+ multiple: boolean
52
+ meta: Meta
53
+ }
54
+
55
+ type ComponentInputFullOptions = Meta & {
56
+ entity: Entity
57
+ required?: boolean
58
+ multiple?: boolean
59
+ }
60
+
61
+ export type ComponentInputOptions = Entity | ComponentInputFullOptions
62
+
63
+ type ComponentInputOptionsToOutputRef<T extends ComponentInputOptions> = T extends Entity
64
+ ? InstanceInput<T["type"]>
65
+ : T extends ComponentInputFullOptions
66
+ ? T["required"] extends false
67
+ ? T["multiple"] extends true
68
+ ? InstanceInput<T["entity"]["type"]>[] | undefined
69
+ : InstanceInput<T["entity"]["type"]> | undefined
70
+ : T["multiple"] extends true
71
+ ? InstanceInput<T["entity"]["type"]>[]
72
+ : InstanceInput<T["entity"]["type"]>
73
+ : never
74
+
75
+ export type ComponentInputSpec = [entity: Entity, required: boolean, multiple: boolean]
76
+
77
+ export type ComponentInputOptionsToSpec<T extends ComponentInputOptions> = T extends Entity
78
+ ? [T, true, false] // [Entity, required, multiple]
79
+ : T extends ComponentInputFullOptions
80
+ ? T["required"] extends false
81
+ ? T["multiple"] extends true
82
+ ? [T["entity"], false, true]
83
+ : [T["entity"], false, false]
84
+ : T["multiple"] extends true
85
+ ? [T["entity"], true, true]
86
+ : [T["entity"], true, false]
87
+ : never
88
+
89
+ export type ComponentInputOptionsMapToSpecMap<T extends Record<string, ComponentInputOptions>> =
90
+ T extends Record<string, never>
91
+ ? Record<string, never>
92
+ : { [K in keyof T]: ComponentInputOptionsToSpec<T[K]> }
93
+
94
+ type ComponentInputMapToValue<T extends Record<string, ComponentInputOptions>> =
95
+ OptionalUndefinedFields<{
96
+ [K in keyof T]: ComponentInputOptionsToOutputRef<T[K]>
97
+ }>
98
+
99
+ type ComponentInputMapToReturnType<T extends Record<string, ComponentInputOptions>> =
100
+ T extends Record<string, never> ? void : ComponentInputMapToValue<T>
101
+
102
+ // Params & Options
103
+ export type ComponentParams<
104
+ TArgs extends Record<string, ComponentArgumentOptions>,
105
+ TInputs extends Record<string, ComponentInputOptions>,
106
+ > = {
107
+ id: string
108
+ name: string
109
+ args: ComponentArgumentMapToValue<TArgs>
110
+ inputs: ComponentInputMapToValue<TInputs>
111
+ }
112
+
113
+ export type InputComponentParams<
114
+ TArgs extends Record<string, ArgumentValue>,
115
+ TInputs extends Record<string, unknown>,
116
+ > = {
117
+ name: string
118
+ } & OptionalEmptyRecords<{
119
+ args: TArgs
120
+ inputs: TInputs
121
+ }>
122
+
123
+ export type ComponentOptions<
124
+ TArgs extends Record<string, ComponentArgumentOptions>,
125
+ TInputs extends Record<string, ComponentInputOptions>,
126
+ TOutputs extends Record<string, ComponentInputOptions>,
127
+ > = {
128
+ type: string
129
+ meta?: ComponentMeta
130
+
131
+ args?: TArgs
132
+ inputs?: TInputs
133
+ outputs?: TOutputs
134
+
135
+ create: (params: ComponentParams<TArgs, TInputs>) => ComponentInputMapToReturnType<TOutputs>
136
+ }
137
+
138
+ // Models
139
+ export type ComponentMeta = Meta & {
140
+ primaryIcon?: string
141
+ primaryIconColor?: string
142
+ secondaryIcon?: string
143
+ secondaryIconColor?: string
144
+ category?: string
145
+ defaultNamePrefix?: string
146
+ }
147
+
148
+ export type ComponentModel = {
149
+ /**
150
+ * The type of the component.
151
+ */
152
+ type: string
153
+
154
+ /**
155
+ * The record of the argument schemas.
156
+ */
157
+ args: Record<string, ComponentArgument>
158
+
159
+ /**
160
+ * The record of the input schemas.
161
+ */
162
+ inputs: Record<string, ComponentInput>
163
+
164
+ /**
165
+ * The record of the output schemas.
166
+ */
167
+ outputs: Record<string, ComponentInput>
168
+
169
+ /**
170
+ * The extra metadata of the component.
171
+ */
172
+ meta: ComponentMeta
173
+
174
+ /**
175
+ * The hash of the component definition.
176
+ */
177
+ definitionHash: string
178
+ }
179
+
180
+ type InputSpecToOutputRef<T extends ComponentInputSpec> = T[1] extends true
181
+ ? T[2] extends true
182
+ ? InstanceInput<T[0]["type"]>[]
183
+ : InstanceInput<T[0]["type"]>
184
+ : T[2] extends true
185
+ ? InstanceInput<T[0]["type"]>[] | undefined
186
+ : InstanceInput<T[0]["type"]> | undefined
187
+
188
+ export type OutputRefMap<TInputs extends Record<string, ComponentInputSpec>> =
189
+ TInputs extends Record<string, [string, never, never]>
190
+ ? Record<string, never>
191
+ : { [K in keyof TInputs]: InputSpecToOutputRef<TInputs[K]> }
192
+
193
+ export const originalCreate = Symbol("originalCreate")
194
+
195
+ export type Component<
196
+ TArgs extends Record<string, ArgumentValue> = Record<string, never>,
197
+ TInputs extends Record<string, ComponentInputSpec> = Record<string, never>,
198
+ TOutputs extends Record<string, ComponentInputSpec> = Record<string, never>,
199
+ > = {
200
+ /**
201
+ * The non-generic model of the component.
202
+ */
203
+ model: ComponentModel
204
+
205
+ /**
206
+ * The entities used in the inputs or outputs of the component.
207
+ */
208
+ entities: Map<string, Entity>
209
+
210
+ /**
211
+ * Creates the component at the evaluation time.
212
+ */
213
+ (context: InputComponentParams<TArgs, OutputRefMap<TInputs>>): OutputRefMap<TOutputs>
214
+
215
+ /**
216
+ * The original create function.
217
+ *
218
+ * Used to calculate the definition hash.
219
+ */
220
+ [originalCreate]: (params: InputComponentParams<any, any>) => any
221
+ }
222
+
223
+ export type ArgumentOptionsMapToStatic<T extends Record<string, ComponentArgumentOptions>> =
224
+ T extends Record<string, never>
225
+ ? Record<string, never>
226
+ : Static<TObject<{ [K in keyof T]: ComponentArgumentOptionsToSchema<T[K]> }>>
227
+
228
+ export function defineComponent<
229
+ TArgs extends Record<string, ComponentArgumentOptions> = Record<string, never>,
230
+ TInputs extends Record<string, ComponentInputOptions> = Record<string, never>,
231
+ TOutputs extends Record<string, ComponentInputOptions> = Record<string, never>,
232
+ >(
233
+ options: ComponentOptions<TArgs, TInputs, TOutputs>,
234
+ ): Component<
235
+ ArgumentOptionsMapToStatic<TArgs>,
236
+ ComponentInputOptionsMapToSpecMap<TInputs>,
237
+ ComponentInputOptionsMapToSpecMap<TOutputs>
238
+ > {
239
+ function create(params: InputComponentParams<any, any>): any {
240
+ const { name, args, inputs } = params
241
+ const id = `${options.type}:${name}`
242
+
243
+ validateArgs(id, create.model as ComponentModel, args ?? {})
244
+
245
+ const flatInputs = mapValues(pickBy(inputs ?? {}, isNonNullish), inputs => [inputs].flat(2))
246
+
247
+ return registerInstance(
248
+ create.model as ComponentModel,
249
+ {
250
+ id,
251
+ type: options.type,
252
+ name,
253
+ args: args ?? {},
254
+ inputs: mapValues(flatInputs, inputs => inputs.map(input => input[boundaryInput] ?? input)),
255
+ resolvedInputs: flatInputs,
256
+ },
257
+ () => {
258
+ const markedInputs = mapValues(flatInputs, (inputs, key) => {
259
+ const result = inputs.map(input => ({
260
+ ...input,
261
+ [boundaryInput]: { instanceId: id, output: key },
262
+ }))
263
+
264
+ return (create.model as ComponentModel).inputs?.[key]?.multiple === false
265
+ ? result[0]
266
+ : result
267
+ })
268
+
269
+ const outputs: Record<string, InstanceInput[]> =
270
+ options.create({
271
+ id,
272
+ name,
273
+ args: (args as any) ?? {},
274
+ inputs: markedInputs as any,
275
+ }) ?? {}
276
+
277
+ return mapValues(pickBy(outputs, isNonNullish), outputs => [outputs].flat(2))
278
+ },
279
+ )
280
+ }
281
+
282
+ create.entities = new Map<string, Entity>()
283
+ const mapInput = createInputMapper(create.entities)
284
+
285
+ create.model = {
286
+ type: options.type,
287
+ args: mapValues(options.args ?? {}, mapArgument),
288
+ inputs: mapValues(options.inputs ?? {}, mapInput),
289
+ outputs: mapValues(options.outputs ?? {}, mapInput),
290
+ meta: options.meta ?? {},
291
+ }
292
+
293
+ create[originalCreate] = options.create
294
+
295
+ return create as any
296
+ }
297
+
298
+ export function isComponent(value: unknown): value is Component {
299
+ return typeof value === "function" && "model" in value
300
+ }
301
+
302
+ export function mapArgument(value: ComponentArgumentOptions) {
303
+ if ("schema" in value) {
304
+ return {
305
+ schema: value.schema,
306
+ required: value.required ?? (!value.schema[OptionalKind] && !value.schema.default),
307
+ meta: {
308
+ displayName: value.displayName,
309
+ description: value.description,
310
+ color: value.color,
311
+ },
312
+ }
313
+ }
314
+
315
+ return {
316
+ schema: value,
317
+ required: !value[OptionalKind] && !value.default,
318
+ meta: {},
319
+ }
320
+ }
321
+
322
+ export function createInputMapper(entities: Map<string, Entity>) {
323
+ return (value: ComponentInputOptions) => {
324
+ if ("entity" in value) {
325
+ entities.set(value.entity.type, value.entity)
326
+
327
+ return {
328
+ type: value.entity.type,
329
+ required: value.required ?? true,
330
+ multiple: value.multiple ?? false,
331
+ meta: {
332
+ displayName: value.displayName,
333
+ description: value.description,
334
+ color: value.color,
335
+ },
336
+ }
337
+ }
338
+
339
+ entities.set(value.type, value)
340
+
341
+ return {
342
+ type: value.type,
343
+ required: true,
344
+ multiple: false,
345
+ meta: {},
346
+ }
347
+ }
348
+ }
349
+
350
+ function validateArgs(instanceId: string, model: ComponentModel, args: Record<string, unknown>) {
351
+ for (const [key, argModel] of Object.entries(model.args)) {
352
+ const value = args[key]
353
+ if (!value && argModel.required) {
354
+ throw new Error(`Missing required argument "${key}" for instance "${instanceId}"`)
355
+ }
356
+
357
+ if (value && !ajv.validate(argModel.schema, value)) {
358
+ throw new Error(`Invalid argument "${key}" for instance "${instanceId}": ${ajv.errorsText()}`)
359
+ }
360
+ }
361
+ }
package/src/entity.ts ADDED
@@ -0,0 +1,54 @@
1
+ import type { TAnySchema, TSchema } from "@sinclair/typebox"
2
+ import type { Meta } from "./types"
3
+ import type { PartialKeys } from "./utils"
4
+
5
+ /**
6
+ * The entity is some abstract object which can be passed from one component to another through their inputs and outputs.
7
+ * Every entity must have a type.
8
+ * Every component inputs and outputs will reference such types and only entities of the same type can be passed.
9
+ */
10
+ export type Entity<TType extends string = string, TEntitySchema extends TSchema = TSchema> = {
11
+ /**
12
+ * The static type of the entity.
13
+ */
14
+ type: TType
15
+
16
+ /**
17
+ * The JSON schema of the entity value.
18
+ */
19
+ schema: TEntitySchema
20
+
21
+ /**
22
+ * The extra metadata of the entity.
23
+ */
24
+ meta: Meta
25
+
26
+ /**
27
+ * The hash of the entity definition.
28
+ */
29
+ definitionHash: string
30
+ }
31
+
32
+ export type EntityOptions<TType extends string, TSchema extends TAnySchema> = Omit<
33
+ PartialKeys<Entity<TType, TSchema>, "meta">,
34
+ "definitionHash"
35
+ >
36
+
37
+ export function defineEntity<TType extends string, TSchema extends TAnySchema>(
38
+ options: EntityOptions<TType, TSchema>,
39
+ ): Entity<TType, TSchema> {
40
+ return {
41
+ meta: {},
42
+ ...options,
43
+ } as Entity<TType, TSchema>
44
+ }
45
+
46
+ export function isEntity(value: unknown): value is Entity {
47
+ return (
48
+ typeof value === "object" &&
49
+ value !== null &&
50
+ "type" in value &&
51
+ "schema" in value &&
52
+ "meta" in value
53
+ )
54
+ }
@@ -0,0 +1,60 @@
1
+ import type { ComponentModel } from "./component"
2
+ import { mapValues } from "remeda"
3
+ import { boundaryInput, type InstanceInput, type InstanceModel } from "./instance"
4
+ import { isUnitModel } from "./unit"
5
+
6
+ export type CompositeInstance = {
7
+ instance: InstanceModel
8
+ children: InstanceModel[]
9
+ }
10
+
11
+ const compositeInstances: Map<string, CompositeInstance> = new Map()
12
+ let currentCompositeInstance: CompositeInstance | null = null
13
+
14
+ export function resetEvaluation(): void {
15
+ compositeInstances.clear()
16
+ currentCompositeInstance = null
17
+ }
18
+
19
+ export function getCompositeInstances(): CompositeInstance[] {
20
+ return Array.from(compositeInstances.values())
21
+ }
22
+
23
+ export function registerInstance<T>(
24
+ component: ComponentModel,
25
+ instance: InstanceModel,
26
+ fn: () => T,
27
+ ): T {
28
+ if (currentCompositeInstance) {
29
+ instance.parentId = currentCompositeInstance.instance.id
30
+ currentCompositeInstance.children.push(instance)
31
+ }
32
+
33
+ let previousParentInstance: CompositeInstance | null = null
34
+ if (!isUnitModel(component)) {
35
+ previousParentInstance = currentCompositeInstance
36
+ currentCompositeInstance = { instance, children: [] }
37
+ compositeInstances.set(currentCompositeInstance.instance.id, currentCompositeInstance)
38
+ }
39
+
40
+ try {
41
+ const outputs = fn() as Record<string, InstanceInput[]>
42
+
43
+ instance.resolvedOutputs = outputs
44
+ instance.outputs = mapValues(outputs ?? {}, outputs =>
45
+ outputs.map(output => output[boundaryInput] ?? output),
46
+ )
47
+
48
+ // mark all outputs with the boundary input of the instance
49
+ return mapValues(outputs, (outputs, outputKey) =>
50
+ outputs.map(output => ({
51
+ ...output,
52
+ [boundaryInput]: { instanceId: instance.id, output: outputKey },
53
+ })),
54
+ ) as T
55
+ } finally {
56
+ if (previousParentInstance) {
57
+ currentCompositeInstance = previousParentInstance
58
+ }
59
+ }
60
+ }
package/src/index.ts ADDED
@@ -0,0 +1,42 @@
1
+ export {
2
+ type InstanceInput,
3
+ type HubInput,
4
+ type InstanceModel,
5
+ type Position,
6
+ getInstanceId,
7
+ parseInstanceId,
8
+ findInput,
9
+ findRequiredInput,
10
+ } from "./instance"
11
+ export { getCompositeInstances, resetEvaluation, type CompositeInstance } from "./evaluation"
12
+ export { type Entity, defineEntity, isEntity } from "./entity"
13
+ export {
14
+ type Component,
15
+ type ComponentModel,
16
+ type ComponentMeta,
17
+ type ComponentArgument,
18
+ type ComponentInput,
19
+ type ComponentInputSpec,
20
+ defineComponent,
21
+ isComponent,
22
+ originalCreate,
23
+ } from "./component"
24
+ export { type Unit, type UnitModel, type UnitSource, defineUnit, isUnitModel } from "./unit"
25
+ export { type RequiredKeys, type PartialKeys, text, trimIndentation } from "./utils"
26
+ export { type ArgumentValue, type ArgumentValueSchema } from "./types"
27
+ export { type Static } from "@sinclair/typebox"
28
+
29
+ import { Type as BaseType, type TLiteral, type TUnion } from "@sinclair/typebox"
30
+
31
+ type MapToLiteral<T extends readonly string[]> = {
32
+ [K in keyof T]: T[K] extends string ? TLiteral<T[K]> : never
33
+ }
34
+
35
+ function StringEnum<T extends string[]>(values: [...T]): TUnion<MapToLiteral<T>> {
36
+ return Type.Union(values.map(value => Type.Literal(value))) as TUnion<MapToLiteral<T>>
37
+ }
38
+
39
+ export const Type = {
40
+ ...BaseType,
41
+ StringEnum,
42
+ } as typeof BaseType & { StringEnum: typeof StringEnum }
@@ -0,0 +1,165 @@
1
+ declare const type: unique symbol
2
+
3
+ export type InstanceInput<TType extends string = string> = {
4
+ [type]?: TType
5
+ [boundaryInput]?: InstanceInput
6
+ instanceId: string
7
+ output: string
8
+ }
9
+
10
+ export const boundaryInput = Symbol("boundaryInput")
11
+
12
+ export type HubInput = {
13
+ hubId: string
14
+ }
15
+
16
+ export type Position = {
17
+ x: number
18
+ y: number
19
+ }
20
+
21
+ export type InstanceModel = {
22
+ /**
23
+ * The id of the instance unique within the project.
24
+ *
25
+ * The format is `${instanceType}:${instanceName}`.
26
+ */
27
+ id: string
28
+
29
+ /**
30
+ * The type of the instance.
31
+ */
32
+ type: string
33
+
34
+ /**
35
+ * The name of the instance.
36
+ *
37
+ * Must be unique within instances of the same type in the project.
38
+ */
39
+ name: string
40
+
41
+ /**
42
+ * The static arguments passed to the instance.
43
+ */
44
+ args?: Record<string, unknown>
45
+
46
+ /**
47
+ * The direct instances passed as inputs to the instance.
48
+ */
49
+ inputs?: Record<string, InstanceInput[]>
50
+
51
+ /**
52
+ * The resolved unit inputs for the instance.
53
+ *
54
+ * Only for computed composite instances.
55
+ */
56
+ resolvedInputs?: Record<string, InstanceInput[]>
57
+
58
+ /**
59
+ * The inputs passed to the instance from the hubs.
60
+ *
61
+ * Only for designer-first instances.
62
+ */
63
+ hubInputs?: Record<string, HubInput[]>
64
+
65
+ /**
66
+ * The inputs injected to the instance from the hubs.
67
+ *
68
+ * While `hubInputs` allows to pass hubs to distinct inputs,
69
+ * `injectionInputs` allows to pass hubs to the instance as a whole filling all inputs with matching types.
70
+ *
71
+ * Only for designer-first instances.
72
+ */
73
+ injectionInputs?: HubInput[]
74
+
75
+ /**
76
+ * The position of the instance on the canvas.
77
+ *
78
+ * Only for designer-first instances.
79
+ */
80
+ position?: Position
81
+
82
+ /**
83
+ * The id of the top level parent instance.
84
+ *
85
+ * Only for child instances of the composite instances.
86
+ */
87
+ parentId?: string
88
+
89
+ /**
90
+ * The direct instance inputs and same-level children outputs returned by the instance as outputs.
91
+ *
92
+ * Only for computed composite instances.
93
+ */
94
+ outputs?: Record<string, InstanceInput[]>
95
+
96
+ /**
97
+ * The resolved unit outputs for the instance.
98
+ *
99
+ * Only for computed composite instances.
100
+ */
101
+ resolvedOutputs?: Record<string, InstanceInput[]>
102
+ }
103
+
104
+ /**
105
+ * Formats the instance id from the instance type and instance name.
106
+ *
107
+ * @param instanceType The type of the instance.
108
+ * @param instanceName The name of the instance.
109
+ *
110
+ * @returns The formatted instance id.
111
+ */
112
+ export function getInstanceId(instanceType: string, instanceName: string): string {
113
+ return `${instanceType}:${instanceName}`
114
+ }
115
+
116
+ /**
117
+ * Parses the instance id into the instance type and instance name.
118
+ *
119
+ * @param instanceId The instance id to parse.
120
+ *
121
+ * @returns The instance type and instance name.
122
+ */
123
+ export function parseInstanceId(instanceId: string): [instanceType: string, instanceName: string] {
124
+ const parts = instanceId.split(":")
125
+
126
+ if (parts.length !== 2) {
127
+ throw new Error(`Invalid instance key: ${instanceId}`)
128
+ }
129
+
130
+ return parts as [string, string]
131
+ }
132
+
133
+ export function findInput<T extends string>(
134
+ inputs: InstanceInput<T>[],
135
+ name: string,
136
+ ): InstanceInput<T> | null {
137
+ const matchedInputs = inputs.filter(
138
+ input => parseInstanceId(input.instanceId)[1] === name || input.instanceId === name,
139
+ )
140
+
141
+ if (matchedInputs.length === 0) {
142
+ return null
143
+ }
144
+
145
+ if (1 < matchedInputs.length) {
146
+ throw new Error(
147
+ `Multiple inputs found for "${name}": ${matchedInputs.map(input => input.instanceId).join(", ")}. Specify the full instance id to disambiguate.`,
148
+ )
149
+ }
150
+
151
+ return matchedInputs[0]
152
+ }
153
+
154
+ export function findRequiredInput<T extends string>(
155
+ inputs: InstanceInput<T>[],
156
+ name: string,
157
+ ): InstanceInput<T> {
158
+ const input = findInput(inputs, name)
159
+
160
+ if (input === null) {
161
+ throw new Error(`Required input "${name}" not found.`)
162
+ }
163
+
164
+ return input
165
+ }