@effect/platform-cloudflare 0.0.0-stage → 4.1.3
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/AGENTS.md +434 -0
- package/CLAUDE.md +434 -0
- package/LICENSE +21 -0
- package/README.md +505 -2
- package/ai-docs/README.md +44 -0
- package/ai-docs/package.json +36 -0
- package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
- package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +52 -0
- package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
- package/ai-docs/src/01_effect/01_basics/index.md +7 -0
- package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
- package/ai-docs/src/01_effect/02_schema/index.md +7 -0
- package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
- package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
- package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
- package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
- package/ai-docs/src/01_effect/03_services/index.md +5 -0
- package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
- package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
- package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
- package/ai-docs/src/01_effect/04_errors/index.md +1 -0
- package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
- package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
- package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
- package/ai-docs/src/01_effect/05_resources/index.md +3 -0
- package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
- package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
- package/ai-docs/src/01_effect/06_running/index.md +1 -0
- package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
- package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
- package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
- package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
- package/ai-docs/src/03_stream/30_encoding.ts +163 -0
- package/ai-docs/src/03_stream/index.md +4 -0
- package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
- package/ai-docs/src/04_integration/index.md +5 -0
- package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
- package/ai-docs/src/05_batching/index.md +3 -0
- package/ai-docs/src/06_schedule/10_schedules.ts +111 -0
- package/ai-docs/src/06_schedule/index.md +3 -0
- package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
- package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
- package/ai-docs/src/07_datetime/index.md +5 -0
- package/ai-docs/src/08_observability/10_logging.ts +66 -0
- package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
- package/ai-docs/src/08_observability/index.md +7 -0
- package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
- package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
- package/ai-docs/src/09_testing/index.md +1 -0
- package/ai-docs/src/10_predicate/01_basics.ts +14 -0
- package/ai-docs/src/10_predicate/index.md +9 -0
- package/ai-docs/src/40_sql/10_basics.ts +175 -0
- package/ai-docs/src/40_sql/index.md +6 -0
- package/ai-docs/src/50_http-client/10_basics.ts +102 -0
- package/ai-docs/src/50_http-client/index.md +3 -0
- package/ai-docs/src/51_http-server/10_basics.ts +118 -0
- package/ai-docs/src/51_http-server/20_testing.ts +100 -0
- package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
- package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
- package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
- package/ai-docs/src/51_http-server/fixtures/api/Users.ts +107 -0
- package/ai-docs/src/51_http-server/fixtures/domain/User.ts +21 -0
- package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
- package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +37 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +80 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users.ts +183 -0
- package/ai-docs/src/51_http-server/index.md +3 -0
- package/ai-docs/src/52_graphql/10_basics.ts +117 -0
- package/ai-docs/src/52_graphql/20_testing.ts +45 -0
- package/ai-docs/src/52_graphql/30_batching.ts +118 -0
- package/ai-docs/src/52_graphql/40_aliased-batching.ts +124 -0
- package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
- package/ai-docs/src/52_graphql/index.md +6 -0
- package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
- package/ai-docs/src/60_child-process/index.md +3 -0
- package/ai-docs/src/70_cli/10_basics.ts +162 -0
- package/ai-docs/src/70_cli/index.md +5 -0
- package/ai-docs/src/71_ai/10_language-model.ts +156 -0
- package/ai-docs/src/71_ai/20_tools.ts +226 -0
- package/ai-docs/src/71_ai/30_chat.ts +158 -0
- package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
- package/ai-docs/src/71_ai/index.md +5 -0
- package/ai-docs/src/80_cluster/10_entities.ts +97 -0
- package/ai-docs/src/80_cluster/index.md +4 -0
- package/ai-docs/src/index.md +10 -0
- package/ai-docs/tsconfig.json +28 -0
- package/dist/AlchemyCloudflareCluster.d.ts +134 -0
- package/dist/AlchemyCloudflareCluster.d.ts.map +1 -0
- package/dist/AlchemyCloudflareCluster.js +111 -0
- package/dist/AlchemyCloudflareCluster.js.map +1 -0
- package/dist/CloudflareCluster.d.ts +196 -0
- package/dist/CloudflareCluster.d.ts.map +1 -0
- package/dist/CloudflareCluster.js +533 -0
- package/dist/CloudflareCluster.js.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts +279 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.js +295 -0
- package/dist/CloudflareDurableObjectPrograms.js.map +1 -0
- package/dist/CloudflareDurableObjects.d.ts +149 -0
- package/dist/CloudflareDurableObjects.d.ts.map +1 -0
- package/dist/CloudflareDurableObjects.js +337 -0
- package/dist/CloudflareDurableObjects.js.map +1 -0
- package/dist/CloudflarePersistedQueue.d.ts +36 -0
- package/dist/CloudflarePersistedQueue.d.ts.map +1 -0
- package/dist/CloudflarePersistedQueue.js +122 -0
- package/dist/CloudflarePersistedQueue.js.map +1 -0
- package/dist/CloudflareWorkflowEngine.d.ts +72 -0
- package/dist/CloudflareWorkflowEngine.d.ts.map +1 -0
- package/dist/CloudflareWorkflowEngine.js +187 -0
- package/dist/CloudflareWorkflowEngine.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/alchemyCluster.d.ts +2 -0
- package/dist/internal/alchemyCluster.d.ts.map +1 -0
- package/dist/internal/alchemyCluster.js +75 -0
- package/dist/internal/alchemyCluster.js.map +1 -0
- package/dist/internal/boundedMap.d.ts +3 -0
- package/dist/internal/boundedMap.d.ts.map +1 -0
- package/dist/internal/boundedMap.js +15 -0
- package/dist/internal/boundedMap.js.map +1 -0
- package/dist/internal/cluster.d.ts +57 -0
- package/dist/internal/cluster.d.ts.map +1 -0
- package/dist/internal/cluster.js +2 -0
- package/dist/internal/cluster.js.map +1 -0
- package/dist/internal/clusterName.d.ts +2 -0
- package/dist/internal/clusterName.d.ts.map +1 -0
- package/dist/internal/clusterName.js +16 -0
- package/dist/internal/clusterName.js.map +1 -0
- package/dist/internal/entityKeepAlive.d.ts +2 -0
- package/dist/internal/entityKeepAlive.d.ts.map +1 -0
- package/dist/internal/entityKeepAlive.js +73 -0
- package/dist/internal/entityKeepAlive.js.map +1 -0
- package/dist/internal/entityMailbox.d.ts +2 -0
- package/dist/internal/entityMailbox.d.ts.map +1 -0
- package/dist/internal/entityMailbox.js +160 -0
- package/dist/internal/entityMailbox.js.map +1 -0
- package/dist/internal/entityRegistry.d.ts +16 -0
- package/dist/internal/entityRegistry.d.ts.map +1 -0
- package/dist/internal/entityRegistry.js +22 -0
- package/dist/internal/entityRegistry.js.map +1 -0
- package/dist/internal/entityReply.d.ts +2 -0
- package/dist/internal/entityReply.d.ts.map +1 -0
- package/dist/internal/entityReply.js +35 -0
- package/dist/internal/entityReply.js.map +1 -0
- package/dist/internal/entityRuntime.d.ts +2 -0
- package/dist/internal/entityRuntime.d.ts.map +1 -0
- package/dist/internal/entityRuntime.js +823 -0
- package/dist/internal/entityRuntime.js.map +1 -0
- package/dist/internal/entitySql.d.ts +2 -0
- package/dist/internal/entitySql.d.ts.map +1 -0
- package/dist/internal/entitySql.js +29 -0
- package/dist/internal/entitySql.js.map +1 -0
- package/dist/internal/entityStorage.d.ts +2 -0
- package/dist/internal/entityStorage.d.ts.map +1 -0
- package/dist/internal/entityStorage.js +98 -0
- package/dist/internal/entityStorage.js.map +1 -0
- package/dist/internal/entityWire.d.ts +2 -0
- package/dist/internal/entityWire.d.ts.map +1 -0
- package/dist/internal/entityWire.js +146 -0
- package/dist/internal/entityWire.js.map +1 -0
- package/dist/internal/queueRuntime.d.ts +2 -0
- package/dist/internal/queueRuntime.d.ts.map +1 -0
- package/dist/internal/queueRuntime.js +88 -0
- package/dist/internal/queueRuntime.js.map +1 -0
- package/dist/internal/queueStorage.d.ts +2 -0
- package/dist/internal/queueStorage.d.ts.map +1 -0
- package/dist/internal/queueStorage.js +129 -0
- package/dist/internal/queueStorage.js.map +1 -0
- package/dist/internal/registry.d.ts +10 -0
- package/dist/internal/registry.d.ts.map +1 -0
- package/dist/internal/registry.js +24 -0
- package/dist/internal/registry.js.map +1 -0
- package/dist/internal/singletonRegistry.d.ts +2 -0
- package/dist/internal/singletonRegistry.d.ts.map +1 -0
- package/dist/internal/singletonRegistry.js +9 -0
- package/dist/internal/singletonRegistry.js.map +1 -0
- package/dist/internal/singletonRuntime.d.ts +2 -0
- package/dist/internal/singletonRuntime.d.ts.map +1 -0
- package/dist/internal/singletonRuntime.js +42 -0
- package/dist/internal/singletonRuntime.js.map +1 -0
- package/dist/internal/singletonStorage.d.ts +2 -0
- package/dist/internal/singletonStorage.d.ts.map +1 -0
- package/dist/internal/singletonStorage.js +34 -0
- package/dist/internal/singletonStorage.js.map +1 -0
- package/dist/internal/transport.d.ts +2 -0
- package/dist/internal/transport.d.ts.map +1 -0
- package/dist/internal/transport.js +49 -0
- package/dist/internal/transport.js.map +1 -0
- package/dist/internal/workflowRegistry.d.ts +2 -0
- package/dist/internal/workflowRegistry.d.ts.map +1 -0
- package/dist/internal/workflowRegistry.js +29 -0
- package/dist/internal/workflowRegistry.js.map +1 -0
- package/dist/internal/workflowRuntime.d.ts +2 -0
- package/dist/internal/workflowRuntime.d.ts.map +1 -0
- package/dist/internal/workflowRuntime.js +322 -0
- package/dist/internal/workflowRuntime.js.map +1 -0
- package/dist/internal/workflowStorage.d.ts +2 -0
- package/dist/internal/workflowStorage.d.ts.map +1 -0
- package/dist/internal/workflowStorage.js +113 -0
- package/dist/internal/workflowStorage.js.map +1 -0
- package/dist/internal/workflowWire.d.ts +2 -0
- package/dist/internal/workflowWire.d.ts.map +1 -0
- package/dist/internal/workflowWire.js +52 -0
- package/dist/internal/workflowWire.js.map +1 -0
- package/package.json +74 -3
- package/src/AlchemyCloudflareCluster.ts +200 -0
- package/src/CloudflareCluster.ts +704 -0
- package/src/CloudflareDurableObjectPrograms.ts +543 -0
- package/src/CloudflareDurableObjects.ts +430 -0
- package/src/CloudflarePersistedQueue.ts +189 -0
- package/src/CloudflareWorkflowEngine.ts +220 -0
- package/src/index.ts +38 -0
- package/src/internal/alchemyCluster.ts +120 -0
- package/src/internal/boundedMap.ts +15 -0
- package/src/internal/cluster.ts +57 -0
- package/src/internal/clusterName.ts +19 -0
- package/src/internal/entityKeepAlive.ts +121 -0
- package/src/internal/entityMailbox.ts +324 -0
- package/src/internal/entityRegistry.ts +46 -0
- package/src/internal/entityReply.ts +54 -0
- package/src/internal/entityRuntime.ts +1318 -0
- package/src/internal/entitySql.ts +60 -0
- package/src/internal/entityStorage.ts +168 -0
- package/src/internal/entityWire.ts +204 -0
- package/src/internal/queueRuntime.ts +174 -0
- package/src/internal/queueStorage.ts +255 -0
- package/src/internal/registry.ts +31 -0
- package/src/internal/singletonRegistry.ts +21 -0
- package/src/internal/singletonRuntime.ts +88 -0
- package/src/internal/singletonStorage.ts +64 -0
- package/src/internal/transport.ts +67 -0
- package/src/internal/workflowRegistry.ts +78 -0
- package/src/internal/workflowRuntime.ts +443 -0
- package/src/internal/workflowStorage.ts +234 -0
- package/src/internal/workflowWire.ts +123 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs durable workflows on the dedicated workflow Durable Object class.
|
|
3
|
+
*
|
|
4
|
+
* On this path one workflow execution is one Durable Object: the engine
|
|
5
|
+
* encodes `(workflowName, executionId)` into a Durable Object name with the
|
|
6
|
+
* same length-prefix scheme as entities and resolves the object through the
|
|
7
|
+
* workflow namespace binding. Execution state, activity results keyed
|
|
8
|
+
* `${name}/${attempt}`, durable deferred exits, and the clock due table all
|
|
9
|
+
* live on the object's SQLite storage behind its single alarm.
|
|
10
|
+
*
|
|
11
|
+
* Every `DurableClock` is durable on this engine: the in-memory short-sleep
|
|
12
|
+
* path is disabled, so even sub-minute sleeps persist a due row and arm the
|
|
13
|
+
* alarm.
|
|
14
|
+
*
|
|
15
|
+
* @stability experimental
|
|
16
|
+
* @since 4.1.3
|
|
17
|
+
*/
|
|
18
|
+
import { Clock } from "effect/Clock"
|
|
19
|
+
import * as Context from "effect/Context"
|
|
20
|
+
import * as Duration from "effect/Duration"
|
|
21
|
+
import * as Effect from "effect/Effect"
|
|
22
|
+
import * as Layer from "effect/Layer"
|
|
23
|
+
import * as Option from "effect/Option"
|
|
24
|
+
import * as Schedule from "effect/Schedule"
|
|
25
|
+
import * as Workflow from "effect/workflow/Workflow"
|
|
26
|
+
import * as WorkflowEngine from "effect/workflow/WorkflowEngine"
|
|
27
|
+
import { encodeName } from "./internal/clusterName.ts"
|
|
28
|
+
import { callDurableObject } from "./internal/transport.ts"
|
|
29
|
+
import {
|
|
30
|
+
CurrentExecutionHandle,
|
|
31
|
+
registerWorkflow,
|
|
32
|
+
unregisterWorkflow,
|
|
33
|
+
type WorkflowRegistration,
|
|
34
|
+
type WorkflowStub
|
|
35
|
+
} from "./internal/workflowRegistry.ts"
|
|
36
|
+
import { InterruptSignalName } from "./internal/workflowRuntime.ts"
|
|
37
|
+
import { decodeExit, decodeResult, encodeExit, encodePayload } from "./internal/workflowWire.ts"
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The workflow Durable Object namespace binding the engine is built from.
|
|
41
|
+
*
|
|
42
|
+
* @stability experimental
|
|
43
|
+
* @category layers
|
|
44
|
+
* @since 4.1.3
|
|
45
|
+
*/
|
|
46
|
+
export interface LayerOptions {
|
|
47
|
+
readonly workflowNamespace: DurableObjectNamespace
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const retryPolicy = Schedule.min([
|
|
51
|
+
Schedule.exponential(200, 1.5),
|
|
52
|
+
Schedule.spaced("1 minute")
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates the `WorkflowEngine` service backed by the workflow Durable Object
|
|
57
|
+
* namespace binding.
|
|
58
|
+
*
|
|
59
|
+
* **Details**
|
|
60
|
+
*
|
|
61
|
+
* Inside a workflow Durable Object the engine operates on the local execution
|
|
62
|
+
* handle, so activities and deferred reads never leave the object. Everywhere
|
|
63
|
+
* else it resolves the target execution's object with `getByName` and drives
|
|
64
|
+
* it over the same-Worker binding.
|
|
65
|
+
*
|
|
66
|
+
* @stability experimental
|
|
67
|
+
* @category constructors
|
|
68
|
+
* @since 4.1.3
|
|
69
|
+
*/
|
|
70
|
+
export const make = Effect.fnUntraced(function*(options: LayerOptions) {
|
|
71
|
+
const clock = yield* Clock
|
|
72
|
+
|
|
73
|
+
// Inside a run the execution's own handle avoids a self-RPC; every other
|
|
74
|
+
// target resolves to its Durable Object stub through the namespace binding.
|
|
75
|
+
//
|
|
76
|
+
// As in `ClusterWorkflowEngine`, a transport failure is retried on an
|
|
77
|
+
// exponential schedule capped at one minute and only then becomes a defect:
|
|
78
|
+
// every call here is idempotent by key, and a permanently unreachable object
|
|
79
|
+
// stalls the caller instead of failing a workflow for good. The object never
|
|
80
|
+
// rejects for application reasons, so a child's own defect arrives as its
|
|
81
|
+
// stored `Complete` result and is never retried.
|
|
82
|
+
const callExecution = <A>(
|
|
83
|
+
workflowName: string,
|
|
84
|
+
executionId: string,
|
|
85
|
+
call: (stub: WorkflowStub) => Promise<A>
|
|
86
|
+
): Effect.Effect<A> =>
|
|
87
|
+
Effect.flatMap(Effect.serviceOption(CurrentExecutionHandle), (handle) => {
|
|
88
|
+
if (Option.isSome(handle) && handle.value.executionId === executionId) {
|
|
89
|
+
const local = handle.value as WorkflowStub
|
|
90
|
+
return Effect.promise(() => call(local))
|
|
91
|
+
}
|
|
92
|
+
const name = encodeName(workflowName, executionId)
|
|
93
|
+
return callDurableObject({
|
|
94
|
+
resolve: () => options.workflowNamespace.getByName(name) as unknown as WorkflowStub,
|
|
95
|
+
call,
|
|
96
|
+
retry: retryPolicy
|
|
97
|
+
}).pipe(Effect.orDie)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const localHandle = Effect.fnUntraced(function*(operation: string) {
|
|
101
|
+
const instance = yield* WorkflowEngine.WorkflowInstance
|
|
102
|
+
const handle = yield* Effect.serviceOption(CurrentExecutionHandle)
|
|
103
|
+
if (Option.isNone(handle)) {
|
|
104
|
+
return yield* Effect.die(
|
|
105
|
+
`CloudflareWorkflowEngine: ${operation} is only available inside a workflow Durable Object execution`
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
return { instance, handle: handle.value } as const
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return WorkflowEngine.makeUnsafe({
|
|
112
|
+
register: Effect.fnUntraced(function*(workflow, execute) {
|
|
113
|
+
const context = yield* Effect.context<never>()
|
|
114
|
+
const registration: WorkflowRegistration = { workflow, execute, context }
|
|
115
|
+
if (!registerWorkflow(workflow._tag, registration)) return
|
|
116
|
+
yield* Effect.addFinalizer(() =>
|
|
117
|
+
Effect.sync(() => {
|
|
118
|
+
unregisterWorkflow(workflow._tag, registration)
|
|
119
|
+
})
|
|
120
|
+
)
|
|
121
|
+
}),
|
|
122
|
+
|
|
123
|
+
execute: Effect.fnUntraced(function*(workflow, opts) {
|
|
124
|
+
const context = yield* Effect.context<never>()
|
|
125
|
+
const payload = yield* encodePayload(workflow, opts.payload, context)
|
|
126
|
+
// A detached child keeps no parent, so neither its completion nor a
|
|
127
|
+
// failed run wakes the parent.
|
|
128
|
+
const parent = opts.parent === undefined || opts.detached
|
|
129
|
+
? undefined
|
|
130
|
+
: { workflowName: opts.parent.workflow._tag, executionId: opts.parent.executionId }
|
|
131
|
+
const text = yield* callExecution(
|
|
132
|
+
workflow._tag,
|
|
133
|
+
opts.executionId,
|
|
134
|
+
(stub) => stub.run(payload, { discard: opts.discard, parent })
|
|
135
|
+
)
|
|
136
|
+
if (opts.discard) return undefined
|
|
137
|
+
return yield* decodeResult(workflow, text, context)
|
|
138
|
+
}) as WorkflowEngine.Encoded["execute"],
|
|
139
|
+
|
|
140
|
+
poll: Effect.fnUntraced(function*(workflow, executionId) {
|
|
141
|
+
const context = yield* Effect.context<never>()
|
|
142
|
+
const text = yield* callExecution(workflow._tag, executionId, (stub) => stub.poll())
|
|
143
|
+
if (text === undefined) return Option.none()
|
|
144
|
+
return Option.some(yield* decodeResult(workflow, text, context))
|
|
145
|
+
}),
|
|
146
|
+
|
|
147
|
+
interrupt: (workflow, executionId) => callExecution(workflow._tag, executionId, (stub) => stub.interrupt()),
|
|
148
|
+
|
|
149
|
+
interruptUnsafe: (workflow, executionId) =>
|
|
150
|
+
callExecution(workflow._tag, executionId, (stub) => stub.interruptUnsafe()),
|
|
151
|
+
|
|
152
|
+
resume: (workflow, executionId) => callExecution(workflow._tag, executionId, (stub) => stub.resume()),
|
|
153
|
+
|
|
154
|
+
activityExecute: Effect.fnUntraced(function*(activity, attempt) {
|
|
155
|
+
const { handle, instance } = yield* localHandle("Activity execution")
|
|
156
|
+
const key = `${activity.name}/${attempt}`
|
|
157
|
+
const stored = handle.loadActivity(key)
|
|
158
|
+
if (stored !== undefined) {
|
|
159
|
+
return new Workflow.Complete({ exit: yield* decodeExit(stored, Context.empty()) })
|
|
160
|
+
}
|
|
161
|
+
// A pending interrupt replays only to rebuild compensations, so it must
|
|
162
|
+
// not start new activities. Suspending lets the run apply the interrupt.
|
|
163
|
+
if (!instance.interrupted && handle.loadDeferred(InterruptSignalName) !== undefined) {
|
|
164
|
+
instance.suspended = true
|
|
165
|
+
return new Workflow.Suspended({})
|
|
166
|
+
}
|
|
167
|
+
const activityInstance = WorkflowEngine.WorkflowInstance.initial(instance.workflow, instance.executionId)
|
|
168
|
+
activityInstance.interrupted = instance.interrupted
|
|
169
|
+
const result = yield* activity.executeEncoded.pipe(
|
|
170
|
+
Workflow.intoResult,
|
|
171
|
+
Effect.provideService(WorkflowEngine.WorkflowInstance, activityInstance)
|
|
172
|
+
)
|
|
173
|
+
if (result._tag === "Complete") {
|
|
174
|
+
handle.saveActivity(key, yield* encodeExit(result.exit, Context.empty()))
|
|
175
|
+
}
|
|
176
|
+
return result
|
|
177
|
+
}),
|
|
178
|
+
|
|
179
|
+
deferredResult: Effect.fnUntraced(function*(deferred) {
|
|
180
|
+
const { handle } = yield* localHandle("DurableDeferred reads")
|
|
181
|
+
const stored = handle.loadDeferred(deferred.name)
|
|
182
|
+
if (stored === undefined) return Option.none()
|
|
183
|
+
return Option.some(yield* decodeExit(stored, Context.empty()))
|
|
184
|
+
}),
|
|
185
|
+
|
|
186
|
+
deferredDone: Effect.fnUntraced(function*({ deferredName, executionId, exit, workflowName }) {
|
|
187
|
+
const text = yield* encodeExit(exit, Context.empty())
|
|
188
|
+
const handle = yield* Effect.serviceOption(CurrentExecutionHandle)
|
|
189
|
+
if (Option.isSome(handle) && handle.value.executionId === executionId) {
|
|
190
|
+
return yield* handle.value.completeOwnDeferred(deferredName, text)
|
|
191
|
+
}
|
|
192
|
+
yield* callExecution(workflowName, executionId, (stub) => stub.deferredDone(deferredName, text))
|
|
193
|
+
}),
|
|
194
|
+
|
|
195
|
+
scheduleClock: Effect.fnUntraced(function*(workflow, opts) {
|
|
196
|
+
const wakeUp = clock.currentTimeMillisUnsafe() + Math.ceil(Duration.toMillis(opts.clock.duration))
|
|
197
|
+
yield* callExecution(
|
|
198
|
+
workflow._tag,
|
|
199
|
+
opts.executionId,
|
|
200
|
+
(stub) => stub.scheduleClock(opts.clock.name, opts.clock.deferred.name, wakeUp)
|
|
201
|
+
)
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Layer that provides the `WorkflowEngine` backed by the workflow Durable
|
|
208
|
+
* Object namespace binding.
|
|
209
|
+
*
|
|
210
|
+
* **Details**
|
|
211
|
+
*
|
|
212
|
+
* `CloudflareCluster.layer` already includes this layer; use it directly only
|
|
213
|
+
* when the workflow engine is needed without the rest of the cluster.
|
|
214
|
+
*
|
|
215
|
+
* @stability experimental
|
|
216
|
+
* @category layers
|
|
217
|
+
* @since 4.1.3
|
|
218
|
+
*/
|
|
219
|
+
export const layer = (options: LayerOptions): Layer.Layer<WorkflowEngine.WorkflowEngine> =>
|
|
220
|
+
Layer.effect(WorkflowEngine.WorkflowEngine)(make(options))
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @since 4.1.3
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// The index stays free of the optional `alchemy` peer dependency, so the
|
|
6
|
+
// Alchemy integration is exposed only through its own entrypoint:
|
|
7
|
+
// `@effect/platform-cloudflare/AlchemyCloudflareCluster`.
|
|
8
|
+
// @barrel(Cloudflare*.ts): Auto-generated exports. Do not edit manually.
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @stability experimental
|
|
12
|
+
* @since 4.1.3
|
|
13
|
+
*/
|
|
14
|
+
export * as CloudflareCluster from "./CloudflareCluster.ts"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @stability experimental
|
|
18
|
+
* @since 4.1.3
|
|
19
|
+
*/
|
|
20
|
+
export * as CloudflareDurableObjectPrograms from "./CloudflareDurableObjectPrograms.ts"
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @stability experimental
|
|
24
|
+
* @since 4.1.3
|
|
25
|
+
*/
|
|
26
|
+
export * as CloudflareDurableObjects from "./CloudflareDurableObjects.ts"
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @stability experimental
|
|
30
|
+
* @since 4.1.3
|
|
31
|
+
*/
|
|
32
|
+
export * as CloudflarePersistedQueue from "./CloudflarePersistedQueue.ts"
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @stability experimental
|
|
36
|
+
* @since 4.1.3
|
|
37
|
+
*/
|
|
38
|
+
export * as CloudflareWorkflowEngine from "./CloudflareWorkflowEngine.ts"
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The alchemy-free half of `AlchemyCloudflareCluster.make`: building the
|
|
3
|
+
* cluster handle from resolved namespace bindings. Kept separate so it can be
|
|
4
|
+
* exercised without importing `alchemy`, whose runtime must match the
|
|
5
|
+
* published `effect` release rather than this workspace.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
import type * as Entity from "effect/cluster/Entity"
|
|
10
|
+
import * as Context from "effect/Context"
|
|
11
|
+
import type * as Duration from "effect/Duration"
|
|
12
|
+
import * as Effect from "effect/Effect"
|
|
13
|
+
import * as Layer from "effect/Layer"
|
|
14
|
+
import * as Scope from "effect/Scope"
|
|
15
|
+
import * as CloudflareCluster from "../CloudflareCluster.ts"
|
|
16
|
+
import type { SingletonWakeResult } from "../CloudflareDurableObjectPrograms.ts"
|
|
17
|
+
import type { Cluster } from "./cluster.ts"
|
|
18
|
+
import { boundedRetry, callDurableObject } from "./transport.ts"
|
|
19
|
+
|
|
20
|
+
interface SingletonStub {
|
|
21
|
+
// An object still on an older deploy resolves `undefined` on success.
|
|
22
|
+
readonly wake: () => Promise<SingletonWakeResult | undefined>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** @internal */
|
|
26
|
+
export interface ClusterHandleOptions {
|
|
27
|
+
readonly entities: ReadonlyArray<Entity.Entity<any, any>>
|
|
28
|
+
readonly layer: Layer.Layer<never, any, any>
|
|
29
|
+
readonly env: Record<string, unknown>
|
|
30
|
+
readonly keepAliveHeartbeat?: Duration.Input | undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @internal */
|
|
34
|
+
export const makeClusterHandle = Effect.fnUntraced(function*(options: ClusterHandleOptions) {
|
|
35
|
+
const entityNamespace = options.env.ClusterEntity as DurableObjectNamespace
|
|
36
|
+
const workflowNamespace = options.env.ClusterWorkflow as DurableObjectNamespace
|
|
37
|
+
const queueNamespace = options.env.ClusterDurableQueue as DurableObjectNamespace
|
|
38
|
+
const singletonNamespace = options.env.ClusterSingleton as DurableObjectNamespace
|
|
39
|
+
|
|
40
|
+
// The isolate-lifetime scope. workerd has no isolate-teardown hook, so it
|
|
41
|
+
// is never closed; the built services live for the isolate, exactly like
|
|
42
|
+
// the Wrangler path's `setInitializer` build.
|
|
43
|
+
const scope = Scope.makeUnsafe()
|
|
44
|
+
const context = yield* Layer.buildWithScope(
|
|
45
|
+
options.layer.pipe(
|
|
46
|
+
Layer.provideMerge(CloudflareCluster.layer({
|
|
47
|
+
entities: options.entities,
|
|
48
|
+
entityNamespace,
|
|
49
|
+
workflowNamespace,
|
|
50
|
+
queueNamespace,
|
|
51
|
+
singletonNamespace,
|
|
52
|
+
keepAliveHeartbeat: options.keepAliveHeartbeat
|
|
53
|
+
}))
|
|
54
|
+
),
|
|
55
|
+
scope
|
|
56
|
+
).pipe(
|
|
57
|
+
// Drop the build's memo map so a Layer the user provides inside a
|
|
58
|
+
// handler builds per event instead of sharing one instance pinned to
|
|
59
|
+
// the first request's IoContext.
|
|
60
|
+
Effect.map(Context.omit(Layer.CurrentMemoMap))
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const handle: Cluster<never> = {
|
|
64
|
+
provide: (effect) => Effect.provideContext(effect, context),
|
|
65
|
+
// Only a transport failure rejects `wake()`, so retrying never reruns a
|
|
66
|
+
// failed singleton. A failed run still fails the Cron Trigger.
|
|
67
|
+
wake: (name) => () =>
|
|
68
|
+
callDurableObject({
|
|
69
|
+
resolve: () => singletonNamespace.getByName(`Singleton/${name}`) as unknown as SingletonStub,
|
|
70
|
+
call: (stub) => stub.wake(),
|
|
71
|
+
retry: boundedRetry
|
|
72
|
+
}).pipe(
|
|
73
|
+
Effect.orDie,
|
|
74
|
+
Effect.flatMap((result) =>
|
|
75
|
+
result?._tag === "Failure"
|
|
76
|
+
? Effect.die(new Error(`Singleton "${name}" failed: ${result.message}`))
|
|
77
|
+
: Effect.void
|
|
78
|
+
)
|
|
79
|
+
),
|
|
80
|
+
entityNamespace,
|
|
81
|
+
workflowNamespace,
|
|
82
|
+
queueNamespace,
|
|
83
|
+
singletonNamespace,
|
|
84
|
+
context
|
|
85
|
+
}
|
|
86
|
+
return handle
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The plan-time handle: the deploy-machine evaluation only discovers the
|
|
91
|
+
* Durable Object declarations, and nothing returned from the Worker init
|
|
92
|
+
* program runs outside the deployed isolate.
|
|
93
|
+
*
|
|
94
|
+
* @internal
|
|
95
|
+
*/
|
|
96
|
+
export const inertClusterHandle = (): Cluster<never> => ({
|
|
97
|
+
provide: (effect) => effect,
|
|
98
|
+
wake: () => () => Effect.void,
|
|
99
|
+
get entityNamespace() {
|
|
100
|
+
return planTimeOnly("entityNamespace")
|
|
101
|
+
},
|
|
102
|
+
get workflowNamespace() {
|
|
103
|
+
return planTimeOnly("workflowNamespace")
|
|
104
|
+
},
|
|
105
|
+
get queueNamespace() {
|
|
106
|
+
return planTimeOnly("queueNamespace")
|
|
107
|
+
},
|
|
108
|
+
get singletonNamespace() {
|
|
109
|
+
return planTimeOnly("singletonNamespace")
|
|
110
|
+
},
|
|
111
|
+
get context() {
|
|
112
|
+
return planTimeOnly("context")
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const planTimeOnly = (field: string): never => {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`AlchemyCloudflareCluster: '${field}' is only available at runtime, not during plan evaluation`
|
|
119
|
+
)
|
|
120
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inserts refreshing the key's recency, then evicts the oldest entry once the
|
|
5
|
+
* map exceeds `capacity`.
|
|
6
|
+
*
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export const setWithEviction = <K, V>(map: Map<K, V>, key: K, value: V, capacity: number): void => {
|
|
10
|
+
map.delete(key)
|
|
11
|
+
map.set(key, value)
|
|
12
|
+
if (map.size <= capacity) return
|
|
13
|
+
const oldest = map.keys().next().value
|
|
14
|
+
if (oldest !== undefined) map.delete(oldest)
|
|
15
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared cluster handle types without the Alchemy dependency graph.
|
|
3
|
+
*
|
|
4
|
+
* @since 4.1.3
|
|
5
|
+
*/
|
|
6
|
+
import type * as Context from "effect/Context"
|
|
7
|
+
import type * as Effect from "effect/Effect"
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The handle returned by `AlchemyCloudflareCluster.make`.
|
|
11
|
+
*
|
|
12
|
+
* **Gotchas**
|
|
13
|
+
*
|
|
14
|
+
* Namespace bindings and `context` are available only in the deployed runtime.
|
|
15
|
+
* Reading them during plan evaluation throws a diagnostic; `provide` and
|
|
16
|
+
* `wake` remain inert during planning.
|
|
17
|
+
*
|
|
18
|
+
* @category models
|
|
19
|
+
* @since 4.1.3
|
|
20
|
+
*/
|
|
21
|
+
export interface Cluster<in R = never> {
|
|
22
|
+
/**
|
|
23
|
+
* Attaches the built cluster context to a user Effect, for the Worker's
|
|
24
|
+
* `fetch` handler and other entrypoints. Only provides the already-built
|
|
25
|
+
* Context; it does not open a new Scope.
|
|
26
|
+
*/
|
|
27
|
+
readonly provide: <A, E, R2>(effect: Effect.Effect<A, E, R2>) => Effect.Effect<A, E, Exclude<R2, R>>
|
|
28
|
+
/**
|
|
29
|
+
* A handler that wakes the named singleton, for `Cloudflare.Workers.cron`.
|
|
30
|
+
* The name is the singleton name without the `Singleton/` prefix, and the
|
|
31
|
+
* returned handler is already context-provided. A `ClusterCron` fires only
|
|
32
|
+
* once its seed singleton is woken; get its name from
|
|
33
|
+
* `CloudflareCluster.clusterCronSingletonName`.
|
|
34
|
+
*/
|
|
35
|
+
readonly wake: (name: string) => () => Effect.Effect<void>
|
|
36
|
+
/**
|
|
37
|
+
* The native namespace binding of the cluster entity class.
|
|
38
|
+
*/
|
|
39
|
+
readonly entityNamespace: DurableObjectNamespace
|
|
40
|
+
/**
|
|
41
|
+
* The native namespace binding of the workflow class.
|
|
42
|
+
*/
|
|
43
|
+
readonly workflowNamespace: DurableObjectNamespace
|
|
44
|
+
/**
|
|
45
|
+
* The native namespace binding of the durable queue class.
|
|
46
|
+
*/
|
|
47
|
+
readonly queueNamespace: DurableObjectNamespace
|
|
48
|
+
/**
|
|
49
|
+
* The native namespace binding of the singleton class.
|
|
50
|
+
*/
|
|
51
|
+
readonly singletonNamespace: DurableObjectNamespace
|
|
52
|
+
/**
|
|
53
|
+
* The Context built from the cluster layer and the user layer into the
|
|
54
|
+
* isolate-lifetime Scope.
|
|
55
|
+
*/
|
|
56
|
+
readonly context: Context.Context<R>
|
|
57
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ClusterName } from "../CloudflareCluster.ts"
|
|
2
|
+
|
|
3
|
+
/** @internal */
|
|
4
|
+
export const encodeName = (type: string, id: string): string => `${type.length}:${type}${id}`
|
|
5
|
+
|
|
6
|
+
const lengthPrefix = /^([1-9]\d*):/
|
|
7
|
+
|
|
8
|
+
/** @internal */
|
|
9
|
+
export const decodeName = (name: string): ClusterName | undefined => {
|
|
10
|
+
const match = lengthPrefix.exec(name)
|
|
11
|
+
if (match === null) return undefined
|
|
12
|
+
const typeLength = Number(match[1])
|
|
13
|
+
const payload = name.slice(match[0].length)
|
|
14
|
+
if (typeLength > payload.length) return undefined
|
|
15
|
+
return {
|
|
16
|
+
type: payload.slice(0, typeLength),
|
|
17
|
+
id: payload.slice(typeLength)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep-alive for one entity Durable Object, in three separate pieces of
|
|
3
|
+
* state:
|
|
4
|
+
*
|
|
5
|
+
* - `wanted` mirrors the persisted `keep_alive` flag that a restart restores.
|
|
6
|
+
* - `hold` is the fiber running the self `hold()` RPC while one is in flight.
|
|
7
|
+
* - The holder count belongs to the current handler build only. Persisting it
|
|
8
|
+
* would break on restart: an `EntityResource` re-acquired by the rebuild
|
|
9
|
+
* adds a holder on top of the stored count, which then never returns to 0.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
import * as Duration from "effect/Duration"
|
|
14
|
+
import * as Effect from "effect/Effect"
|
|
15
|
+
import type * as Fiber from "effect/Fiber"
|
|
16
|
+
import * as Latch from "effect/Latch"
|
|
17
|
+
import * as Schedule from "effect/Schedule"
|
|
18
|
+
|
|
19
|
+
/** @internal */
|
|
20
|
+
export interface EntityKeepAlive {
|
|
21
|
+
/** The `Entity.keepAlive` handler. */
|
|
22
|
+
readonly update: (enabled: boolean) => Effect.Effect<void>
|
|
23
|
+
/** The body of the self `hold()` RPC; resolves once the hold is released. */
|
|
24
|
+
readonly await: Effect.Effect<void>
|
|
25
|
+
/** Forgets the holders of a closed handler build. The hold stays. */
|
|
26
|
+
readonly resetHolders: () => void
|
|
27
|
+
/** Starts the self hold when keep-alive is wanted and none is in flight. */
|
|
28
|
+
readonly restore: () => void
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** @internal */
|
|
32
|
+
export interface EntityKeepAliveOptions {
|
|
33
|
+
/** Starts the self `hold()` RPC; the promise settles when the hold ends. */
|
|
34
|
+
readonly startHold: () => Promise<void>
|
|
35
|
+
/** The persisted flag, read when the Durable Object starts. */
|
|
36
|
+
readonly wanted: boolean
|
|
37
|
+
/**
|
|
38
|
+
* Persists each requested flag in a forked fiber. The hold is released only
|
|
39
|
+
* after a successful `false` write while keep-alive is still unwanted.
|
|
40
|
+
*/
|
|
41
|
+
readonly persist: (wanted: boolean) => Effect.Effect<void>
|
|
42
|
+
/** Upper bound of the retry delay after a failed hold RPC. */
|
|
43
|
+
readonly retryCapMillis: () => number
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** @internal */
|
|
47
|
+
export const makeEntityKeepAlive = (options: EntityKeepAliveOptions): EntityKeepAlive => {
|
|
48
|
+
const latch = Latch.makeUnsafe(true)
|
|
49
|
+
let holders = 0
|
|
50
|
+
let wanted = options.wanted
|
|
51
|
+
let hold: Fiber.Fiber<void, unknown> | undefined
|
|
52
|
+
|
|
53
|
+
// A safeguard only: production probes never saw a rejected hold.
|
|
54
|
+
const runHold = Effect.tryPromise(() => options.startHold()).pipe(
|
|
55
|
+
Effect.tapError((error) => Effect.logWarning("Entity keep-alive hold failed, retrying", error)),
|
|
56
|
+
Effect.retry(
|
|
57
|
+
Schedule.exponential(100).pipe(
|
|
58
|
+
Schedule.modifyDelay(({ duration }) =>
|
|
59
|
+
Effect.succeed(Duration.min(duration, Duration.millis(options.retryCapMillis())))
|
|
60
|
+
),
|
|
61
|
+
Schedule.jittered
|
|
62
|
+
)
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
const restore = () => {
|
|
67
|
+
if (!wanted || hold !== undefined) return
|
|
68
|
+
latch.closeUnsafe()
|
|
69
|
+
const fiber = Effect.runFork(runHold)
|
|
70
|
+
hold = fiber
|
|
71
|
+
fiber.addObserver(() => {
|
|
72
|
+
if (hold === fiber) hold = undefined
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const release = () => {
|
|
77
|
+
if (hold === undefined) return
|
|
78
|
+
hold.interruptUnsafe()
|
|
79
|
+
hold = undefined
|
|
80
|
+
latch.openUnsafe()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const setWanted = (next: boolean) => {
|
|
84
|
+
if (wanted === next) return
|
|
85
|
+
wanted = next
|
|
86
|
+
// Mailbox writes wait for an open user transaction, so writing inline
|
|
87
|
+
// would deadlock `Entity.keepAlive` called inside `withTransaction`.
|
|
88
|
+
Effect.runFork(
|
|
89
|
+
Effect.suspend(() => options.persist(next)).pipe(
|
|
90
|
+
Effect.andThen(Effect.sync(() => {
|
|
91
|
+
if (!next && !wanted) release()
|
|
92
|
+
})),
|
|
93
|
+
Effect.catchCause((cause) => Effect.logError("Entity keep-alive persistence failed", cause))
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const update = (enabled: boolean): Effect.Effect<void> =>
|
|
99
|
+
Effect.sync(() => {
|
|
100
|
+
if (enabled) {
|
|
101
|
+
holders++
|
|
102
|
+
setWanted(true)
|
|
103
|
+
// Also restarts a hold that ended without a release.
|
|
104
|
+
restore()
|
|
105
|
+
return
|
|
106
|
+
}
|
|
107
|
+
// With no holders in memory the flag may still be set: a fresh object
|
|
108
|
+
// after a restart has count 0 while its stored row says 1.
|
|
109
|
+
if (holders > 0) holders--
|
|
110
|
+
if (holders === 0) setWanted(false)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
update,
|
|
115
|
+
await: latch.await,
|
|
116
|
+
resetHolders: () => {
|
|
117
|
+
holders = 0
|
|
118
|
+
},
|
|
119
|
+
restore
|
|
120
|
+
}
|
|
121
|
+
}
|