@cloudflare/workspace 0.0.0-alpha.2 → 0.0.0-alpha.4

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/README.md CHANGED
@@ -64,6 +64,63 @@ export default {
64
64
  } satisfies ExportedHandler<Env>;
65
65
  ```
66
66
 
67
+ ## Observability
68
+
69
+ The package emits one span per documented operation through an optional
70
+ observer hook. Pass an observer to the `Workspace` constructor:
71
+
72
+ ```ts
73
+ import { Workspace, type WorkspaceObserver } from "@cloudflare/workspace";
74
+
75
+ const observer: WorkspaceObserver = {
76
+ async span(name, attributes, run) {
77
+ // Wrap `run` however your tracing backend wants. The Cloudflare
78
+ // runtime, OpenTelemetry, and a plain console.log adapter all fit
79
+ // the same shape.
80
+ return run({ setAttribute: () => {} });
81
+ },
82
+ };
83
+
84
+ const ws = new Workspace({
85
+ storage: this.ctx.storage,
86
+ backends: [...],
87
+ observer,
88
+ });
89
+ ```
90
+
91
+ The observer's `span(name, attributes, run)` wraps each operation. It
92
+ starts a span, runs the callback, and ends the span when the callback
93
+ returns or its promise settles. Errors thrown by the work record
94
+ `error.name` and `error.message` and propagate.
95
+
96
+ The span names the package emits today:
97
+
98
+ - `workspace.connect` — one per `connect()` attempt against a single
99
+ backend. Tagged with `workspace.backend.id`.
100
+ - `workspace.sync.push` / `workspace.sync.pull` — one per sync call.
101
+ Tagged with the entry counts (`workspace.sync.pushed`,
102
+ `workspace.sync.applied`, `workspace.sync.skipped`).
103
+ - `workspace.shell.exec` — the full exec bracket from the
104
+ `WorkspaceStub`. Contains `workspace.sync.push`,
105
+ `workspace.shell.exec.spawn`, and `workspace.sync.pull` as nested
106
+ children. Tagged with `workspace.shell.exit_code`,
107
+ `workspace.shell.pushed`, `workspace.shell.pulled`, and
108
+ `workspace.shell.skipped`.
109
+ - `workspace.fs.<op>` — one per filesystem call routed through the
110
+ stub (`readFile`, `writeFile`, `stat`, `readdir`, `find`, `ls`,
111
+ `grep`, `mkdir`, `rm`). Tagged with `workspace.fs.path` and, where
112
+ meaningful, `workspace.fs.entries` or `workspace.fs.matches`.
113
+
114
+ Attribute values are restricted to `boolean | number | string` so the
115
+ same observer shape works against the Cloudflare runtime's built-in
116
+ `ctx.tracing.enterSpan(...)` API, OpenTelemetry, or a recording test
117
+ observer. Adapter packages for the Cloudflare runtime and for
118
+ OpenTelemetry are forthcoming.
119
+
120
+ The default is a no-op observer with no allocation or async overhead
121
+ beyond what the callback itself does, so the package has no
122
+ observability cost when callers do not opt in.
123
+
67
124
  ## Stub disposal
68
125
 
69
126
  capnweb does not garbage-collect remote stubs. On the long-lived
Binary file
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { _ as Database, a as SQLiteWorkspaceProviderOptions, c as WriteFileOptions, d as ReadFileOptions, f as WorkspaceDirentResult, g as WorkspaceFoundEntry, h as WorkspaceGrepMatch, i as SQLiteWorkspaceProvider, l as WorkspaceStatResult, m as GrepOptions, n as SkippedEntry, o as WorkspaceFilesystem, p as MkdirOptions, r as ChangeEntry, s as WriteFileContent, t as ApplyResult, u as RmOptions, v as DurableObjectStorageLike } from "./shared-RIdME5uo.js";
2
+ import { a as noopObserver, i as WorkspaceSpan, n as WorkspaceAttributes, r as WorkspaceObserver, t as WorkspaceAttributeValue } from "./shared-DYgflRlD.js";
2
3
  import { RpcTarget } from "capnweb";
3
4
  import { RpcTarget as RpcTarget$1, WorkerEntrypoint } from "cloudflare:workers";
4
5
 
@@ -261,7 +262,7 @@ interface Sync {
261
262
  }
262
263
  declare class WorkspaceShell {
263
264
  #private;
264
- constructor(shell: ShellRPC, sync: Sync);
265
+ constructor(shell: ShellRPC, sync: Sync, observer?: WorkspaceObserver);
265
266
  exec(command: string): Promise<ExecHandle<undefined>>;
266
267
  exec(command: string, options: ExecOptions<undefined>): Promise<ExecHandle<undefined>>;
267
268
  exec(command: string, options: ExecOptions<"utf8">): Promise<ExecHandle<"utf8">>;
@@ -280,6 +281,7 @@ interface WorkspaceOptions {
280
281
  now?: () => number;
281
282
  sessionId?: string;
282
283
  mounts?: Record<string, MountValue>;
284
+ observer?: WorkspaceObserver;
283
285
  reconnect?: ReconnectOptions;
284
286
  }
285
287
  interface ReconnectOptions {
@@ -293,6 +295,7 @@ declare class Workspace {
293
295
  ensureMountsIndexed(): Promise<void>;
294
296
  mounts(): Map<string, Mount>;
295
297
  get db(): Database;
298
+ get observer(): WorkspaceObserver;
296
299
  get fs(): WorkspaceFilesystem;
297
300
  /**
298
301
  * Underlying dofs `SQLiteWorkspaceProvider` over the local store.
@@ -380,5 +383,5 @@ declare class WorkspaceStub extends RpcTarget {
380
383
  get shell(): WorkspaceShellStub;
381
384
  }
382
385
  //#endregion
383
- export { type ApplyResult, type BackendHandle, CloudflareContainerBackend, type CloudflareContainerBackendOptions, type DurableObjectStorageLike, type EagerMount, type ExecEncoding, type ExecHandle, type ExecOptions, type ExecResult, type GetExecOptions, type IWorkspaceContainerAPI, type KillSignal, type Mount, type MountBase, type MountContext, type MountFactory, type MountWriteAPI, R2Bucket, type R2BucketBinding, type R2BucketOptions, SQLiteWorkspaceProvider, type SQLiteWorkspaceProviderOptions, type SkippedEntry, TestBackend, type TestBackendOptions, Workspace, type WorkspaceBackend, WorkspaceContainerAPI, type WorkspaceExecEvent, WorkspaceExecHandleStub, type WorkspaceExecOptions, type WorkspaceExecResult, WorkspaceFilesystemStub, type WorkspaceOptions, WorkspaceProxy, type WorkspaceProxyProps, type WorkspaceRef, WorkspaceShell, WorkspaceShellStub, WorkspaceStub, withWorkspaceContainer };
386
+ export { type ApplyResult, type BackendHandle, CloudflareContainerBackend, type CloudflareContainerBackendOptions, type DurableObjectStorageLike, type EagerMount, type ExecEncoding, type ExecHandle, type ExecOptions, type ExecResult, type GetExecOptions, type IWorkspaceContainerAPI, type KillSignal, type Mount, type MountBase, type MountContext, type MountFactory, type MountWriteAPI, R2Bucket, type R2BucketBinding, type R2BucketOptions, SQLiteWorkspaceProvider, type SQLiteWorkspaceProviderOptions, type SkippedEntry, TestBackend, type TestBackendOptions, Workspace, type WorkspaceAttributeValue, type WorkspaceAttributes, type WorkspaceBackend, WorkspaceContainerAPI, type WorkspaceExecEvent, WorkspaceExecHandleStub, type WorkspaceExecOptions, type WorkspaceExecResult, WorkspaceFilesystemStub, type WorkspaceObserver, type WorkspaceOptions, WorkspaceProxy, type WorkspaceProxyProps, type WorkspaceRef, WorkspaceShell, WorkspaceShellStub, type WorkspaceSpan, WorkspaceStub, noopObserver, withWorkspaceContainer };
384
387
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -2035,6 +2035,63 @@ function R2Bucket(bucket, options = {}) {
2035
2035
  };
2036
2036
  }
2037
2037
  //#endregion
2038
+ //#region src/observe.ts
2039
+ const NOOP_SPAN = { setAttribute() {} };
2040
+ /**
2041
+ * Observer that does no work. Used when the caller does not pass one.
2042
+ * Calling `span(...)` returns the callback's promise directly, with no
2043
+ * extra `await` and no allocation beyond what the callback itself does.
2044
+ */
2045
+ const noopObserver = { span(_name, _attributes, run) {
2046
+ return run(NOOP_SPAN);
2047
+ } };
2048
+ /**
2049
+ * Internal helper: wraps `run` with `observer.span(...)`, applies any
2050
+ * `undefined`-filtered attributes the work produces on settlement, and
2051
+ * records error details on rejection before re-throwing.
2052
+ *
2053
+ * The `finalize` callback runs with the result (on success) or the
2054
+ * thrown error (on failure) and is the single place call sites attach
2055
+ * post-hoc attributes like byte counts, exit codes, or applied counts.
2056
+ * Both branches are wrapped in try/catch so a buggy `finalize` does
2057
+ * not mask the original outcome.
2058
+ */
2059
+ function withSpan(observer, name, attributes, run, finalize) {
2060
+ return observer.span(name, attributes, async (span) => {
2061
+ try {
2062
+ const value = await run();
2063
+ if (finalize) try {
2064
+ finalize(span, {
2065
+ ok: true,
2066
+ value
2067
+ });
2068
+ } catch {}
2069
+ return value;
2070
+ } catch (error) {
2071
+ recordError(span, error);
2072
+ if (finalize) try {
2073
+ finalize(span, {
2074
+ ok: false,
2075
+ error
2076
+ });
2077
+ } catch {}
2078
+ throw error;
2079
+ }
2080
+ });
2081
+ }
2082
+ /**
2083
+ * Records `error.message` and `error.name` on `span`. Adapters that
2084
+ * want richer error reporting can subscribe to the underlying span
2085
+ * system directly; the workspace itself only forwards what the
2086
+ * Cloudflare `Span` surface accepts.
2087
+ */
2088
+ function recordError(span, error) {
2089
+ if (error instanceof Error) {
2090
+ span.setAttribute("error.name", error.name);
2091
+ span.setAttribute("error.message", error.message);
2092
+ } else span.setAttribute("error.message", String(error));
2093
+ }
2094
+ //#endregion
2038
2095
  //#region src/proxy.ts
2039
2096
  var WorkspaceProxy = class extends WorkerEntrypoint {
2040
2097
  async fetch(request) {
@@ -2054,20 +2111,28 @@ var WorkspaceProxy = class extends WorkerEntrypoint {
2054
2111
  var WorkspaceShell = class {
2055
2112
  #shell;
2056
2113
  #sync;
2057
- constructor(shell, sync) {
2114
+ #observer;
2115
+ constructor(shell, sync, observer = noopObserver) {
2058
2116
  this.#shell = shell;
2059
2117
  this.#sync = sync;
2118
+ this.#observer = observer;
2060
2119
  }
2061
2120
  async exec(command, options = {}) {
2062
2121
  let pushed = 0;
2063
2122
  try {
2064
2123
  pushed = await this.#sync.push();
2065
2124
  } catch {}
2066
- const envelope = await this.#shell.exec({
2125
+ const envelope = await withSpan(this.#observer, "workspace.shell.exec.spawn", {
2126
+ "workspace.shell.cwd": options.cwd,
2127
+ "workspace.shell.timeout_ms": options.timeoutMs,
2128
+ "workspace.shell.id": options.id
2129
+ }, () => this.#shell.exec({
2067
2130
  command,
2068
2131
  id: options.id,
2069
2132
  cwd: options.cwd,
2070
2133
  timeoutMs: options.timeoutMs
2134
+ }), (span, outcome) => {
2135
+ if (outcome.ok) span.setAttribute("workspace.shell.id", outcome.value.id);
2071
2136
  });
2072
2137
  const events = disposeOnDone$1(envelope.events, () => maybeDispose$1(envelope));
2073
2138
  return wrapHandle(this.#shell, this.#sync, envelope.id, events, options.encoding, pushed);
@@ -2263,31 +2328,52 @@ var WorkspaceFilesystemStub = class extends RpcTarget {
2263
2328
  untrackStub(this);
2264
2329
  }
2265
2330
  readFile(path, optionsOrEncoding) {
2266
- return this.#ws.fs.readFile(path, optionsOrEncoding);
2331
+ return withSpan(this.#ws.observer, "workspace.fs.readFile", { "workspace.fs.path": path }, () => this.#ws.fs.readFile(path, optionsOrEncoding));
2267
2332
  }
2268
2333
  stat(path) {
2269
- return this.#ws.fs.stat(path);
2334
+ return withSpan(this.#ws.observer, "workspace.fs.stat", { "workspace.fs.path": path }, () => this.#ws.fs.stat(path));
2270
2335
  }
2271
2336
  readdir(path) {
2272
- return this.#ws.fs.readdir(path);
2337
+ return withSpan(this.#ws.observer, "workspace.fs.readdir", { "workspace.fs.path": path }, () => this.#ws.fs.readdir(path), (span, outcome) => {
2338
+ if (outcome.ok) span.setAttribute("workspace.fs.entries", outcome.value.length);
2339
+ });
2273
2340
  }
2274
2341
  find(directory, pattern) {
2275
- return this.#ws.fs.find(directory, pattern);
2342
+ return withSpan(this.#ws.observer, "workspace.fs.find", {
2343
+ "workspace.fs.path": directory,
2344
+ "workspace.fs.pattern": pattern
2345
+ }, () => this.#ws.fs.find(directory, pattern), (span, outcome) => {
2346
+ if (outcome.ok) span.setAttribute("workspace.fs.matches", outcome.value.length);
2347
+ });
2276
2348
  }
2277
2349
  ls(prefix) {
2278
- return this.#ws.fs.ls(prefix);
2350
+ return withSpan(this.#ws.observer, "workspace.fs.ls", { "workspace.fs.path": prefix }, () => this.#ws.fs.ls(prefix), (span, outcome) => {
2351
+ if (outcome.ok) span.setAttribute("workspace.fs.entries", outcome.value.length);
2352
+ });
2279
2353
  }
2280
2354
  grep(pattern, path, options = {}) {
2281
- return this.#ws.fs.grep(pattern, path, options);
2355
+ return withSpan(this.#ws.observer, "workspace.fs.grep", {
2356
+ "workspace.fs.path": path,
2357
+ "workspace.fs.pattern": pattern
2358
+ }, () => this.#ws.fs.grep(pattern, path, options), (span, outcome) => {
2359
+ if (outcome.ok) span.setAttribute("workspace.fs.matches", outcome.value.length);
2360
+ });
2282
2361
  }
2283
2362
  writeFile(path, content, options = {}) {
2284
- return this.#ws.fs.writeFile(path, content, options);
2363
+ return withSpan(this.#ws.observer, "workspace.fs.writeFile", { "workspace.fs.path": path }, () => this.#ws.fs.writeFile(path, content, options));
2285
2364
  }
2286
2365
  mkdir(path, options = {}) {
2287
- return this.#ws.fs.mkdir(path, options);
2366
+ return withSpan(this.#ws.observer, "workspace.fs.mkdir", {
2367
+ "workspace.fs.path": path,
2368
+ "workspace.fs.recursive": options.recursive
2369
+ }, () => this.#ws.fs.mkdir(path, options));
2288
2370
  }
2289
2371
  rm(path, options = {}) {
2290
- return this.#ws.fs.rm(path, options);
2372
+ return withSpan(this.#ws.observer, "workspace.fs.rm", {
2373
+ "workspace.fs.path": path,
2374
+ "workspace.fs.recursive": options.recursive,
2375
+ "workspace.fs.force": options.force
2376
+ }, () => this.#ws.fs.rm(path, options));
2291
2377
  }
2292
2378
  };
2293
2379
  var WorkspaceExecHandleStub = class extends RpcTarget {
@@ -2320,10 +2406,19 @@ var WorkspaceShellStub = class extends RpcTarget {
2320
2406
  untrackStub(this);
2321
2407
  }
2322
2408
  async exec(command, options = {}) {
2323
- return new WorkspaceExecHandleStub(options.encoding === "utf8" ? this.#ws.shell.exec(command, {
2409
+ return new WorkspaceExecHandleStub(withSpan(this.#ws.observer, "workspace.shell.exec", {
2410
+ "workspace.shell.cwd": options.cwd,
2411
+ "workspace.shell.encoding": options.encoding
2412
+ }, () => options.encoding === "utf8" ? this.#ws.shell.exec(command, {
2324
2413
  cwd: options.cwd,
2325
2414
  encoding: "utf8"
2326
- }).then((handle) => handle.result()) : this.#ws.shell.exec(command, { cwd: options.cwd }).then((handle) => handle.result()));
2415
+ }).then((handle) => handle.result()) : this.#ws.shell.exec(command, { cwd: options.cwd }).then((handle) => handle.result()), (span, outcome) => {
2416
+ if (!outcome.ok) return;
2417
+ span.setAttribute("workspace.shell.exit_code", outcome.value.exitCode);
2418
+ span.setAttribute("workspace.shell.pushed", outcome.value.pushed);
2419
+ span.setAttribute("workspace.shell.pulled", outcome.value.pulled);
2420
+ span.setAttribute("workspace.shell.skipped", outcome.value.skipped.length);
2421
+ }));
2327
2422
  }
2328
2423
  };
2329
2424
  var WorkspaceStub = class extends RpcTarget {
@@ -2736,6 +2831,7 @@ var Workspace = class {
2736
2831
  #provider;
2737
2832
  #backends;
2738
2833
  #reconnect;
2834
+ #observer;
2739
2835
  #now;
2740
2836
  #mounts;
2741
2837
  #mountIndex;
@@ -2755,6 +2851,7 @@ var Workspace = class {
2755
2851
  initialDelayMs: 0,
2756
2852
  maxDelayMs: 0
2757
2853
  };
2854
+ this.#observer = options.observer ?? noopObserver;
2758
2855
  this.#mounts = buildMountRegistry(options.mounts, {
2759
2856
  sessionId: options.sessionId,
2760
2857
  vfs: () => this.provider()
@@ -2774,6 +2871,9 @@ var Workspace = class {
2774
2871
  get db() {
2775
2872
  return this.#db;
2776
2873
  }
2874
+ get observer() {
2875
+ return this.#observer;
2876
+ }
2777
2877
  get fs() {
2778
2878
  return this.#fs;
2779
2879
  }
@@ -2825,18 +2925,24 @@ var Workspace = class {
2825
2925
  return new WorkspaceStub(this);
2826
2926
  }
2827
2927
  push() {
2828
- return this.#serialize(async () => {
2928
+ return this.#serialize(() => withSpan(this.#observer, "workspace.sync.push", {}, async () => {
2829
2929
  await this.ready();
2830
2930
  if (!this.#handle) throw new Error("Workspace not connected");
2831
2931
  return pushOnce(this.#db, this.#handle.rpc.sync);
2832
- });
2932
+ }, (span, outcome) => {
2933
+ if (outcome.ok) span.setAttribute("workspace.sync.pushed", outcome.value);
2934
+ }));
2833
2935
  }
2834
2936
  pull() {
2835
- return this.#serialize(async () => {
2937
+ return this.#serialize(() => withSpan(this.#observer, "workspace.sync.pull", {}, async () => {
2836
2938
  await this.ready();
2837
2939
  if (!this.#handle) throw new Error("Workspace not connected");
2838
2940
  return pullOnce(this.#db, this.#handle.rpc.sync);
2839
- });
2941
+ }, (span, outcome) => {
2942
+ if (!outcome.ok) return;
2943
+ span.setAttribute("workspace.sync.applied", outcome.value.applied);
2944
+ span.setAttribute("workspace.sync.skipped", outcome.value.skipped.length);
2945
+ }));
2840
2946
  }
2841
2947
  #serialize(fn) {
2842
2948
  const run = this.#mutationTail.then(fn, fn);
@@ -2870,10 +2976,10 @@ var Workspace = class {
2870
2976
  async #connectOnce() {
2871
2977
  const errors = [];
2872
2978
  for (const backend of this.#backends) try {
2873
- const handle = await backend.connect();
2979
+ const handle = await withSpan(this.#observer, "workspace.connect", { "workspace.backend.id": backend.id }, () => backend.connect());
2874
2980
  await reconcileWatermarks(this.#db, handle.rpc.sync);
2875
2981
  this.#handle = handle;
2876
- this.#shell = new WorkspaceShell(handle.rpc.shell, this);
2982
+ this.#shell = new WorkspaceShell(handle.rpc.shell, this, this.#observer);
2877
2983
  if (handle.closed) handle.closed.catch(() => {}).then(() => {
2878
2984
  if (this.#handle === handle) {
2879
2985
  this.#handle = void 0;
@@ -2897,6 +3003,6 @@ function sleep(ms) {
2897
3003
  return new Promise((resolve) => setTimeout(resolve, ms));
2898
3004
  }
2899
3005
  //#endregion
2900
- export { CloudflareContainerBackend, R2Bucket, SQLiteWorkspaceProvider, TestBackend, Workspace, WorkspaceContainerAPI, WorkspaceExecHandleStub, WorkspaceFilesystemStub, WorkspaceProxy, WorkspaceShell, WorkspaceShellStub, WorkspaceStub, withWorkspaceContainer };
3006
+ export { CloudflareContainerBackend, R2Bucket, SQLiteWorkspaceProvider, TestBackend, Workspace, WorkspaceContainerAPI, WorkspaceExecHandleStub, WorkspaceFilesystemStub, WorkspaceProxy, WorkspaceShell, WorkspaceShellStub, WorkspaceStub, noopObserver, withWorkspaceContainer };
2901
3007
 
2902
3008
  //# sourceMappingURL=index.js.map