@pikku/core 0.12.38 → 0.12.40
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/CHANGELOG.md +76 -0
- package/dist/errors/error-handler.d.ts +8 -0
- package/dist/errors/error-handler.js +8 -0
- package/dist/function/functions.types.d.ts +6 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/in-memory-queue-service.d.ts +9 -0
- package/dist/services/in-memory-queue-service.js +42 -11
- package/dist/services/in-memory-workflow-service.js +2 -0
- package/dist/services/workflow-service.d.ts +3 -0
- package/dist/testing/service-tests.js +35 -0
- package/dist/types/core.types.d.ts +7 -2
- package/dist/wirings/cli/cli-runner.js +12 -3
- package/dist/wirings/workflow/graph/graph-runner.js +51 -61
- package/dist/wirings/workflow/index.d.ts +2 -0
- package/dist/wirings/workflow/index.js +2 -0
- package/dist/wirings/workflow/pikku-workflow-service.d.ts +32 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +142 -132
- package/dist/wirings/workflow/run-timeline.d.ts +85 -0
- package/dist/wirings/workflow/run-timeline.js +153 -0
- package/dist/wirings/workflow/workflow.types.d.ts +2 -0
- package/package.json +1 -1
- package/src/errors/error-handler.ts +10 -0
- package/src/function/functions.types.ts +6 -2
- package/src/index.ts +1 -0
- package/src/services/in-memory-queue-service.test.ts +97 -0
- package/src/services/in-memory-queue-service.ts +51 -16
- package/src/services/in-memory-workflow-service.ts +2 -0
- package/src/services/workflow-service.ts +9 -0
- package/src/testing/service-tests.ts +65 -0
- package/src/types/core.types.ts +10 -2
- package/src/wirings/cli/cli-runner.ts +11 -3
- package/src/wirings/workflow/graph/graph-runner.test.ts +72 -0
- package/src/wirings/workflow/graph/graph-runner.ts +95 -86
- package/src/wirings/workflow/index.ts +14 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +13 -24
- package/src/wirings/workflow/pikku-workflow-service.ts +200 -151
- package/src/wirings/workflow/run-timeline.test.ts +211 -0
- package/src/wirings/workflow/run-timeline.ts +241 -0
- package/src/wirings/workflow/workflow-dispatch-durability.test.ts +3 -3
- package/src/wirings/workflow/workflow.types.ts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -565,14 +565,21 @@ export async function continueGraph(
|
|
|
565
565
|
}
|
|
566
566
|
}
|
|
567
567
|
|
|
568
|
-
|
|
568
|
+
/**
|
|
569
|
+
* Invoke a graph node's RPC with the graph + workflow wires, capturing any
|
|
570
|
+
* branch the node selects and persisting it. Shared by the queued
|
|
571
|
+
* (executeGraphStep) and inline (executeGraphNodeInline) executors so both build
|
|
572
|
+
* the wire and record the branch identically — only their child-workflow and
|
|
573
|
+
* onError handling differs around this call.
|
|
574
|
+
*/
|
|
575
|
+
async function invokeGraphNodeRpc(
|
|
569
576
|
workflowService: PikkuWorkflowService,
|
|
570
577
|
rpcService: any,
|
|
571
578
|
runId: string,
|
|
572
579
|
stepId: string,
|
|
573
580
|
nodeId: string,
|
|
574
581
|
rpcName: string,
|
|
575
|
-
|
|
582
|
+
input: any,
|
|
576
583
|
graphName: string
|
|
577
584
|
): Promise<any> {
|
|
578
585
|
const wireState: GraphWireState = {}
|
|
@@ -588,6 +595,28 @@ export async function executeGraphStep(
|
|
|
588
595
|
getState: () => workflowService.getRunState(runId),
|
|
589
596
|
}
|
|
590
597
|
|
|
598
|
+
const result = await rpcService.rpcWithWire(rpcName, input, {
|
|
599
|
+
graph: graphWire,
|
|
600
|
+
workflow: workflowService.createWorkflowWire(graphName, runId, rpcService),
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
if (wireState.branchKey) {
|
|
604
|
+
await workflowService.setBranchTaken(stepId, wireState.branchKey)
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
return result
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export async function executeGraphStep(
|
|
611
|
+
workflowService: PikkuWorkflowService,
|
|
612
|
+
rpcService: any,
|
|
613
|
+
runId: string,
|
|
614
|
+
stepId: string,
|
|
615
|
+
nodeId: string,
|
|
616
|
+
rpcName: string,
|
|
617
|
+
data: any,
|
|
618
|
+
graphName: string
|
|
619
|
+
): Promise<any> {
|
|
591
620
|
try {
|
|
592
621
|
let result: any
|
|
593
622
|
|
|
@@ -622,18 +651,16 @@ export async function executeGraphStep(
|
|
|
622
651
|
throw new ChildWorkflowStartedException(runId, stepId, childRunId)
|
|
623
652
|
}
|
|
624
653
|
} else {
|
|
625
|
-
result = await
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
if (wireState.branchKey) {
|
|
636
|
-
await workflowService.setBranchTaken(stepId, wireState.branchKey)
|
|
654
|
+
result = await invokeGraphNodeRpc(
|
|
655
|
+
workflowService,
|
|
656
|
+
rpcService,
|
|
657
|
+
runId,
|
|
658
|
+
stepId,
|
|
659
|
+
nodeId,
|
|
660
|
+
rpcName,
|
|
661
|
+
data,
|
|
662
|
+
graphName
|
|
663
|
+
)
|
|
637
664
|
}
|
|
638
665
|
|
|
639
666
|
return result
|
|
@@ -705,37 +732,30 @@ async function executeGraphNodeInline(
|
|
|
705
732
|
runId: string,
|
|
706
733
|
graphName: string,
|
|
707
734
|
nodeId: string,
|
|
735
|
+
instanceKey: string,
|
|
708
736
|
input: any,
|
|
709
|
-
nodes: Record<string, any
|
|
737
|
+
nodes: Record<string, any>,
|
|
738
|
+
fromStepName?: string
|
|
710
739
|
): Promise<void> {
|
|
711
740
|
const node = nodes[nodeId]
|
|
712
741
|
if (!node) return
|
|
713
742
|
|
|
714
743
|
const rpcName = node.rpcName
|
|
715
744
|
|
|
745
|
+
// Persist under the physical instance key (node, node#1 … for revisits) and
|
|
746
|
+
// record the predecessor — same as the queued path (queueGraphNode), so an
|
|
747
|
+
// inline graph run stores identical step rows + provenance.
|
|
716
748
|
const stepState = await workflowService.insertStepState(
|
|
717
749
|
runId,
|
|
718
|
-
|
|
750
|
+
instanceKey,
|
|
719
751
|
rpcName,
|
|
720
752
|
input,
|
|
721
|
-
{ retries: node.retries ?? 0, retryDelay: node.retryDelay }
|
|
753
|
+
{ retries: node.retries ?? 0, retryDelay: node.retryDelay },
|
|
754
|
+
fromStepName
|
|
722
755
|
)
|
|
723
756
|
|
|
724
757
|
await workflowService.setStepRunning(stepState.stepId)
|
|
725
758
|
|
|
726
|
-
const wireState: GraphWireState = {}
|
|
727
|
-
const graphWire: PikkuGraphWire = {
|
|
728
|
-
runId,
|
|
729
|
-
graphName,
|
|
730
|
-
nodeId,
|
|
731
|
-
branch: (key: string) => {
|
|
732
|
-
wireState.branchKey = key
|
|
733
|
-
},
|
|
734
|
-
setState: (name: string, value: unknown) =>
|
|
735
|
-
workflowService.updateRunState(runId, name, value),
|
|
736
|
-
getState: () => workflowService.getRunState(runId),
|
|
737
|
-
}
|
|
738
|
-
|
|
739
759
|
try {
|
|
740
760
|
let result: any
|
|
741
761
|
|
|
@@ -764,20 +784,15 @@ async function executeGraphNodeInline(
|
|
|
764
784
|
}
|
|
765
785
|
result = childRun?.output
|
|
766
786
|
} else {
|
|
767
|
-
result = await
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
runId,
|
|
772
|
-
rpcService
|
|
773
|
-
),
|
|
774
|
-
})
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
if (wireState.branchKey) {
|
|
778
|
-
await workflowService.setBranchTaken(
|
|
787
|
+
result = await invokeGraphNodeRpc(
|
|
788
|
+
workflowService,
|
|
789
|
+
rpcService,
|
|
790
|
+
runId,
|
|
779
791
|
stepState.stepId,
|
|
780
|
-
|
|
792
|
+
nodeId,
|
|
793
|
+
rpcName,
|
|
794
|
+
input,
|
|
795
|
+
graphName
|
|
781
796
|
)
|
|
782
797
|
}
|
|
783
798
|
|
|
@@ -811,8 +826,10 @@ async function executeGraphNodeInline(
|
|
|
811
826
|
runId,
|
|
812
827
|
graphName,
|
|
813
828
|
errorNodeId,
|
|
829
|
+
errorNodeId,
|
|
814
830
|
{ error: { message: (error as Error).message } },
|
|
815
|
-
nodes
|
|
831
|
+
nodes,
|
|
832
|
+
nodeId
|
|
816
833
|
)
|
|
817
834
|
)
|
|
818
835
|
)
|
|
@@ -831,20 +848,22 @@ async function continueGraphInline(
|
|
|
831
848
|
triggerInput: any,
|
|
832
849
|
entryNodeIds: string[]
|
|
833
850
|
): Promise<void> {
|
|
851
|
+
// Drive the run to completion in-process using the SAME planner as the queued
|
|
852
|
+
// path (continueGraph): each loop plans the next wave of transitions, executes
|
|
853
|
+
// them inline (vs queueGraphNode dispatch), then re-plans. Sharing the planner
|
|
854
|
+
// gives the inline path joins, cycle revisits and fromStepName provenance
|
|
855
|
+
// identical to the queue — instead of a second, weaker traversal.
|
|
834
856
|
while (true) {
|
|
835
857
|
const {
|
|
836
|
-
completedNodeIds: rawCompleted,
|
|
837
858
|
failedNodeIds: rawFailed,
|
|
838
|
-
branchKeys:
|
|
859
|
+
branchKeys: branchByStep,
|
|
860
|
+
completedNodeIds: rawCompleted,
|
|
839
861
|
} = await workflowService.getCompletedGraphState(runId)
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
)
|
|
845
|
-
const completedNodeIdSet = new Set(completedNodeIds)
|
|
862
|
+
// Validate step/branch names map to unambiguous nodes (planning keys
|
|
863
|
+
// physically; these calls only surface ambiguous template configs).
|
|
864
|
+
remapStepNamesToNodeIds(rawCompleted, nodes, graphName)
|
|
865
|
+
remapBranchKeys(branchByStep, nodes, graphName)
|
|
846
866
|
const failedNodeIds = remapStepNamesToNodeIds(rawFailed, nodes, graphName)
|
|
847
|
-
const branchKeys = remapBranchKeys(rawBranch, nodes, graphName)
|
|
848
867
|
|
|
849
868
|
if (failedNodeIds.length > 0) {
|
|
850
869
|
const failedNode = failedNodeIds[0]!
|
|
@@ -856,46 +875,31 @@ async function continueGraphInline(
|
|
|
856
875
|
return
|
|
857
876
|
}
|
|
858
877
|
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
for (const nodeId of completedNodeIds) {
|
|
862
|
-
const node = nodes[nodeId]
|
|
863
|
-
if (!node?.next) continue
|
|
864
|
-
|
|
865
|
-
const nextNodes = resolveNextFromConfig(node.next, branchKeys[nodeId])
|
|
866
|
-
for (const nextNode of nextNodes) {
|
|
867
|
-
candidateNodes.add(nextNode)
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
for (const entryId of entryNodeIds) {
|
|
872
|
-
candidateNodes.add(entryId)
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
if (candidateNodes.size === 0 && completedNodeIds.length > 0) {
|
|
876
|
-
await workflowService.updateRunStatus(runId, 'completed')
|
|
878
|
+
const run = await workflowService.getRun(runId)
|
|
879
|
+
if (run?.status === 'suspended') {
|
|
877
880
|
return
|
|
878
881
|
}
|
|
879
882
|
|
|
880
|
-
const
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
883
|
+
const instances = await workflowService.getStepInstances(runId)
|
|
884
|
+
const plan = planGraphTransitions(
|
|
885
|
+
nodes,
|
|
886
|
+
instances,
|
|
887
|
+
branchByStep,
|
|
888
|
+
entryNodeIds,
|
|
889
|
+
graphName
|
|
890
|
+
)
|
|
888
891
|
|
|
889
|
-
if (
|
|
890
|
-
if (
|
|
892
|
+
if (plan.toFire.length === 0) {
|
|
893
|
+
if (!plan.hasInFlight && !plan.blockedWaiting) {
|
|
891
894
|
await workflowService.updateRunStatus(runId, 'completed')
|
|
892
895
|
}
|
|
893
896
|
return
|
|
894
897
|
}
|
|
895
898
|
|
|
899
|
+
let executed = 0
|
|
896
900
|
await Promise.all(
|
|
897
|
-
|
|
898
|
-
const node = nodes[
|
|
901
|
+
plan.toFire.map(async (fire) => {
|
|
902
|
+
const node = nodes[fire.logical]
|
|
899
903
|
if (!node?.rpcName) return
|
|
900
904
|
|
|
901
905
|
const referencedNodeIds = extractReferencedNodeIds(node.input).filter(
|
|
@@ -905,21 +909,25 @@ async function continueGraphInline(
|
|
|
905
909
|
runId,
|
|
906
910
|
referencedNodeIds
|
|
907
911
|
)
|
|
908
|
-
|
|
909
912
|
const nodeResults = { trigger: triggerInput, ...fetchedResults }
|
|
910
913
|
const resolvedInput = resolveSerializedInput(node.input, nodeResults)
|
|
911
914
|
|
|
915
|
+
executed++
|
|
912
916
|
await executeGraphNodeInline(
|
|
913
917
|
workflowService,
|
|
914
918
|
rpcService,
|
|
915
919
|
runId,
|
|
916
920
|
graphName,
|
|
917
|
-
|
|
921
|
+
fire.logical,
|
|
922
|
+
fire.instanceKey,
|
|
918
923
|
resolvedInput,
|
|
919
|
-
nodes
|
|
924
|
+
nodes,
|
|
925
|
+
fire.fromStepName
|
|
920
926
|
)
|
|
921
927
|
})
|
|
922
928
|
)
|
|
929
|
+
// Nothing executable fired (e.g. nodes without an rpcName) → can't progress.
|
|
930
|
+
if (executed === 0) return
|
|
923
931
|
}
|
|
924
932
|
}
|
|
925
933
|
|
|
@@ -1002,6 +1010,7 @@ export async function runWorkflowGraph(
|
|
|
1002
1010
|
runId,
|
|
1003
1011
|
graphName,
|
|
1004
1012
|
nodeId,
|
|
1013
|
+
nodeId,
|
|
1005
1014
|
resolvedInput,
|
|
1006
1015
|
nodes
|
|
1007
1016
|
)
|
|
@@ -12,6 +12,20 @@ export {
|
|
|
12
12
|
} from './pikku-workflow-service.js'
|
|
13
13
|
export { deriveInvocationId, uuidv5 } from './workflow-invocation-id.js'
|
|
14
14
|
|
|
15
|
+
// Time-travel: reconstruct run state at any point from durable history
|
|
16
|
+
export {
|
|
17
|
+
buildRunTimeline,
|
|
18
|
+
reconstructStateAt,
|
|
19
|
+
reconstructFinalState,
|
|
20
|
+
} from './run-timeline.js'
|
|
21
|
+
export type {
|
|
22
|
+
RunTimeline,
|
|
23
|
+
RunTimelineEvent,
|
|
24
|
+
ReconstructedRunState,
|
|
25
|
+
ReconstructedStep,
|
|
26
|
+
RunPhase,
|
|
27
|
+
} from './run-timeline.js'
|
|
28
|
+
|
|
15
29
|
// Internal registration functions (used by generated code)
|
|
16
30
|
export { addWorkflow } from './dsl/workflow-runner.js'
|
|
17
31
|
|
|
@@ -126,19 +126,19 @@ describe('pikku-workflow-service run-level inline', () => {
|
|
|
126
126
|
describe('pikku-workflow-service per-function step dispatch', () => {
|
|
127
127
|
// Expose the protected dispatchStep so we can assert the dispatch decision
|
|
128
128
|
// in isolation: a step queues only when its function opts out of inline
|
|
129
|
-
// execution (
|
|
129
|
+
// execution (workflowQueued: true).
|
|
130
130
|
class TestWorkflowService extends InMemoryWorkflowService {
|
|
131
131
|
public callDispatchStep(rpcName: string) {
|
|
132
132
|
return this.dispatchStep('run-1', 'step-1', rpcName, {})
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
const setupFunction = (rpcName: string,
|
|
136
|
+
const setupFunction = (rpcName: string, workflowQueued?: boolean) => {
|
|
137
137
|
pikkuState(null, 'rpc', 'meta')[rpcName] = rpcName as any
|
|
138
138
|
pikkuState(null, 'function', 'meta')[rpcName] = {
|
|
139
139
|
pikkuFuncId: rpcName,
|
|
140
140
|
sessionless: true,
|
|
141
|
-
...(
|
|
141
|
+
...(workflowQueued === undefined ? {} : { workflowQueued }),
|
|
142
142
|
} as any
|
|
143
143
|
}
|
|
144
144
|
|
|
@@ -166,7 +166,7 @@ describe('pikku-workflow-service per-function step dispatch', () => {
|
|
|
166
166
|
cleanup('defaultInlineStep')
|
|
167
167
|
})
|
|
168
168
|
|
|
169
|
-
test('
|
|
169
|
+
test('workflowQueued: true step IS dispatched to the queue when a queueService exists', async () => {
|
|
170
170
|
const ws = new TestWorkflowService()
|
|
171
171
|
let queued = false
|
|
172
172
|
pikkuState(null, 'package', 'singletonServices', {
|
|
@@ -178,36 +178,25 @@ describe('pikku-workflow-service per-function step dispatch', () => {
|
|
|
178
178
|
},
|
|
179
179
|
} as any)
|
|
180
180
|
|
|
181
|
-
setupFunction('queuedStep',
|
|
181
|
+
setupFunction('queuedStep', true)
|
|
182
182
|
const dispatched = await ws.callDispatchStep('queuedStep')
|
|
183
|
-
assert.equal(dispatched, true, '
|
|
184
|
-
assert.equal(queued, true, '
|
|
183
|
+
assert.equal(dispatched, true, 'workflowQueued step should dispatch')
|
|
184
|
+
assert.equal(queued, true, 'workflowQueued step should queue')
|
|
185
185
|
cleanup('queuedStep')
|
|
186
186
|
})
|
|
187
187
|
|
|
188
|
-
test('
|
|
188
|
+
test('workflowQueued: true step throws when no queueService is configured', async () => {
|
|
189
189
|
const ws = new TestWorkflowService()
|
|
190
|
-
let warned = false
|
|
191
190
|
pikkuState(null, 'package', 'singletonServices', {
|
|
192
|
-
logger: {
|
|
193
|
-
error() {},
|
|
194
|
-
info() {},
|
|
195
|
-
warn: () => {
|
|
196
|
-
warned = true
|
|
197
|
-
},
|
|
198
|
-
debug() {},
|
|
199
|
-
},
|
|
191
|
+
logger: { error() {}, info() {}, warn() {}, debug() {} },
|
|
200
192
|
// no queueService
|
|
201
193
|
} as any)
|
|
202
194
|
|
|
203
|
-
setupFunction('queuedStepNoQueue',
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
false,
|
|
208
|
-
'falls back to inline when no queue service'
|
|
195
|
+
setupFunction('queuedStepNoQueue', true)
|
|
196
|
+
await assert.rejects(
|
|
197
|
+
() => ws.callDispatchStep('queuedStepNoQueue'),
|
|
198
|
+
/workflowQueued: true.*no queue service/i
|
|
209
199
|
)
|
|
210
|
-
assert.equal(warned, true, 'should warn about the misconfiguration')
|
|
211
200
|
cleanup('queuedStepNoQueue')
|
|
212
201
|
})
|
|
213
202
|
})
|