@highstate/backend 0.9.9 → 0.9.10

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,73 @@
1
+ export type AsyncBatcherOptions = {
2
+ waitMs?: number
3
+ maxWaitTimeMs?: number
4
+ }
5
+
6
+ export function createAsyncBatcher<T>(
7
+ fn: (items: T[]) => Promise<void> | void,
8
+ { waitMs = 100, maxWaitTimeMs = 1000 }: AsyncBatcherOptions = {},
9
+ ) {
10
+ let batch: T[] = []
11
+ let activeTimeout: NodeJS.Timeout | null = null
12
+ let maxWaitTimeout: NodeJS.Timeout | null = null
13
+ let firstCallTimestamp: number | null = null
14
+
15
+ async function processBatch() {
16
+ if (batch.length === 0) return
17
+
18
+ const currentBatch = batch
19
+ batch = [] // Reset batch before async call
20
+
21
+ await fn(currentBatch)
22
+
23
+ // Clear max wait timer since batch has been processed
24
+ if (maxWaitTimeout) {
25
+ clearTimeout(maxWaitTimeout)
26
+ maxWaitTimeout = null
27
+ }
28
+ firstCallTimestamp = null
29
+ }
30
+
31
+ function schedule() {
32
+ if (activeTimeout) clearTimeout(activeTimeout)
33
+ activeTimeout = setTimeout(() => {
34
+ activeTimeout = null
35
+ void processBatch()
36
+ }, waitMs)
37
+
38
+ // Ensure batch is executed within maxWaitTimeMs
39
+ if (!firstCallTimestamp) {
40
+ firstCallTimestamp = Date.now()
41
+ maxWaitTimeout = setTimeout(() => {
42
+ if (activeTimeout) clearTimeout(activeTimeout)
43
+ activeTimeout = null
44
+ void processBatch()
45
+ }, maxWaitTimeMs)
46
+ }
47
+ }
48
+
49
+ return {
50
+ /**
51
+ * Add an item to the batch.
52
+ */
53
+ call(item: T): void {
54
+ batch.push(item)
55
+ schedule()
56
+ },
57
+
58
+ /**
59
+ * Immediately flush the pending batch (if any).
60
+ */
61
+ async flush(): Promise<void> {
62
+ if (activeTimeout) {
63
+ clearTimeout(activeTimeout)
64
+ activeTimeout = null
65
+ }
66
+ if (maxWaitTimeout) {
67
+ clearTimeout(maxWaitTimeout)
68
+ maxWaitTimeout = null
69
+ }
70
+ await processBatch()
71
+ },
72
+ }
73
+ }
@@ -8,3 +8,4 @@ export * from "./resolvers/input-hash"
8
8
  export * from "./resolvers/validation"
9
9
  export * from "./terminal"
10
10
  export * from "./library"
11
+ export * from "./async-batcher"
@@ -1,111 +1,178 @@
1
1
  import type { Logger } from "pino"
2
2
  import { unique } from "remeda"
3
3
 
4
- export type GraphResolverOptions<TNode, TOutput> = {
5
- name: string
4
+ export type ResolverOutputHandler<TOutput> = (id: string, value: TOutput) => void
6
5
 
7
- getNodeId(input: TNode): string
6
+ export abstract class GraphResolver<TNode, TOutput> {
7
+ private readonly workset: Set<string> = new Set()
8
+ private readonly dependentMap: Map<string, Set<string>> = new Map()
9
+ private readonly outputMap: Map<string, TOutput> = new Map()
8
10
 
9
- getNodeDependencies(input: TNode): string[]
11
+ constructor(
12
+ private readonly nodes: ReadonlyMap<string, TNode>,
13
+ protected readonly logger: Logger,
14
+ private readonly outputHandler?: ResolverOutputHandler<TOutput>,
15
+ ) {}
10
16
 
11
- process(
12
- node: TNode,
13
- dependencies: ReadonlyMap<string, TOutput>,
14
- logger: Logger,
15
- ): TOutput | Promise<TOutput>
16
- }
17
-
18
- export interface GraphResolverBackend<TOutput> {
19
- promiseCache: Map<string, Promise<TOutput>>
20
-
21
- setOutput?(id: string, value: TOutput): void
22
- setDependencies?(id: string, dependencies: string[]): void
23
- }
24
-
25
- export type GraphResolverFactory<TNode, TOutput> = {
26
- factoryName: string
27
- } & ((
28
- nodes: ReadonlyMap<string, TNode>,
29
- logger: Logger,
30
- backend?: GraphResolverBackend<TOutput>,
31
- ) => GraphResolver<TOutput>)
17
+ addToWorkset(nodeId: string): void {
18
+ this.workset.add(nodeId)
19
+ }
32
20
 
33
- export type GraphResolver<TOutput> = (id: string) => Promise<TOutput>
21
+ addAllNodesToWorkset(): void {
22
+ for (const nodeId of this.nodes.keys()) {
23
+ this.workset.add(nodeId)
24
+ }
25
+ }
34
26
 
35
- export class CircularDependencyError extends Error {
36
- constructor(path: string[]) {
37
- super(`Circular dependency detected: ${path.join(" -> ")}`)
38
- this.name = "CircularDependencyError"
27
+ /**
28
+ * The map of calculated outputs.
29
+ */
30
+ get outputs(): ReadonlyMap<string, TOutput> {
31
+ return this.outputMap
39
32
  }
40
- }
41
33
 
42
- export function createDefaultGraphResolverBackend<TOutput>(): GraphResolverBackend<TOutput> {
43
- const promiseCache = new Map<string, Promise<TOutput>>()
34
+ requireOutput(nodeId: string): TOutput {
35
+ const output = this.outputMap.get(nodeId)
36
+ if (!output) {
37
+ throw new Error(`Output for node ${nodeId} is not available`)
38
+ }
44
39
 
45
- return {
46
- promiseCache,
40
+ return output
47
41
  }
48
- }
49
-
50
- export function defineGraphResolver<TInput, TOutput>(
51
- options: GraphResolverOptions<TInput, TOutput>,
52
- ): GraphResolverFactory<TInput, TOutput> {
53
- const factory: GraphResolverFactory<TInput, TOutput> = (nodes, logger, backend) => {
54
- backend ??= createDefaultGraphResolverBackend<TOutput>()
55
- logger = logger.child({ resolver: options.name })
56
42
 
57
- const outputs = new Map<string, TOutput>()
43
+ /**
44
+ * Gets the list of the identifiers of the dependencies for the node.
45
+ *
46
+ * Used to produce the dependency graph.
47
+ */
48
+ protected abstract getNodeDependencies(node: TNode): string[]
49
+
50
+ /**
51
+ * Processes the node and returns the output.
52
+ */
53
+ protected abstract processNode(node: TNode, logger: Logger): TOutput | Promise<TOutput>
54
+
55
+ /**
56
+ * Invalidates the node and all nodes that depend on it.
57
+ *
58
+ * Also adds the node to the work set for processing.
59
+ */
60
+ invalidate(nodeId: string): void {
61
+ const stack = [nodeId]
62
+
63
+ while (stack.length > 0) {
64
+ const nodeId = stack.pop()!
65
+ if (!this.nodes.has(nodeId)) {
66
+ // it is ok to invalidate deleted nodes
67
+ continue
68
+ }
58
69
 
59
- const resolver = (itemId: string, dependencyChain: string[]) => {
60
- logger.trace({ itemId }, "resolving item")
70
+ // remove the node from the output map
71
+ this.outputMap.delete(nodeId)
72
+ this.workset.add(nodeId)
61
73
 
62
- const existingPromise = backend.promiseCache.get(itemId)
63
- if (existingPromise) {
64
- return existingPromise
74
+ const dependents = this.dependentMap.get(nodeId)
75
+ if (!dependents) {
76
+ continue
65
77
  }
66
78
 
67
- if (dependencyChain.includes(itemId)) {
68
- throw new CircularDependencyError([...dependencyChain, itemId])
79
+ for (const dependentId of dependents) {
80
+ if (this.outputMap.has(dependentId)) {
81
+ // add the dependent to the stack for further processing
82
+ stack.push(dependentId)
83
+ }
69
84
  }
70
85
 
71
- const item = nodes.get(itemId)
72
- if (!item) {
73
- return Promise.resolve(undefined!)
74
- }
86
+ // clear the dependent map for the node
87
+ this.dependentMap.delete(nodeId)
88
+ }
89
+ }
75
90
 
76
- const resolve = async () => {
77
- const dependencies = unique(options.getNodeDependencies(item))
91
+ /**
92
+ * Resolves all not-resolved or invalidated nodes in the graph.
93
+ *
94
+ * The abort signal of the previous operation must be called before calling this method again.
95
+ */
96
+ async process(signal?: AbortSignal): Promise<void> {
97
+ while (this.workset.size > 0) {
98
+ const rootNodeId = this.workset.values().next().value!
99
+ const stack = [{ nodeId: rootNodeId, resolved: false, dependencies: [] as string[] }]
100
+
101
+ while (stack.length > 0) {
102
+ const stackItem = stack[stack.length - 1]
103
+ const { nodeId, resolved } = stackItem
104
+
105
+ const node = this.nodes.get(nodeId)
106
+ if (!node) {
107
+ this.logger.warn({ nodeId }, "node not found in the graph, skipping")
108
+ stack.pop()
109
+ continue
110
+ }
78
111
 
79
- backend.setDependencies?.(itemId, dependencies)
80
- logger.trace({ itemId, dependencies }, "resolving item dependencies")
112
+ if (this.outputMap.has(nodeId)) {
113
+ // already processed
114
+ stack.pop()
115
+ continue
116
+ }
81
117
 
82
- const newChain = [...dependencyChain, itemId]
118
+ if (!resolved) {
119
+ stackItem.dependencies = unique(this.getNodeDependencies(node))
120
+
121
+ let hasUnresolvedDependencies = false
122
+
123
+ for (const depId of stackItem.dependencies) {
124
+ if (!this.nodes.has(depId)) {
125
+ this.logger.warn({ depId, nodeId }, "dependency not found in the graph, skipping")
126
+ continue
127
+ }
128
+
129
+ // if (stack.some(item => item.nodeId === depId)) {
130
+ // this.logger.warn(
131
+ // { depId, nodeId, stack },
132
+ // "dependency is already in the stack, looks like a circular dependency, skipping",
133
+ // )
134
+ // continue
135
+ // }
136
+
137
+ if (!this.outputMap.has(depId)) {
138
+ stack.push({ nodeId: depId, resolved: false, dependencies: [] })
139
+ hasUnresolvedDependencies = true
140
+ }
141
+ }
142
+
143
+ if (hasUnresolvedDependencies) {
144
+ // wait for dependencies to be resolved
145
+ stackItem.resolved = true
146
+ continue
147
+ }
148
+ }
83
149
 
84
- for (const depId of dependencies) {
85
- await resolver(depId, newChain)
150
+ // all dependencies are resolved, process the node
151
+ const output = await this.processNode(node, this.logger)
152
+
153
+ if (signal?.aborted) {
154
+ this.logger.warn({ nodeId }, "processing aborted, skipping output")
155
+ return
86
156
  }
87
157
 
88
- return await options.process(item, outputs, logger)
89
- }
158
+ // update the dependent map
159
+ for (const depId of stackItem.dependencies) {
160
+ let dependantSet = this.dependentMap.get(depId)
161
+ if (!dependantSet) {
162
+ dependantSet = new Set()
163
+ this.dependentMap.set(depId, dependantSet)
164
+ }
90
165
 
91
- const promise = resolve().then(result => {
92
- if (backend.promiseCache.get(itemId) === promise) {
93
- // persist the result to the state if provided
94
- // ignore if the promise has been replaced (which means this calculation was invalidated)
95
- backend.setOutput?.(itemId, result)
96
- outputs.set(itemId, result)
166
+ dependantSet.add(nodeId)
97
167
  }
98
168
 
99
- return result
100
- })
169
+ this.outputMap.set(nodeId, output)
170
+ this.outputHandler?.(nodeId, output)
101
171
 
102
- backend.promiseCache.set(itemId, promise)
103
- return promise
104
- }
172
+ stack.pop()
173
+ }
105
174
 
106
- return id => resolver(id, [])
175
+ this.workset.delete(rootNodeId)
176
+ }
107
177
  }
108
-
109
- factory.factoryName = options.name
110
- return factory
111
178
  }
@@ -1,10 +1,10 @@
1
- import type { ComponentModel, InstanceModel } from "@highstate/contract"
2
1
  import type { InstanceState } from "../state"
3
2
  import type { ResolvedInstanceInput } from "./input"
3
+ import { isUnitModel, type ComponentModel, type InstanceModel } from "@highstate/contract"
4
4
  import { sha256 } from "crypto-hash"
5
- import { defineGraphResolver } from "./graph-resolver"
5
+ import { GraphResolver } from "./graph-resolver"
6
6
 
7
- export type InputHashResolverInput = {
7
+ export type InputHashNode = {
8
8
  instance: InstanceModel
9
9
  component: ComponentModel
10
10
  resolvedInputs: Record<string, ResolvedInstanceInput[]>
@@ -12,7 +12,7 @@ export type InputHashResolverInput = {
12
12
  sourceHash: string | undefined
13
13
  }
14
14
 
15
- export type InputHashResolverOutput = {
15
+ export type InputHashOutput = {
16
16
  inputHash: string
17
17
  outputHash: string
18
18
  }
@@ -20,14 +20,8 @@ export type InputHashResolverOutput = {
20
20
  /**
21
21
  * Resolves the hash of the instance based on its args, resolved input hashes, source hash, and the output hash.
22
22
  */
23
- export const createInputHashResolver = defineGraphResolver<
24
- InputHashResolverInput,
25
- InputHashResolverOutput
26
- >({
27
- name: "input-hash-resolver",
28
- getNodeId: node => node.instance.id,
29
-
30
- getNodeDependencies({ resolvedInputs }) {
23
+ export class InputHashResolver extends GraphResolver<InputHashNode, InputHashOutput> {
24
+ getNodeDependencies({ resolvedInputs }: InputHashNode): string[] {
31
25
  const dependencies: string[] = []
32
26
 
33
27
  for (const inputs of Object.values(resolvedInputs ?? {})) {
@@ -37,13 +31,24 @@ export const createInputHashResolver = defineGraphResolver<
37
31
  }
38
32
 
39
33
  return dependencies
40
- },
34
+ }
41
35
 
42
- async process({ instance, component, resolvedInputs, sourceHash, state }, dependencies) {
36
+ async processNode({
37
+ instance,
38
+ component,
39
+ resolvedInputs,
40
+ sourceHash,
41
+ state,
42
+ }: InputHashNode): Promise<InputHashOutput> {
43
43
  let sink = component.definitionHash + JSON.stringify(instance.args ?? {})
44
44
 
45
45
  if (sourceHash) {
46
46
  sink += sourceHash
47
+ } else if (isUnitModel(component)) {
48
+ this.logger.warn(
49
+ { instanceId: instance.id },
50
+ "missing source hash for unit model, this may lead to incorrect input hash",
51
+ )
47
52
  }
48
53
 
49
54
  const sortedInputs = Object.entries(resolvedInputs)
@@ -61,7 +66,7 @@ export const createInputHashResolver = defineGraphResolver<
61
66
  instanceIds.sort()
62
67
 
63
68
  for (const instanceId of instanceIds) {
64
- const dependency = dependencies.get(instanceId)
69
+ const dependency = this.outputs.get(instanceId)
65
70
  if (!dependency) continue
66
71
 
67
72
  sink += dependency.inputHash
@@ -73,5 +78,5 @@ export const createInputHashResolver = defineGraphResolver<
73
78
  inputHash: await sha256(sink),
74
79
  outputHash: state?.outputHash ?? "",
75
80
  }
76
- },
77
- })
81
+ }
82
+ }
@@ -7,9 +7,9 @@ import {
7
7
  type InstanceModel,
8
8
  } from "@highstate/contract"
9
9
  import { fromEntries, mapValues } from "remeda"
10
- import { defineGraphResolver } from "./graph-resolver"
10
+ import { GraphResolver } from "./graph-resolver"
11
11
 
12
- export type InputResolverInput =
12
+ export type InputResolverNode =
13
13
  | {
14
14
  kind: "instance"
15
15
  instance: InstanceModel
@@ -28,6 +28,7 @@ export type ResolvedInstanceInput = {
28
28
  export type InputResolverOutput =
29
29
  | {
30
30
  kind: "instance"
31
+ instance: InstanceModel
31
32
  component: ComponentModel
32
33
  resolvedInputs: Record<string, ResolvedInstanceInput[]>
33
34
  resolvedOutputs: Record<string, InstanceInput[]> | undefined
@@ -42,17 +43,16 @@ export type InputResolverOutput =
42
43
  /**
43
44
  * Resolves the all recursive instance and hub inputs based on its direct inputs and injected inputs.
44
45
  */
45
- export const createInputResolver = defineGraphResolver<InputResolverInput, InputResolverOutput>({
46
- name: "input-resolver",
47
- getNodeId(node) {
48
- if (node.kind === "hub") {
49
- return `hub:${node.hub.id}`
50
- }
51
-
52
- return `instance:${node.instance.id}`
53
- },
54
-
55
- getNodeDependencies(node) {
46
+ export class InputResolver extends GraphResolver<InputResolverNode, InputResolverOutput> {
47
+ // constructor(
48
+ // nodes: ReadonlyMap<string, InputResolverNode>,
49
+ // logger: Logger,
50
+ // outputHandler?: ResolverOutputHandler<InputResolverOutput>,
51
+ // ) {
52
+ // super(nodes, logger, outputHandler)
53
+ // }
54
+
55
+ getNodeDependencies(node: InputResolverNode): string[] {
56
56
  const dependencies: string[] = []
57
57
 
58
58
  if (node.kind === "hub") {
@@ -84,11 +84,11 @@ export const createInputResolver = defineGraphResolver<InputResolverInput, Input
84
84
  }
85
85
 
86
86
  return dependencies
87
- },
87
+ }
88
88
 
89
- process(node, dependencies, logger) {
89
+ processNode(node: InputResolverNode): InputResolverOutput {
90
90
  const getHubOutput = (input: HubInput) => {
91
- const output = dependencies.get(`hub:${input.hubId}`)
91
+ const output = this.outputs.get(`hub:${input.hubId}`)
92
92
  if (!output) {
93
93
  return { resolvedInputs: [] }
94
94
  }
@@ -101,7 +101,7 @@ export const createInputResolver = defineGraphResolver<InputResolverInput, Input
101
101
  }
102
102
 
103
103
  const getInstanceOutput = (input: InstanceInput) => {
104
- const output = dependencies.get(`instance:${input.instanceId}`)
104
+ const output = this.outputs.get(`instance:${input.instanceId}`)
105
105
  if (!output) {
106
106
  return {
107
107
  component: null,
@@ -122,7 +122,6 @@ export const createInputResolver = defineGraphResolver<InputResolverInput, Input
122
122
  }
123
123
 
124
124
  // resolve inputs for hub
125
-
126
125
  if (node.kind === "hub") {
127
126
  const hubResult: Map<string, ResolvedInstanceInput> = new Map()
128
127
 
@@ -135,7 +134,7 @@ export const createInputResolver = defineGraphResolver<InputResolverInput, Input
135
134
  const componentInput = component?.outputs[input.output]
136
135
 
137
136
  if (!componentInput) {
138
- logger.warn({ msg: "output not found in the component", input, component })
137
+ this.logger.warn({ msg: "output not found in the component", input, component })
139
138
  continue
140
139
  }
141
140
 
@@ -160,11 +159,12 @@ export const createInputResolver = defineGraphResolver<InputResolverInput, Input
160
159
  if (node.instance.resolvedInputs) {
161
160
  return {
162
161
  kind: "instance",
162
+ instance: node.instance,
163
163
  component: node.component,
164
164
  resolvedInputs: mapValues(node.instance.resolvedInputs, (inputs, inputName) => {
165
165
  const componentInput = node.component.inputs[inputName]
166
166
  if (!componentInput) {
167
- logger.warn({
167
+ this.logger.warn({
168
168
  msg: "input not found in the component",
169
169
  inputName,
170
170
  component: node.component,
@@ -181,7 +181,6 @@ export const createInputResolver = defineGraphResolver<InputResolverInput, Input
181
181
  }
182
182
 
183
183
  // resolve inputs for instance
184
-
185
184
  const resolvedInputsMap: Map<string, Map<string, ResolvedInstanceInput>> = new Map()
186
185
 
187
186
  const addInstanceResult = (inputName: string, input: ResolvedInstanceInput) => {
@@ -197,14 +196,18 @@ export const createInputResolver = defineGraphResolver<InputResolverInput, Input
197
196
  const addInstanceInput = (inputName: string, input: InstanceInput) => {
198
197
  const componentInput = node.component.inputs[inputName]
199
198
  if (!componentInput) {
200
- logger.warn({ msg: "input not found in the component", input, component: node.component })
199
+ this.logger.warn({
200
+ msg: "input not found in the component",
201
+ input,
202
+ component: node.component,
203
+ })
201
204
  return
202
205
  }
203
206
 
204
207
  const { component, resolvedOutputs } = getInstanceOutput(input)
205
208
 
206
209
  if (!component) {
207
- logger.warn({ instanceId: node.instance.id, input }, "no output found for the input")
210
+ this.logger.warn({ instanceId: node.instance.id, input }, "no output found for the input")
208
211
  return
209
212
  }
210
213
 
@@ -219,7 +222,6 @@ export const createInputResolver = defineGraphResolver<InputResolverInput, Input
219
222
  }
220
223
  } else {
221
224
  // if the instance is not evaluated, we a forced to use the input as is
222
-
223
225
  addInstanceResult(inputName, { input, type: componentInput.type })
224
226
  }
225
227
  }
@@ -272,14 +274,15 @@ export const createInputResolver = defineGraphResolver<InputResolverInput, Input
272
274
 
273
275
  return {
274
276
  kind: "instance",
277
+ instance: node.instance,
275
278
  component: node.component,
276
279
  resolvedInputs,
277
280
  resolvedOutputs: node.instance.resolvedOutputs,
278
281
  resolvedInjectionInputs: Array.from(injectionInputs.values()),
279
282
  matchedInjectionInputs: Array.from(matchedInjectionInputs.values()),
280
283
  }
281
- },
282
- })
284
+ }
285
+ }
283
286
 
284
287
  export function getResolvedHubInputs(output: InputResolverOutput): ResolvedInstanceInput[] {
285
288
  if (output.kind !== "hub") {
@@ -1,10 +1,20 @@
1
- import type { GraphResolverFactory } from "./graph-resolver"
2
- import { createInputResolver } from "./input"
3
- import { createInputHashResolver } from "./input-hash"
4
- import { createValidationResolver } from "./validation"
1
+ import type { Logger } from "pino"
2
+ import type { GraphResolver, ResolverOutputHandler } from "./graph-resolver"
3
+ import { InputResolver } from "./input"
4
+ import { InputHashResolver } from "./input-hash"
5
+ import { ValidationResolver } from "./validation"
5
6
 
6
- export const resolverFactories: Record<string, GraphResolverFactory<any, any>> = {
7
- [createInputResolver.factoryName]: createInputResolver,
8
- [createInputHashResolver.factoryName]: createInputHashResolver,
9
- [createValidationResolver.factoryName]: createValidationResolver,
10
- }
7
+ export type GraphResolverType = "InputResolver" | "InputHashResolver" | "ValidationResolver"
8
+
9
+ export const resolverFactories = {
10
+ InputResolver,
11
+ InputHashResolver,
12
+ ValidationResolver,
13
+ } as Record<
14
+ GraphResolverType,
15
+ new (
16
+ nodes: ReadonlyMap<string, unknown>,
17
+ logger: Logger,
18
+ outputHandler?: ResolverOutputHandler<unknown>,
19
+ ) => GraphResolver<unknown, unknown>
20
+ >
@@ -1,15 +1,15 @@
1
1
  import type { ComponentModel, InstanceModel } from "@highstate/contract"
2
2
  import type { ResolvedInstanceInput } from "./input"
3
3
  import { Ajv } from "ajv"
4
- import { defineGraphResolver } from "./graph-resolver"
4
+ import { GraphResolver } from "./graph-resolver"
5
5
 
6
- export type ValidationResolverInput = {
6
+ export type ValidationNode = {
7
7
  instance: InstanceModel
8
8
  component: ComponentModel
9
9
  resolvedInputs: Record<string, ResolvedInstanceInput[]>
10
10
  }
11
11
 
12
- export type ValidationResolverOutput =
12
+ export type ValidationOutput =
13
13
  | {
14
14
  status: "ok"
15
15
  }
@@ -25,14 +25,8 @@ export type ValidationResolverOutput =
25
25
  /**
26
26
  * Validates the instance based on its arguments and inputs.
27
27
  */
28
- export const createValidationResolver = defineGraphResolver<
29
- ValidationResolverInput,
30
- ValidationResolverOutput
31
- >({
32
- name: "validation-resolver",
33
- getNodeId: node => node.instance.id,
34
-
35
- getNodeDependencies({ resolvedInputs }) {
28
+ export class ValidationResolver extends GraphResolver<ValidationNode, ValidationOutput> {
29
+ getNodeDependencies({ resolvedInputs }: ValidationNode): string[] {
36
30
  const dependencies: string[] = []
37
31
 
38
32
  for (const inputs of Object.values(resolvedInputs)) {
@@ -42,12 +36,12 @@ export const createValidationResolver = defineGraphResolver<
42
36
  }
43
37
 
44
38
  return dependencies
45
- },
39
+ }
46
40
 
47
- process({ instance, component, resolvedInputs }, dependencies, logger) {
41
+ processNode({ instance, component, resolvedInputs }: ValidationNode): ValidationOutput {
48
42
  const ajv = new Ajv({ strict: false })
49
43
 
50
- logger.debug({ instanceId: instance.id }, "validating instance")
44
+ this.logger.debug({ instanceId: instance.id }, "validating instance")
51
45
 
52
46
  for (const [name, argument] of Object.entries(component.args)) {
53
47
  if (!argument.required && !instance.args?.[name]) {
@@ -55,7 +49,7 @@ export const createValidationResolver = defineGraphResolver<
55
49
  }
56
50
 
57
51
  if (!ajv.validate(argument.schema, instance.args?.[name])) {
58
- logger.debug({ instanceId: instance.id, argumentName: name }, "invalid argument")
52
+ this.logger.debug({ instanceId: instance.id, argumentName: name }, "invalid argument")
59
53
 
60
54
  return {
61
55
  status: "invalid-args",
@@ -66,7 +60,7 @@ export const createValidationResolver = defineGraphResolver<
66
60
 
67
61
  for (const inputs of Object.values(resolvedInputs)) {
68
62
  for (const input of inputs) {
69
- const inputInstance = dependencies.get(input.input.instanceId)
63
+ const inputInstance = this.outputs.get(input.input.instanceId)
70
64
  if (inputInstance?.status !== "ok") {
71
65
  return {
72
66
  status: "invalid-inputs",
@@ -90,5 +84,5 @@ export const createValidationResolver = defineGraphResolver<
90
84
  }
91
85
 
92
86
  return { status: "ok" }
93
- },
94
- })
87
+ }
88
+ }