@highstate/contract 0.18.0 → 0.20.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/src/evaluation.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Component } from "./component"
2
- import type { InstanceInput, InstanceModel } from "./instance"
2
+ import type { InstanceInput, InstanceModel, RuntimeInput } from "./instance"
3
3
  import { mapValues } from "remeda"
4
4
 
5
5
  export type RuntimeInstance = {
@@ -8,7 +8,17 @@ export type RuntimeInstance = {
8
8
  }
9
9
 
10
10
  export const boundaryInput = Symbol("boundaryInput")
11
- export const boundaryInputs = Symbol("boundaryInputs")
11
+
12
+ function isStableInstanceInput(value: unknown): value is InstanceInput {
13
+ return (
14
+ typeof value === "object" &&
15
+ value !== null &&
16
+ "instanceId" in value &&
17
+ "output" in value &&
18
+ typeof (value as { instanceId: unknown }).instanceId === "string" &&
19
+ typeof (value as { output: unknown }).output === "string"
20
+ )
21
+ }
12
22
 
13
23
  function formatInstancePath(instance: InstanceModel): string {
14
24
  let result = instance.id
@@ -86,20 +96,60 @@ export function registerInstance<T>(component: Component, instance: InstanceMode
86
96
  }
87
97
 
88
98
  try {
89
- const outputs = fn() as Record<string, InstanceInput[]>
99
+ const rawOutputs = fn() as Record<string, RuntimeInput | RuntimeInput[] | undefined>
100
+ const outputs = mapValues(rawOutputs ?? {}, outputGroup => {
101
+ return [outputGroup].flat(2).filter(Boolean) as RuntimeInput[]
102
+ })
103
+
104
+ const toStableInputs = (
105
+ outputGroup: RuntimeInput[],
106
+ useBoundaryFallback: boolean,
107
+ ): InstanceInput[] => {
108
+ return outputGroup
109
+ .map(output => (useBoundaryFallback ? output[boundaryInput] : output))
110
+ .filter(isStableInstanceInput)
111
+ .map(output => {
112
+ return output.path
113
+ ? {
114
+ instanceId: output.instanceId,
115
+ output: output.output,
116
+ path: output.path,
117
+ }
118
+ : {
119
+ instanceId: output.instanceId,
120
+ output: output.output,
121
+ }
122
+ })
123
+ }
90
124
 
91
- instance.resolvedOutputs = outputs
92
- instance.outputs = mapValues(outputs ?? {}, outputs =>
93
- outputs.map(output => output[boundaryInput] ?? output),
125
+ instance.resolvedOutputs = mapValues(outputs ?? {}, outputGroup =>
126
+ toStableInputs(outputGroup, false),
94
127
  )
128
+ instance.outputs = mapValues(outputs ?? {}, outputGroup => toStableInputs(outputGroup, true))
95
129
 
96
130
  // mark all outputs with the boundary input of the instance
97
- return mapValues(outputs, (outputs, outputKey) =>
98
- outputs.map(output => ({
99
- ...output,
100
- [boundaryInput]: { instanceId: instance.id, output: outputKey },
101
- })),
102
- ) as T
131
+ // keep object identity to preserve proxy-based deep accessors
132
+ return mapValues(rawOutputs, (rawOutputGroup, outputKey) => {
133
+ const outputRefs = (outputs[outputKey] ?? []).map(output => {
134
+ if (output.provided) {
135
+ output[boundaryInput] = { instanceId: instance.id, output: outputKey }
136
+ }
137
+
138
+ return output
139
+ })
140
+
141
+ if (component.model.outputs[outputKey]?.multiple) {
142
+ const multipleOutput = (
143
+ Array.isArray(rawOutputGroup) ? rawOutputGroup : outputRefs
144
+ ) as RuntimeInput[] & Partial<{ [boundaryInput]: InstanceInput }>
145
+
146
+ multipleOutput[boundaryInput] ??= { instanceId: instance.id, output: outputKey }
147
+
148
+ return multipleOutput
149
+ }
150
+
151
+ return rawOutputGroup ?? outputRefs[0]
152
+ }) as T
103
153
  } finally {
104
154
  if (previousParentInstance) {
105
155
  currentInstance = previousParentInstance
package/src/index.ts CHANGED
@@ -3,13 +3,19 @@
3
3
  export {
4
4
  type Entity,
5
5
  type EntityModel,
6
+ type EntityMeta,
7
+ type EntityWithMeta,
8
+ type EntityValue,
9
+ type EntityValueInput,
6
10
  isEntity,
7
11
  isAssignableTo,
8
12
  defineEntity,
9
13
  entityModelSchema,
14
+ getEntityId,
10
15
  } from "./entity"
11
16
 
12
17
  export * from "./instance"
18
+ export { createInput, createNonProvidedInput } from "./instance-input"
13
19
  export * from "./unit"
14
20
  export * from "./i18n"
15
21
  export * from "./meta"
@@ -17,7 +23,7 @@ export * from "./terminal"
17
23
  export * from "./page"
18
24
  export * from "./trigger"
19
25
  export * from "./worker"
20
- export * from "./compaction"
26
+ export * from "./cuidv2d"
21
27
 
22
28
  export {
23
29
  // common utilities
@@ -58,6 +64,8 @@ export {
58
64
  type ComponentArgumentOptions,
59
65
  type FullComponentArgumentOptions,
60
66
  type ComponentInputOptions,
67
+ type ComponentOutputOptions,
68
+ type FromInputComponentOutputOptions,
61
69
  type FullComponentInputOptions,
62
70
  type ComponentArgumentOptionsToSchema,
63
71
  // schemas
@@ -0,0 +1,219 @@
1
+ import type { InstanceInput, MultipleInput, RequiredInput, RuntimeInput } from "./instance"
2
+ import { boundaryInput } from "./evaluation"
3
+
4
+ type CreateInputOptions = {
5
+ boundary?: InstanceInput
6
+ }
7
+
8
+ function appendInputPath(currentPath: string | undefined, segment: string): string {
9
+ return currentPath ? `${currentPath}.${segment}` : segment
10
+ }
11
+
12
+ function createRuntimeInputAccessor(input: RuntimeInput, boundary: InstanceInput): RuntimeInput {
13
+ const accessorCache = input.provided ? undefined : new Map<string, RuntimeInput>()
14
+ let currentBoundary = boundary
15
+
16
+ return new Proxy(input as Record<string | symbol, unknown>, {
17
+ get(target, property, receiver): unknown {
18
+ const runtimeTarget = target as RuntimeInput
19
+
20
+ if (property === boundaryInput) {
21
+ return currentBoundary
22
+ }
23
+
24
+ if (typeof property !== "string") {
25
+ return Reflect.get(target, property, receiver)
26
+ }
27
+
28
+ if (property in target) {
29
+ return Reflect.get(target, property, receiver)
30
+ }
31
+
32
+ if (property === "instanceId" || property === "output" || property === "path") {
33
+ return Reflect.get(target, property, receiver)
34
+ }
35
+
36
+ if (runtimeTarget.provided) {
37
+ return Reflect.get(runtimeTarget, property, receiver)
38
+ }
39
+
40
+ const cached = accessorCache?.get(property)
41
+ if (cached) {
42
+ return cached
43
+ }
44
+
45
+ const nested = createNonProvidedInput(boundary, appendInputPath(runtimeTarget.path, property))
46
+ accessorCache?.set(property, nested)
47
+
48
+ return nested
49
+ },
50
+
51
+ set(target, property, value, receiver): boolean {
52
+ if (property === boundaryInput) {
53
+ currentBoundary = value as InstanceInput
54
+ return true
55
+ }
56
+
57
+ return Reflect.set(target, property, value, receiver)
58
+ },
59
+ }) as RuntimeInput
60
+ }
61
+
62
+ /**
63
+ * Creates a runtime input object with boundary and nested accessor behavior.
64
+ *
65
+ * If the source input is non-provided, this function returns a non-provided proxy
66
+ * that can still safely chain nested properties.
67
+ *
68
+ * @param input The source input reference.
69
+ * @param options Optional boundary override.
70
+ */
71
+ export function createInput(
72
+ input: RuntimeInput | InstanceInput,
73
+ options: CreateInputOptions = {},
74
+ ): RuntimeInput {
75
+ if (!("provided" in input)) {
76
+ const boundary = options.boundary ?? input
77
+
78
+ const normalizedInput: RequiredInput = {
79
+ provided: true,
80
+ instanceId: input.instanceId,
81
+ output: input.output,
82
+ ...(input.path ? { path: input.path } : {}),
83
+ [boundaryInput]: boundary,
84
+ }
85
+
86
+ return createRuntimeInputAccessor(normalizedInput, boundary)
87
+ }
88
+
89
+ if (!input.provided) {
90
+ const boundary = options.boundary ?? input[boundaryInput]
91
+
92
+ if (!boundary) {
93
+ throw new Error("Cannot create non-provided input without boundary metadata")
94
+ }
95
+
96
+ return createNonProvidedInput(boundary, input.path)
97
+ }
98
+
99
+ const boundary = options.boundary ?? input[boundaryInput]
100
+
101
+ if (!boundary) {
102
+ throw new Error("Cannot create provided input without boundary metadata")
103
+ }
104
+
105
+ return createRuntimeInputAccessor(input as RequiredInput, boundary)
106
+ }
107
+
108
+ /**
109
+ * Creates a non-provided runtime input with recursive chained access support.
110
+ *
111
+ * @param boundary The boundary reference propagated across chained properties.
112
+ * @param path Optional path to assign to the non-provided input.
113
+ */
114
+ export function createNonProvidedInput(boundary: InstanceInput, path?: string): RuntimeInput {
115
+ const target: RuntimeInput = {
116
+ provided: false,
117
+ ...(path ? { path } : {}),
118
+ [boundaryInput]: boundary,
119
+ }
120
+
121
+ return createRuntimeInputAccessor(target, boundary)
122
+ }
123
+
124
+ export function createMultipleInputAccessor(
125
+ inputs: RuntimeInput[],
126
+ boundary: InstanceInput,
127
+ ): MultipleInput {
128
+ const arrayInputs = [...inputs] as MultipleInput
129
+ arrayInputs[boundaryInput] = boundary
130
+
131
+ return new Proxy(arrayInputs as RuntimeInput[], {
132
+ get(target, property, receiver): unknown {
133
+ if (typeof property !== "string") {
134
+ return Reflect.get(target, property, receiver)
135
+ }
136
+
137
+ if (
138
+ property === "length" ||
139
+ property === "map" ||
140
+ property === "filter" ||
141
+ property === "path" ||
142
+ property in target
143
+ ) {
144
+ return Reflect.get(target, property, receiver)
145
+ }
146
+
147
+ const unfolded = target.flatMap(input => {
148
+ if (!input.provided) {
149
+ return []
150
+ }
151
+
152
+ const selected = (input as Record<string, unknown>)[property]
153
+
154
+ if (selected === undefined || selected === null) {
155
+ return []
156
+ }
157
+
158
+ if (Array.isArray(selected)) {
159
+ return selected as RuntimeInput[]
160
+ }
161
+
162
+ return [selected as RuntimeInput]
163
+ })
164
+
165
+ return createMultipleInputAccessor(unfolded, boundary)
166
+ },
167
+ }) as MultipleInput
168
+ }
169
+
170
+ export function createDeepOutputAccessor(output: RuntimeInput): RuntimeInput {
171
+ const normalizedOutput = createInput(output)
172
+
173
+ if (!normalizedOutput.provided) {
174
+ return normalizedOutput
175
+ }
176
+
177
+ const accessorCache = new Map<string, unknown>()
178
+
179
+ return new Proxy(normalizedOutput as Record<string | symbol, unknown>, {
180
+ get(target, property, receiver): unknown {
181
+ if (typeof property !== "string") {
182
+ return Reflect.get(target, property, receiver)
183
+ }
184
+
185
+ if (
186
+ property === "provided" ||
187
+ property === "instanceId" ||
188
+ property === "output" ||
189
+ property === "path"
190
+ ) {
191
+ return Reflect.get(target, property, receiver)
192
+ }
193
+
194
+ if (property in target) {
195
+ return Reflect.get(target, property, receiver)
196
+ }
197
+
198
+ const cached = accessorCache.get(property)
199
+ if (cached !== undefined) {
200
+ return cached
201
+ }
202
+
203
+ const providedTarget = target as RequiredInput
204
+
205
+ const nextInput = createInput(
206
+ {
207
+ ...providedTarget,
208
+ path: appendInputPath(providedTarget.path, property),
209
+ },
210
+ { boundary: providedTarget[boundaryInput] },
211
+ )
212
+
213
+ const resolved = createDeepOutputAccessor(nextInput)
214
+ accessorCache.set(property, resolved)
215
+
216
+ return resolved
217
+ },
218
+ }) as RuntimeInput
219
+ }