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.
Files changed (106) hide show
  1. package/dist/types/api/dispatch.d.ts +2 -2
  2. package/dist/types/api/handlers/workflows.d.ts +2 -2
  3. package/dist/types/bindings/container-cleanup.d.ts +43 -0
  4. package/dist/types/bindings/container-docker.d.ts +24 -0
  5. package/dist/types/bindings/container.d.ts +4 -2
  6. package/dist/types/bindings/do-executor-worker.d.ts +73 -89
  7. package/dist/types/bindings/do-executor.d.ts +22 -1
  8. package/dist/types/bindings/do-worker-env.d.ts +16 -7
  9. package/dist/types/bindings/durable-object.d.ts +66 -4
  10. package/dist/types/bindings/queue.d.ts +4 -1
  11. package/dist/types/bindings/rpc-stub.d.ts +23 -1
  12. package/dist/types/bindings/scheduled.d.ts +13 -3
  13. package/dist/types/bindings/service-binding.d.ts +13 -4
  14. package/dist/types/bindings/static-assets.d.ts +1 -1
  15. package/dist/types/bindings/websocket-pair.d.ts +12 -3
  16. package/dist/types/bindings/workflow.d.ts +29 -0
  17. package/dist/types/config.d.ts +2 -0
  18. package/dist/types/env.d.ts +6 -0
  19. package/dist/types/error-page-render.d.ts +6 -0
  20. package/dist/types/execution-context.d.ts +8 -0
  21. package/dist/types/generation-manager.d.ts +21 -2
  22. package/dist/types/generation.d.ts +16 -21
  23. package/dist/types/import-graph.d.ts +28 -0
  24. package/dist/types/lopata-config.d.ts +3 -3
  25. package/dist/types/plugin.d.ts +4 -1
  26. package/dist/types/rpc-validate.d.ts +9 -0
  27. package/dist/types/setup-globals.d.ts +5 -1
  28. package/dist/types/tracing/context.d.ts +12 -0
  29. package/dist/types/tracing/span.d.ts +21 -3
  30. package/dist/types/tracing/store.d.ts +17 -0
  31. package/dist/types/tsconfig.tsbuildinfo +1 -1
  32. package/dist/types/vite-plugin/index.d.ts +8 -1
  33. package/dist/types/worker-registry.d.ts +11 -4
  34. package/dist/types/worker-thread/do-protocol.d.ts +256 -0
  35. package/dist/types/worker-thread/entry.d.ts +2 -0
  36. package/dist/types/worker-thread/execution-context.d.ts +22 -0
  37. package/dist/types/worker-thread/executor.d.ts +123 -0
  38. package/dist/types/worker-thread/protocol.d.ts +553 -0
  39. package/dist/types/worker-thread/remote-trace-store.d.ts +27 -0
  40. package/dist/types/worker-thread/rpc-client.d.ts +4 -0
  41. package/dist/types/worker-thread/rpc-shared.d.ts +138 -0
  42. package/dist/types/worker-thread/serialize.d.ts +12 -0
  43. package/dist/types/worker-thread/stream-shared.d.ts +147 -0
  44. package/dist/types/worker-thread/thread-env.d.ts +42 -0
  45. package/dist/types/worker-thread/wire-handlers.d.ts +16 -0
  46. package/dist/types/worker-thread/ws-bridge-shared.d.ts +163 -0
  47. package/package.json +1 -1
  48. package/src/api/handlers/containers.ts +2 -1
  49. package/src/api/handlers/workflows.ts +39 -34
  50. package/src/bindings/container-cleanup.ts +125 -0
  51. package/src/bindings/container-docker.ts +49 -34
  52. package/src/bindings/container.ts +24 -9
  53. package/src/bindings/do-executor-inprocess.ts +9 -5
  54. package/src/bindings/do-executor-worker.ts +386 -158
  55. package/src/bindings/do-executor.ts +23 -1
  56. package/src/bindings/do-worker-entry.ts +242 -60
  57. package/src/bindings/do-worker-env.ts +296 -11
  58. package/src/bindings/durable-object.ts +231 -27
  59. package/src/bindings/email.ts +6 -0
  60. package/src/bindings/queue.ts +11 -1
  61. package/src/bindings/rpc-stub.ts +79 -10
  62. package/src/bindings/scheduled.ts +37 -30
  63. package/src/bindings/service-binding.ts +87 -33
  64. package/src/bindings/static-assets.ts +3 -2
  65. package/src/bindings/websocket-pair.ts +19 -3
  66. package/src/bindings/workflow.ts +91 -0
  67. package/src/cli/dev.ts +106 -41
  68. package/src/config.ts +6 -3
  69. package/src/db.ts +1 -0
  70. package/src/env.ts +40 -21
  71. package/src/error-page-render.ts +21 -0
  72. package/src/execution-context.ts +13 -3
  73. package/src/generation-manager.ts +144 -143
  74. package/src/generation.ts +150 -306
  75. package/src/import-graph.ts +140 -0
  76. package/src/lopata-config.ts +3 -3
  77. package/src/plugin.ts +7 -0
  78. package/src/rpc-validate.ts +29 -0
  79. package/src/setup-globals.ts +6 -17
  80. package/src/testing/durable-object.ts +5 -3
  81. package/src/testing/index.ts +8 -4
  82. package/src/tracing/context.ts +18 -0
  83. package/src/tracing/span.ts +83 -57
  84. package/src/tracing/store.ts +41 -3
  85. package/src/virtual-modules.ts +2 -0
  86. package/src/vite-plugin/dev-server-plugin.ts +4 -0
  87. package/src/vite-plugin/index.ts +8 -1
  88. package/src/worker-registry.ts +15 -2
  89. package/src/worker-thread/do-protocol.ts +237 -0
  90. package/src/worker-thread/entry.ts +453 -0
  91. package/src/worker-thread/execution-context.ts +53 -0
  92. package/src/worker-thread/executor.ts +595 -0
  93. package/src/worker-thread/protocol.ts +552 -0
  94. package/src/worker-thread/remote-trace-store.ts +90 -0
  95. package/src/worker-thread/rpc-client.ts +5 -0
  96. package/src/worker-thread/rpc-shared.ts +503 -0
  97. package/src/worker-thread/serialize.ts +37 -0
  98. package/src/worker-thread/stream-shared.ts +414 -0
  99. package/src/worker-thread/thread-env.ts +350 -0
  100. package/src/worker-thread/wire-handlers.ts +80 -0
  101. package/src/worker-thread/ws-bridge-shared.ts +482 -0
  102. package/dist/types/bindings/do-websocket-bridge.d.ts +0 -60
  103. package/dist/types/module-cache.d.ts +0 -23
  104. package/src/bindings/do-websocket-bridge.ts +0 -79
  105. package/src/module-cache.ts +0 -58
  106. 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 { DurableObjectNamespaceImpl } from './durable-object'
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
- * DOs created here use InProcessExecutor (nested DOs run in the worker thread).
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
- ): { db: Database; env: Record<string, unknown>; doNamespaces: { className: string; namespace: DurableObjectNamespaceImpl }[] } {
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 — each runs in-process within this worker thread
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
- const namespace = new DurableObjectNamespaceImpl(db, doBinding.class_name, dataDir)
59
- env[doBinding.name] = namespace
60
- doNamespaces.push({ className: doBinding.class_name, namespace })
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
- return { db, env, doNamespaces }
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
  }