@highstate/contract 0.19.1 → 0.21.1

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,14 +1,23 @@
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
+ import { boundaryInput } from "./shared"
4
5
 
5
6
  export type RuntimeInstance = {
6
7
  instance: InstanceModel
7
8
  component: Component
8
9
  }
9
10
 
10
- export const boundaryInput = Symbol("boundaryInput")
11
- export const boundaryInputs = Symbol("boundaryInputs")
11
+ function isStableInstanceInput(value: unknown): value is InstanceInput {
12
+ return (
13
+ typeof value === "object" &&
14
+ value !== null &&
15
+ "instanceId" in value &&
16
+ "output" in value &&
17
+ typeof (value as { instanceId: unknown }).instanceId === "string" &&
18
+ typeof (value as { output: unknown }).output === "string"
19
+ )
20
+ }
12
21
 
13
22
  function formatInstancePath(instance: InstanceModel): string {
14
23
  let result = instance.id
@@ -86,20 +95,60 @@ export function registerInstance<T>(component: Component, instance: InstanceMode
86
95
  }
87
96
 
88
97
  try {
89
- const outputs = fn() as Record<string, InstanceInput[]>
98
+ const rawOutputs = fn() as Record<string, RuntimeInput | RuntimeInput[] | undefined>
99
+ const outputs = mapValues(rawOutputs ?? {}, outputGroup => {
100
+ return [outputGroup].flat(2).filter(Boolean) as RuntimeInput[]
101
+ })
102
+
103
+ const toStableInputs = (
104
+ outputGroup: RuntimeInput[],
105
+ useBoundaryFallback: boolean,
106
+ ): InstanceInput[] => {
107
+ return outputGroup
108
+ .map(output => (useBoundaryFallback ? output[boundaryInput] : output))
109
+ .filter(isStableInstanceInput)
110
+ .map(output => {
111
+ return output.path
112
+ ? {
113
+ instanceId: output.instanceId,
114
+ output: output.output,
115
+ path: output.path,
116
+ }
117
+ : {
118
+ instanceId: output.instanceId,
119
+ output: output.output,
120
+ }
121
+ })
122
+ }
90
123
 
91
- instance.resolvedOutputs = outputs
92
- instance.outputs = mapValues(outputs ?? {}, outputs =>
93
- outputs.map(output => output[boundaryInput] ?? output),
124
+ instance.resolvedOutputs = mapValues(outputs ?? {}, outputGroup =>
125
+ toStableInputs(outputGroup, false),
94
126
  )
127
+ instance.outputs = mapValues(outputs ?? {}, outputGroup => toStableInputs(outputGroup, true))
95
128
 
96
129
  // 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
130
+ // keep object identity to preserve proxy-based deep accessors
131
+ return mapValues(rawOutputs, (rawOutputGroup, outputKey) => {
132
+ const outputRefs = (outputs[outputKey] ?? []).map(output => {
133
+ if (output.provided) {
134
+ output[boundaryInput] = { instanceId: instance.id, output: outputKey }
135
+ }
136
+
137
+ return output
138
+ })
139
+
140
+ if (component.model.outputs[outputKey]?.multiple) {
141
+ const multipleOutput = (
142
+ Array.isArray(rawOutputGroup) ? rawOutputGroup : outputRefs
143
+ ) as RuntimeInput[] & Partial<{ [boundaryInput]: InstanceInput }>
144
+
145
+ multipleOutput[boundaryInput] ??= { instanceId: instance.id, output: outputKey }
146
+
147
+ return multipleOutput
148
+ }
149
+
150
+ return rawOutputGroup ?? outputRefs[0]
151
+ }) as T
103
152
  } finally {
104
153
  if (previousParentInstance) {
105
154
  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
@@ -47,17 +53,17 @@ export {
47
53
  getInstanceId,
48
54
  // for unit API
49
55
  type ComponentInputSpec,
50
- runtimeSchema,
51
56
  // types
52
57
  type Component,
53
58
  type ComponentModel,
54
59
  type ComponentInput,
55
60
  type ComponentArgument,
56
- type ComponentKind,
57
61
  // extra types
58
62
  type ComponentArgumentOptions,
59
63
  type FullComponentArgumentOptions,
60
64
  type ComponentInputOptions,
65
+ type ComponentOutputOptions,
66
+ type FromInputComponentOutputOptions,
61
67
  type FullComponentInputOptions,
62
68
  type ComponentArgumentOptionsToSchema,
63
69
  // schemas
@@ -68,6 +74,15 @@ export {
68
74
  originalCreate,
69
75
  } from "./component"
70
76
 
77
+ export {
78
+ boundaryInput,
79
+ inputKey,
80
+ kind,
81
+ runtimeSchema,
82
+ componentKindSchema,
83
+ type ComponentKind,
84
+ } from "./shared"
85
+
71
86
  // for runner <-> stack communication
72
87
  export * from "./pulumi"
73
88
 
@@ -0,0 +1,219 @@
1
+ import type { InstanceInput, MultipleInput, RequiredInput, RuntimeInput } from "./instance"
2
+ import { boundaryInput } from "./shared"
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
+ }