@pikku/core 0.12.94 → 0.12.95
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 +136 -0
- package/dist/services/email-template.d.ts +43 -0
- package/dist/services/email-template.js +139 -0
- package/dist/services/http-personas.d.ts +6 -1
- package/dist/services/http-personas.js +4 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/wirings/agent/agent-prepare.d.ts +14 -0
- package/dist/wirings/agent/agent-prepare.js +24 -0
- package/dist/wirings/agent/index.d.ts +1 -1
- package/dist/wirings/agent/index.js +1 -1
- package/dist/wirings/scheduler/scheduler-runner.js +0 -1
- package/dist/wirings/virtual-user/index.d.ts +1 -0
- package/dist/wirings/virtual-user/index.js +1 -0
- package/dist/wirings/virtual-user/virtual-user-derive.js +9 -0
- package/dist/wirings/virtual-user/virtual-user-scaffold.d.ts +267 -0
- package/dist/wirings/virtual-user/virtual-user-scaffold.js +400 -0
- package/dist/wirings/workflow/index.d.ts +1 -0
- package/dist/wirings/workflow/index.js +1 -0
- package/dist/wirings/workflow/pikku-workflow-service.js +3 -9
- package/dist/wirings/workflow/workflow-queue-routing.d.ts +18 -0
- package/dist/wirings/workflow/workflow-queue-routing.js +35 -0
- package/dist/wirings/workflow/workflow-status-stream.d.ts +28 -0
- package/dist/wirings/workflow/workflow-status-stream.js +105 -0
- package/package.json +1 -1
- package/src/public-surface.json +17 -1
- package/src/services/email-template.test.ts +311 -0
- package/src/services/email-template.ts +254 -0
- package/src/services/http-personas.ts +10 -2
- package/src/services/index.ts +8 -0
- package/src/services/persona-sign-in.test.ts +22 -0
- package/src/wirings/agent/agent-helpers.test.ts +63 -0
- package/src/wirings/agent/agent-prepare.ts +25 -0
- package/src/wirings/agent/index.ts +1 -0
- package/src/wirings/scheduler/scheduler-runner.test.ts +178 -0
- package/src/wirings/scheduler/scheduler-runner.ts +0 -1
- package/src/wirings/virtual-user/index.ts +20 -0
- package/src/wirings/virtual-user/virtual-user-derive.test.ts +28 -0
- package/src/wirings/virtual-user/virtual-user-derive.ts +9 -0
- package/src/wirings/virtual-user/virtual-user-scaffold.test.ts +795 -0
- package/src/wirings/virtual-user/virtual-user-scaffold.ts +634 -0
- package/src/wirings/workflow/index.ts +4 -0
- package/src/wirings/workflow/pikku-workflow-service.test.ts +71 -2
- package/src/wirings/workflow/pikku-workflow-service.ts +5 -11
- package/src/wirings/workflow/workflow-child-run-session.test.ts +79 -0
- package/src/wirings/workflow/workflow-queue-routing.ts +44 -0
- package/src/wirings/workflow/workflow-status-stream.test.ts +354 -0
- package/src/wirings/workflow/workflow-status-stream.ts +144 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { CoreUserSession } from '../../types/core.types.js'
|
|
2
|
+
import type { PikkuChannel } from '../channel/channel.types.js'
|
|
3
|
+
import { assertWorkflowRunOwner } from './workflow-run-ownership.js'
|
|
4
|
+
import type { WorkflowRunService, WorkflowStatus } from './workflow.types.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The status stream behind the scaffolded workflow SSE routes.
|
|
8
|
+
*
|
|
9
|
+
* Two routes share it, and they differ by one thing: whether the caller is
|
|
10
|
+
* trusted with what the run produced. A user-facing frontend gets step names
|
|
11
|
+
* and statuses; an admin console also gets the output, the error and the child
|
|
12
|
+
* run ids. That is a parameter, not a second copy of the loop.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const TERMINAL: ReadonlySet<string> = new Set<WorkflowStatus>([
|
|
16
|
+
'completed',
|
|
17
|
+
'failed',
|
|
18
|
+
'cancelled',
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
const DEFAULT_POLL_INTERVAL_MS = 500
|
|
22
|
+
|
|
23
|
+
export interface WorkflowStatusStreamParams {
|
|
24
|
+
workflowRunService: WorkflowRunService
|
|
25
|
+
runId: string
|
|
26
|
+
channel: Pick<PikkuChannel<unknown, any>, 'send' | 'close'>
|
|
27
|
+
session: CoreUserSession | undefined
|
|
28
|
+
/**
|
|
29
|
+
* Whether to include what the run produced. Off for the user-facing route:
|
|
30
|
+
* a workflow's output and its error messages are internal detail, and a step
|
|
31
|
+
* that spawned a child run says so only to tooling that can follow it.
|
|
32
|
+
*/
|
|
33
|
+
detailed?: boolean
|
|
34
|
+
pollIntervalMs?: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Streams one run's progress until it reaches a terminal state.
|
|
39
|
+
*
|
|
40
|
+
* Polled rather than subscribed because a run's steps are written by whichever
|
|
41
|
+
* worker picked them up, in whichever process — there is no in-memory event to
|
|
42
|
+
* listen for that every deployment shape would deliver.
|
|
43
|
+
*
|
|
44
|
+
* Each poll sends only when something changed, compared by a hash of exactly
|
|
45
|
+
* what this stream reports. A run that sits on a slow step for a minute costs
|
|
46
|
+
* one message, not a hundred and twenty.
|
|
47
|
+
*/
|
|
48
|
+
export const streamWorkflowRunStatus = async ({
|
|
49
|
+
workflowRunService,
|
|
50
|
+
runId,
|
|
51
|
+
channel,
|
|
52
|
+
session,
|
|
53
|
+
detailed = false,
|
|
54
|
+
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
|
|
55
|
+
}: WorkflowStatusStreamParams): Promise<void> => {
|
|
56
|
+
let lastHash = ''
|
|
57
|
+
let initSent = false
|
|
58
|
+
|
|
59
|
+
const poll = async (): Promise<boolean> => {
|
|
60
|
+
const run = await workflowRunService.getRun(runId)
|
|
61
|
+
if (!run) {
|
|
62
|
+
await channel.close()
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
// Checked on every poll, not just the first: ownership is read from the run
|
|
66
|
+
// itself, and a stream that outlives a session should stop rather than keep
|
|
67
|
+
// reporting.
|
|
68
|
+
assertWorkflowRunOwner(run.wire, session)
|
|
69
|
+
|
|
70
|
+
const steps = await workflowRunService.getRunSteps(runId)
|
|
71
|
+
|
|
72
|
+
// A deterministic run knows its whole shape up front, so the client can
|
|
73
|
+
// draw every step — including the ones not started — before anything runs.
|
|
74
|
+
// A dynamic run has nothing to send here, and gets no init frame.
|
|
75
|
+
if (!initSent && run.deterministic) {
|
|
76
|
+
const statusByStep = new Map(
|
|
77
|
+
steps.map((step) => [step.stepName, step.status])
|
|
78
|
+
)
|
|
79
|
+
await channel.send({
|
|
80
|
+
type: 'init',
|
|
81
|
+
deterministic: true,
|
|
82
|
+
steps: (run.plannedSteps ?? []).map((step) => ({
|
|
83
|
+
stepName: step.stepName,
|
|
84
|
+
status: statusByStep.get(step.stepName) ?? 'pending',
|
|
85
|
+
})),
|
|
86
|
+
})
|
|
87
|
+
initSent = true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const hash = JSON.stringify({
|
|
91
|
+
s: run.status,
|
|
92
|
+
...(detailed ? { o: run.output } : {}),
|
|
93
|
+
steps: steps.map((step) => [step.stepName, step.status]),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (hash !== lastHash) {
|
|
97
|
+
lastHash = hash
|
|
98
|
+
await channel.send({
|
|
99
|
+
type: 'update',
|
|
100
|
+
status: run.status,
|
|
101
|
+
...(detailed ? { output: run.output, error: run.error } : {}),
|
|
102
|
+
steps: steps.map((step) => ({
|
|
103
|
+
stepName: step.stepName,
|
|
104
|
+
status: step.status,
|
|
105
|
+
...(detailed && step.childRunId
|
|
106
|
+
? { childRunId: step.childRunId }
|
|
107
|
+
: {}),
|
|
108
|
+
})),
|
|
109
|
+
})
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (TERMINAL.has(run.status)) {
|
|
113
|
+
await channel.send({ type: 'done' })
|
|
114
|
+
await channel.close()
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Every exit from here closes the channel, including the ones a throw takes:
|
|
121
|
+
// `assertWorkflowRunOwner` rejecting a session that lost access is exactly
|
|
122
|
+
// the case where the stream should end rather than be left hanging open.
|
|
123
|
+
try {
|
|
124
|
+
// A run that is already finished is answered without ever starting a timer.
|
|
125
|
+
if (!(await poll())) {
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// The next poll is scheduled when the previous one resolves rather than on
|
|
130
|
+
// a fixed interval. A timer that fires regardless would let two polls
|
|
131
|
+
// overlap on a slow store — both seeing `initSent` unset and sending the
|
|
132
|
+
// init frame twice, and racing `lastHash` into out-of-order updates.
|
|
133
|
+
while (
|
|
134
|
+
await new Promise<boolean>((resolve, reject) => {
|
|
135
|
+
setTimeout(() => void poll().then(resolve, reject), pollIntervalMs)
|
|
136
|
+
})
|
|
137
|
+
) {
|
|
138
|
+
// The condition is the whole loop: poll until it says to stop.
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
await channel.close()
|
|
142
|
+
throw error
|
|
143
|
+
}
|
|
144
|
+
}
|