lopata 0.18.3 → 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 +21 -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 +96 -35
  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 +28 -0
  83. package/src/tracing/span.ts +88 -56
  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
@@ -5,14 +5,18 @@
5
5
  * Messages sent on one side appear on the other.
6
6
  * Events are buffered until accept() is called.
7
7
  */
8
- type WSEventType = 'message' | 'close' | 'error' | 'open';
9
- interface WSEvent {
8
+ export type WSEventType = 'message' | 'close' | 'error' | 'open';
9
+ export interface WSEvent {
10
10
  type: WSEventType;
11
11
  data?: string | ArrayBuffer;
12
12
  code?: number;
13
13
  reason?: string;
14
14
  wasClean?: boolean;
15
15
  }
16
+ /** Response with optional CF `webSocket` property — used by upgrade flows. */
17
+ export type ResponseWithWebSocket = Response & {
18
+ webSocket?: CFWebSocket;
19
+ };
16
20
  /**
17
21
  * A single side of a WebSocketPair. Implements the CF WebSocket interface
18
22
  * with accept() gating — events are queued until accept() is called.
@@ -48,6 +52,12 @@ export declare class CFWebSocket extends EventTarget {
48
52
  accept(): void;
49
53
  send(message: string | ArrayBuffer | ArrayBufferView): void;
50
54
  close(code?: number, reason?: string): void;
55
+ /**
56
+ * Deliver `evt` to local listeners if the peer is already `accept()`ed;
57
+ * otherwise queue it for replay. Centralises the "dispatch or queue" branch
58
+ * that every bridge implementation otherwise inlines.
59
+ */
60
+ dispatchOrQueue(evt: WSEvent): void;
51
61
  /** @internal */
52
62
  _dispatchWSEvent(evt: WSEvent): void;
53
63
  }
@@ -63,4 +73,3 @@ export declare class WebSocketPair {
63
73
  readonly 1: CFWebSocket;
64
74
  constructor();
65
75
  }
66
- export {};
@@ -1,5 +1,6 @@
1
1
  import type { Database } from 'bun:sqlite';
2
2
  import type { Clock } from '../testing/clock';
3
+ import type { WorkflowControlOp, WorkflowControlResult } from '../worker-thread/protocol';
3
4
  export interface WorkflowLimits {
4
5
  maxConcurrentInstances?: number;
5
6
  maxRetentionMs?: number;
@@ -53,6 +54,12 @@ export declare function onEventWaitRegistered(instanceId: string, eventType: str
53
54
  /** Register a callback for when the instance status changes. Returns an unsubscribe function. */
54
55
  export declare function onStatusChange(instanceId: string, cb: (status: string) => void): () => void;
55
56
  export declare function parseDuration(duration: string | number): number;
57
+ /**
58
+ * Wire a workflow class from the user module onto a `SqliteWorkflowBinding`.
59
+ * Both the in-process `wireClassRefs` and the worker-thread entry use this
60
+ * — keeps the lookup-throw-setClass-resumeInterrupted contract in one place.
61
+ */
62
+ export declare function wireWorkflowClass(binding: SqliteWorkflowBinding, className: string, workerModule: Record<string, unknown>, env: Record<string, unknown>): void;
56
63
  export declare class WorkflowEntrypointBase {
57
64
  ctx: {
58
65
  env: unknown;
@@ -97,6 +104,13 @@ export declare class SqliteWorkflowBinding {
97
104
  private counter;
98
105
  private limits;
99
106
  private clock;
107
+ /**
108
+ * Thread-mode router for dashboard control ops. The real state machine lives
109
+ * in the worker thread, so when this is set `executeControl` forwards there
110
+ * instead of running against this (hollow) main-side binding. `null`/unset =
111
+ * in-process: run locally. Installed by `GenerationManager` on each reload.
112
+ */
113
+ private _threadRouter?;
100
114
  constructor(db: Database, workflowName: string, className: string, limits?: WorkflowLimits, clock?: Clock);
101
115
  _setClass(cls: new (ctx: unknown, env: unknown) => WorkflowEntrypointBase, env: unknown): void;
102
116
  _getClass(): (new (ctx: unknown, env: unknown) => WorkflowEntrypointBase) | undefined;
@@ -126,6 +140,21 @@ export declare class SqliteWorkflowBinding {
126
140
  params?: unknown;
127
141
  }[]): Promise<SqliteWorkflowInstance[]>;
128
142
  get(id: string): Promise<SqliteWorkflowInstance>;
143
+ /**
144
+ * Install the thread-mode router (see {@link _threadRouter}). Called by
145
+ * `GenerationManager` on each reload so the dashboard's control ops reach the
146
+ * live worker-side binding. Mirrors the DO namespace's `_setExternalClass`.
147
+ */
148
+ _setThreadRouter(router: (op: WorkflowControlOp) => Promise<WorkflowControlResult>): void;
149
+ /**
150
+ * Execute a dashboard control operation. In thread mode the real state
151
+ * machine (abort controllers, event waiters, sleep resolvers, the wired
152
+ * class) lives in the worker, so when a thread router is installed this
153
+ * forwards there; otherwise (in-process / worker-side binding) it runs
154
+ * locally. Mutating ops resolve to `{ kind: 'ok' }`; `create` reports the new
155
+ * id; the introspection reads report their value.
156
+ */
157
+ executeControl(op: WorkflowControlOp): Promise<WorkflowControlResult>;
129
158
  /** Resume any workflow instances that were running/waiting when the process last exited. */
130
159
  resumeInterrupted(): void;
131
160
  /** Try to start any queued instances for this workflow (called after an instance completes). */
@@ -124,3 +124,5 @@ export declare function loadConfig(path: string, envName?: string): Promise<Wran
124
124
  * Auto-detect config file in a directory. Tries wrangler.jsonc, wrangler.json, wrangler.toml.
125
125
  */
126
126
  export declare function autoLoadConfig(baseDir: string, envName?: string): Promise<WranglerConfig>;
127
+ /** Resolve the wrangler config path under `baseDir` (jsonc | json | toml). */
128
+ export declare function findConfigPath(baseDir: string): string;
@@ -57,4 +57,10 @@ export declare function buildEnv(config: WranglerConfig, devVarsDir?: string, ex
57
57
  registry: ClassRegistry;
58
58
  };
59
59
  export declare function wireClassRefs(registry: ClassRegistry, workerModule: Record<string, unknown>, env: Record<string, unknown>, workerRegistry?: WorkerRegistry, generationId?: number): void;
60
+ /**
61
+ * Service-binding wiring extracted so thread-mode generations (which skip
62
+ * `wireClassRefs` entirely — DO/Workflow classes live in the worker) can
63
+ * still resolve cross-worker fetches through the registry.
64
+ */
65
+ export declare function wireServiceBindings(registry: ClassRegistry, workerModule: Record<string, unknown>, env: Record<string, unknown>, workerRegistry?: WorkerRegistry): void;
60
66
  export {};
@@ -1,2 +1,8 @@
1
1
  import type { WranglerConfig } from './config';
2
+ /**
3
+ * Stitch a captured caller stack onto a short async error so dev-time stacks
4
+ * show where the failing call originated. Mutates `err.stack` in place; no-op
5
+ * if either side is missing, already stitched, or `err.stack` looks deep.
6
+ */
7
+ export declare function stitchAsyncStack(err: Error, callerError: Error | null): void;
2
8
  export declare function renderErrorPage(error: unknown, request: Request, env: Record<string, unknown>, config: WranglerConfig, workerName?: string): Promise<Response>;
@@ -1,8 +1,16 @@
1
1
  export declare function getActiveExecutionContext(): ExecutionContext | undefined;
2
2
  export declare function runWithExecutionContext<T>(ctx: ExecutionContext, fn: () => T): T;
3
+ /** Swallow + log a `waitUntil` rejection. Single source of truth for the log string.
4
+ * Coerces non-thenables (CF tolerates `ctx.waitUntil(undefined)`) — a synchronous
5
+ * throw here would strand the wait-until-settle accounting on the worker side. */
6
+ export declare function logIfRejected(promise: Promise<unknown>): Promise<unknown>;
3
7
  export declare class ExecutionContext {
4
8
  private _promises;
5
9
  readonly props: Record<string, unknown>;
10
+ /** Cloudflare-compatible custom span API: `ctx.tracing.enterSpan(...)`. */
11
+ readonly tracing: {
12
+ enterSpan: typeof import("./tracing/span").enterSpan;
13
+ };
6
14
  constructor(props?: Record<string, unknown>);
7
15
  waitUntil(promise: Promise<unknown>): void;
8
16
  passThroughOnException(): void;
@@ -7,9 +7,20 @@ export declare class GenerationManager {
7
7
  private nextGenId;
8
8
  private _activeGenId;
9
9
  private _reloading;
10
- private _pendingReload;
10
+ /** Coalesced follow-up: one reload queued to run after the in-flight one, with
11
+ * its real promise handed to every caller that arrived during the in-flight
12
+ * reload (so they observe the result/error of the reload reflecting their
13
+ * request, not a stale `this.active`). */
14
+ private _pendingReloadPromise;
11
15
  /** DO namespaces shared across generations to preserve WebSocket connections on reload */
12
16
  private _doNamespaces;
17
+ /** Per-cron last-fired-minute dedup, shared across generations so a reload
18
+ * mid-minute doesn't re-run a scheduled handler the previous generation already
19
+ * fired (keyed by cron expression). */
20
+ private _cronLastFired;
21
+ /** Generation ids whose interrupted workflows have already been resumed, so a
22
+ * generation isn't resumed twice (which would re-execute the same instances). */
23
+ private _resumedGenIds;
13
24
  gracePeriodMs: number;
14
25
  readonly config: WranglerConfig;
15
26
  readonly baseDir: string;
@@ -24,7 +35,7 @@ export declare class GenerationManager {
24
35
  executablePath?: string;
25
36
  headless?: boolean;
26
37
  } | undefined;
27
- /** @internal Path to the wrangler config file (for isolated mode worker threads) */
38
+ /** @internal Path to the wrangler config file (DO worker threads re-load it). */
28
39
  _configPath: string;
29
40
  constructor(config: WranglerConfig, baseDir: string, options?: {
30
41
  workerName?: string;
@@ -47,6 +58,14 @@ export declare class GenerationManager {
47
58
  */
48
59
  reload(): Promise<Generation>;
49
60
  private _doReload;
61
+ /**
62
+ * Resume interrupted (running/waiting) workflow instances on `gen`'s worker,
63
+ * routed through the live worker-side binding. Guarded so a generation is
64
+ * resumed at most once even when several older generations stop in sequence —
65
+ * a second resume would re-execute the same instances (the bug this avoids).
66
+ */
67
+ private _resumeWorkflows;
68
+ private _scheduleDrainAndStop;
50
69
  private _stopGeneration;
51
70
  /** Force-drain a specific generation */
52
71
  drain(genId: number): void;
@@ -1,7 +1,9 @@
1
+ import { type Server } from 'bun';
1
2
  import type { DurableObjectNamespaceImpl } from './bindings/durable-object';
2
3
  import type { SqliteWorkflowBinding } from './bindings/workflow';
3
4
  import type { WranglerConfig } from './config';
4
- interface ClassRegistry {
5
+ import type { WorkerThreadExecutor } from './worker-thread/executor';
6
+ export interface ClassRegistry {
5
7
  durableObjects: {
6
8
  bindingName: string;
7
9
  className: string;
@@ -54,39 +56,32 @@ export declare class Generation {
54
56
  readonly id: number;
55
57
  state: GenerationState;
56
58
  readonly createdAt: number;
57
- readonly workerModule: Record<string, unknown>;
58
- readonly defaultExport: unknown;
59
- readonly classBasedExport: boolean;
60
59
  readonly env: Record<string, unknown>;
61
60
  readonly registry: ClassRegistry;
62
61
  readonly config: WranglerConfig;
63
62
  readonly workerName: string | undefined;
64
63
  readonly cronEnabled: boolean;
64
+ readonly threadExecutor: WorkerThreadExecutor;
65
65
  activeRequests: number;
66
- private queueConsumers;
67
66
  private cronTimer;
68
- drainTimer: ReturnType<typeof setTimeout> | null;
69
- drainPollTimer: ReturnType<typeof setInterval> | null;
70
- constructor(id: number, workerModule: Record<string, unknown>, defaultExport: unknown, classBasedExport: boolean, env: Record<string, unknown>, registry: ClassRegistry, config: WranglerConfig, workerName?: string, cronEnabled?: boolean);
71
- /** Get a handler method from the worker module (class-based or object-based) */
72
- private getHandler;
73
- /** Dispatch a fetch request through this generation's handler */
74
- callFetch(request: Request, server: any): Promise<Response | undefined>;
67
+ constructor(id: number, env: Record<string, unknown>, registry: ClassRegistry, config: WranglerConfig, threadExecutor: WorkerThreadExecutor, workerName?: string, cronEnabled?: boolean);
68
+ /** Dispatch a fetch request through the worker thread. */
69
+ callFetch(request: Request, server: Server<unknown>): Promise<Response | undefined>;
70
+ private _dispatchFetch;
75
71
  /** Handle manual /cdn-cgi/handler/scheduled trigger */
76
72
  callScheduled(cronExpr: string): Promise<Response>;
77
73
  /** Handle incoming email — dispatches to the worker's email() handler */
78
74
  callEmail(rawBytes: Uint8Array, from: string, to: string): Promise<Response>;
79
- /** Start queue consumers and cron scheduler */
80
- startConsumers(): void;
81
- /** Stop queue consumers and cron scheduler */
75
+ /** Start cron timer. Queue consumers run in the worker thread; no main-side action needed.
76
+ * `cronLastFired` is the manager-owned per-minute dedup state, shared across
77
+ * generations so a reload mid-minute doesn't re-fire a cron already handled. */
78
+ startConsumers(cronLastFired?: Map<string, number>): void;
79
+ private _persistAndRethrow;
82
80
  stopConsumers(): void;
83
- /** Transition to draining — stops consumers, clears alarm timers, keeps in-flight requests alive */
84
- drain(): void;
85
- /** Force-stop: drain + destroy all DO namespaces + abort workflows */
81
+ /** Transition to draining — stops consumers + alarm timers, keeps in-flight requests alive. */
82
+ drain(sharedNamespaces?: Set<DurableObjectNamespaceImpl>): void;
83
+ /** Force-stop: drain + destroy all DO namespaces + abort workflows + terminate the worker. */
86
84
  stop(sharedNamespaces?: Set<DurableObjectNamespaceImpl>): void;
87
- /** Check if this generation has no more work */
88
85
  isIdle(): boolean;
89
- /** Get info for dashboard */
90
86
  getInfo(): GenerationInfo;
91
87
  }
92
- export {};
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Walk the worker's transitive import graph from `entry`, returning the set of
3
+ * absolute project-source paths it depends on (entry included). Anything that
4
+ * resolves outside `baseDir` or into `node_modules` is treated as external and
5
+ * not followed.
6
+ */
7
+ export declare function collectImportGraph(entry: string, baseDir: string): Set<string>;
8
+ /**
9
+ * Polls the worker's import graph for changes (mtime-based) and fires `onChange`.
10
+ * After a reload the caller should `rescan()` so newly-added imports start being
11
+ * watched (and deleted files stop being watched).
12
+ */
13
+ export declare class ImportGraphWatcher {
14
+ private entry;
15
+ private baseDir;
16
+ private onChange;
17
+ private pollIntervalMs;
18
+ private mtimes;
19
+ private timer;
20
+ constructor(entry: string, baseDir: string, onChange: () => void, pollIntervalMs?: number);
21
+ start(): void;
22
+ stop(): void;
23
+ /** Recompute the watched set from the current import graph. */
24
+ rescan(): void;
25
+ /** Number of files currently watched (for the startup log line). */
26
+ get size(): number;
27
+ private poll;
28
+ }
@@ -12,9 +12,9 @@ export interface LopataConfig {
12
12
  /** Enable real cron scheduling based on wrangler triggers.crons (default: false) */
13
13
  cron?: boolean;
14
14
  /**
15
- * DO isolation mode:
16
- * - "dev" (default) all DO instances run in-process (fast, shared memory, hot reload)
17
- * - "isolated" each DO instance runs in a separate Bun Worker thread (faithful to CF production)
15
+ * @deprecated Has no effect. Workers and DO instances always run in their own
16
+ * Bun Worker thread now; the in-process path was removed. Kept only so existing
17
+ * configs don't fail to parse see the deprecation warning in `cli/dev.ts`.
18
18
  */
19
19
  isolation?: 'dev' | 'isolated';
20
20
  /** AI SQL generation config for the D1 console */
@@ -1 +1,4 @@
1
- export {};
1
+ /** The fetch handler registered via the legacy `addEventListener('fetch', …)`
2
+ * service-worker syntax, if any. The worker-thread runtime falls back to this
3
+ * when the module has no `export default { fetch }`. */
4
+ export declare function getServiceWorkerFetchHandler(): ((event: unknown) => void) | undefined;
@@ -14,3 +14,12 @@
14
14
  export declare function validateRpcValue(value: unknown, path?: string): string[];
15
15
  export declare function warnInvalidRpcArgs(args: unknown[], methodName: string): void;
16
16
  export declare function warnInvalidRpcReturn(value: unknown, methodName: string): void;
17
+ /**
18
+ * Thread-mode pre-flight for RPC args. Functions and RpcTarget instances are
19
+ * VALID on real Cloudflare (they cross as callback / target stubs) and pass
20
+ * {@link validateRpcValue}, but they can't be structured-cloned across lopata's
21
+ * worker-thread boundary — `postMessage` throws an opaque DataCloneError with no
22
+ * guidance. Warn clearly (top-level args; the common `stub.method(cb)` case) so
23
+ * the dev knows why the call is about to fail.
24
+ */
25
+ export declare function warnCrossThreadRpcArgs(args: unknown[], methodName: string): void;
@@ -2,7 +2,11 @@
2
2
  * Sets up global Cloudflare-compatible APIs:
3
3
  * caches, HTMLRewriter, WebSocketPair, IdentityTransformStream, FixedLengthStream,
4
4
  * navigator.userAgent, navigator.language, performance.timeOrigin, scheduler.wait(),
5
- * crypto extensions, and __lopata userland tracing API.
5
+ * and crypto extensions.
6
+ *
7
+ * Custom user spans are provided via the Cloudflare-native `tracing.enterSpan`
8
+ * API exported from `cloudflare:workers` (see src/virtual-modules.ts), not a
9
+ * Lopata-specific global.
6
10
  *
7
11
  * Idempotent — safe to call multiple times.
8
12
  */
@@ -4,6 +4,13 @@
4
4
  export interface FetchStackRef {
5
5
  current: Error | null;
6
6
  }
7
+ /** Mutable subrequest counter shared across all spans in the same trace.
8
+ * Scopes the binding subrequest budget to a single top-level request, the
9
+ * way Cloudflare resets the budget per incoming request: a fresh ref is
10
+ * minted at the root span and inherited by every child span. */
11
+ export interface SubrequestCounterRef {
12
+ count: number;
13
+ }
7
14
  export interface SpanContext {
8
15
  traceId: string;
9
16
  spanId: string;
@@ -13,9 +20,23 @@ export interface SpanContext {
13
20
  * call still contains the user's code frames — we stitch it onto caught
14
21
  * errors. */
15
22
  fetchStack: FetchStackRef;
23
+ /** Per-top-level-request subrequest counter, shared across the trace. */
24
+ subrequests: SubrequestCounterRef;
16
25
  }
17
26
  export declare function getActiveContext(): SpanContext | undefined;
18
27
  export declare function runWithContext<T>(ctx: SpanContext, fn: () => T): T;
28
+ /**
29
+ * Adopt a parent (traceId + spanId) sent across an isolate boundary — refs
30
+ * (`fetchStack`, `subrequests`) can't cross postMessage, so we re-seed empty
31
+ * ones and let sub-spans created on this side share them via `getActiveContext`.
32
+ * Note: the per-request subrequest budget resets on each side of the boundary;
33
+ * a nested service-binding hop into another thread effectively gets a fresh
34
+ * budget. Acceptable for dev; a real fix would round-trip the count.
35
+ */
36
+ export declare function runWithParentContext<T>(parent: {
37
+ traceId: string;
38
+ spanId: string;
39
+ } | undefined, fn: () => T): T;
19
40
  export declare function generateId(byteCount?: number): string;
20
41
  /** Generate a 128-bit trace ID (OTel standard: 16 bytes / 32 hex chars) */
21
42
  export declare function generateTraceId(): string;
@@ -7,12 +7,30 @@ export interface SpanOptions {
7
7
  /** Force a new root trace, ignoring any active parent context. */
8
8
  newTrace?: boolean;
9
9
  }
10
+ /** Handle passed to span callbacks, mirroring Cloudflare's custom-span Span object. */
11
+ export interface SpanHandle {
12
+ /** Set an attribute on the span. Passing `undefined` is a no-op, matching Cloudflare (allows optional chaining). */
13
+ setAttribute(key: string, value: string | number | boolean | undefined): void;
14
+ /** Whether this invocation is being traced. Always true in lopata's dev runtime. */
15
+ readonly isTraced: boolean;
16
+ }
10
17
  export declare function startSpan<T>(opts: SpanOptions, fn: () => T | Promise<T>): Promise<T>;
11
18
  /** Synchronous variant of startSpan for instrumenting non-async APIs (e.g. DO
12
- * state.storage.sql.exec is sync). Inserts the span before fn() runs and ends
13
- * it after, mirroring the async version's status/exception handling. fn is
14
- * invoked inside runWithContext so any spans it creates nest correctly. */
19
+ * state.storage.sql.exec is sync). The span ends as soon as fn returns; fn runs
20
+ * inside the span context so any spans it creates nest correctly. */
15
21
  export declare function startSyncSpan<T>(opts: SpanOptions, fn: () => T): T;
22
+ /**
23
+ * Cloudflare-compatible custom span API (`tracing.enterSpan` from `cloudflare:workers`, also
24
+ * `ctx.tracing`). Runs `fn` inside a child span of the active trace context, passing a span
25
+ * handle for `setAttribute` / `isTraced`. The callback's value is returned as-is (sync stays
26
+ * sync, async returns the promise) and the span auto-ends when the callback returns or its
27
+ * returned promise settles — matching Cloudflare's semantics.
28
+ */
29
+ export declare function enterSpan<T>(name: string, fn: (span: SpanHandle) => T): T;
30
+ /** Cloudflare-compatible `tracing` namespace exported from `cloudflare:workers` and exposed as `ctx.tracing`. */
31
+ export declare const tracing: {
32
+ enterSpan: typeof enterSpan;
33
+ };
16
34
  export declare function setSpanStatus(status: 'ok' | 'error', message?: string): void;
17
35
  export declare function setSpanAttribute(key: string, value: unknown): void;
18
36
  export declare function addSpanEvent(name: string, level: string, message: string, attrs?: Record<string, unknown>): void;
@@ -1,6 +1,15 @@
1
1
  import type { Database } from 'bun:sqlite';
2
2
  import type { SpanData, SpanEventData, TraceDetail, TraceEvent, TraceSummary } from './types';
3
3
  type Listener = (event: TraceEvent) => void;
4
+ /**
5
+ * `JSON.stringify` for user-controlled span attributes / event payloads.
6
+ * Plain `JSON.stringify` THROWS on a `BigInt` and on a circular reference —
7
+ * both trivially reachable from user code (`setAttribute('x', 1n)`, logging a
8
+ * request/response object). In thread mode these writes run on the main thread
9
+ * (inside `worker.onmessage`), so an unguarded throw would take down the whole
10
+ * dev server. A dropped/coerced value is always preferable to a crash here.
11
+ */
12
+ export declare function safeStringify(value: unknown): string;
4
13
  export declare class TraceStore {
5
14
  private db;
6
15
  private listeners;
@@ -126,4 +135,12 @@ export declare class TraceStore {
126
135
  private rowToSpan;
127
136
  }
128
137
  export declare function getTraceStore(): TraceStore;
138
+ /** Override the process-wide default store. Used by tests to inject an in-memory DB. */
139
+ export declare function setTraceStore(store: TraceStore | null): void;
140
+ /**
141
+ * Swap the process-local trace store. Used by the worker-thread runtime to
142
+ * install a forwarding store. **Do not call from main** — that mutes the
143
+ * dashboard subscribers attached to the real store.
144
+ */
145
+ export declare function setTraceStoreOverride(store: Pick<TraceStore, 'insertSpan' | 'endSpan' | 'setSpanStatus' | 'getSpanStatus' | 'updateAttributes' | 'addEvent' | 'insertError'> | null): void;
129
146
  export {};