lopata 0.18.4 → 0.19.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/dist/types/api/dispatch.d.ts +2 -2
- package/dist/types/api/handlers/workflows.d.ts +2 -2
- package/dist/types/bindings/container-cleanup.d.ts +43 -0
- package/dist/types/bindings/container-docker.d.ts +24 -0
- package/dist/types/bindings/container.d.ts +4 -2
- package/dist/types/bindings/do-executor-worker.d.ts +73 -89
- package/dist/types/bindings/do-executor.d.ts +22 -1
- package/dist/types/bindings/do-worker-env.d.ts +16 -7
- package/dist/types/bindings/durable-object.d.ts +66 -4
- package/dist/types/bindings/queue.d.ts +4 -1
- package/dist/types/bindings/rpc-stub.d.ts +23 -1
- package/dist/types/bindings/scheduled.d.ts +13 -3
- package/dist/types/bindings/service-binding.d.ts +13 -4
- package/dist/types/bindings/static-assets.d.ts +1 -1
- package/dist/types/bindings/websocket-pair.d.ts +12 -3
- package/dist/types/bindings/workflow.d.ts +29 -0
- package/dist/types/config.d.ts +2 -0
- package/dist/types/env.d.ts +6 -0
- package/dist/types/error-page-render.d.ts +6 -0
- package/dist/types/execution-context.d.ts +8 -0
- package/dist/types/generation-manager.d.ts +21 -2
- package/dist/types/generation.d.ts +16 -21
- package/dist/types/import-graph.d.ts +28 -0
- package/dist/types/lopata-config.d.ts +3 -3
- package/dist/types/plugin.d.ts +4 -1
- package/dist/types/rpc-validate.d.ts +9 -0
- package/dist/types/setup-globals.d.ts +5 -1
- package/dist/types/tracing/context.d.ts +12 -0
- package/dist/types/tracing/span.d.ts +21 -3
- package/dist/types/tracing/store.d.ts +17 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/types/vite-plugin/index.d.ts +8 -1
- package/dist/types/worker-registry.d.ts +11 -4
- package/dist/types/worker-thread/do-protocol.d.ts +256 -0
- package/dist/types/worker-thread/entry.d.ts +2 -0
- package/dist/types/worker-thread/execution-context.d.ts +22 -0
- package/dist/types/worker-thread/executor.d.ts +123 -0
- package/dist/types/worker-thread/protocol.d.ts +553 -0
- package/dist/types/worker-thread/remote-trace-store.d.ts +27 -0
- package/dist/types/worker-thread/rpc-client.d.ts +4 -0
- package/dist/types/worker-thread/rpc-shared.d.ts +138 -0
- package/dist/types/worker-thread/serialize.d.ts +12 -0
- package/dist/types/worker-thread/stream-shared.d.ts +147 -0
- package/dist/types/worker-thread/thread-env.d.ts +42 -0
- package/dist/types/worker-thread/wire-handlers.d.ts +16 -0
- package/dist/types/worker-thread/ws-bridge-shared.d.ts +163 -0
- package/package.json +1 -1
- package/src/api/handlers/containers.ts +2 -1
- package/src/api/handlers/workflows.ts +39 -34
- package/src/bindings/container-cleanup.ts +125 -0
- package/src/bindings/container-docker.ts +49 -34
- package/src/bindings/container.ts +24 -9
- package/src/bindings/do-executor-inprocess.ts +9 -5
- package/src/bindings/do-executor-worker.ts +386 -158
- package/src/bindings/do-executor.ts +23 -1
- package/src/bindings/do-worker-entry.ts +242 -60
- package/src/bindings/do-worker-env.ts +296 -11
- package/src/bindings/durable-object.ts +231 -27
- package/src/bindings/email.ts +6 -0
- package/src/bindings/queue.ts +11 -1
- package/src/bindings/rpc-stub.ts +79 -10
- package/src/bindings/scheduled.ts +37 -30
- package/src/bindings/service-binding.ts +87 -33
- package/src/bindings/static-assets.ts +3 -2
- package/src/bindings/websocket-pair.ts +19 -3
- package/src/bindings/workflow.ts +91 -0
- package/src/cli/dev.ts +106 -41
- package/src/config.ts +6 -3
- package/src/db.ts +1 -0
- package/src/env.ts +40 -21
- package/src/error-page-render.ts +21 -0
- package/src/execution-context.ts +13 -3
- package/src/generation-manager.ts +144 -143
- package/src/generation.ts +150 -306
- package/src/import-graph.ts +140 -0
- package/src/lopata-config.ts +3 -3
- package/src/plugin.ts +7 -0
- package/src/rpc-validate.ts +29 -0
- package/src/setup-globals.ts +6 -17
- package/src/testing/durable-object.ts +5 -3
- package/src/testing/index.ts +8 -4
- package/src/tracing/context.ts +18 -0
- package/src/tracing/span.ts +83 -57
- package/src/tracing/store.ts +41 -3
- package/src/virtual-modules.ts +2 -0
- package/src/vite-plugin/dev-server-plugin.ts +4 -0
- package/src/vite-plugin/index.ts +8 -1
- package/src/worker-registry.ts +15 -2
- package/src/worker-thread/do-protocol.ts +237 -0
- package/src/worker-thread/entry.ts +453 -0
- package/src/worker-thread/execution-context.ts +53 -0
- package/src/worker-thread/executor.ts +595 -0
- package/src/worker-thread/protocol.ts +552 -0
- package/src/worker-thread/remote-trace-store.ts +90 -0
- package/src/worker-thread/rpc-client.ts +5 -0
- package/src/worker-thread/rpc-shared.ts +503 -0
- package/src/worker-thread/serialize.ts +37 -0
- package/src/worker-thread/stream-shared.ts +414 -0
- package/src/worker-thread/thread-env.ts +350 -0
- package/src/worker-thread/wire-handlers.ts +80 -0
- package/src/worker-thread/ws-bridge-shared.ts +482 -0
- package/dist/types/bindings/do-websocket-bridge.d.ts +0 -60
- package/dist/types/module-cache.d.ts +0 -23
- package/src/bindings/do-websocket-bridge.ts +0 -79
- package/src/module-cache.ts +0 -58
- package/src/tracing/global.d.ts +0 -50
|
@@ -8,33 +8,161 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { Database } from 'bun:sqlite'
|
|
11
|
-
import { mkdirSync } from 'node:fs'
|
|
12
|
-
import { join } from 'node:path'
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync } from 'node:fs'
|
|
12
|
+
import { join, resolve } from 'node:path'
|
|
13
13
|
import type { WranglerConfig } from '../config'
|
|
14
14
|
import { runMigrations } from '../db'
|
|
15
|
+
import { parseDevVars } from '../env'
|
|
16
|
+
import { warnCrossThreadRpcArgs, warnInvalidRpcArgs } from '../rpc-validate'
|
|
17
|
+
import { getActiveContext } from '../tracing/context'
|
|
18
|
+
import type { BindingTarget, ParentSpanContext, SerializedResponse, WorkflowControlOp, WorkflowControlResult } from '../worker-thread/protocol'
|
|
19
|
+
import { RpcClient } from '../worker-thread/rpc-shared'
|
|
20
|
+
import { tagCloneable } from '../worker-thread/rpc-shared'
|
|
21
|
+
import type { WsGuestBridge } from '../worker-thread/ws-bridge-shared'
|
|
22
|
+
import { AiBinding } from './ai'
|
|
23
|
+
import { SqliteAnalyticsEngine } from './analytics-engine'
|
|
24
|
+
import { BrowserBinding } from './browser'
|
|
15
25
|
import { openD1Database } from './d1'
|
|
16
|
-
import {
|
|
26
|
+
import type { DOMainMessage } from './do-executor-worker'
|
|
27
|
+
import { DurableObjectIdImpl, hashIdFromName, randomUniqueIdHex } from './durable-object'
|
|
28
|
+
import { EmailMessage } from './email'
|
|
29
|
+
import { HyperdriveBinding } from './hyperdrive'
|
|
30
|
+
import { ImagesBinding } from './images'
|
|
17
31
|
import { SqliteKVNamespace } from './kv'
|
|
32
|
+
import { MediaBinding } from './media'
|
|
18
33
|
import { SqliteQueueProducer } from './queue'
|
|
19
34
|
import { FileR2Bucket } from './r2'
|
|
35
|
+
import { makeBindingProxy } from './rpc-stub'
|
|
36
|
+
import { StaticAssets } from './static-assets'
|
|
37
|
+
import type { ResponseWithWebSocket } from './websocket-pair'
|
|
38
|
+
|
|
39
|
+
/** Build an RpcClient that bridges DO-worker → main over the DO executor channel. */
|
|
40
|
+
export function createDoEnvRpc(post: (msg: DOMainMessage) => void): RpcClient {
|
|
41
|
+
const getParent = (): ParentSpanContext | undefined => {
|
|
42
|
+
const active = getActiveContext()
|
|
43
|
+
return active ? { traceId: active.traceId, spanId: active.spanId } : undefined
|
|
44
|
+
}
|
|
45
|
+
return new RpcClient(req => post(req as DOMainMessage), getParent)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildBridgedFetchResponse(
|
|
49
|
+
r: SerializedResponse,
|
|
50
|
+
rpc: RpcClient,
|
|
51
|
+
envWsBridge: WsGuestBridge<DOMainMessage>,
|
|
52
|
+
): Response {
|
|
53
|
+
const response = rpc.makeResponse(r) as ResponseWithWebSocket
|
|
54
|
+
if (r.webSocketId !== undefined) {
|
|
55
|
+
response.webSocket = envWsBridge.createBridgedSocket(r.webSocketId)
|
|
56
|
+
}
|
|
57
|
+
return response
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function makeRpcProxy(
|
|
61
|
+
target: BindingTarget,
|
|
62
|
+
rpc: RpcClient,
|
|
63
|
+
envWsBridge: WsGuestBridge<DOMainMessage>,
|
|
64
|
+
extras: Record<string | symbol, unknown> = {},
|
|
65
|
+
): Record<string, unknown> {
|
|
66
|
+
return makeBindingProxy(
|
|
67
|
+
{
|
|
68
|
+
fetch: async (input, init) => {
|
|
69
|
+
const req = input instanceof Request ? input : new Request(input instanceof URL ? input.href : input, init)
|
|
70
|
+
const r = await rpc.callFetch(target, req)
|
|
71
|
+
return buildBridgedFetchResponse(r, rpc, envWsBridge)
|
|
72
|
+
},
|
|
73
|
+
call: (prop, args) => {
|
|
74
|
+
warnInvalidRpcArgs(args, prop)
|
|
75
|
+
warnCrossThreadRpcArgs(args, prop)
|
|
76
|
+
return rpc.call(target, prop, args)
|
|
77
|
+
},
|
|
78
|
+
getProperty: prop => rpc.callGet(target, prop),
|
|
79
|
+
},
|
|
80
|
+
extras,
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function makeEnvBindingProxy(binding: string, rpc: RpcClient, envWsBridge: WsGuestBridge<DOMainMessage>): Record<string, unknown> {
|
|
85
|
+
return makeRpcProxy({ binding }, rpc, envWsBridge)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function makeDoEnvStubProxy(
|
|
89
|
+
bindingName: string,
|
|
90
|
+
idStr: string,
|
|
91
|
+
id: DurableObjectIdImpl,
|
|
92
|
+
rpc: RpcClient,
|
|
93
|
+
envWsBridge: WsGuestBridge<DOMainMessage>,
|
|
94
|
+
): Record<string, unknown> {
|
|
95
|
+
return makeRpcProxy({ binding: bindingName, instanceId: idStr, instanceName: id.name }, rpc, envWsBridge, { id, name: id.name })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* DO namespace proxy for use inside a DO worker thread. ID factories run
|
|
100
|
+
* locally (deterministic); `.get()` produces a stub that ships
|
|
101
|
+
* `{ binding, instanceId, instanceName }` to main, where the namespace's
|
|
102
|
+
* `.get()` resolves the singleton executor for that id. Mirrors the
|
|
103
|
+
* user-worker `makeDONamespaceProxy` shape.
|
|
104
|
+
*/
|
|
105
|
+
function makeDoEnvNamespaceProxy(bindingName: string, rpc: RpcClient, envWsBridge: WsGuestBridge<DOMainMessage>): Record<string, unknown> {
|
|
106
|
+
const stubs = new Map<string, unknown>()
|
|
107
|
+
const idFromName = (name: string) => new DurableObjectIdImpl(hashIdFromName(name), name)
|
|
108
|
+
const idFromString = (idStr: string) => new DurableObjectIdImpl(idStr)
|
|
109
|
+
const newUniqueId = (_opts?: { jurisdiction?: string }) => new DurableObjectIdImpl(randomUniqueIdHex())
|
|
110
|
+
const get = (id: DurableObjectIdImpl) => {
|
|
111
|
+
const idStr = id.toString()
|
|
112
|
+
// Key includes the name so a nameless `idFromString(hash)` and a named
|
|
113
|
+
// `idFromName(...)` resolving to the same hash get distinct stubs, each
|
|
114
|
+
// preserving its caller's `id.name`. Matches thread-env's makeDONamespaceProxy.
|
|
115
|
+
const key = `${idStr}:${id.name ?? ''}`
|
|
116
|
+
let stub = stubs.get(key)
|
|
117
|
+
if (!stub) {
|
|
118
|
+
stub = makeDoEnvStubProxy(bindingName, idStr, id, rpc, envWsBridge)
|
|
119
|
+
stubs.set(key, stub)
|
|
120
|
+
}
|
|
121
|
+
return stub
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
idFromName,
|
|
125
|
+
idFromString,
|
|
126
|
+
newUniqueId,
|
|
127
|
+
get,
|
|
128
|
+
getByName: (name: string) => get(idFromName(name)),
|
|
129
|
+
}
|
|
130
|
+
}
|
|
20
131
|
|
|
21
132
|
/**
|
|
22
133
|
* Build a minimal env for use inside a DO worker thread.
|
|
23
|
-
*
|
|
134
|
+
*
|
|
135
|
+
* Stateless bindings (KV/R2/D1/queue producer/nested DO) are instantiated
|
|
136
|
+
* locally against the shared SQLite/filesystem. Stateful ones (service
|
|
137
|
+
* bindings, email, workflow, …) become RPC proxies that route through main
|
|
138
|
+
* via the DO-executor message channel.
|
|
139
|
+
*
|
|
140
|
+
* `envWsBridge` is shared with `do-worker-entry.ts`'s message router: when an
|
|
141
|
+
* env-binding fetch returns a 101 response with a `webSocketId`, the proxy
|
|
142
|
+
* here calls `envWsBridge.createBridgedSocket(id)` to reconstruct a
|
|
143
|
+
* user-facing CFWebSocket whose events flow over the bridge to main.
|
|
24
144
|
*/
|
|
25
145
|
export function buildWorkerEnv(
|
|
26
146
|
config: WranglerConfig,
|
|
27
147
|
dataDir: string,
|
|
28
|
-
|
|
148
|
+
baseDir: string,
|
|
149
|
+
rpc: RpcClient,
|
|
150
|
+
_hostNamespaceName: string,
|
|
151
|
+
envWsBridge: WsGuestBridge<DOMainMessage>,
|
|
152
|
+
): { db: Database; env: Record<string, unknown> } {
|
|
29
153
|
// Open own DB connection (WAL mode for safe concurrency)
|
|
30
154
|
const dbPath = join(dataDir, 'data.sqlite')
|
|
31
155
|
mkdirSync(dataDir, { recursive: true })
|
|
32
156
|
const db = new Database(dbPath, { create: true })
|
|
33
157
|
db.run('PRAGMA journal_mode=WAL')
|
|
158
|
+
// Match db.ts / thread-env.ts: WAL allows only one writer at a time across
|
|
159
|
+
// connections, so without busy_timeout a concurrent write from the main or
|
|
160
|
+
// user-worker connection fails this DO-worker write instantly with
|
|
161
|
+
// SQLITE_BUSY instead of waiting.
|
|
162
|
+
db.run('PRAGMA busy_timeout=5000')
|
|
34
163
|
runMigrations(db)
|
|
35
164
|
|
|
36
165
|
const env: Record<string, unknown> = {}
|
|
37
|
-
const doNamespaces: { className: string; namespace: DurableObjectNamespaceImpl }[] = []
|
|
38
166
|
|
|
39
167
|
// Environment variables
|
|
40
168
|
if (config.vars) {
|
|
@@ -43,6 +171,17 @@ export function buildWorkerEnv(
|
|
|
43
171
|
}
|
|
44
172
|
}
|
|
45
173
|
|
|
174
|
+
// `.dev.vars` / `.env` — on real CF a DO's env equals the worker's env, which
|
|
175
|
+
// includes these secrets. Without this `this.env.MY_SECRET` is undefined.
|
|
176
|
+
const devVarsPath = join(baseDir, '.dev.vars')
|
|
177
|
+
const envPath = join(baseDir, '.env')
|
|
178
|
+
const filePath = existsSync(devVarsPath) ? devVarsPath : existsSync(envPath) ? envPath : null
|
|
179
|
+
if (filePath) {
|
|
180
|
+
for (const [key, value] of Object.entries(parseDevVars(readFileSync(filePath, 'utf-8')))) {
|
|
181
|
+
env[key] = value
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
46
185
|
// KV namespaces
|
|
47
186
|
for (const kv of config.kv_namespaces ?? []) {
|
|
48
187
|
env[kv.binding] = new SqliteKVNamespace(db, kv.id)
|
|
@@ -53,11 +192,23 @@ export function buildWorkerEnv(
|
|
|
53
192
|
env[r2.binding] = new FileR2Bucket(db, r2.bucket_name, dataDir)
|
|
54
193
|
}
|
|
55
194
|
|
|
56
|
-
// Durable Objects —
|
|
195
|
+
// Durable Objects — every DO binding (including the host class) routes via
|
|
196
|
+
// main's namespace over env-RPC. The stub ships `{ instanceId, instanceName }`
|
|
197
|
+
// in `BindingTarget`; main's `_resolveBinding` reconstructs the
|
|
198
|
+
// `DurableObjectId` and resolves the singleton executor, so both self-DO
|
|
199
|
+
// and cross-DO access reach the same instance state main owns.
|
|
200
|
+
const doBindingNames = new Set<string>()
|
|
57
201
|
for (const doBinding of config.durable_objects?.bindings ?? []) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
202
|
+
env[doBinding.name] = makeDoEnvNamespaceProxy(doBinding.name, rpc, envWsBridge)
|
|
203
|
+
doBindingNames.add(doBinding.name)
|
|
204
|
+
}
|
|
205
|
+
// Container DOs whose binding isn't already declared under `durable_objects`
|
|
206
|
+
// (main synthesises a DO namespace for them); the worker env needs the
|
|
207
|
+
// matching proxy or the binding is missing at runtime.
|
|
208
|
+
for (const container of config.containers ?? []) {
|
|
209
|
+
const bindingName = container.name ?? container.class_name
|
|
210
|
+
if (doBindingNames.has(bindingName)) continue
|
|
211
|
+
env[bindingName] = makeDoEnvNamespaceProxy(bindingName, rpc, envWsBridge)
|
|
61
212
|
}
|
|
62
213
|
|
|
63
214
|
// D1 databases
|
|
@@ -70,5 +221,139 @@ export function buildWorkerEnv(
|
|
|
70
221
|
env[producer.binding] = new SqliteQueueProducer(db, producer.queue, producer.delivery_delay ?? 0)
|
|
71
222
|
}
|
|
72
223
|
|
|
73
|
-
|
|
224
|
+
for (const svc of config.services ?? []) {
|
|
225
|
+
env[svc.binding] = makeEnvBindingProxy(svc.binding, rpc, envWsBridge)
|
|
226
|
+
}
|
|
227
|
+
for (const email of config.send_email ?? []) {
|
|
228
|
+
// A plain RPC proxy would DataCloneError on an EmailMessage whose `raw` is a
|
|
229
|
+
// ReadableStream; materialize it and tag the class so main can rebuild it.
|
|
230
|
+
env[email.name] = makeSendEmailProxy(email.name, rpc, envWsBridge)
|
|
231
|
+
}
|
|
232
|
+
for (const wf of config.workflows ?? []) {
|
|
233
|
+
env[wf.binding] = makeWorkflowEnvProxy(wf.binding, rpc, envWsBridge)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Stateless bindings missing until now — on real CF a DO's env equals the
|
|
237
|
+
// worker's env, so these must be present here too (mirrors thread-env.ts).
|
|
238
|
+
if (config.assets?.binding) {
|
|
239
|
+
const assetsDir = resolve(baseDir, config.assets.directory)
|
|
240
|
+
env[config.assets.binding] = new StaticAssets(assetsDir, config.assets.html_handling, config.assets.not_found_handling)
|
|
241
|
+
}
|
|
242
|
+
if (config.images) {
|
|
243
|
+
env[config.images.binding] = new ImagesBinding()
|
|
244
|
+
}
|
|
245
|
+
if (config.media) {
|
|
246
|
+
env[config.media.binding] = new MediaBinding()
|
|
247
|
+
}
|
|
248
|
+
for (const hd of config.hyperdrive ?? []) {
|
|
249
|
+
env[hd.binding] = new HyperdriveBinding(hd.localConnectionString ?? '')
|
|
250
|
+
}
|
|
251
|
+
if (config.browser) {
|
|
252
|
+
env[config.browser.binding] = new BrowserBinding({})
|
|
253
|
+
}
|
|
254
|
+
if (config.ai) {
|
|
255
|
+
const accountId = typeof env.CLOUDFLARE_ACCOUNT_ID === 'string' ? env.CLOUDFLARE_ACCOUNT_ID : process.env.CLOUDFLARE_ACCOUNT_ID
|
|
256
|
+
const apiToken = typeof env.CLOUDFLARE_API_TOKEN === 'string' ? env.CLOUDFLARE_API_TOKEN : process.env.CLOUDFLARE_API_TOKEN
|
|
257
|
+
env[config.ai.binding] = new AiBinding(db, accountId, apiToken)
|
|
258
|
+
}
|
|
259
|
+
for (const ae of config.analytics_engine_datasets ?? []) {
|
|
260
|
+
env[ae.binding] = new SqliteAnalyticsEngine(db, ae.dataset ?? ae.binding)
|
|
261
|
+
}
|
|
262
|
+
if (config.version_metadata) {
|
|
263
|
+
env[config.version_metadata.binding] = { id: 'local-dev', tag: '', timestamp: new Date().toISOString() }
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { db, env }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function materializeEmailRaw(raw: unknown): Promise<Uint8Array | ArrayBuffer | string> {
|
|
270
|
+
if (typeof raw === 'string' || raw instanceof Uint8Array || raw instanceof ArrayBuffer) return raw
|
|
271
|
+
if (raw && typeof (raw as ReadableStream).getReader === 'function') {
|
|
272
|
+
return new Response(raw as ReadableStream).arrayBuffer()
|
|
273
|
+
}
|
|
274
|
+
throw new Error('EmailMessage.raw must be a string, Uint8Array, ArrayBuffer, or ReadableStream')
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Workflow binding proxy for DO worker threads. A generic RPC proxy would land
|
|
279
|
+
* on main's HOLLOW `SqliteWorkflowBinding` (in thread mode main never wires the
|
|
280
|
+
* class — `create()` throws and instance handles silently no-op against empty
|
|
281
|
+
* per-process registries), and the `SqliteWorkflowInstance` a real call returns
|
|
282
|
+
* couldn't cross the boundary anyway. Instead, model the public Workflow API as
|
|
283
|
+
* `executeControl` ops: main's binding forwards them through its thread router
|
|
284
|
+
* to the user worker that owns the live state machine.
|
|
285
|
+
*/
|
|
286
|
+
function makeWorkflowEnvProxy(bindingName: string, rpc: RpcClient, envWsBridge: WsGuestBridge<DOMainMessage>): Record<string, unknown> {
|
|
287
|
+
const target: BindingTarget = { binding: bindingName }
|
|
288
|
+
const control = async (op: WorkflowControlOp): Promise<WorkflowControlResult> => {
|
|
289
|
+
const result: unknown = await rpc.call(target, 'executeControl', [op])
|
|
290
|
+
if (typeof result === 'object' && result !== null && 'kind' in result && typeof result.kind === 'string') {
|
|
291
|
+
return result as WorkflowControlResult
|
|
292
|
+
}
|
|
293
|
+
throw new Error('Malformed workflow control result')
|
|
294
|
+
}
|
|
295
|
+
const expectCreate = (result: WorkflowControlResult) => {
|
|
296
|
+
if (result.kind !== 'create') throw new Error(`Unexpected workflow control result "${result.kind}" (expected "create")`)
|
|
297
|
+
return result
|
|
298
|
+
}
|
|
299
|
+
const expectStatus = (result: WorkflowControlResult) => {
|
|
300
|
+
if (result.kind !== 'status') throw new Error(`Unexpected workflow control result "${result.kind}" (expected "status")`)
|
|
301
|
+
return result
|
|
302
|
+
}
|
|
303
|
+
const makeHandle = (id: string) => ({
|
|
304
|
+
id,
|
|
305
|
+
status: async () => expectStatus(await control({ kind: 'status', instanceId: id })).value,
|
|
306
|
+
pause: async () => {
|
|
307
|
+
await control({ kind: 'pause', instanceId: id })
|
|
308
|
+
},
|
|
309
|
+
resume: async () => {
|
|
310
|
+
await control({ kind: 'resume', instanceId: id })
|
|
311
|
+
},
|
|
312
|
+
terminate: async () => {
|
|
313
|
+
await control({ kind: 'terminate', instanceId: id })
|
|
314
|
+
},
|
|
315
|
+
restart: async (options?: { fromStep?: string }) => {
|
|
316
|
+
await control({ kind: 'restart', instanceId: id, fromStep: options?.fromStep })
|
|
317
|
+
},
|
|
318
|
+
skipSleep: async () => {
|
|
319
|
+
await control({ kind: 'skipSleep', instanceId: id })
|
|
320
|
+
},
|
|
321
|
+
sendEvent: async (event: { type: string; payload?: unknown }) => {
|
|
322
|
+
await control({ kind: 'sendEvent', instanceId: id, eventType: event.type, payload: event.payload })
|
|
323
|
+
},
|
|
324
|
+
})
|
|
325
|
+
return makeRpcProxy(target, rpc, envWsBridge, {
|
|
326
|
+
create: async (options?: { id?: string; params?: unknown }) => {
|
|
327
|
+
const r = expectCreate(await control({ kind: 'create', params: options?.params ?? {}, id: options?.id }))
|
|
328
|
+
return makeHandle(r.id)
|
|
329
|
+
},
|
|
330
|
+
createBatch: async (batch: { id?: string; params?: unknown }[]) => {
|
|
331
|
+
const handles = []
|
|
332
|
+
for (const item of batch) {
|
|
333
|
+
const r = expectCreate(await control({ kind: 'create', params: item.params ?? {}, id: item.id }))
|
|
334
|
+
handles.push(makeHandle(r.id))
|
|
335
|
+
}
|
|
336
|
+
return handles
|
|
337
|
+
},
|
|
338
|
+
get: async (id: string) => {
|
|
339
|
+
// Existence check (worker-side `get` throws for unknown ids).
|
|
340
|
+
expectStatus(await control({ kind: 'status', instanceId: id }))
|
|
341
|
+
return makeHandle(id)
|
|
342
|
+
},
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function makeSendEmailProxy(bindingName: string, rpc: RpcClient, envWsBridge: WsGuestBridge<DOMainMessage>): Record<string, unknown> {
|
|
347
|
+
const target: BindingTarget = { binding: bindingName }
|
|
348
|
+
const taggedSend = async (message: unknown) => {
|
|
349
|
+
const arg = message instanceof EmailMessage
|
|
350
|
+
? tagCloneable('EmailMessage', {
|
|
351
|
+
from: message.from,
|
|
352
|
+
to: message.to,
|
|
353
|
+
raw: await materializeEmailRaw(message.raw),
|
|
354
|
+
})
|
|
355
|
+
: message
|
|
356
|
+
return rpc.call(target, 'send', [arg])
|
|
357
|
+
}
|
|
358
|
+
return makeRpcProxy(target, rpc, envWsBridge, { send: taggedSend })
|
|
74
359
|
}
|