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
|
@@ -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,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 }
|