@cosmicdrift/kumiko-framework 0.230.0 → 0.231.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/package.json +3 -3
- package/src/compliance/__tests__/sub-processors.test.ts +12 -12
- package/src/compliance/sub-processors.ts +11 -11
- package/src/engine/__tests__/steps-workflow.test.ts +146 -3
- package/src/engine/index.ts +1 -0
- package/src/engine/steps/_event-match.ts +98 -0
- package/src/engine/steps/wait-for-event.ts +9 -5
- package/src/files/__tests__/file-handle.test.ts +26 -0
- package/src/files/file-handle.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.231.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
"./package.json": "./package.json"
|
|
199
199
|
},
|
|
200
200
|
"dependencies": {
|
|
201
|
-
"@cosmicdrift/kumiko-types": "0.
|
|
201
|
+
"@cosmicdrift/kumiko-types": "0.231.0",
|
|
202
202
|
"bullmq": "^5.76.7",
|
|
203
203
|
"bun-types": "^1.3.13",
|
|
204
204
|
"hono": "^4.13.1",
|
|
@@ -214,7 +214,7 @@
|
|
|
214
214
|
"zod": "^4.4.3"
|
|
215
215
|
},
|
|
216
216
|
"devDependencies": {
|
|
217
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
217
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.231.0",
|
|
218
218
|
"bun-types": "^1.3.13",
|
|
219
219
|
"pino-pretty": "^13.1.3"
|
|
220
220
|
},
|
|
@@ -82,16 +82,6 @@ describe("KUMIKO_SUB_PROCESSORS", () => {
|
|
|
82
82
|
"sccRequired": false,
|
|
83
83
|
"status": "active",
|
|
84
84
|
},
|
|
85
|
-
{
|
|
86
|
-
"appliesTo": [
|
|
87
|
-
"all-tiers",
|
|
88
|
-
],
|
|
89
|
-
"name": "Heinlein Hosting (Mailbox.org)",
|
|
90
|
-
"optInOnly": false,
|
|
91
|
-
"region": "EU (Germany)",
|
|
92
|
-
"sccRequired": false,
|
|
93
|
-
"status": "active",
|
|
94
|
-
},
|
|
95
85
|
{
|
|
96
86
|
"appliesTo": [
|
|
97
87
|
"business",
|
|
@@ -101,7 +91,7 @@ describe("KUMIKO_SUB_PROCESSORS", () => {
|
|
|
101
91
|
"optInOnly": true,
|
|
102
92
|
"region": "US",
|
|
103
93
|
"sccRequired": true,
|
|
104
|
-
"status": "
|
|
94
|
+
"status": "active",
|
|
105
95
|
},
|
|
106
96
|
{
|
|
107
97
|
"appliesTo": [
|
|
@@ -111,7 +101,17 @@ describe("KUMIKO_SUB_PROCESSORS", () => {
|
|
|
111
101
|
"optInOnly": false,
|
|
112
102
|
"region": "Global (US-headquartered)",
|
|
113
103
|
"sccRequired": true,
|
|
114
|
-
"status": "
|
|
104
|
+
"status": "active",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"appliesTo": [
|
|
108
|
+
"all-tiers",
|
|
109
|
+
],
|
|
110
|
+
"name": "ALL-INKL.COM – Neue Medien Münnich",
|
|
111
|
+
"optInOnly": false,
|
|
112
|
+
"region": "EU (Germany)",
|
|
113
|
+
"sccRequired": false,
|
|
114
|
+
"status": "active",
|
|
115
115
|
},
|
|
116
116
|
]
|
|
117
117
|
`);
|
|
@@ -101,15 +101,6 @@ export const KUMIKO_SUB_PROCESSORS: readonly SubProcessor[] = [
|
|
|
101
101
|
appliesTo: ["standard", "business", "enterprise"],
|
|
102
102
|
status: "active",
|
|
103
103
|
},
|
|
104
|
-
{
|
|
105
|
-
name: "Heinlein Hosting (Mailbox.org)",
|
|
106
|
-
purpose: "Marketing Email Delivery",
|
|
107
|
-
region: "EU (Germany)",
|
|
108
|
-
dpa: "https://mailbox.org/de/datenschutzerklaerung",
|
|
109
|
-
addedAt: "2024-03-01",
|
|
110
|
-
appliesTo: ["all-tiers"],
|
|
111
|
-
status: "active",
|
|
112
|
-
},
|
|
113
104
|
{
|
|
114
105
|
name: "Anthropic PBC",
|
|
115
106
|
purpose: "AI Model Inference (L2 Composition Layer, AI-Foundation)",
|
|
@@ -120,7 +111,7 @@ export const KUMIKO_SUB_PROCESSORS: readonly SubProcessor[] = [
|
|
|
120
111
|
sccRequired: true,
|
|
121
112
|
optInOnly: true,
|
|
122
113
|
hipaaBaaAvailable: true,
|
|
123
|
-
status: "
|
|
114
|
+
status: "active",
|
|
124
115
|
},
|
|
125
116
|
{
|
|
126
117
|
name: "Stripe, Inc.",
|
|
@@ -130,7 +121,16 @@ export const KUMIKO_SUB_PROCESSORS: readonly SubProcessor[] = [
|
|
|
130
121
|
addedAt: "2026-06-01",
|
|
131
122
|
appliesTo: ["all-tiers"],
|
|
132
123
|
sccRequired: true,
|
|
133
|
-
status: "
|
|
124
|
+
status: "active",
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "ALL-INKL.COM – Neue Medien Münnich",
|
|
128
|
+
purpose: "Mailbox Hosting (support and contact mailboxes)",
|
|
129
|
+
region: "EU (Germany)",
|
|
130
|
+
dpa: "https://all-inkl.com/members/avv_muster_print.php",
|
|
131
|
+
addedAt: "2026-09-02",
|
|
132
|
+
appliesTo: ["all-tiers"],
|
|
133
|
+
status: "active",
|
|
134
134
|
},
|
|
135
135
|
];
|
|
136
136
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, mock } from "bun:test";
|
|
2
2
|
import { getStep } from "../define-step";
|
|
3
|
+
import { evaluateEventMatch } from "../steps/_event-match";
|
|
3
4
|
import {
|
|
4
5
|
SUSPEND_SENTINEL,
|
|
5
6
|
WORKFLOW_AGGREGATE_TYPE,
|
|
@@ -9,7 +10,7 @@ import {
|
|
|
9
10
|
import { buildRetryStep, calculateBackoff } from "../steps/retry";
|
|
10
11
|
import { buildWaitStep } from "../steps/wait";
|
|
11
12
|
import { buildWaitForEventStep } from "../steps/wait-for-event";
|
|
12
|
-
import type { PipelineCtx } from "../types/step";
|
|
13
|
+
import type { EventMatch, PipelineCtx } from "../types/step";
|
|
13
14
|
|
|
14
15
|
const mockUnsafeAppendEvent = mock();
|
|
15
16
|
|
|
@@ -96,10 +97,14 @@ describe("buildWaitForEventStep", () => {
|
|
|
96
97
|
expect(step.kind).toBe("workflow.waitForEvent");
|
|
97
98
|
});
|
|
98
99
|
|
|
99
|
-
it("accepts an optional match
|
|
100
|
+
it("accepts an optional match AST", () => {
|
|
101
|
+
const match: EventMatch = {
|
|
102
|
+
version: 1,
|
|
103
|
+
expr: { kind: "atom", path: ["email"], op: { kind: "eq", value: "test@test.com" } },
|
|
104
|
+
};
|
|
100
105
|
const step = buildWaitForEventStep({
|
|
101
106
|
event: "user.confirmed-email",
|
|
102
|
-
match
|
|
107
|
+
match,
|
|
103
108
|
timeout: "P7D",
|
|
104
109
|
});
|
|
105
110
|
expect(step.kind).toBe("workflow.waitForEvent");
|
|
@@ -138,6 +143,144 @@ describe("workflow.waitForEvent run", () => {
|
|
|
138
143
|
expect(typeof eventArg.payload.timeoutAt).toBe("string");
|
|
139
144
|
expect(eventArg.payload.workflowName).toBe("test-workflow");
|
|
140
145
|
});
|
|
146
|
+
|
|
147
|
+
it("omits the match key from the payload when no match is given", async () => {
|
|
148
|
+
const stepDef = getStep("workflow.waitForEvent");
|
|
149
|
+
expect(stepDef).toBeDefined();
|
|
150
|
+
|
|
151
|
+
await stepDef!.run({ event: "user.confirmed-email", timeout: "P7D" }, workflowCtx);
|
|
152
|
+
|
|
153
|
+
const eventArg = mockUnsafeAppendEvent.mock.calls[0]![0];
|
|
154
|
+
expect("match" in eventArg.payload).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("resolves a match resolver to a serializable AST that round-trips through JSON and evaluates against real payloads", async () => {
|
|
158
|
+
const stepDef = getStep("workflow.waitForEvent");
|
|
159
|
+
expect(stepDef).toBeDefined();
|
|
160
|
+
|
|
161
|
+
const result = await stepDef!.run(
|
|
162
|
+
{
|
|
163
|
+
event: "user.confirmed-email",
|
|
164
|
+
match: (ctx: PipelineCtx): EventMatch => ({
|
|
165
|
+
version: 1,
|
|
166
|
+
expr: {
|
|
167
|
+
kind: "atom",
|
|
168
|
+
path: ["email"],
|
|
169
|
+
op: { kind: "eq", value: (ctx.event.payload as { email: string }).email },
|
|
170
|
+
},
|
|
171
|
+
}),
|
|
172
|
+
timeout: "P7D",
|
|
173
|
+
},
|
|
174
|
+
workflowCtx,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(result).toBe(SUSPEND_SENTINEL);
|
|
178
|
+
const eventArg = mockUnsafeAppendEvent.mock.calls[0]![0];
|
|
179
|
+
const persistedMatch = eventArg.payload.match;
|
|
180
|
+
|
|
181
|
+
// Proves the persisted value is the resolved literal AST, not the resolver function.
|
|
182
|
+
expect(persistedMatch).toEqual({
|
|
183
|
+
version: 1,
|
|
184
|
+
expr: {
|
|
185
|
+
kind: "atom",
|
|
186
|
+
path: ["email"],
|
|
187
|
+
op: { kind: "eq", value: "test@example.com" },
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Proves serializability — the whole point of the AST switch — by round-tripping
|
|
192
|
+
// through JSON before feeding it to the evaluator, same as a rehydrated event would.
|
|
193
|
+
const roundTripped = JSON.parse(JSON.stringify(persistedMatch));
|
|
194
|
+
expect(evaluateEventMatch(roundTripped, { email: "test@example.com" })).toBe(true);
|
|
195
|
+
expect(evaluateEventMatch(roundTripped, { email: "someone-else@example.com" })).toBe(false);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("evaluateEventMatch", () => {
|
|
200
|
+
it("throws on an unsupported version", () => {
|
|
201
|
+
const stale = JSON.parse(
|
|
202
|
+
JSON.stringify({
|
|
203
|
+
version: 2,
|
|
204
|
+
expr: { kind: "atom", path: ["x"], op: { kind: "eq", value: 1 } },
|
|
205
|
+
}),
|
|
206
|
+
);
|
|
207
|
+
expect(() => evaluateEventMatch(stale, {})).toThrow(/version/i);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("throws on an unrecognized expr kind", () => {
|
|
211
|
+
const bad = JSON.parse(JSON.stringify({ version: 1, expr: { kind: "not-a-kind" } }));
|
|
212
|
+
expect(() => evaluateEventMatch(bad, {})).toThrow(/kind/i);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("throws on an unrecognized op kind", () => {
|
|
216
|
+
const bad = JSON.parse(
|
|
217
|
+
JSON.stringify({
|
|
218
|
+
version: 1,
|
|
219
|
+
expr: { kind: "atom", path: ["x"], op: { kind: "not-an-op" } },
|
|
220
|
+
}),
|
|
221
|
+
);
|
|
222
|
+
expect(() => evaluateEventMatch(bad, {})).toThrow(/kind/i);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("and with empty nodes is true, or with empty nodes is false", () => {
|
|
226
|
+
expect(evaluateEventMatch({ version: 1, expr: { kind: "and", nodes: [] } }, {})).toBe(true);
|
|
227
|
+
expect(evaluateEventMatch({ version: 1, expr: { kind: "or", nodes: [] } }, {})).toBe(false);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it("combines atoms with and/or", () => {
|
|
231
|
+
const match: EventMatch = {
|
|
232
|
+
version: 1,
|
|
233
|
+
expr: {
|
|
234
|
+
kind: "and",
|
|
235
|
+
nodes: [
|
|
236
|
+
{ kind: "atom", path: ["status"], op: { kind: "eq", value: "confirmed" } },
|
|
237
|
+
{ kind: "atom", path: ["tier"], op: { kind: "in", values: ["gold", "platinum"] } },
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
expect(evaluateEventMatch(match, { status: "confirmed", tier: "gold" })).toBe(true);
|
|
242
|
+
expect(evaluateEventMatch(match, { status: "confirmed", tier: "silver" })).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("ne matches when the resolved value differs", () => {
|
|
246
|
+
const match: EventMatch = {
|
|
247
|
+
version: 1,
|
|
248
|
+
expr: { kind: "atom", path: ["status"], op: { kind: "ne", value: "cancelled" } },
|
|
249
|
+
};
|
|
250
|
+
expect(evaluateEventMatch(match, { status: "confirmed" })).toBe(true);
|
|
251
|
+
expect(evaluateEventMatch(match, { status: "cancelled" })).toBe(false);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("compares numbers with gte", () => {
|
|
255
|
+
const match: EventMatch = {
|
|
256
|
+
version: 1,
|
|
257
|
+
expr: { kind: "atom", path: ["amount"], op: { kind: "gte", value: 100 } },
|
|
258
|
+
};
|
|
259
|
+
expect(evaluateEventMatch(match, { amount: 150 })).toBe(true);
|
|
260
|
+
expect(evaluateEventMatch(match, { amount: 50 })).toBe(false);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("does not match a comparison op when the payload value's type differs from the operand", () => {
|
|
264
|
+
const match: EventMatch = {
|
|
265
|
+
version: 1,
|
|
266
|
+
expr: { kind: "atom", path: ["amount"], op: { kind: "gt", value: 100 } },
|
|
267
|
+
};
|
|
268
|
+
expect(evaluateEventMatch(match, { amount: "150" })).toBe(false);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("resolves nested paths and treats a path running into a non-object as unmatched", () => {
|
|
272
|
+
const match: EventMatch = {
|
|
273
|
+
version: 1,
|
|
274
|
+
expr: {
|
|
275
|
+
kind: "atom",
|
|
276
|
+
path: ["user", "email"],
|
|
277
|
+
op: { kind: "eq", value: "a@b.com" },
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
expect(evaluateEventMatch(match, { user: { email: "a@b.com" } })).toBe(true);
|
|
281
|
+
expect(evaluateEventMatch(match, { user: "not-an-object" })).toBe(false);
|
|
282
|
+
expect(evaluateEventMatch(match, {})).toBe(false);
|
|
283
|
+
});
|
|
141
284
|
});
|
|
142
285
|
|
|
143
286
|
describe("buildRetryStep", () => {
|
package/src/engine/index.ts
CHANGED
|
@@ -247,6 +247,7 @@ export {
|
|
|
247
247
|
} from "./screen-helpers";
|
|
248
248
|
export type { TransitionGraph } from "./state-machine";
|
|
249
249
|
export { defineTransitions, guardTransition } from "./state-machine";
|
|
250
|
+
export { evaluateEventMatch } from "./steps/_event-match";
|
|
250
251
|
export {
|
|
251
252
|
SUSPEND_SENTINEL,
|
|
252
253
|
WORKFLOW_AGGREGATE_TYPE,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Evaluator for the persisted r.step.waitForEvent `match` AST (EventMatch).
|
|
2
|
+
// Runtime-only: kept in the framework package (not packages/types) so it
|
|
3
|
+
// stays out of the type graph, same steps/-vs-packages/types split as
|
|
4
|
+
// _duration-utils.ts.
|
|
5
|
+
|
|
6
|
+
import type { EventMatch, EventMatchExpr, EventMatchOp } from "../types/step";
|
|
7
|
+
|
|
8
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
9
|
+
return typeof value === "object" && value !== null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolvePath(payload: unknown, path: readonly string[]): unknown {
|
|
13
|
+
let current: unknown = payload;
|
|
14
|
+
for (const segment of path) {
|
|
15
|
+
if (!isRecord(current)) return undefined;
|
|
16
|
+
current = current[segment];
|
|
17
|
+
}
|
|
18
|
+
return current;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function compareOrdered(
|
|
22
|
+
actual: number | string,
|
|
23
|
+
expected: number | string,
|
|
24
|
+
kind: "gt" | "gte" | "lt" | "lte",
|
|
25
|
+
): boolean {
|
|
26
|
+
if (typeof actual === "number" && typeof expected === "number") {
|
|
27
|
+
switch (kind) {
|
|
28
|
+
case "gt":
|
|
29
|
+
return actual > expected;
|
|
30
|
+
case "gte":
|
|
31
|
+
return actual >= expected;
|
|
32
|
+
case "lt":
|
|
33
|
+
return actual < expected;
|
|
34
|
+
case "lte":
|
|
35
|
+
return actual <= expected;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
if (typeof actual === "string" && typeof expected === "string") {
|
|
39
|
+
switch (kind) {
|
|
40
|
+
case "gt":
|
|
41
|
+
return actual > expected;
|
|
42
|
+
case "gte":
|
|
43
|
+
return actual >= expected;
|
|
44
|
+
case "lt":
|
|
45
|
+
return actual < expected;
|
|
46
|
+
case "lte":
|
|
47
|
+
return actual <= expected;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function evaluateOp(op: EventMatchOp, actual: unknown): boolean {
|
|
54
|
+
switch (op.kind) {
|
|
55
|
+
case "eq":
|
|
56
|
+
return actual === op.value;
|
|
57
|
+
case "ne":
|
|
58
|
+
return actual !== op.value;
|
|
59
|
+
case "in":
|
|
60
|
+
return op.values.some((value) => value === actual);
|
|
61
|
+
case "gt":
|
|
62
|
+
case "gte":
|
|
63
|
+
case "lt":
|
|
64
|
+
case "lte":
|
|
65
|
+
// A payload value of a different primitive kind than the operand
|
|
66
|
+
// doesn't match — bad input, not a malformed AST.
|
|
67
|
+
if (typeof actual !== "number" && typeof actual !== "string") return false;
|
|
68
|
+
return compareOrdered(actual, op.value, op.kind);
|
|
69
|
+
default: {
|
|
70
|
+
// op.kind comes from a persisted event, not from this call's static
|
|
71
|
+
// type — an unrecognized kind means a format we don't understand.
|
|
72
|
+
const unrecognized: { readonly kind: string } = op;
|
|
73
|
+
throw new Error(`Unknown EventMatchOp kind "${unrecognized.kind}"`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function evaluateExpr(expr: EventMatchExpr, payload: unknown): boolean {
|
|
79
|
+
switch (expr.kind) {
|
|
80
|
+
case "and":
|
|
81
|
+
return expr.nodes.every((node) => evaluateExpr(node, payload));
|
|
82
|
+
case "or":
|
|
83
|
+
return expr.nodes.some((node) => evaluateExpr(node, payload));
|
|
84
|
+
case "atom":
|
|
85
|
+
return evaluateOp(expr.op, resolvePath(payload, expr.path));
|
|
86
|
+
default: {
|
|
87
|
+
const unrecognized: { readonly kind: string } = expr;
|
|
88
|
+
throw new Error(`Unknown EventMatchExpr kind "${unrecognized.kind}"`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function evaluateEventMatch(match: EventMatch, payload: unknown): boolean {
|
|
94
|
+
if (match.version !== 1) {
|
|
95
|
+
throw new Error(`Unsupported EventMatch version ${JSON.stringify(match.version)} — expected 1`);
|
|
96
|
+
}
|
|
97
|
+
return evaluateExpr(match.expr, payload);
|
|
98
|
+
}
|
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
// it writes workflow.step.resumed with the matched event's data.
|
|
9
9
|
//
|
|
10
10
|
// The `match` resolver is optional — when omitted, any event of the given
|
|
11
|
-
// type resumes the workflow. When provided, it
|
|
12
|
-
//
|
|
11
|
+
// type resumes the workflow. When provided, it resolves to a serializable
|
|
12
|
+
// EventMatch AST (not a closure) that is persisted into the waiting event's
|
|
13
|
+
// payload; the Resume-Loop evaluates it via evaluateEventMatch against the
|
|
14
|
+
// candidate event's payload.
|
|
13
15
|
|
|
14
16
|
import { defineStep } from "../define-step";
|
|
15
|
-
import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
|
|
17
|
+
import type { EventMatch, PipelineCtx, StepInstance, StepResolver } from "../types/step";
|
|
16
18
|
import { addDuration } from "./_duration-utils";
|
|
17
|
-
import { resolveRequired } from "./_resolver-utils";
|
|
19
|
+
import { resolveOptional, resolveRequired } from "./_resolver-utils";
|
|
18
20
|
import {
|
|
19
21
|
SUSPEND_SENTINEL,
|
|
20
22
|
WORKFLOW_AGGREGATE_TYPE,
|
|
@@ -23,7 +25,7 @@ import {
|
|
|
23
25
|
|
|
24
26
|
type WaitForEventArgs = {
|
|
25
27
|
readonly event: string;
|
|
26
|
-
readonly match?: StepResolver<
|
|
28
|
+
readonly match?: StepResolver<EventMatch>;
|
|
27
29
|
readonly timeout: StepResolver<string>;
|
|
28
30
|
};
|
|
29
31
|
|
|
@@ -40,6 +42,7 @@ defineStep<WaitForEventArgs, undefined | typeof SUSPEND_SENTINEL>({
|
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
const timeout = resolveRequired(args.timeout, ctx);
|
|
45
|
+
const resolvedMatch = resolveOptional(args.match, ctx);
|
|
43
46
|
|
|
44
47
|
const now = Temporal.Now.instant().toString();
|
|
45
48
|
const timeoutAt =
|
|
@@ -51,6 +54,7 @@ defineStep<WaitForEventArgs, undefined | typeof SUSPEND_SENTINEL>({
|
|
|
51
54
|
type: WORKFLOW_WAITING_FOR_EVENT_TYPE,
|
|
52
55
|
payload: {
|
|
53
56
|
eventType: args.event,
|
|
57
|
+
...(resolvedMatch && { match: resolvedMatch }),
|
|
54
58
|
timeoutAt,
|
|
55
59
|
stepIndex: ctx.workflow.stepIndex,
|
|
56
60
|
workflowName: ctx.workflow.workflowName,
|
|
@@ -90,6 +90,32 @@ describe("createFileContext", () => {
|
|
|
90
90
|
const h2 = files.ref("tenant/foo.pdf");
|
|
91
91
|
expect(Array.from(await h2.read())).toEqual([7]);
|
|
92
92
|
});
|
|
93
|
+
|
|
94
|
+
test("list delegates to the provider and returns its result", async () => {
|
|
95
|
+
const provider = createInMemoryFileProvider();
|
|
96
|
+
await provider.write("tenant/photo.jpg", new Uint8Array([1]));
|
|
97
|
+
await provider.write("tenant/photo.medium.jpg", new Uint8Array([2]));
|
|
98
|
+
await provider.write("other/photo.jpg", new Uint8Array([3]));
|
|
99
|
+
const files = createFileContext(() => Promise.resolve(provider));
|
|
100
|
+
|
|
101
|
+
const keys = await files.list("tenant/");
|
|
102
|
+
expect([...keys].sort()).toEqual(["tenant/photo.jpg", "tenant/photo.medium.jpg"].sort());
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("the provider is resolved once and memoized across ref() and list()", async () => {
|
|
106
|
+
const provider = createInMemoryFileProvider();
|
|
107
|
+
let resolveCount = 0;
|
|
108
|
+
const files = createFileContext(() => {
|
|
109
|
+
resolveCount++;
|
|
110
|
+
return Promise.resolve(provider);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
await files.ref("tenant/a.jpg").write(new Uint8Array([1]));
|
|
114
|
+
await files.list("tenant/");
|
|
115
|
+
await files.ref("tenant/b.jpg").write(new Uint8Array([2]));
|
|
116
|
+
|
|
117
|
+
expect(resolveCount).toBe(1);
|
|
118
|
+
});
|
|
93
119
|
});
|
|
94
120
|
|
|
95
121
|
describe("InMemoryFileProvider", () => {
|
package/src/files/file-handle.ts
CHANGED