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
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Cross-thread binding RPC — shared core used by both worker channels.
3
+ *
4
+ * One frame ({@link RpcCallRequest} / {@link RpcFetchRequest} + their
5
+ * replies in `./protocol.ts`) covers both:
6
+ * - main ↔ user-worker thread (`WorkerThreadExecutor` in `./executor.ts`)
7
+ * - main ↔ DO-instance worker thread (`WorkerExecutor` in `../bindings/do-executor-worker.ts`)
8
+ *
9
+ * The two channels carry channel-specific messages on top (fetch/scheduled,
10
+ * ws-bridge events, container lifecycle, …) but the binding RPC half is
11
+ * identical: resolve binding from `target`, invoke under the caller's trace
12
+ * context, serialize the result. This module hosts that half.
13
+ */
14
+ import type { BindingTarget, ParentSpanContext, RpcCallRequest, RpcFetchRequest, RpcGetRequest, RpcReply, RpcReqStreamChunk, RpcReqStreamEnd, RpcReqStreamError, RpcStreamAck, RpcStreamCancel, SerializedResponse } from './protocol';
15
+ import { OutboundStreamRegistry, StreamReceiver } from './stream-shared';
16
+ /**
17
+ * Cross-thread class-identity registry. `structuredClone` (Bun postMessage)
18
+ * strips class prototypes — bindings whose RPC args/returns rely on class
19
+ * identity register a reviver here, and the sender side wraps instances with
20
+ * `tagCloneable` to ship them over the wire.
21
+ */
22
+ type Reviver = (raw: Record<string, unknown>) => unknown;
23
+ export declare function registerCloneable(tag: string, revive: Reviver): void;
24
+ /** Sender-side tag: produces a structured-clone-safe payload that the
25
+ * receiver rebuilds via the registered reviver. */
26
+ export declare function tagCloneable<T extends Record<string, unknown>>(tag: string, payload: T): T & {
27
+ __lopata_class: string;
28
+ };
29
+ export declare function reifyArgs(args: unknown[]): unknown[];
30
+ export interface RpcDispatchHooks {
31
+ /** Resolve a binding from main env (channel-specific: user-worker supports
32
+ * `instanceId` namespace .get(), DO channel doesn't). */
33
+ resolveBinding(target: BindingTarget): Record<string, unknown>;
34
+ /** Post a reply back through the channel's transport. */
35
+ post(reply: RpcReply): void;
36
+ /** Return false once the channel is torn down so we drop late replies. */
37
+ isAlive(): boolean;
38
+ /** Optional hook to add transport-specific fields (e.g. webSocketId) to a
39
+ * serialized response after fetch resolves. */
40
+ decorateResponse?(response: Response, serialized: SerializedResponse): void;
41
+ }
42
+ export declare function dispatchRpcCall(req: RpcCallRequest, hooks: RpcDispatchHooks): Promise<void>;
43
+ /**
44
+ * Resolve a property read on a binding (`await env.SVC.prop`). The main-side
45
+ * binding is itself a thenable-property proxy (service-binding `toProxy`), so
46
+ * awaiting `resolved[property]` runs its property-get (including the nested
47
+ * main→target-thread hop when the target is a worker thread). The resolved
48
+ * value ships back via the same reply types as a method call. Returning a
49
+ * non-cloneable value (RpcTarget/function) surfaces as `rpc-call-error` — the
50
+ * documented cross-thread limitation, same as method returns.
51
+ */
52
+ export declare function dispatchRpcGet(req: RpcGetRequest, hooks: RpcDispatchHooks): Promise<void>;
53
+ /**
54
+ * Resolve a binding's fetch and stream the body back to the caller. Headers +
55
+ * status ship immediately (TTFB preserved); the body flows in
56
+ * `rpc-stream-chunk` messages and terminates with `rpc-stream-end` or
57
+ * `rpc-stream-error`. WS upgrades (status 101) are short-circuited — they
58
+ * carry `webSocketId`, not a body stream. Body-less responses ship without a
59
+ * `streamId` and no pump.
60
+ *
61
+ * The request body is reconstructed from the inbound request-stream messages
62
+ * when `req.request.streamId` is set, so streaming uploads / proxies reach the
63
+ * binding's `fetch()` incrementally.
64
+ */
65
+ export declare function dispatchRpcFetch(req: RpcFetchRequest, hooks: RpcDispatchHooks, streams: OutboundStreamRegistry, requestStreams: StreamReceiver): Promise<void>;
66
+ /**
67
+ * Main-side host of the unified cross-thread RPC channel. Both
68
+ * `WorkerThreadExecutor` (user-worker channel) and `WorkerExecutor` (DO-instance
69
+ * channel) own one. It bundles the outbound response-body pump registry, the
70
+ * inbound request-body receiver (with backpressure), the call/get/fetch
71
+ * dispatch, and teardown — so the two executors share a single wiring +
72
+ * `isAlive`/teardown contract instead of re-implementing (and drifting on) the
73
+ * glue. The executor supplies channel-specific behavior via {@link RpcDispatchHooks}:
74
+ * `resolveBinding` (env lookup), `post` (transport), `isAlive` (liveness), and an
75
+ * optional `decorateResponse` (WS adoption). All hooks should close over the
76
+ * executor so they read live state lazily.
77
+ */
78
+ export declare class RpcHostChannel {
79
+ /** Outbound response-body pumps started by `dispatchRpcFetch`. */
80
+ private _streams;
81
+ /** Inbound request-body streams (worker → main binding fetch with body). */
82
+ private _requestStreams;
83
+ private _hooks;
84
+ constructor(hooks: RpcDispatchHooks);
85
+ /**
86
+ * Route an inbound message if it belongs to the RPC channel; returns true if
87
+ * consumed (the caller stops processing it). Mirrors {@link RpcClient.handle}
88
+ * on the sender side.
89
+ */
90
+ handle(msg: {
91
+ type: string;
92
+ }): boolean;
93
+ disposeAll(err: Error): void;
94
+ }
95
+ type RpcClientPost = (req: RpcCallRequest | RpcGetRequest | RpcFetchRequest | RpcStreamCancel | RpcStreamAck | RpcReqStreamChunk | RpcReqStreamEnd | RpcReqStreamError) => void;
96
+ /**
97
+ * Worker-side RPC caller: posts {@link RpcCallRequest}/{@link RpcFetchRequest},
98
+ * resolves the matching reply.
99
+ *
100
+ * Reads the active span context on every call so spans created on the
101
+ * receiving thread (including spans inside nested cross-thread hops) nest
102
+ * under the caller's current span.
103
+ *
104
+ * Also reconstructs streamed response bodies: when a `rpc-fetch-result`
105
+ * carries a `streamId`, the matching `Response` is built around a
106
+ * `ReadableStream` fed by `rpc-stream-chunk` messages. Consumer cancel posts
107
+ * `rpc-stream-cancel` so an unbounded source on main stops pumping.
108
+ */
109
+ export declare class RpcClient {
110
+ private _pending;
111
+ private _nextId;
112
+ private _post;
113
+ private _getParent;
114
+ /** Reconstructed inbound response-body streams (rpc-fetch-result.streamId). */
115
+ private _streams;
116
+ /** Outbound request-body pumps started by `callFetch`. A receiver-side
117
+ * `rpc-req-stream-cancel` arrives via `handle()` and stops the source
118
+ * reader so an unbounded upload doesn't pump forever. */
119
+ private _requestStreams;
120
+ constructor(post: RpcClientPost, getParent: () => ParentSpanContext | undefined);
121
+ call(target: BindingTarget, method: string, args: unknown[]): Promise<unknown>;
122
+ /** Property read on a binding (`await env.SVC.prop`). Reply reuses the
123
+ * rpc-call-result/error path via {@link handle}. */
124
+ callGet(target: BindingTarget, property: string): Promise<unknown>;
125
+ callFetch(target: BindingTarget, request: Request): Promise<SerializedResponse>;
126
+ /**
127
+ * Build a `Response` from a `SerializedResponse`. When `streamId` is set,
128
+ * the body becomes a `ReadableStream` fed by `rpc-stream-chunk` messages;
129
+ * otherwise a buffered body. WebSocket adoption is the caller's job (only
130
+ * `proxyFetch` / `makeEnvBindingProxy` know the channel's wsId convention).
131
+ */
132
+ makeResponse(serialized: SerializedResponse): Response;
133
+ /** Returns true when `msg` was a unified RPC reply we consumed. */
134
+ handle(msg: {
135
+ type: string;
136
+ }): boolean;
137
+ }
138
+ export {};
@@ -0,0 +1,12 @@
1
+ /** Request/Response serialization helpers shared between the worker bridges. */
2
+ import type { SerializedRequest, SerializedResponse } from './protocol';
3
+ /**
4
+ * Build the headers/method/url shell of a `SerializedRequest`. Body handling
5
+ * is the caller's responsibility — sender code allocates a `streamId` and
6
+ * pumps the body via channel-specific stream messages, or sets `body: null`
7
+ * for body-less requests. (`request.body` is *never* materialised here, which
8
+ * is what allows streaming uploads to cross the worker boundary incrementally.)
9
+ */
10
+ export declare function serializeRequestShell(request: Request): Omit<SerializedRequest, 'body' | 'streamId'>;
11
+ export declare function deserializeRequest(req: SerializedRequest, body?: ReadableStream<Uint8Array> | null, signal?: AbortSignal): Request;
12
+ export declare function deserializeResponse(serialized: SerializedResponse): Response;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Cross-thread stream plumbing — sender and receiver primitives used by every
3
+ * worker channel (top-level user-worker fetch, DO instance fetch, unified
4
+ * cross-thread binding-fetch RPC) on both request and response sides.
5
+ *
6
+ * `OutboundStreamRegistry` tracks active source pumps so an inbound cancel can
7
+ * stop the reader. `StreamReceiver` reconstructs a `ReadableStream` on the
8
+ * receiving end, buffering chunks that race ahead of `start()` (the controller
9
+ * only registers when the consumer pulls the body, which on Bun lands after
10
+ * the first chunk message in some interleavings). `pumpStream` is the
11
+ * symmetric sender-side helper: it consumes a `ReadableStream`, registers the
12
+ * source reader so an inbound cancel can stop it, and posts channel-specific
13
+ * envelope messages until the body completes or errors.
14
+ */
15
+ import type { SerializedError } from './protocol';
16
+ /**
17
+ * Default cross-thread backpressure window (chunk count) for response-body
18
+ * streams. Bounds in-flight chunks so a fast producer (e.g. proxying a large R2
19
+ * object, a tight SSE/generated stream) can't race ahead of a slow consumer and
20
+ * grow memory unbounded. Small enough to bound memory, large enough to keep the
21
+ * pipe full across the postMessage round-trip.
22
+ *
23
+ * The effective ceiling is ~2x this value: the sender is seeded `window`
24
+ * credits up front (so it can burst without waiting a round-trip) AND the
25
+ * receiver's `ReadableStream` grants one more credit per `pull()` while its
26
+ * own queue (highWaterMark = `window`) has room. Both halves are intentional —
27
+ * seeding 0 would clock the pump to one chunk per postMessage round-trip from
28
+ * a cold start. "Window" below means this seeded half; double it for the hard
29
+ * memory bound.
30
+ */
31
+ export declare const STREAM_BACKPRESSURE_WINDOW = 8;
32
+ export declare class OutboundStreamRegistry {
33
+ private _nextStreamId;
34
+ private _streams;
35
+ allocateId(): number;
36
+ /** Number of source pumps still running. Reload drain consults this so an
37
+ * in-flight upload / response-body pump isn't force-terminated mid-stream. */
38
+ activeCount(): number;
39
+ register(streamId: number, reader: {
40
+ cancel(reason?: unknown): Promise<unknown>;
41
+ }, initialCredits?: number): void;
42
+ /**
43
+ * Sender: take one permit to post a chunk, parking until the receiver grants
44
+ * one if none are left. Returns `false` if the stream was cancelled/disposed
45
+ * while parked (the pump should stop). With the default `Infinity` credits
46
+ * this never blocks — eager behavior, unchanged.
47
+ */
48
+ acquireCredit(streamId: number): Promise<boolean>;
49
+ /** Receiver granted `n` more permits — replenish and wake a parked pump. */
50
+ grantCredit(streamId: number, n?: number): void;
51
+ complete(streamId: number): void;
52
+ /** Receiver-side cancel arrived — stop the source pump if still running. */
53
+ cancel(streamId: number): void;
54
+ disposeAll(): void;
55
+ }
56
+ /**
57
+ * Receiver-side state for an inbound stream channel. Holds open `ReadableStream`
58
+ * controllers keyed by streamId and a small per-streamId pending-events queue
59
+ * for chunks that arrive before the consumer's `start()` registers the
60
+ * controller.
61
+ *
62
+ * Wiring code (each channel's `onmessage` dispatcher) routes channel-specific
63
+ * `*-chunk` / `*-end` / `*-error` messages into {@link push} / {@link end} /
64
+ * {@link error}. The `onCancel` callback is invoked when the consumer cancels
65
+ * the reconstructed stream, so the wiring code can post the channel-specific
66
+ * `*-cancel` message back to the sender.
67
+ */
68
+ export interface StreamReceiverOptions {
69
+ /**
70
+ * Enable cross-thread backpressure. When set, the reconstructed
71
+ * `ReadableStream` uses this as its highWaterMark (chunk count) and the
72
+ * receiver grants the sender a credit (via {@link StreamReceiverOptions.onCredit})
73
+ * as it pulls — bounding the number of in-flight chunks instead of letting a
74
+ * fast producer race ahead and grow memory unbounded. Omit for eager
75
+ * (unbounded) behavior — the default.
76
+ */
77
+ window?: number;
78
+ /** Post the channel-specific `*-stream-ack` message granting one credit. Only
79
+ * consulted when `window` is set. */
80
+ onCredit?: (streamId: number) => void;
81
+ }
82
+ export declare class StreamReceiver {
83
+ private _controllers;
84
+ private _pending;
85
+ private _cancelled;
86
+ /** Streams reconstructed via `open()` that haven't reached a terminal event
87
+ * (end/error/cancel) yet. Reload drain consults `activeCount()` so a response
88
+ * the client is still downloading (SSE, large proxy) isn't cut off mid-body. */
89
+ private _open;
90
+ private _onCancel;
91
+ private _window?;
92
+ private _onCredit?;
93
+ /**
94
+ * Cancels a reconstructed stream that was dropped (garbage-collected) before
95
+ * reaching a terminal event — e.g. a handler that never read a request body, or
96
+ * a caller that ignored a binding-fetch response body. Without this the sender's
97
+ * pump parks in `acquireCredit` forever (holding the locked source reader) until
98
+ * the generation is disposed. GC only collects truly-unreferenced streams, so a
99
+ * body still held by `waitUntil` is never wrongly cancelled.
100
+ */
101
+ private _finalizer;
102
+ constructor(onCancel: (streamId: number) => void, options?: StreamReceiverOptions);
103
+ /** Number of reconstructed streams not yet terminated. */
104
+ activeCount(): number;
105
+ open(streamId: number): ReadableStream<Uint8Array>;
106
+ /**
107
+ * Receiver-side cancel for an inbound stream that never reached a consumer
108
+ * (e.g. dispatcher errored before `open()` registered the controller, or the
109
+ * request was torn down post-open). Drops any buffered events, marks the id
110
+ * stale so late chunks are ignored, and signals the sender via `onCancel`.
111
+ */
112
+ cancel(streamId: number): void;
113
+ push(streamId: number, chunk: Uint8Array): void;
114
+ end(streamId: number): void;
115
+ error(streamId: number, err: Error): void;
116
+ disposeAll(err: Error): void;
117
+ private _onEvent;
118
+ private _apply;
119
+ }
120
+ /**
121
+ * Channel-specific envelope builders. Each callsite supplies its own message
122
+ * shapes (e.g. `{ type: 'stream-chunk', id, chunk }` vs `{ type:
123
+ * 'do-stream-chunk', streamId, chunk }`); the pump loop itself is identical.
124
+ */
125
+ export interface PumpEnvelopes<TChunk, TEnd, TError> {
126
+ chunk: (streamId: number, chunk: Uint8Array) => TChunk;
127
+ end: (streamId: number) => TEnd;
128
+ error: (streamId: number, error: SerializedError) => TError;
129
+ }
130
+ /**
131
+ * Read a body to completion and post channel-specific envelopes for each
132
+ * chunk + the terminator. The reader is registered with `registry` so an
133
+ * inbound cancel can stop the source; `complete()` runs on every exit path.
134
+ *
135
+ * `isAlive` is optional — worker-side callers omit it (worker termination
136
+ * kills the loop). Main-side callers pass a closure over their disposal flag
137
+ * so a posted message after teardown is dropped at the source (matches the
138
+ * pre-refactor main-side pumps).
139
+ */
140
+ export declare function pumpStream<TChunk, TEnd, TError>(streamId: number, body: ReadableStream<Uint8Array>, registry: OutboundStreamRegistry, post: (msg: TChunk | TEnd | TError) => void, envelopes: PumpEnvelopes<TChunk, TEnd, TError>, isAlive?: () => boolean,
141
+ /** When set, post at most `window` chunks ahead of the receiver's credits
142
+ * (cross-thread backpressure). Requires the receiver's `StreamReceiver` to be
143
+ * constructed with a matching `window` + `onCredit`. Omit for eager. */
144
+ window?: number,
145
+ /** Called once the pump exits for any reason (end / error / cancel / teardown).
146
+ * Lets callers tie per-stream resource cleanup to the body's completion. */
147
+ onComplete?: () => void): void;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Stateless-binding env builder for the main worker-thread runtime.
3
+ *
4
+ * Builds the bindings whose state lives on disk (.lopata SQLite/files) —
5
+ * the same physical files the main thread uses. WAL mode + busy_timeout
6
+ * make multiple `bun:sqlite` handles to the same file safe.
7
+ */
8
+ import { Database } from 'bun:sqlite';
9
+ import { SqliteWorkflowBinding } from '../bindings/workflow';
10
+ import type { WranglerConfig } from '../config';
11
+ import type { WorkerMessage } from './protocol';
12
+ import type { RpcClient } from './rpc-client';
13
+ import type { WsGuestBridge } from './ws-bridge-shared';
14
+ export interface ThreadEnvOptions {
15
+ config: WranglerConfig;
16
+ /** Per-worker dir for `.dev.vars`/`.env`/assets resolution only. */
17
+ baseDir: string;
18
+ /** Shared `.lopata` data dir (main's `getDataDir()`) — the SQLite/r2/d1 files
19
+ * main and the DO workers use. Distinct from `baseDir` in multi-worker mode. */
20
+ dataDir: string;
21
+ rpc: RpcClient;
22
+ /** Guest-side bridge for WebSockets returned by env-binding fetches. */
23
+ envWsBridge: WsGuestBridge<WorkerMessage>;
24
+ browserConfig?: {
25
+ wsEndpoint?: string;
26
+ executablePath?: string;
27
+ headless?: boolean;
28
+ };
29
+ }
30
+ export interface ThreadEnvBuilt {
31
+ env: Record<string, unknown>;
32
+ /** Thread-local DB handle — shared with the workflow + queue consumer wiring
33
+ * the caller does after the user module loads. */
34
+ db: Database;
35
+ /** Workflows the caller still needs to wire after the user module loads. */
36
+ workflows: {
37
+ bindingName: string;
38
+ className: string;
39
+ binding: SqliteWorkflowBinding;
40
+ }[];
41
+ }
42
+ export declare function buildThreadEnv({ config, baseDir, dataDir, rpc, envWsBridge, browserConfig }: ThreadEnvOptions): ThreadEnvBuilt;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Workflow + queue wiring that runs after the user module loads in the worker
3
+ * thread. Kept out of `thread-env.ts` so the env builder doesn't need to
4
+ * receive the user module — buildThreadEnv runs before `import(modulePath)`.
5
+ */
6
+ import type { Database } from 'bun:sqlite';
7
+ import { QueueConsumer } from '../bindings/queue';
8
+ import type { WranglerConfig } from '../config';
9
+ import type { ThreadEnvBuilt } from './thread-env';
10
+ export declare function wireWorkflows(built: ThreadEnvBuilt, workerModule: Record<string, unknown>): void;
11
+ /**
12
+ * Spawn queue consumers in the worker thread. Shared SQLite means the consumer
13
+ * can poll, manage leases, and apply ack/retry decisions locally — exactly
14
+ * like the in-process flow — without any cross-thread RPC.
15
+ */
16
+ export declare function startThreadQueueConsumers(config: WranglerConfig, db: Database, env: Record<string, unknown>, workerModule: Record<string, unknown>, workerName?: string, trackBatch?: (p: Promise<unknown>) => void): QueueConsumer[];
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Cross-thread WebSocket bridge — shared primitive.
3
+ *
4
+ * Used by both worker channels (main ↔ user-worker, main ↔ DO-instance worker)
5
+ * to ferry events for a CFWebSocket whose two halves live on opposite threads.
6
+ *
7
+ * Direction & terminology:
8
+ * - *Guest* — the side that owns the user-facing peer (worker thread).
9
+ * The user code created a `WebSocketPair` locally and shipped `pair[0]`
10
+ * in a `Response{webSocket}`. The guest keeps `pair[1]` and forwards
11
+ * everything the user does on it through to the host.
12
+ * - *Host* — the side that hands the upgraded `CFWebSocket` to `Bun.serve`
13
+ * (main thread). It owns a synthetic CFWebSocket whose `_peer` posts every
14
+ * inbound event (from the real client) back to the guest.
15
+ *
16
+ * Each bridged socket is identified by an opaque `wsId` generated on the
17
+ * guest side. The host-side `register()` call is the message that ferries
18
+ * the id over and creates the local half. A late `deliverRemote*` for a
19
+ * wsId that hasn't been `register()`ed yet is buffered until it appears —
20
+ * see `_pendingEvents` for why.
21
+ *
22
+ * Channel-specific envelopes (`ws-worker-send` vs `fetch-ws-outgoing`, etc.)
23
+ * are encoded by the `WsBridgeEnvelopes` callbacks each consumer supplies.
24
+ */
25
+ import { CFWebSocket } from '../bindings/websocket-pair';
26
+ /**
27
+ * Channel-specific message builders. Each callback returns the exact envelope
28
+ * the consumer's transport expects; the bridge just calls `post(envelope)`.
29
+ */
30
+ export interface WsHostEnvelopes<O> {
31
+ /** Inbound from the real client → guest's user peer. */
32
+ clientMessage(wsId: string, data: string | ArrayBuffer): O;
33
+ clientClose(wsId: string, code: number, reason: string, wasClean: boolean): O;
34
+ }
35
+ export interface WsGuestEnvelopes<O> {
36
+ /** User-facing peer sent bytes → forward to real client via host. */
37
+ remoteMessage(wsId: string, data: string | ArrayBuffer): O;
38
+ remoteClose(wsId: string, code: number, reason: string, wasClean: boolean): O;
39
+ }
40
+ /**
41
+ * Host side: owns the `CFWebSocket` that gets handed to `Bun.serve.upgrade`
42
+ * and bridges events to/from the guest worker.
43
+ */
44
+ export declare class WsHostBridge<O> {
45
+ /** wsId → cfSocket (the side handed to `Bun.serve.upgrade`). */
46
+ private _sockets;
47
+ /**
48
+ * Events from the guest that arrived before the matching `register()` /
49
+ * `adoptExisting()` was called. The guest's `accept()` flushes queued events
50
+ * synchronously and posts them; those posts race ahead of the binding-fetch /
51
+ * fetch result that triggers host-side registration. Without buffering, the
52
+ * first message would be lost.
53
+ */
54
+ private _pendingEvents;
55
+ /**
56
+ * Subset of `_sockets` that was adopted with `bridgeEvents: true` — outbound
57
+ * `deliverRemoteMessage` must `cfSocket.send(data)` (dispatch on peer) for
58
+ * these, instead of `dispatchOrQueue` (dispatch on the adopted socket
59
+ * itself, which is the right behavior only for the `Bun.serve` passthrough).
60
+ */
61
+ private _bridgedAdoptions;
62
+ /**
63
+ * wsIds that were previously registered/adopted and have since closed.
64
+ * Used to drop late `deliverRemote*` calls cleanly instead of stashing
65
+ * them in `_pendingEvents` (where they'd be unreachable — wsIds are
66
+ * unique per upgrade, so no future `register()` will ever match).
67
+ */
68
+ private _forgotten;
69
+ private _post;
70
+ private _envelopes;
71
+ constructor(post: (msg: O) => void, envelopes: WsHostEnvelopes<O>);
72
+ private _forget;
73
+ /**
74
+ * Build a host-side `CFWebSocket` paired with a bridge peer that posts every
75
+ * inbound event back to the guest. Drains any pending events queued before
76
+ * this call.
77
+ */
78
+ register(wsId: string): CFWebSocket;
79
+ /**
80
+ * Adopt an already-real `CFWebSocket` (typically the client peer returned
81
+ * from a DO/service binding inside a nested binding fetch). The peer is
82
+ * already wired to its server counterpart, so we just need to keep it
83
+ * addressable by id when the guest echoes the response back up.
84
+ *
85
+ * For the *passthrough* use (user-worker channel): the guest never reads or
86
+ * writes through this socket — main eventually hands the same `cfSocket`
87
+ * to `Bun.serve.upgrade()`, which owns both directions.
88
+ *
89
+ * For the *guest-driven* use (DO-worker env-binding channel): pass
90
+ * `bridgeEvents: true` so we accept the socket here, forward every inbound
91
+ * message/close to the guest as `clientMessage` / `clientClose` envelopes,
92
+ * and `deliverRemoteMessage` routes outbound bytes via `cfSocket.send`
93
+ * (which dispatches on its real peer) instead of dispatching on the
94
+ * adopted socket itself.
95
+ */
96
+ adoptExisting(ws: CFWebSocket, options?: {
97
+ bridgeEvents?: boolean;
98
+ }): string;
99
+ private _wireBridgedAdoption;
100
+ /** Look up a previously-registered or adopted CFWebSocket. */
101
+ getSocket(wsId: string): CFWebSocket | undefined;
102
+ /** Guest reports its user-facing peer emitted a message → fire it on the host socket. */
103
+ deliverRemoteMessage(wsId: string, data: string | ArrayBuffer): void;
104
+ /** Guest reports its user-facing peer closed → fire close on the host socket. */
105
+ deliverRemoteClose(wsId: string, code: number, reason: string, wasClean: boolean): void;
106
+ private _bufferPending;
107
+ /**
108
+ * Notify any active real clients that this generation is going away, then
109
+ * drop them. Mirrors the `1012 Service Restart` close code WebSockets use
110
+ * for planned restarts. Also drops any stranded pending events for ids that
111
+ * never reached `register()` (e.g. binding-fetch errored mid-flight).
112
+ */
113
+ disposeAll(): void;
114
+ }
115
+ /**
116
+ * Guest side: hooks the CFWebSocket that's about to ship in a Response and
117
+ * forwards events between the user-facing peer and the host.
118
+ */
119
+ export declare class WsGuestBridge<O> {
120
+ private _sockets;
121
+ /**
122
+ * Client events that arrived before the matching `createBridgedSocket()` was
123
+ * called. On the env-binding channel main adopts the upstream WS with
124
+ * `bridgeEvents: true`, whose `accept()` flushes already-queued client events
125
+ * synchronously — those posts race ahead of the `rpc-fetch-result` that
126
+ * triggers `createBridgedSocket` here. Without buffering, the first inbound
127
+ * message (or an early close) would be lost. Mirrors `WsHostBridge`.
128
+ */
129
+ private _pendingEvents;
130
+ /** wsIds already created+closed; drop late client events instead of re-buffering. */
131
+ private _forgotten;
132
+ private _post;
133
+ private _envelopes;
134
+ constructor(post: (msg: O) => void, envelopes: WsGuestEnvelopes<O>);
135
+ private _forget;
136
+ private _bufferPending;
137
+ /**
138
+ * Reverse of `register`: build a fresh `CFWebSocket` for a wsId allocated on
139
+ * the host (e.g. main adopted an upstream WS returned from an env-binding
140
+ * fetch). The returned socket is what user code interacts with — calling
141
+ * `accept()` / `send()` / `addEventListener('message')` here drives the
142
+ * remote peer through the bridge.
143
+ */
144
+ createBridgedSocket(wsId: string): CFWebSocket;
145
+ /**
146
+ * Hook up a `CFWebSocket` that's about to ship in a Response. Listeners must
147
+ * be attached BEFORE `accept()` so the synchronous flush of any queued events
148
+ * (e.g. the user already called `server.send()` before returning the response)
149
+ * reaches the bridge instead of being lost.
150
+ *
151
+ * If the shipped peer was pre-accepted by user code (e.g. `client.accept()`
152
+ * before returning the Response), any events dispatched between that
153
+ * pre-accept and this `register()` call were emitted with no listeners
154
+ * attached — they're already gone. We still attach listeners so anything
155
+ * *after* this point flows correctly, and emit a console.warn so the user
156
+ * knows to drop the early `accept()`.
157
+ */
158
+ register(shipped: CFWebSocket): string;
159
+ /** Host delivered a message from the real client → fire it on the user peer. */
160
+ deliverClientMessage(wsId: string, data: string | ArrayBuffer): void;
161
+ /** Host delivered a close from the real client → fire close on the user peer. */
162
+ deliverClientClose(wsId: string, code: number, reason: string, wasClean: boolean): void;
163
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lopata",
3
- "version": "0.18.4",
3
+ "version": "0.19.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,5 +1,6 @@
1
1
  import { $ } from 'bun'
2
2
  import type { ContainerBase } from '../../bindings/container'
3
+ import { unregisterContainer } from '../../bindings/container-cleanup'
3
4
  import { DockerManager } from '../../bindings/container-docker'
4
5
  import { getDatabase } from '../../db'
5
6
  import type { ContainerDetail, ContainerInstance, ContainerSummary, HandlerContext, OkResponse } from '../types'
@@ -186,7 +187,7 @@ export const handlers = {
186
187
  },
187
188
 
188
189
  async 'containers.destroy'({ className, id }: { className: string; id: string }, ctx: HandlerContext): Promise<OkResponse> {
189
- const docker = new DockerManager()
190
+ const docker = new DockerManager({ onRemove: unregisterContainer })
190
191
  const containerName = `lopata-${className}-${id.slice(0, 12)}`
191
192
  await docker.remove(containerName)
192
193
  return { ok: true }