@effect-agent/platform-node 0.1.0-beta.42 → 0.1.0-beta.45
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/dist/index.d.mts +88 -45
- package/dist/index.mjs +33 -25
- package/dist/index.mjs.map +1 -1
- package/dist/workflow.d.mts +27 -0
- package/dist/workflow.mjs +133 -0
- package/dist/workflow.mjs.map +1 -0
- package/package.json +1 -48
- package/src/host.ts +81 -86
- package/src/layers.ts +132 -42
- package/src/scheduling.ts +9 -0
- package/src/subscriptions.ts +7 -0
- package/src/wake-scheduler.ts +3 -0
- package/src/workflow.ts +209 -0
package/src/host.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { ThreadId, SubmissionId } from "@effect-agent/core";
|
|
2
2
|
import {
|
|
3
|
-
compileRegistrations,
|
|
4
3
|
type AgentRegistration,
|
|
5
4
|
DurableAgentRuntime,
|
|
6
5
|
type AbortCommand,
|
|
@@ -31,15 +30,14 @@ import {
|
|
|
31
30
|
type RetryCommand,
|
|
32
31
|
type Settlement,
|
|
33
32
|
} from "@effect-agent/thread";
|
|
34
|
-
import { NodeCrypto } from "@effect/platform-node";
|
|
35
33
|
import { type Stream, Context, type Crypto, Effect, Layer, Ref, Schema } from "effect";
|
|
36
34
|
|
|
37
35
|
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
type
|
|
41
|
-
type
|
|
42
|
-
type
|
|
36
|
+
NodeDurableAgentRuntime,
|
|
37
|
+
NodeDurableAgentRuntimeConfig,
|
|
38
|
+
type NodeDurableAgentRuntimeInitializationError,
|
|
39
|
+
type NodeDurableAgentRuntimeOptions,
|
|
40
|
+
type NodeDurableAgentRuntimeServices,
|
|
43
41
|
} from "./layers.ts";
|
|
44
42
|
|
|
45
43
|
/**
|
|
@@ -50,74 +48,74 @@ export class AdmissionClosed extends Schema.TaggedError<AdmissionClosed>()("Admi
|
|
|
50
48
|
message: Schema.String,
|
|
51
49
|
}) {}
|
|
52
50
|
|
|
53
|
-
const makeHost = (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const config = yield* NodeDurableRuntimeConfig;
|
|
51
|
+
const makeHost = Effect.gen(function* () {
|
|
52
|
+
const runtime = yield* DurableAgentRuntime;
|
|
53
|
+
const config = yield* NodeDurableAgentRuntimeConfig;
|
|
57
54
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
55
|
+
// Startup gate (deployment §5, plan §host): configuration decoding and storage compatibility
|
|
56
|
+
// already gated this Layer's dependencies; the last gate before admission opens is recovering
|
|
57
|
+
// EVERY nonterminal Submission. Work needing a live Agent Binding is reported `deferred` and
|
|
58
|
+
// stays a visible obligation for `runWorkers`; lanes durably blocked on an Unknown Outcome are
|
|
59
|
+
// reported `unknown` and wait for the authorized `resolveUnknown` path (DUR-017) — they consume
|
|
60
|
+
// no worker permit while the settlement obligation stays owed.
|
|
61
|
+
const startupRecovery = yield* runtime.runRecovery;
|
|
65
62
|
|
|
66
|
-
|
|
67
|
-
// Shutdown step 1 (DEPLOY-005): admission closes before the ownership drain in the runtime
|
|
68
|
-
// Layer below releases claims and before the stores close in reverse acquisition order.
|
|
69
|
-
yield* Effect.addFinalizer(() => Ref.set(admission, false));
|
|
63
|
+
const admission = yield* Ref.make(true);
|
|
70
64
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
? Effect.void
|
|
75
|
-
: Effect.fail(
|
|
76
|
-
AdmissionClosed.make({ message: "The host is shutting down; admission is closed." }),
|
|
77
|
-
),
|
|
78
|
-
),
|
|
79
|
-
);
|
|
65
|
+
// Shutdown step 1 (DEPLOY-005): admission closes before the ownership drain in the runtime
|
|
66
|
+
// Layer below releases claims and before the stores close in reverse acquisition order.
|
|
67
|
+
yield* Effect.addFinalizer(() => Ref.set(admission, false));
|
|
80
68
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
69
|
+
const requireAdmission: Effect.Effect<void, AdmissionClosed> = Ref.get(admission).pipe(
|
|
70
|
+
Effect.flatMap((open) =>
|
|
71
|
+
open
|
|
72
|
+
? Effect.void
|
|
73
|
+
: Effect.fail(
|
|
74
|
+
AdmissionClosed.make({ message: "The host is shutting down; admission is closed." }),
|
|
75
|
+
),
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const submit = <InputSchema extends Schema.Top>(
|
|
80
|
+
agent: DurableSubmitAgent<InputSchema>,
|
|
81
|
+
input: InputSchema["Type"],
|
|
82
|
+
options: DurableSubmitOptions,
|
|
83
|
+
): Effect.Effect<
|
|
84
|
+
Receipt,
|
|
85
|
+
AdmissionClosed | DurableSubmitFailure,
|
|
86
|
+
InputSchema["EncodingServices"]
|
|
87
|
+
> => requireAdmission.pipe(Effect.andThen(runtime.submit(agent, input, options)));
|
|
90
88
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
const runWorkers = <A, E, R>(worker: Effect.Effect<A, E, R>): Effect.Effect<void, E, R> =>
|
|
90
|
+
Effect.forEach(
|
|
91
|
+
Array.from({ length: config.workerConcurrency }, (_, index) => index),
|
|
92
|
+
() => worker,
|
|
93
|
+
{ concurrency: "unbounded", discard: true },
|
|
94
|
+
);
|
|
97
95
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
// S2 multi-binding pool: every claimed head resolves its exact registered Binding through
|
|
97
|
+
// the host's exact registrations, so one bounded
|
|
98
|
+
// pool serves parent and child lanes — the spec §12 smallest-pool suspension/wakeup proof
|
|
99
|
+
// runs `workerConcurrency: 1` over exactly this loop.
|
|
100
|
+
const runResolvedWorkers = runWorkers(runtime.runResolvedWorker);
|
|
103
101
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
});
|
|
102
|
+
return NodeDurableHost.of({
|
|
103
|
+
startupRecovery,
|
|
104
|
+
admissionOpen: Ref.get(admission),
|
|
105
|
+
submit,
|
|
106
|
+
awaitSettlement: runtime.awaitSettlement,
|
|
107
|
+
observe: runtime.observe,
|
|
108
|
+
abort: runtime.abort,
|
|
109
|
+
explain: runtime.explain,
|
|
110
|
+
explainThread: runtime.explainThread,
|
|
111
|
+
verify: runtime.verify,
|
|
112
|
+
retry: runtime.retry,
|
|
113
|
+
wake: runtime.wake,
|
|
114
|
+
scanObligations: runtime.scanObligations,
|
|
115
|
+
runWorkers,
|
|
116
|
+
runResolvedWorkers,
|
|
120
117
|
});
|
|
118
|
+
});
|
|
121
119
|
|
|
122
120
|
/**
|
|
123
121
|
* Operational host lifecycle for the DN runtime (deployment §2/§5/§6).
|
|
@@ -209,56 +207,53 @@ export class NodeDurableHost extends Context.Service<
|
|
|
209
207
|
AuthorizationRequirements = never,
|
|
210
208
|
>(
|
|
211
209
|
registrations: Entries,
|
|
212
|
-
options:
|
|
210
|
+
options: NodeDurableAgentRuntimeOptions<
|
|
213
211
|
ContextError,
|
|
214
212
|
ContextRequirements,
|
|
215
213
|
AuthorizationError,
|
|
216
214
|
AuthorizationRequirements
|
|
217
215
|
>,
|
|
218
216
|
) {
|
|
219
|
-
return
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
),
|
|
223
|
-
).pipe(Layer.provide(NodeCrypto.layer));
|
|
217
|
+
return NodeDurableHost.layer.pipe(
|
|
218
|
+
Layer.provideMerge(NodeDurableAgentRuntime.layerRegistered(registrations, options)),
|
|
219
|
+
);
|
|
224
220
|
}
|
|
225
221
|
|
|
226
222
|
/**
|
|
227
|
-
* Host gates over an assembled `
|
|
228
|
-
*
|
|
223
|
+
* Host gates over an assembled `NodeDurableAgentRuntime` stack. The runtime Layer owns
|
|
224
|
+
* executable registrations; omission registers no Agents, so resolved work fails closed.
|
|
229
225
|
*/
|
|
230
|
-
static readonly layer
|
|
231
|
-
bindings: ReadonlyArray<ResolvedBinding> = [],
|
|
232
|
-
): Layer.Layer<
|
|
226
|
+
static readonly layer: Layer.Layer<
|
|
233
227
|
NodeDurableHost,
|
|
234
228
|
DurableWorkerFailure,
|
|
235
|
-
DurableAgentRuntime |
|
|
236
|
-
>
|
|
229
|
+
DurableAgentRuntime | NodeDurableAgentRuntimeConfig
|
|
230
|
+
> = Layer.effect(NodeDurableHost)(makeHost);
|
|
237
231
|
|
|
238
|
-
/** The complete DN host: `
|
|
232
|
+
/** The complete DN host: `NodeDurableAgentRuntime.layer(options)` plus the host lifecycle gates. */
|
|
239
233
|
static layerStack<
|
|
240
234
|
ContextError = never,
|
|
241
235
|
ContextRequirements = never,
|
|
242
236
|
AuthorizationError = never,
|
|
243
237
|
AuthorizationRequirements = never,
|
|
244
238
|
>(
|
|
245
|
-
options:
|
|
239
|
+
options: NodeDurableAgentRuntimeOptions<
|
|
246
240
|
ContextError,
|
|
247
241
|
ContextRequirements,
|
|
248
242
|
AuthorizationError,
|
|
249
243
|
AuthorizationRequirements
|
|
250
244
|
> & { readonly bindings?: ReadonlyArray<ResolvedBinding> },
|
|
251
245
|
): Layer.Layer<
|
|
252
|
-
NodeDurableHost |
|
|
246
|
+
NodeDurableHost | NodeDurableAgentRuntimeServices,
|
|
253
247
|
| DurableWorkerFailure
|
|
254
|
-
|
|
|
248
|
+
| NodeDurableAgentRuntimeInitializationError
|
|
255
249
|
| ContextError
|
|
256
250
|
| AuthorizationError,
|
|
257
251
|
Exclude<ContextRequirements | AuthorizationRequirements, Crypto.Crypto>
|
|
258
252
|
> {
|
|
259
253
|
const { bindings = [], ...runtimeOptions } = options;
|
|
260
|
-
|
|
261
|
-
|
|
254
|
+
|
|
255
|
+
return NodeDurableHost.layer.pipe(
|
|
256
|
+
Layer.provideMerge(NodeDurableAgentRuntime.layerWithBindings(bindings, runtimeOptions)),
|
|
262
257
|
);
|
|
263
258
|
}
|
|
264
259
|
}
|
package/src/layers.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
type SqliteStorageInitializationError,
|
|
20
20
|
} from "@effect-agent/storage-sqlite";
|
|
21
21
|
import {
|
|
22
|
+
type AgentRegistration,
|
|
23
|
+
type ResolvedBinding,
|
|
22
24
|
type ScheduleStore,
|
|
23
25
|
type ThreadStore,
|
|
24
26
|
type WakeScheduler,
|
|
@@ -42,6 +44,7 @@ import { NodeWakeSchedulerConfig, nodeWakeSchedulerLayer } from "./wake-schedule
|
|
|
42
44
|
|
|
43
45
|
const PositiveMillis = Schema.Int.check(Schema.isGreaterThan(0));
|
|
44
46
|
const NonNegativeMillis = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
47
|
+
|
|
45
48
|
const WorkerConcurrency = Schema.Int.check(
|
|
46
49
|
Schema.isGreaterThanOrEqualTo(1),
|
|
47
50
|
Schema.isLessThanOrEqualTo(64),
|
|
@@ -61,8 +64,8 @@ export class NodePlatformConfigError extends Schema.TaggedError<NodePlatformConf
|
|
|
61
64
|
* construction, exposed as a typed service). Every cadence is in milliseconds and every bound is
|
|
62
65
|
* finite; `workerConcurrency` caps how many worker loops `NodeDurableHost.runWorkers` drives.
|
|
63
66
|
*/
|
|
64
|
-
export class
|
|
65
|
-
"@effect-agent/platform-node/
|
|
67
|
+
export class NodeDurableAgentRuntimeConfigValue extends Schema.Class<NodeDurableAgentRuntimeConfigValue>(
|
|
68
|
+
"@effect-agent/platform-node/NodeDurableAgentRuntimeConfigValue",
|
|
66
69
|
)({
|
|
67
70
|
/** SQLite database file backing BOTH the Thread Log and the Submission Ledger. */
|
|
68
71
|
filename: Schema.NonEmptyString,
|
|
@@ -89,17 +92,17 @@ export class NodeDurableRuntimeConfigValue extends Schema.Class<NodeDurableRunti
|
|
|
89
92
|
}) {}
|
|
90
93
|
|
|
91
94
|
/** Explicit configuration authority for the assembled Node durable runtime. */
|
|
92
|
-
export class
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
>()("@effect-agent/platform-node/
|
|
95
|
+
export class NodeDurableAgentRuntimeConfig extends Context.Service<
|
|
96
|
+
NodeDurableAgentRuntimeConfig,
|
|
97
|
+
NodeDurableAgentRuntimeConfigValue
|
|
98
|
+
>()("@effect-agent/platform-node/NodeDurableAgentRuntimeConfig") {}
|
|
96
99
|
|
|
97
100
|
/**
|
|
98
|
-
* Raw (unvalidated) construction options for `
|
|
101
|
+
* Raw (unvalidated) construction options for `NodeDurableAgentRuntime.layer`. Optional fields default
|
|
99
102
|
* to the documented production values; everything is schema-decoded into
|
|
100
|
-
* `
|
|
103
|
+
* `NodeDurableAgentRuntimeConfigValue` before any resource opens (deployment §5 gate 1).
|
|
101
104
|
*/
|
|
102
|
-
export interface
|
|
105
|
+
export interface NodeDurableAgentRuntimeOptions<
|
|
103
106
|
ContextError = never,
|
|
104
107
|
ContextRequirements = never,
|
|
105
108
|
AuthorizationError = never,
|
|
@@ -160,25 +163,25 @@ export interface NodeDurableRuntimeOptions<
|
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
/** Built-in construction failures. `layer` also preserves supplied service Layers' errors. */
|
|
163
|
-
export type
|
|
166
|
+
export type NodeDurableAgentRuntimeInitializationError =
|
|
164
167
|
| NodePlatformConfigError
|
|
165
168
|
| SqliteStorageInitializationError;
|
|
166
169
|
|
|
167
|
-
/** The services `
|
|
168
|
-
export type
|
|
170
|
+
/** The services `NodeDurableAgentRuntime.layer` provides. */
|
|
171
|
+
export type NodeDurableAgentRuntimeServices =
|
|
169
172
|
| DurableAgentRuntime
|
|
170
173
|
| SubmissionLedger
|
|
171
174
|
| ThreadStore
|
|
172
175
|
| ScheduleStore
|
|
173
176
|
| WakeScheduler
|
|
174
177
|
| DurableRuntimeConfig
|
|
175
|
-
|
|
|
178
|
+
| NodeDurableAgentRuntimeConfig;
|
|
176
179
|
|
|
177
|
-
const decodeConfigValue = Schema.decodeUnknownEffect(
|
|
180
|
+
const decodeConfigValue = Schema.decodeUnknownEffect(NodeDurableAgentRuntimeConfigValue);
|
|
178
181
|
|
|
179
182
|
const configFromOptions = (
|
|
180
|
-
options: Omit<
|
|
181
|
-
): Effect.Effect<
|
|
183
|
+
options: Omit<NodeDurableAgentRuntimeOptions, "runContext" | "toolAuthorization">,
|
|
184
|
+
): Effect.Effect<NodeDurableAgentRuntimeConfigValue, NodePlatformConfigError> =>
|
|
182
185
|
decodeConfigValue({
|
|
183
186
|
filename: options.filename,
|
|
184
187
|
deploymentId: options.deploymentId,
|
|
@@ -203,26 +206,31 @@ const configFromOptions = (
|
|
|
203
206
|
);
|
|
204
207
|
|
|
205
208
|
/** SQLite storage configuration derived from the single validated Node configuration. */
|
|
206
|
-
const sqliteStorageConfigLayer: Layer.Layer<
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
209
|
+
const sqliteStorageConfigLayer: Layer.Layer<
|
|
210
|
+
SqliteStorageConfig,
|
|
211
|
+
never,
|
|
212
|
+
NodeDurableAgentRuntimeConfig
|
|
213
|
+
> = Layer.effect(SqliteStorageConfig)(
|
|
214
|
+
Effect.gen(function* () {
|
|
215
|
+
const config = yield* NodeDurableAgentRuntimeConfig;
|
|
216
|
+
|
|
217
|
+
return SqliteStorageConfigValue.make({
|
|
218
|
+
observationPollInterval: config.observationPollInterval,
|
|
219
|
+
busyTimeout: config.busyTimeout,
|
|
220
|
+
ownershipLeaseDuration: config.ownershipLeaseDuration,
|
|
221
|
+
verifyOnOpen: config.verifyOnOpen,
|
|
222
|
+
});
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
218
225
|
|
|
219
226
|
/** Thread coordinator configuration derived from the single validated Node configuration. */
|
|
220
227
|
const durableRuntimeConfigLayer = (
|
|
221
228
|
estimateCostMicrousd: RunCostEstimator | undefined,
|
|
222
|
-
): Layer.Layer<DurableRuntimeConfig, never,
|
|
229
|
+
): Layer.Layer<DurableRuntimeConfig, never, NodeDurableAgentRuntimeConfig> =>
|
|
223
230
|
Layer.effect(DurableRuntimeConfig)(
|
|
224
231
|
Effect.gen(function* () {
|
|
225
|
-
const config = yield*
|
|
232
|
+
const config = yield* NodeDurableAgentRuntimeConfig;
|
|
233
|
+
|
|
226
234
|
return DurableRuntimeConfig.make({
|
|
227
235
|
deploymentId: config.deploymentId,
|
|
228
236
|
producerId: config.producerId,
|
|
@@ -238,10 +246,11 @@ const durableRuntimeConfigLayer = (
|
|
|
238
246
|
const wakeSchedulerConfigLayer: Layer.Layer<
|
|
239
247
|
NodeWakeSchedulerConfig,
|
|
240
248
|
never,
|
|
241
|
-
|
|
249
|
+
NodeDurableAgentRuntimeConfig
|
|
242
250
|
> = Layer.effect(NodeWakeSchedulerConfig)(
|
|
243
251
|
Effect.gen(function* () {
|
|
244
|
-
const config = yield*
|
|
252
|
+
const config = yield* NodeDurableAgentRuntimeConfig;
|
|
253
|
+
|
|
245
254
|
return { scanInterval: Duration.millis(config.wakeScanInterval) };
|
|
246
255
|
}),
|
|
247
256
|
);
|
|
@@ -252,6 +261,7 @@ const releaseTrackedOwnership = (
|
|
|
252
261
|
): Effect.Effect<void> =>
|
|
253
262
|
Effect.gen(function* () {
|
|
254
263
|
const tracked = yield* Ref.getAndSet(registry, new Map<SubmissionId, OwnershipToken>());
|
|
264
|
+
|
|
255
265
|
for (const [submissionId, ownershipToken] of tracked) {
|
|
256
266
|
yield* ledger
|
|
257
267
|
.releaseOwnership(ReleaseOwnershipRequest.make({ submissionId, ownershipToken }))
|
|
@@ -280,16 +290,20 @@ export const ownershipDrainLayer: Layer.Layer<SubmissionLedger, never, Submissio
|
|
|
280
290
|
Layer.effect(SubmissionLedger)(
|
|
281
291
|
Effect.gen(function* () {
|
|
282
292
|
const ledger = yield* SubmissionLedger;
|
|
293
|
+
|
|
283
294
|
const registry = yield* Ref.make<ReadonlyMap<SubmissionId, OwnershipToken>>(
|
|
284
295
|
new Map<SubmissionId, OwnershipToken>(),
|
|
285
296
|
);
|
|
286
297
|
|
|
287
298
|
const track = (submissionId: SubmissionId, ownershipToken: OwnershipToken) =>
|
|
288
299
|
Ref.update(registry, (tracked) => new Map(tracked).set(submissionId, ownershipToken));
|
|
300
|
+
|
|
289
301
|
const untrack = (submissionId: SubmissionId) =>
|
|
290
302
|
Ref.update(registry, (tracked) => {
|
|
291
303
|
const next = new Map(tracked);
|
|
304
|
+
|
|
292
305
|
next.delete(submissionId);
|
|
306
|
+
|
|
293
307
|
return next;
|
|
294
308
|
});
|
|
295
309
|
|
|
@@ -364,7 +378,7 @@ export const ownershipDrainLayer: Layer.Layer<SubmissionLedger, never, Submissio
|
|
|
364
378
|
* verified during construction: an incompatible database file fails the Layer with
|
|
365
379
|
* `SqliteStorageCompatibilityError` before anything is mutated (DEPLOY-008).
|
|
366
380
|
*/
|
|
367
|
-
export class
|
|
381
|
+
export class NodeDurableAgentRuntime {
|
|
368
382
|
/** Validated configuration Layer; fails typed when the supplied options are out of bounds. */
|
|
369
383
|
static configLayer<
|
|
370
384
|
ContextError = never,
|
|
@@ -372,14 +386,14 @@ export class NodeDurableRuntime {
|
|
|
372
386
|
AuthorizationError = never,
|
|
373
387
|
AuthorizationRequirements = never,
|
|
374
388
|
>(
|
|
375
|
-
options:
|
|
389
|
+
options: NodeDurableAgentRuntimeOptions<
|
|
376
390
|
ContextError,
|
|
377
391
|
ContextRequirements,
|
|
378
392
|
AuthorizationError,
|
|
379
393
|
AuthorizationRequirements
|
|
380
394
|
>,
|
|
381
|
-
): Layer.Layer<
|
|
382
|
-
return Layer.effect(
|
|
395
|
+
): Layer.Layer<NodeDurableAgentRuntimeConfig, NodePlatformConfigError> {
|
|
396
|
+
return Layer.effect(NodeDurableAgentRuntimeConfig)(configFromOptions(options));
|
|
383
397
|
}
|
|
384
398
|
|
|
385
399
|
/** The full DN runtime stack over one SQLite file. */
|
|
@@ -389,20 +403,83 @@ export class NodeDurableRuntime {
|
|
|
389
403
|
AuthorizationError = never,
|
|
390
404
|
AuthorizationRequirements = never,
|
|
391
405
|
>(
|
|
392
|
-
options:
|
|
406
|
+
options: NodeDurableAgentRuntimeOptions<
|
|
393
407
|
ContextError,
|
|
394
408
|
ContextRequirements,
|
|
395
409
|
AuthorizationError,
|
|
396
410
|
AuthorizationRequirements
|
|
397
411
|
>,
|
|
398
412
|
): Layer.Layer<
|
|
399
|
-
|
|
400
|
-
|
|
413
|
+
NodeDurableAgentRuntimeServices,
|
|
414
|
+
NodeDurableAgentRuntimeInitializationError | ContextError | AuthorizationError,
|
|
401
415
|
Exclude<ContextRequirements | AuthorizationRequirements, Crypto.Crypto>
|
|
402
416
|
> {
|
|
403
|
-
return
|
|
417
|
+
return NodeDurableAgentRuntime.assemble(DurableAgentRuntime.layerWithServices, options);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Own typed executable registrations for every worker using this Node runtime. */
|
|
421
|
+
static layerRegistered<
|
|
422
|
+
const Entries extends ReadonlyArray<AgentRegistration>,
|
|
423
|
+
ContextError = never,
|
|
424
|
+
ContextRequirements = never,
|
|
425
|
+
AuthorizationError = never,
|
|
426
|
+
AuthorizationRequirements = never,
|
|
427
|
+
>(
|
|
428
|
+
registrations: Entries,
|
|
429
|
+
options: NodeDurableAgentRuntimeOptions<
|
|
430
|
+
ContextError,
|
|
431
|
+
ContextRequirements,
|
|
432
|
+
AuthorizationError,
|
|
433
|
+
AuthorizationRequirements
|
|
434
|
+
>,
|
|
435
|
+
) {
|
|
436
|
+
return NodeDurableAgentRuntime.assemble(
|
|
437
|
+
DurableAgentRuntime.layerRegistered(registrations),
|
|
438
|
+
options,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/** Construct from registrations already resolved within the enclosing application Scope. */
|
|
443
|
+
static layerWithBindings<
|
|
444
|
+
ContextError = never,
|
|
445
|
+
ContextRequirements = never,
|
|
446
|
+
AuthorizationError = never,
|
|
447
|
+
AuthorizationRequirements = never,
|
|
448
|
+
>(
|
|
449
|
+
bindings: ReadonlyArray<ResolvedBinding>,
|
|
450
|
+
options: NodeDurableAgentRuntimeOptions<
|
|
451
|
+
ContextError,
|
|
452
|
+
ContextRequirements,
|
|
453
|
+
AuthorizationError,
|
|
454
|
+
AuthorizationRequirements
|
|
455
|
+
>,
|
|
456
|
+
) {
|
|
457
|
+
return NodeDurableAgentRuntime.assemble(
|
|
458
|
+
DurableAgentRuntime.layerWithBindings(bindings),
|
|
459
|
+
options,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private static assemble<
|
|
464
|
+
RuntimeError,
|
|
465
|
+
RuntimeRequirements,
|
|
466
|
+
ContextError,
|
|
467
|
+
ContextRequirements,
|
|
468
|
+
AuthorizationError,
|
|
469
|
+
AuthorizationRequirements,
|
|
470
|
+
>(
|
|
471
|
+
runtimeLayer: Layer.Layer<DurableAgentRuntime, RuntimeError, RuntimeRequirements>,
|
|
472
|
+
options: NodeDurableAgentRuntimeOptions<
|
|
473
|
+
ContextError,
|
|
474
|
+
ContextRequirements,
|
|
475
|
+
AuthorizationError,
|
|
476
|
+
AuthorizationRequirements
|
|
477
|
+
>,
|
|
478
|
+
) {
|
|
479
|
+
const assembled = Layer.unwrap(
|
|
404
480
|
Effect.map(configFromOptions(options), (config) => {
|
|
405
|
-
const nodeConfigLayer = Layer.succeed(
|
|
481
|
+
const nodeConfigLayer = Layer.succeed(NodeDurableAgentRuntimeConfig)(config);
|
|
482
|
+
|
|
406
483
|
const infrastructure = Layer.mergeAll(
|
|
407
484
|
sqliteStorageConfigLayer,
|
|
408
485
|
storageFailpointLayer({
|
|
@@ -412,15 +489,19 @@ export class NodeDurableRuntime {
|
|
|
412
489
|
SqliteClient.layer({ filename: config.filename }),
|
|
413
490
|
NodeCrypto.layer,
|
|
414
491
|
);
|
|
492
|
+
|
|
415
493
|
const runtimeFailpointLayer =
|
|
416
494
|
options.runtimeFailpoint === undefined
|
|
417
495
|
? DurableRuntimeFailpoint.layer
|
|
418
496
|
: Layer.succeed(DurableRuntimeFailpoint)({ hit: options.runtimeFailpoint });
|
|
497
|
+
|
|
419
498
|
const reconcilerLayer = options.toolReconciler ?? ToolReconciler.uncertain;
|
|
499
|
+
|
|
420
500
|
const observerLayer =
|
|
421
501
|
options.toolFailureObserver === undefined
|
|
422
502
|
? Layer.succeed(CurrentToolFailureObserver)(undefined)
|
|
423
503
|
: toolFailureObserverLayer(options.toolFailureObserver);
|
|
504
|
+
|
|
424
505
|
const ports = Layer.mergeAll(
|
|
425
506
|
threadStoreLayer,
|
|
426
507
|
scheduleStoreLayer,
|
|
@@ -428,7 +509,8 @@ export class NodeDurableRuntime {
|
|
|
428
509
|
Layer.provideMerge(ownershipDrainLayer.pipe(Layer.provide(submissionLedgerLayer))),
|
|
429
510
|
),
|
|
430
511
|
);
|
|
431
|
-
|
|
512
|
+
|
|
513
|
+
return runtimeLayer.pipe(
|
|
432
514
|
Layer.provideMerge(
|
|
433
515
|
Layer.mergeAll(ports, durableRuntimeConfigLayer(options.estimateCostMicrousd)),
|
|
434
516
|
),
|
|
@@ -451,5 +533,13 @@ export class NodeDurableRuntime {
|
|
|
451
533
|
);
|
|
452
534
|
}),
|
|
453
535
|
);
|
|
536
|
+
|
|
537
|
+
const runtime: Layer.Layer<
|
|
538
|
+
NodeDurableAgentRuntimeServices,
|
|
539
|
+
Layer.Error<typeof assembled>,
|
|
540
|
+
Layer.Services<typeof assembled>
|
|
541
|
+
> = assembled;
|
|
542
|
+
|
|
543
|
+
return runtime;
|
|
454
544
|
}
|
|
455
545
|
}
|
package/src/scheduling.ts
CHANGED
|
@@ -22,7 +22,9 @@ export const nodeScheduleWakeLayer: Layer.Layer<ScheduleWake> = Layer.effect(
|
|
|
22
22
|
Effect.gen(function* () {
|
|
23
23
|
const hints = yield* PubSub.sliding<void>(1);
|
|
24
24
|
const subscription = yield* PubSub.subscribe(hints);
|
|
25
|
+
|
|
25
26
|
yield* Effect.addFinalizer(() => PubSub.shutdown(hints));
|
|
27
|
+
|
|
26
28
|
return ScheduleWake.of({
|
|
27
29
|
notify: PubSub.publish(hints, undefined).pipe(Effect.asVoid),
|
|
28
30
|
await: PubSub.take(subscription),
|
|
@@ -58,20 +60,25 @@ const nodeSchedulingDriverLayer = (
|
|
|
58
60
|
Effect.map((pass) => pass.failed === 0),
|
|
59
61
|
Effect.catchCause(reportPassFailure),
|
|
60
62
|
);
|
|
63
|
+
|
|
61
64
|
const deadlineResult = passSucceeded
|
|
62
65
|
? yield* store.nextDeadline().pipe(Effect.result)
|
|
63
66
|
: Result.fail(
|
|
64
67
|
ScheduleStorageError.make({ operation: "driver pass", reason: "unavailable" }),
|
|
65
68
|
);
|
|
69
|
+
|
|
66
70
|
if (Result.isFailure(deadlineResult) && passSucceeded) {
|
|
67
71
|
yield* Effect.logWarning("Node scheduling deadline query failed");
|
|
68
72
|
}
|
|
69
73
|
const nowMillis = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
74
|
+
|
|
70
75
|
const deadlineDelay =
|
|
71
76
|
Result.isSuccess(deadlineResult) && deadlineResult.success !== null
|
|
72
77
|
? Math.max(0, deadlineResult.success - nowMillis)
|
|
73
78
|
: limits.recoveryPollMillis;
|
|
79
|
+
|
|
74
80
|
const delay = Math.min(deadlineDelay, limits.recoveryPollMillis);
|
|
81
|
+
|
|
75
82
|
yield* Effect.raceFirst(wake.await, Effect.sleep(Duration.millis(delay)));
|
|
76
83
|
}
|
|
77
84
|
});
|
|
@@ -97,10 +104,12 @@ export class NodeScheduling {
|
|
|
97
104
|
NodeDurableHost | ScheduleStore | ScheduleAuthorizer
|
|
98
105
|
> {
|
|
99
106
|
const limits = options.limits ?? defaultSchedulingLimits;
|
|
107
|
+
|
|
100
108
|
const schedulingWithDriver = nodeSchedulingDriverLayer(limits).pipe(
|
|
101
109
|
Layer.provide(ScheduleDriver.layer(limits)),
|
|
102
110
|
Layer.merge(Scheduling.layer(limits)),
|
|
103
111
|
);
|
|
112
|
+
|
|
104
113
|
return schedulingWithDriver.pipe(
|
|
105
114
|
Layer.provide(
|
|
106
115
|
Layer.mergeAll(nodeScheduledInputAdmissionLayer, nodeScheduleWakeLayer, NodeCrypto.layer),
|
package/src/subscriptions.ts
CHANGED
|
@@ -44,6 +44,7 @@ export const nodePreparedInputAdmissionLayer: Layer.Layer<
|
|
|
44
44
|
PreparedInputAdmission,
|
|
45
45
|
Effect.gen(function* () {
|
|
46
46
|
const host = yield* NodeDurableHost;
|
|
47
|
+
|
|
47
48
|
return PreparedInputAdmission.of({
|
|
48
49
|
submit: (envelope) =>
|
|
49
50
|
host
|
|
@@ -140,6 +141,7 @@ const nodeSubscriptionDriverLayer = (
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
const deadline = yield* store.nextDeadline.pipe(Effect.exit);
|
|
144
|
+
|
|
143
145
|
if (Exit.isFailure(deadline)) {
|
|
144
146
|
yield* reportPassFailure(deadline.cause);
|
|
145
147
|
yield* Effect.sleep(Duration.millis(limits.retryMillis));
|
|
@@ -147,10 +149,12 @@ const nodeSubscriptionDriverLayer = (
|
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
const nowMillis = yield* Effect.clockWith((clock) => clock.currentTimeMillis);
|
|
152
|
+
|
|
150
153
|
const delay =
|
|
151
154
|
deadline.value === null
|
|
152
155
|
? limits.retryMillis
|
|
153
156
|
: Math.max(1, Math.min(deadline.value - nowMillis, limits.retryMillis));
|
|
157
|
+
|
|
154
158
|
yield* Effect.sleep(Duration.millis(delay));
|
|
155
159
|
}
|
|
156
160
|
});
|
|
@@ -180,13 +184,16 @@ export class NodeSubscriptions {
|
|
|
180
184
|
| SubscriptionInputBindings
|
|
181
185
|
> {
|
|
182
186
|
const limits = options.limits ?? defaultSubscriptionLimits;
|
|
187
|
+
|
|
183
188
|
const publicServices = Layer.merge(
|
|
184
189
|
Subscriptions.layer(limits),
|
|
185
190
|
SubscriptionIntake.layer(limits),
|
|
186
191
|
);
|
|
192
|
+
|
|
187
193
|
const driver = nodeSubscriptionDriverLayer(limits).pipe(
|
|
188
194
|
Layer.provide(SubscriptionDriver.layer(limits)),
|
|
189
195
|
);
|
|
196
|
+
|
|
190
197
|
return Layer.merge(publicServices, driver).pipe(
|
|
191
198
|
Layer.provide(nodePreparedInputAdmissionLayer),
|
|
192
199
|
Layer.provide(NodeCrypto.layer),
|