@t4h.framework/core 0.7.0 → 0.8.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/.ai/skills/framework-core/SKILL.md +103 -287
- package/.ai/skills/framework-workflow-creation/SKILL.md +108 -60
- package/.ai/skills/framework-workflow-testing/SKILL.md +165 -111
- package/README.md +123 -37
- package/dist/models/ExecutionContext.d.ts +8 -0
- package/dist/models/ExecutionContext.d.ts.map +1 -0
- package/dist/models/ExecutionContext.js +22 -0
- package/dist/models/ExecutionContext.js.map +1 -0
- package/dist/testing/ActivityTesting.d.ts +25 -0
- package/dist/testing/ActivityTesting.d.ts.map +1 -0
- package/dist/testing/ActivityTesting.js +74 -0
- package/dist/testing/ActivityTesting.js.map +1 -0
- package/dist/testing/NonDeterministicWorkflowError.d.ts +4 -0
- package/dist/testing/NonDeterministicWorkflowError.d.ts.map +1 -0
- package/dist/testing/NonDeterministicWorkflowError.js +7 -0
- package/dist/testing/NonDeterministicWorkflowError.js.map +1 -0
- package/dist/testing/StrictGuard.d.ts +4 -0
- package/dist/testing/StrictGuard.d.ts.map +1 -0
- package/dist/testing/StrictGuard.js +26 -0
- package/dist/testing/StrictGuard.js.map +1 -0
- package/dist/testing/TestClock.d.ts +12 -0
- package/dist/testing/TestClock.d.ts.map +1 -0
- package/dist/testing/TestClock.js +64 -0
- package/dist/testing/TestClock.js.map +1 -0
- package/dist/testing/TestWorkflowEnvironment.d.ts +27 -0
- package/dist/testing/TestWorkflowEnvironment.d.ts.map +1 -0
- package/dist/testing/TestWorkflowEnvironment.js +62 -0
- package/dist/testing/TestWorkflowEnvironment.js.map +1 -0
- package/dist/testing/WorkflowTesting.d.ts +1 -1
- package/dist/testing/async-activity-results.d.ts +12 -0
- package/dist/testing/async-activity-results.d.ts.map +1 -0
- package/dist/testing/async-activity-results.js +10 -0
- package/dist/testing/async-activity-results.js.map +1 -0
- package/dist/testing/create-mock-claim.d.ts +11 -0
- package/dist/testing/create-mock-claim.d.ts.map +1 -0
- package/dist/testing/create-mock-claim.js +26 -0
- package/dist/testing/create-mock-claim.js.map +1 -0
- package/dist/testing/create-mock-claims.d.ts +13 -0
- package/dist/testing/create-mock-claims.d.ts.map +1 -0
- package/dist/testing/create-mock-claims.js +8 -0
- package/dist/testing/create-mock-claims.js.map +1 -0
- package/dist/testing/create-test-env.d.ts +10 -0
- package/dist/testing/create-test-env.d.ts.map +1 -0
- package/dist/testing/create-test-env.js +7 -0
- package/dist/testing/create-test-env.js.map +1 -0
- package/dist/testing/exceptions/NonDeterministicWorkflowError.d.ts +4 -0
- package/dist/testing/exceptions/NonDeterministicWorkflowError.d.ts.map +1 -0
- package/dist/testing/exceptions/NonDeterministicWorkflowError.js +7 -0
- package/dist/testing/exceptions/NonDeterministicWorkflowError.js.map +1 -0
- package/dist/testing/index.d.ts +8 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +8 -1
- package/dist/testing/index.js.map +1 -1
- package/dist/testing/mock-async.d.ts +48 -0
- package/dist/testing/mock-async.d.ts.map +1 -0
- package/dist/testing/mock-async.js +58 -0
- package/dist/testing/mock-async.js.map +1 -0
- package/dist/testing/outcomes.d.ts +12 -0
- package/dist/testing/outcomes.d.ts.map +1 -0
- package/dist/testing/outcomes.js +10 -0
- package/dist/testing/outcomes.js.map +1 -0
- package/dist/testing/run-workflow.d.ts +22 -21
- package/dist/testing/run-workflow.d.ts.map +1 -1
- package/dist/testing/run-workflow.js +47 -23
- package/dist/testing/run-workflow.js.map +1 -1
- package/dist/testing/tools/create-mock-claim.d.ts +5 -0
- package/dist/testing/tools/create-mock-claim.d.ts.map +1 -0
- package/dist/testing/tools/create-mock-claim.js +26 -0
- package/dist/testing/tools/create-mock-claim.js.map +1 -0
- package/dist/testing/tools/create-mock-claims.d.ts +8 -0
- package/dist/testing/tools/create-mock-claims.d.ts.map +1 -0
- package/dist/testing/tools/create-mock-claims.js +5 -0
- package/dist/testing/tools/create-mock-claims.js.map +1 -0
- package/dist/testing/tools/create-test-env.d.ts +4 -0
- package/dist/testing/tools/create-test-env.d.ts.map +1 -0
- package/dist/testing/tools/create-test-env.js +7 -0
- package/dist/testing/tools/create-test-env.js.map +1 -0
- package/dist/testing/tools/mock-async.d.ts +20 -0
- package/dist/testing/tools/mock-async.d.ts.map +1 -0
- package/dist/testing/tools/mock-async.js +57 -0
- package/dist/testing/tools/mock-async.js.map +1 -0
- package/dist/testing/tools/resolve-activity.d.ts +11 -0
- package/dist/testing/tools/resolve-activity.d.ts.map +1 -0
- package/dist/testing/tools/resolve-activity.js +23 -0
- package/dist/testing/tools/resolve-activity.js.map +1 -0
- package/dist/testing/tools/resolve-async-activity.d.ts +15 -0
- package/dist/testing/tools/resolve-async-activity.d.ts.map +1 -0
- package/dist/testing/tools/resolve-async-activity.js +69 -0
- package/dist/testing/tools/resolve-async-activity.js.map +1 -0
- package/dist/testing/tools/run-with-clock.d.ts +3 -0
- package/dist/testing/tools/run-with-clock.d.ts.map +1 -0
- package/dist/testing/tools/run-with-clock.js +7 -0
- package/dist/testing/tools/run-with-clock.js.map +1 -0
- package/dist/testing/tools/run-workflow-ticks.d.ts +8 -0
- package/dist/testing/tools/run-workflow-ticks.d.ts.map +1 -0
- package/dist/testing/tools/run-workflow-ticks.js +26 -0
- package/dist/testing/tools/run-workflow-ticks.js.map +1 -0
- package/dist/testing/tools/run-workflow.d.ts +25 -0
- package/dist/testing/tools/run-workflow.d.ts.map +1 -0
- package/dist/testing/tools/run-workflow.js +25 -0
- package/dist/testing/tools/run-workflow.js.map +1 -0
- package/dist/testing/types.d.ts +19 -0
- package/dist/testing/types.d.ts.map +1 -0
- package/dist/testing/types.js +2 -0
- package/dist/testing/types.js.map +1 -0
- package/dist/testing/typings/AsyncActivityResult.d.ts +10 -0
- package/dist/testing/typings/AsyncActivityResult.d.ts.map +1 -0
- package/dist/testing/typings/AsyncActivityResult.js +2 -0
- package/dist/testing/typings/AsyncActivityResult.js.map +1 -0
- package/dist/testing/typings/ClaimConstructor.d.ts +2 -0
- package/dist/testing/typings/ClaimConstructor.d.ts.map +1 -0
- package/dist/testing/typings/ClaimConstructor.js +2 -0
- package/dist/testing/typings/ClaimConstructor.js.map +1 -0
- package/dist/testing/typings/MockAsyncActivity.d.ts +10 -0
- package/dist/testing/typings/MockAsyncActivity.d.ts.map +1 -0
- package/dist/testing/typings/MockAsyncActivity.js +2 -0
- package/dist/testing/typings/MockAsyncActivity.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,23 +1,32 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: framework-core
|
|
3
3
|
description: >-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
4
|
+
Build, register, and test T4H Framework workflows with @t4h.framework/core:
|
|
5
|
+
the Workflow and App author API, env/EnvDesign configuration, nested workflows
|
|
6
|
+
via startWorkflow, Serializable/Authorization manifests, and the in-memory
|
|
7
|
+
TestWorkflowEnvironment. Use when importing @t4h.framework/core to create a
|
|
8
|
+
Workflow or App, wire env, orchestrate nested workflows, or test workflows
|
|
9
|
+
with @t4h.framework/core/testing. Core composes with the other framework
|
|
10
|
+
packages (@t4h.framework/http, @t4h.framework/sleep, …) that do the real work.
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
# @t4h.framework/core
|
|
14
14
|
|
|
15
|
-
Core
|
|
15
|
+
Core is the **orchestration layer** of the T4H Framework. A workflow is a deterministic,
|
|
16
|
+
replay-safe async function you register in an **App**. This package gives you the author
|
|
17
|
+
API (`Workflow`, `App`, `env`, `startWorkflow`) and an in-memory test harness. The actual
|
|
18
|
+
side effects — HTTP requests, delays, storage — come from the companion packages you
|
|
19
|
+
compose inside the workflow body.
|
|
20
|
+
|
|
21
|
+
> **Building a workflow?** You only need the primitives on this page plus the package(s)
|
|
22
|
+
> that do the real work — `@t4h.framework/http` for HTTP, `@t4h.framework/sleep` for
|
|
23
|
+
> delays, and so on. You do **not** implement any of the framework's internal building
|
|
24
|
+
> blocks to write a workflow.
|
|
16
25
|
|
|
17
26
|
| Import | Purpose |
|
|
18
27
|
|--------|---------|
|
|
19
|
-
| `@t4h.framework/core` |
|
|
20
|
-
| `@t4h.framework/core/testing` | `
|
|
28
|
+
| `@t4h.framework/core` | `Workflow`, `App`, `env`, `EnvDesign`, `startWorkflow`, `Serializable`/`Binary`, `Authorization` |
|
|
29
|
+
| `@t4h.framework/core/testing` | `TestWorkflowEnvironment` — tests/local only |
|
|
21
30
|
|
|
22
31
|
Node `>=22`. Schemas use TypeBox (`typebox`). See [README.md](../../../README.md) for diagrams.
|
|
23
32
|
|
|
@@ -25,12 +34,11 @@ Node `>=22`. Schemas use TypeBox (`typebox`). See [README.md](../../../README.md
|
|
|
25
34
|
|
|
26
35
|
## Concepts
|
|
27
36
|
|
|
28
|
-
- **Workflow** — orchestration
|
|
29
|
-
- **
|
|
30
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
|
|
33
|
-
Async helpers on `AsyncActivity`: `this.pending()`, `this.done()`, `this.rejected()`.
|
|
37
|
+
- **Workflow** — an orchestration entrypoint: a plain async function wrapped in `new Workflow(...)`. It coordinates steps; it does not implement the steps.
|
|
38
|
+
- **App** — registers workflows so the runtime can discover and run them.
|
|
39
|
+
- **Replay & determinism** — the runtime may re-run a workflow from its recorded history. Keep the body deterministic: branch on inputs and on the results of the steps you call, never on `Date.now()`, `Math.random()`, or ambient state. Use framework primitives (e.g. `sleep`) when you need time to pass.
|
|
40
|
+
- **env** — configuration values supplied by the runtime; the single source for URLs, tokens, and tenants.
|
|
41
|
+
- **Composition** — external work is performed by calling primitives from the other framework packages inside the workflow body.
|
|
34
42
|
|
|
35
43
|
---
|
|
36
44
|
|
|
@@ -38,7 +46,7 @@ Async helpers on `AsyncActivity`: `this.pending()`, `this.done()`, `this.rejecte
|
|
|
38
46
|
|
|
39
47
|
```typescript
|
|
40
48
|
import Type from 'typebox'
|
|
41
|
-
import { Workflow
|
|
49
|
+
import { Workflow } from '@t4h.framework/core'
|
|
42
50
|
|
|
43
51
|
const bare = new Workflow(async () => ({ ok: true }))
|
|
44
52
|
|
|
@@ -53,141 +61,26 @@ const validated = new Workflow({ id: 'cashback/v1', schema }, async input => ({
|
|
|
53
61
|
}))
|
|
54
62
|
```
|
|
55
63
|
|
|
56
|
-
- `new Workflow(fn)` or `new Workflow(options, fn)` — bad overload throws `Invalid constructor arguments`.
|
|
57
|
-
- `WorkflowInput<T>` — from schema
|
|
58
|
-
- `workflow.validate` — Ajv when `schema` set;
|
|
59
|
-
- Optional `authorization` on workflow
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
## Invoking activities
|
|
64
|
-
|
|
65
|
-
```typescript
|
|
66
|
-
import { History } from '@t4h.framework/core'
|
|
67
|
-
|
|
68
|
-
const syncResult = await History.reconciler(MySyncActivity, arg1)
|
|
69
|
-
|
|
70
|
-
const handle = await History.reconciler(WaitAsyncActivity, 'order-1')
|
|
71
|
-
const out = await handle.wait()
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
- No history: `toInput` then `PushActivity` → runtime runs `run`/`bootstrap`.
|
|
75
|
-
- Replay: uses stored output; async returns `AsyncActivityAwaitable`; `wait()` throws `PushWaitAsyncActivity` if still `pending`.
|
|
76
|
-
- Must run inside `History.run` (workflow runtime / `WorkflowTesting` provide this).
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
## SyncActivity
|
|
81
|
-
|
|
82
|
-
```typescript
|
|
83
|
-
import { SyncActivity } from '@t4h.framework/core'
|
|
84
|
-
|
|
85
|
-
class CalculateCashbackActivity extends SyncActivity<
|
|
86
|
-
[transaction: { amount: number }], // TArgs
|
|
87
|
-
{ amount: number }, // TInput (Serializable)
|
|
88
|
-
number, // TSerializableOutput
|
|
89
|
-
number // TOutput
|
|
90
|
-
> {
|
|
91
|
-
public async toInput(transaction: { amount: number }) {
|
|
92
|
-
return { amount: transaction.amount }
|
|
93
|
-
}
|
|
94
|
-
public run(input: { amount: number }) {
|
|
95
|
-
return Math.floor(input.amount * 0.05)
|
|
96
|
-
}
|
|
97
|
-
public toOutput(output: number) {
|
|
98
|
-
return output
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
`toOutput(output, ...args)` receives original designer args.
|
|
104
|
-
|
|
105
|
-
---
|
|
106
|
-
|
|
107
|
-
## AsyncActivity
|
|
108
|
-
|
|
109
|
-
```typescript
|
|
110
|
-
import Type from 'typebox'
|
|
111
|
-
import { AsyncActivity, AsyncActivityResultPending } from '@t4h.framework/core'
|
|
112
|
-
|
|
113
|
-
const ApprovalSchema = Type.Object({ approved: Type.Boolean() })
|
|
114
|
-
|
|
115
|
-
class WaitForApprovalActivity extends AsyncActivity<
|
|
116
|
-
[orderId: string],
|
|
117
|
-
{ orderId: string },
|
|
118
|
-
{ approved: boolean },
|
|
119
|
-
{ approved: boolean },
|
|
120
|
-
{ requestId: string; orderId: string },
|
|
121
|
-
typeof ApprovalSchema
|
|
122
|
-
> {
|
|
123
|
-
public async toInput(orderId: string) {
|
|
124
|
-
return { orderId }
|
|
125
|
-
}
|
|
126
|
-
public async bootstrap(input: { orderId: string }) {
|
|
127
|
-
return this.pending({
|
|
128
|
-
payload: { requestId: 'req-123', orderId: input.orderId },
|
|
129
|
-
schema: ApprovalSchema,
|
|
130
|
-
timeoutAt: new Date(Date.now() + 3_600_000),
|
|
131
|
-
})
|
|
132
|
-
}
|
|
133
|
-
public async advance(
|
|
134
|
-
payload: Type.Static<typeof ApprovalSchema>,
|
|
135
|
-
bootstrap: AsyncActivityResultPending<{ requestId: string; orderId: string }>,
|
|
136
|
-
) {
|
|
137
|
-
return payload.approved
|
|
138
|
-
? this.done({ approved: true })
|
|
139
|
-
: this.rejected({ approved: false, requestId: bootstrap.payload?.requestId })
|
|
140
|
-
}
|
|
141
|
-
public toOutput(output: { approved: boolean }) {
|
|
142
|
-
return output
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
```
|
|
146
|
-
|
|
147
|
-
Results: `AsyncActivityResultPending` | `AsyncActivityResultFulfilled` | `AsyncActivityResultRejected`. Default `onTimeout` → `rejected({ reason: 'timeout' })`.
|
|
148
|
-
|
|
149
|
-
---
|
|
150
|
-
|
|
151
|
-
## Claims
|
|
152
|
-
|
|
153
|
-
```typescript
|
|
154
|
-
import { SyncActivity, Claim } from '@t4h.framework/core'
|
|
155
|
-
|
|
156
|
-
abstract class UserRepositoryClaim {
|
|
157
|
-
abstract findById(id: string): Promise<unknown>
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
class FetchUserActivity extends SyncActivity<[id: string], { id: string }, unknown, unknown> {
|
|
161
|
-
private readonly claims = new Claim({ repo: UserRepositoryClaim })
|
|
162
|
-
|
|
163
|
-
public async toInput(id: string) {
|
|
164
|
-
return { id }
|
|
165
|
-
}
|
|
166
|
-
public async run(input: { id: string }) {
|
|
167
|
-
return await this.claims.repo.findById(input.id)
|
|
168
|
-
}
|
|
169
|
-
public toOutput(output: unknown) {
|
|
170
|
-
return output
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
Provider: `{ provide: UserRepositoryClaim, value: impl }`. Built-ins: `GetWorkflowUniqueIdentifierClaim`, `GetActivityUniqueIdentifierClaim`, `WorkflowClaim` (for `startWorkflow`).
|
|
176
|
-
|
|
177
|
-
Exceptions: `ClaimKeyNotFoundException`, `ClaimConstructorNotFoundException`, `ClaimStorageNotActiveException`, `ClaimProviderNotFoundException`. Access `claims.*` only inside `Claim.run`.
|
|
64
|
+
- `new Workflow(fn)` or `new Workflow(options, fn)` — a bad overload throws `Invalid constructor arguments`.
|
|
65
|
+
- `WorkflowInput<T>` — derived from `schema`; `undefined` when no schema is set.
|
|
66
|
+
- `workflow.validate` — an Ajv validator when `schema` is set; otherwise `null`.
|
|
67
|
+
- Optional `authorization` on a workflow or app surfaces in `toManifest()`.
|
|
178
68
|
|
|
179
69
|
---
|
|
180
70
|
|
|
181
71
|
## Nested workflows
|
|
182
72
|
|
|
183
73
|
```typescript
|
|
184
|
-
import {
|
|
74
|
+
import { Workflow, startWorkflow } from '@t4h.framework/core'
|
|
75
|
+
import { childWorkflow } from './child-workflow.js'
|
|
185
76
|
|
|
186
|
-
const
|
|
187
|
-
|
|
77
|
+
export const parent = new Workflow({ id: 'parent' }, async input => {
|
|
78
|
+
const ref = await startWorkflow(childWorkflow, { ...input, parallel: false })
|
|
79
|
+
return ref.output // WorkflowRef: status 'fulfilled' | 'rejected', output Serializable
|
|
80
|
+
})
|
|
188
81
|
```
|
|
189
82
|
|
|
190
|
-
`
|
|
83
|
+
`parallel: true` returns as soon as the child starts; otherwise the parent waits for the child to settle.
|
|
191
84
|
|
|
192
85
|
---
|
|
193
86
|
|
|
@@ -204,200 +97,123 @@ const app = new App({
|
|
|
204
97
|
app.addWorkflow(anotherWorkflow) // throws on duplicate workflow.id
|
|
205
98
|
```
|
|
206
99
|
|
|
207
|
-
`toManifest()` includes workflows
|
|
100
|
+
`toManifest()` includes the registered workflows plus any `EnvDesign` entries.
|
|
208
101
|
|
|
209
102
|
---
|
|
210
103
|
|
|
211
104
|
## Environment
|
|
212
105
|
|
|
213
|
-
|
|
214
|
-
import { env, EnvDesign } from '@t4h.framework/core'
|
|
106
|
+
**Always configure environment access through `EnvDesign`** — not the raw `env` proxy. `EnvDesign` gives you typed, validated, self-documenting configuration that also feeds the app manifest automatically. Reach for raw `env` only for a one-off, dynamic key that can't be declared ahead of time.
|
|
215
107
|
|
|
216
|
-
|
|
217
|
-
|
|
108
|
+
### The standard: one `constants/environments.ts` with the `EnvDesign`
|
|
109
|
+
|
|
110
|
+
Declare **every** env var the project needs in a single `EnvDesign` and export it. Import that one object anywhere you need configuration.
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// src/constants/environments.ts
|
|
114
|
+
import { EnvDesign } from '@t4h.framework/core'
|
|
218
115
|
|
|
219
|
-
|
|
220
|
-
const config = EnvDesign({
|
|
116
|
+
export const environments = EnvDesign({
|
|
221
117
|
API_URL: { type: String },
|
|
222
118
|
RETRIES: { type: Number, defaultValue: '3' },
|
|
223
119
|
DEBUG: { type: Boolean, isOptional: true },
|
|
120
|
+
// custom parser: raw string -> your type
|
|
121
|
+
REAJUSTE_INDICES: { type: (raw: string) => JSON.parse(raw) as Record<string, number> },
|
|
224
122
|
})
|
|
225
123
|
```
|
|
226
124
|
|
|
227
|
-
---
|
|
228
|
-
|
|
229
|
-
## Serializable & Authorization
|
|
230
|
-
|
|
231
|
-
**Serializable:** primitives, `null`, `undefined`, `Binary`, `SerializableClass`, arrays/objects thereof. `Binary`: `contentType`, `contentLength`, `stream()`.
|
|
232
|
-
|
|
233
|
-
**Authorization:** subclass with `toManifest(): AuthorizationManifest` (`pathToModule`, optional `context`); attach to `Workflow` or `App`.
|
|
234
|
-
|
|
235
|
-
---
|
|
236
|
-
|
|
237
|
-
## Internals (runtime / unit tests)
|
|
238
|
-
|
|
239
125
|
```typescript
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
Internals.runWorkflow(workflow, {
|
|
243
|
-
input,
|
|
244
|
-
env: { API_URL: 'https://api.example.com' },
|
|
245
|
-
claims: [{ provide: UserRepositoryClaim, value: impl }],
|
|
246
|
-
history: [],
|
|
247
|
-
logs: [],
|
|
248
|
-
})
|
|
249
|
-
|
|
250
|
-
Internals.runActivity(FetchUserActivity, { env, claims }, a => a.run({ id: '123' }))
|
|
251
|
-
```
|
|
252
|
-
|
|
253
|
-
Stack: `Env.run` → `Claim.run` → `History.run` → `Logger.run` (workflows). **Test `advance` here** — not via `WorkflowTesting`.
|
|
254
|
-
|
|
255
|
-
---
|
|
256
|
-
|
|
257
|
-
## Testing (`@t4h.framework/core/testing`)
|
|
258
|
-
|
|
259
|
-
Test-only entrypoint — **not** part of the production runtime contract. Import from the subpath export:
|
|
126
|
+
// anywhere — a workflow, a helper, etc.
|
|
127
|
+
import { environments } from '../constants/environments.js'
|
|
260
128
|
|
|
261
|
-
|
|
262
|
-
|
|
129
|
+
const res = await http.get(`${environments.API_URL}/status`) // string
|
|
130
|
+
if (environments.RETRIES > 0) { /* number, parsed */ }
|
|
263
131
|
```
|
|
264
132
|
|
|
265
|
-
|
|
133
|
+
**Why this is safe and preferred:**
|
|
266
134
|
|
|
267
|
-
|
|
135
|
+
- `EnvDesign({...})` does **not** read `env` at construction — only when you access a key (`environments.API_URL`). So the top-level `export const environments = EnvDesign(...)` is safe at import time; the read + parse + validation happen lazily, inside a running workflow.
|
|
136
|
+
- Access **keys inside the workflow body / functions**, never at module top level — reading a key outside a run throws (`env` has no storage until the runtime executes the workflow).
|
|
137
|
+
- Each key is parsed to its declared `type` (`String` | `Number` | `Boolean` | a custom `(raw: string) => T` parser), `defaultValue` fills in when unset, and `isOptional: true` allows `undefined` instead of throwing.
|
|
138
|
+
- A required key that is unset throws `Environment variable X is not set` on access — fail-fast, no silent `undefined`.
|
|
139
|
+
- Declaring via `EnvDesign` registers the entry in the **app manifest** (`app.toManifest().env`) automatically — the runtime knows exactly which vars the app requires.
|
|
268
140
|
|
|
269
|
-
|
|
270
|
-
const output = await WorkflowTesting.run(workflow, input?, options?)
|
|
271
|
-
```
|
|
141
|
+
Never inline URLs, secrets, tokens, or tenants in workflow code; never read `process.env` directly.
|
|
272
142
|
|
|
273
|
-
|
|
274
|
-
|--------|------|---------|---------|
|
|
275
|
-
| `claims` | `readonly ClaimProvider[]` | `[]` | Concrete claim implementations for the run |
|
|
276
|
-
| `env` | `Env.Entries` | `{}` | String env vars (`Env.run`) |
|
|
277
|
-
| `mockAsyncActivity` | function \| outcome array | — | Required when any async activity returns `pending` |
|
|
278
|
-
| `logs` | `Log[]` | internal | Capture `console.log` / `console.error` once per run |
|
|
279
|
-
| `maxTicks` | `number` | `10_000` | Max activity-resolution loops before error |
|
|
143
|
+
---
|
|
280
144
|
|
|
281
|
-
|
|
145
|
+
## Serializable & Authorization
|
|
282
146
|
|
|
283
|
-
|
|
147
|
+
**Serializable:** primitives, `null`, `undefined`, `Binary`, `SerializableClass`, and arrays/objects thereof. Workflow input, output, and every value that crosses a step boundary must be `Serializable`. `Binary`: `contentType`, `contentLength`, `stream()`.
|
|
284
148
|
|
|
285
|
-
|
|
286
|
-
2. On **`PushActivity`**, runs the pushed activity in-process:
|
|
287
|
-
- **Sync:** `Internals.runActivity` → `run(input)` → append `{ type: 'sync', output }` to history.
|
|
288
|
-
- **Async:** real `bootstrap`; if already `done`/`rejected`, record and continue; if **`pending`**, apply `mockAsyncActivity` (throws if missing).
|
|
289
|
-
3. Re-runs the workflow with accumulated history until it returns normally or `maxTicks` is exceeded.
|
|
149
|
+
**Authorization:** subclass with `toManifest(): AuthorizationManifest` (`pathToModule`, optional `context`); attach to a `Workflow` or `App`.
|
|
290
150
|
|
|
291
|
-
|
|
151
|
+
---
|
|
292
152
|
|
|
293
|
-
|
|
153
|
+
## Testing (`@t4h.framework/core/testing`)
|
|
294
154
|
|
|
295
|
-
|
|
155
|
+
Test-only entrypoint — **not** part of the production runtime contract. `package.json` maps `"./testing"` → `dist/testing/index.js`.
|
|
296
156
|
|
|
297
157
|
```typescript
|
|
298
|
-
import {
|
|
299
|
-
import { History, type Log } from '@t4h.framework/core'
|
|
158
|
+
import { TestWorkflowEnvironment } from '@t4h.framework/core/testing'
|
|
300
159
|
|
|
301
|
-
const
|
|
302
|
-
const output = await
|
|
160
|
+
const env = TestWorkflowEnvironment.create({ now: 0 })
|
|
161
|
+
const output = await env.execute(workflow, input, {
|
|
303
162
|
env: { API_URL: 'https://example.com' },
|
|
304
|
-
claims: [
|
|
305
|
-
mockAsyncActivity: () => ({ status: 'fulfilled', output: true }),
|
|
306
|
-
logs,
|
|
163
|
+
claims: [/* provider implementations required by the packages your workflow uses */],
|
|
307
164
|
})
|
|
308
165
|
```
|
|
309
166
|
|
|
310
|
-
|
|
167
|
+
`create()` options:
|
|
311
168
|
|
|
312
|
-
|
|
169
|
+
| Option | Default | Purpose |
|
|
170
|
+
|--------|---------|---------|
|
|
171
|
+
| `now` | `0` | Pins `Date.now()` to this value at run start |
|
|
172
|
+
| `resolveActivityTimeouts` | `true` | Auto-advances the clock through time-based steps (e.g. `sleep`) and resolves them |
|
|
313
173
|
|
|
314
|
-
|
|
174
|
+
`execute()` options:
|
|
315
175
|
|
|
316
|
-
|
|
|
317
|
-
|
|
318
|
-
| `
|
|
319
|
-
| `
|
|
320
|
-
| `
|
|
176
|
+
| Option | Type | Purpose |
|
|
177
|
+
|--------|------|---------|
|
|
178
|
+
| `env` | `Env.Entries` | String env vars for the run |
|
|
179
|
+
| `claims` | `readonly ClaimProvider[]` | Provider implementations required by the packages the workflow calls (see each package's skill) |
|
|
180
|
+
| `mockAsyncActivity` | `MockAsyncActivity` | Controls the outcome of a time-/event-based step when auto-resolution doesn't cover it |
|
|
321
181
|
|
|
322
|
-
|
|
182
|
+
`execute()` returns the workflow output directly, then advances `env.clock` to reflect time consumed.
|
|
323
183
|
|
|
324
|
-
|
|
325
|
-
mockAsyncActivity: [
|
|
326
|
-
{ status: 'fulfilled', output: true },
|
|
327
|
-
{ status: 'rejected', reason: false },
|
|
328
|
-
]
|
|
329
|
-
```
|
|
184
|
+
**Replay validation is automatic.** After each `execute()`, the harness re-runs the workflow from the recorded history and from a fresh replay; a mismatch throws `NonDeterministicWorkflowError`.
|
|
330
185
|
|
|
331
|
-
|
|
186
|
+
Clock helpers:
|
|
332
187
|
|
|
333
188
|
```typescript
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (index === 1) return { status: 'timeout' }
|
|
338
|
-
return { status: 'rejected', reason: 'denied' }
|
|
339
|
-
}
|
|
189
|
+
env.clock.now() // current pinned timestamp (ms)
|
|
190
|
+
env.clock.advance(ms) // advance the pinned clock
|
|
191
|
+
env.clock.set(now) // set the pinned clock to a Date | number
|
|
340
192
|
```
|
|
341
193
|
|
|
342
|
-
|
|
194
|
+
> Wiring specific providers or controlling a specific step's outcome (e.g. an HTTP response or a `sleep` timeout) is documented in that package's skill — **framework-http**, **framework-sleep**, etc. `@t4h.framework/core/testing` exposes helpers those skills reference; you don't wire them by hand here.
|
|
343
195
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
**Sync workflow:**
|
|
347
|
-
|
|
348
|
-
```typescript
|
|
349
|
-
const workflow = new Workflow({ schema }, async input => {
|
|
350
|
-
const n = await History.reconciler(AddOneActivity, input.value)
|
|
351
|
-
return n * 10
|
|
352
|
-
})
|
|
353
|
-
await expect(WorkflowTesting.run(workflow, { value: 4 })).resolves.toBe(50)
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
**Capture logs:**
|
|
357
|
-
|
|
358
|
-
```typescript
|
|
359
|
-
const logs: Log[] = []
|
|
360
|
-
await WorkflowTesting.run(workflow, undefined, { logs })
|
|
361
|
-
expect(logs.map(entry => entry.args[0])).toEqual(['before activity', 'after activity'])
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
**Async fulfilled / rejected / timeout:**
|
|
365
|
-
|
|
366
|
-
```typescript
|
|
367
|
-
await WorkflowTesting.run(workflow, undefined, {
|
|
368
|
-
mockAsyncActivity: () => ({ status: 'fulfilled', output: true }),
|
|
369
|
-
})
|
|
370
|
-
await WorkflowTesting.run(workflow, undefined, {
|
|
371
|
-
mockAsyncActivity: () => ({ status: 'rejected', reason: false }),
|
|
372
|
-
})
|
|
373
|
-
await WorkflowTesting.run(workflow, undefined, {
|
|
374
|
-
mockAsyncActivity: () => ({ status: 'timeout' }),
|
|
375
|
-
})
|
|
376
|
-
```
|
|
377
|
-
|
|
378
|
-
**Vitest setup** — add `@t4h.framework/core` and `@t4h.framework/core/testing` as devDependencies; run with Vitest (`vitest run`). See `packages/core/src/testing/__tests__/WorkflowTesting.spec.ts`.
|
|
379
|
-
|
|
380
|
-
### Limits (use `Internals` instead)
|
|
381
|
-
|
|
382
|
-
- No real **`advance`** — test `advance` via `Internals.runActivity` on the async class.
|
|
383
|
-
- No **`PushWaitAsyncActivity`** / resuming pending `handle.wait()` across ticks.
|
|
384
|
-
- Do not import `@t4h.framework/core/testing` in production app code.
|
|
196
|
+
Do **not** import `@t4h.framework/core/testing` in production app code.
|
|
385
197
|
|
|
386
198
|
---
|
|
387
199
|
|
|
388
200
|
## Anti-patterns
|
|
389
201
|
|
|
390
|
-
-
|
|
391
|
-
-
|
|
392
|
-
-
|
|
393
|
-
-
|
|
394
|
-
- Non-`Serializable`
|
|
395
|
-
- Duplicate `workflow.id`
|
|
396
|
-
- `@t4h.framework/core/testing`
|
|
397
|
-
-
|
|
202
|
+
- Reading `process.env` directly, or reading the raw `env` proxy when the project should declare an `EnvDesign` in `constants/environments.ts`.
|
|
203
|
+
- Inlining URLs/secrets/tenants in workflow code instead of pulling them from the shared `EnvDesign`.
|
|
204
|
+
- Accessing an `EnvDesign` key at module top level (throws — no env storage outside a run); read keys inside the workflow body/functions.
|
|
205
|
+
- Non-deterministic workflow bodies (`Date.now()`, `Math.random()`, ambient state) — they break replay.
|
|
206
|
+
- Non-`Serializable` workflow input, output, or step values.
|
|
207
|
+
- Duplicate `workflow.id` within an `App`.
|
|
208
|
+
- Importing `@t4h.framework/core/testing` from production code.
|
|
209
|
+
- Building low-level infrastructure inside a workflow instead of composing the framework packages that already provide it.
|
|
398
210
|
|
|
399
211
|
---
|
|
400
212
|
|
|
401
|
-
## Exports
|
|
213
|
+
## Exports
|
|
214
|
+
|
|
215
|
+
**Main (`@t4h.framework/core`):** `Workflow`, `App`, `startWorkflow`, `WorkflowRef`, `env`, `EnvDesign`, `Authorization`, `Binary`, `Serializable`.
|
|
216
|
+
|
|
217
|
+
**Testing (`@t4h.framework/core/testing`):** `TestWorkflowEnvironment`, `TestClock`, `createTestEnv`, `createTestEnvEntries`, `NonDeterministicWorkflowError`.
|
|
402
218
|
|
|
403
|
-
|
|
219
|
+
> Core also exports lower-level building blocks used to **author framework packages** themselves. Those are out of scope for building workflows and are not covered here — reach for the primitives above and the companion packages instead.
|