@lunora/do 1.0.0-alpha.40 → 1.0.0-alpha.42

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/index.d.mts CHANGED
@@ -2378,14 +2378,69 @@ interface TraceAnchor {
2378
2378
  rootSpanId: string;
2379
2379
  traceId: string;
2380
2380
  }
2381
+ /**
2382
+ * Minimal structural shape of one **Cloudflare custom span** — the object CF's
2383
+ * `tracing.enterSpan(name, (span) => …)` callback receives (GA 2026-06-16). Only
2384
+ * the surface the bridge touches is declared, so `@lunora/do` needs no runtime
2385
+ * dependency on `cloudflare:workers`; the real platform span is structurally
2386
+ * assignable.
2387
+ */
2388
+ interface CloudflareSpanLike {
2389
+ /**
2390
+ * Whether this span is actually being recorded by the runtime's sampler.
2391
+ * `false` off the traced path (unsampled) — the bridge skips its
2392
+ * `setAttribute` work in that case rather than building attribute strings for
2393
+ * a span nobody will read.
2394
+ */
2395
+ readonly isTraced: boolean;
2396
+ /** Attach one primitive attribute to the CF span. */
2397
+ setAttribute: (key: string, value: boolean | number | string | undefined) => void;
2398
+ }
2399
+ /**
2400
+ * Minimal structural shape of the `tracing` namespace exported by
2401
+ * `cloudflare:workers`. `enterSpan` opens a custom span that auto-nests under the
2402
+ * runtime's ambient span and ends when `callback` settles.
2403
+ */
2404
+ interface CloudflareTracingLike {
2405
+ enterSpan: <T>(name: string, callback: (span: CloudflareSpanLike) => T) => T;
2406
+ }
2407
+ /**
2408
+ * Resolves CF's `tracing` namespace, or `undefined` when it is unavailable —
2409
+ * off-Cloudflare, on a compat date predating custom spans, or when
2410
+ * `tracing.enterSpan` is not a function. **Injected, never imported here**, so the
2411
+ * tracer stays pure and unit-testable without `cloudflare:workers`; the shard
2412
+ * supplies the real resolver, tests a fake or `undefined`.
2413
+ */
2414
+ type CloudflareTracingResolver = () => CloudflareTracingLike | Promise<CloudflareTracingLike | undefined> | undefined;
2381
2415
  /** What {@link createTracer} needs from the shard to build a span. */
2382
2416
  interface TracerDeps {
2383
2417
  /** The trace this ctx's spans belong to. */
2384
2418
  anchor: TraceAnchor;
2385
2419
  /** Function path the spans are attributed to. */
2386
2420
  functionPath: string;
2421
+ /**
2422
+ * **Opt-in, EXPERIMENTAL, default off.** When `true` *and*
2423
+ * {@link TracerDeps.resolveCloudflareTracing} yields a working
2424
+ * `tracing.enterSpan`, each `ctx.trace` span is ALSO emitted as a Cloudflare
2425
+ * **custom span**, so it nests inside CF's native binding/fetch/handler trace
2426
+ * tree on the hosted path. This only ADDS a CF-side span — the recorded
2427
+ * {@link SpanEvent} (our `SpanBuffer`/`otlpSink`) is untouched and remains the
2428
+ * source of truth plus the local studio waterfall. The `enterSpan` call itself
2429
+ * is now workerd-validated as available and side-effect-free inside a Durable
2430
+ * Object; CF's exported parent-linking under sampling remains unverified. See
2431
+ * {@link createTracer} for the double-export and Durable-Object async-context
2432
+ * caveats.
2433
+ */
2434
+ fuseCloudflareSpans?: boolean;
2387
2435
  /** Hand a finished span to the buffer + sink. */
2388
2436
  record: (span: SpanEvent) => void;
2437
+ /**
2438
+ * Injected resolver for CF's `tracing` namespace (see
2439
+ * {@link CloudflareTracingResolver}). Only consulted when
2440
+ * {@link TracerDeps.fuseCloudflareSpans} is `true`, so the default path never
2441
+ * calls it.
2442
+ */
2443
+ resolveCloudflareTracing?: CloudflareTracingResolver;
2389
2444
  /** Shard key for single-shard calls; absent for the unnamed root DO. */
2390
2445
  shardKey: string | undefined;
2391
2446
  /** Read lazily — the acting user is resolved per span, not per ctx. */
@@ -2418,6 +2473,40 @@ interface MetricsDeps {
2418
2473
  * cleared in the dispatch `finally`, and a subscription re-run builds its ctx
2419
2474
  * during* the writing mutation's flush — so reading that shared field at span
2420
2475
  * time would file the re-run's spans under the mutation's trace.
2476
+ *
2477
+ * **Cloudflare custom-spans bridge (opt-in, EXPERIMENTAL).** When
2478
+ * `deps.fuseCloudflareSpans` is `true` and `deps.resolveCloudflareTracing` yields
2479
+ * a working `tracing.enterSpan` (`cloudflare:workers`, GA 2026-06-16), each span
2480
+ * body runs inside a CF custom span so our span nests under CF's native
2481
+ * binding/fetch/handler trace tree on the hosted path, and the finished span's
2482
+ * key attributes are mirrored onto it (gated on `span.isTraced`). Two deliberate
2483
+ * boundaries hold.
2484
+ *
2485
+ * **No double-export by default, and never a replacement.** The bridge only ADDS a
2486
+ * CF-side span; the recorded {@link SpanEvent} handed to `record` (our
2487
+ * `SpanBuffer`/`otlpSink`) is byte-for-byte the same as without the bridge and
2488
+ * stays the source of truth. It is off unless explicitly enabled precisely
2489
+ * because, once on, a deployment that ALSO ships our `otlpSink` to a collector
2490
+ * AND lets CF export its trace tree emits the same logical span down two
2491
+ * pipelines — an intentional, documented trade the operator opts into, not a
2492
+ * default.
2493
+ *
2494
+ * **DO async-context caveat (EXPERIMENTAL, partially workerd-validated).**
2495
+ * `tracing.enterSpan` is now confirmed to EXIST and RUN inside a real Durable
2496
+ * Object under `@cloudflare/vitest-pool-workers` (see
2497
+ * `__tests__/workerd/context-telemetry-cf-bridge.workerd.test.ts`): it resolves
2498
+ * from `cloudflare:workers`, its callback executes and returns the body value
2499
+ * without throwing, `span.isTraced` is a real boolean, and — the key additive
2500
+ * guarantee — our recorded {@link SpanEvent} tree (parent/child via the threaded
2501
+ * `parentSpanId`) is byte-for-byte identical with the bridge on vs off. What that
2502
+ * harness CANNOT prove is CF's own *exported* parent-linking: with no trace head
2503
+ * attached the run is unsampled (`isTraced === false`), so CF records nothing and
2504
+ * its span tree is not introspectable. So `enterSpan`'s ambient-span parent-linking
2505
+ * inside a DO stays unverified upstream, and this remains capability-probed: an
2506
+ * absent/undefined `tracing`, a missing `enterSpan`, or an off-CF/unsampled run all
2507
+ * resolve to `undefined`/no-op — exact prior behavior. If CF's ambient-span linkage
2508
+ * misbehaves in a DO, the worst case is a mis-parented CF span; our own recorded
2509
+ * waterfall is unaffected.
2421
2510
  */
2422
2511
  declare const createTracer: (deps: TracerDeps) => ContextTracer;
2423
2512
  /**
@@ -4324,6 +4413,20 @@ declare const subscriptionListDeltas: (previousJson: string, nextResult: unknown
4324
4413
  * `@lunora/runtime`'s `ObservabilitySink` without taking a dependency on it.
4325
4414
  */
4326
4415
  interface TelemetrySink {
4416
+ /**
4417
+ * **Opt-in, EXPERIMENTAL, default off.** When `true`, each `ctx.trace` span is
4418
+ * ALSO emitted as a Cloudflare **custom span** (`tracing.enterSpan` from
4419
+ * `cloudflare:workers`, GA 2026-06-16) so it nests inside CF's native trace
4420
+ * tree on the hosted path — capability-probed, and a safe no-op off-CF / on an
4421
+ * older compat date / when unsampled. This only ADDS a CF-side span; the
4422
+ * `onSpan` event below (our `SpanBuffer`/`otlpSink`) is unchanged and stays the
4423
+ * source of truth. The bridge is now workerd-validated as available and
4424
+ * side-effect-free inside a real DO (our recorded spans stay intact); CF's
4425
+ * exported parent-linking under sampling is still unverified, so it stays
4426
+ * EXPERIMENTAL. Mirror of `@lunora/runtime`'s `ObservabilitySink`
4427
+ * `fuseCloudflareTraces`; see {@link createTracer} for the double-export caveat.
4428
+ */
4429
+ fuseCloudflareTraces?: boolean;
4327
4430
  onLog?: (event: LogEventInput, context?: LogSinkContext) => void;
4328
4431
  onMetric?: (event: MetricEvent, context?: LogSinkContext) => void;
4329
4432
  onSpan?: (event: SpanEvent, context?: LogSinkContext) => void;
@@ -5921,6 +6024,9 @@ declare abstract class ShardDO {
5921
6024
  * `anchor` is the trace this ctx's spans belong to; omit it for a ctx with no
5922
6025
  * owning dispatch (an alarm, a subscription re-run) to mint a fresh anchor, so
5923
6026
  * `ctx.trace` still yields a coherent self-contained trace there.
6027
+ *
6028
+ * The Cloudflare custom-spans bridge is threaded here but stays off unless the
6029
+ * resolved sink sets `fuseCloudflareTraces` (see {@link resolveCloudflareTracing}).
5924
6030
  */
5925
6031
  protected makeTracer(functionPath: string, sink?: TelemetrySink, anchor?: TraceAnchor): ContextTracer;
5926
6032
  /**
package/dist/index.d.ts CHANGED
@@ -2378,14 +2378,69 @@ interface TraceAnchor {
2378
2378
  rootSpanId: string;
2379
2379
  traceId: string;
2380
2380
  }
2381
+ /**
2382
+ * Minimal structural shape of one **Cloudflare custom span** — the object CF's
2383
+ * `tracing.enterSpan(name, (span) => …)` callback receives (GA 2026-06-16). Only
2384
+ * the surface the bridge touches is declared, so `@lunora/do` needs no runtime
2385
+ * dependency on `cloudflare:workers`; the real platform span is structurally
2386
+ * assignable.
2387
+ */
2388
+ interface CloudflareSpanLike {
2389
+ /**
2390
+ * Whether this span is actually being recorded by the runtime's sampler.
2391
+ * `false` off the traced path (unsampled) — the bridge skips its
2392
+ * `setAttribute` work in that case rather than building attribute strings for
2393
+ * a span nobody will read.
2394
+ */
2395
+ readonly isTraced: boolean;
2396
+ /** Attach one primitive attribute to the CF span. */
2397
+ setAttribute: (key: string, value: boolean | number | string | undefined) => void;
2398
+ }
2399
+ /**
2400
+ * Minimal structural shape of the `tracing` namespace exported by
2401
+ * `cloudflare:workers`. `enterSpan` opens a custom span that auto-nests under the
2402
+ * runtime's ambient span and ends when `callback` settles.
2403
+ */
2404
+ interface CloudflareTracingLike {
2405
+ enterSpan: <T>(name: string, callback: (span: CloudflareSpanLike) => T) => T;
2406
+ }
2407
+ /**
2408
+ * Resolves CF's `tracing` namespace, or `undefined` when it is unavailable —
2409
+ * off-Cloudflare, on a compat date predating custom spans, or when
2410
+ * `tracing.enterSpan` is not a function. **Injected, never imported here**, so the
2411
+ * tracer stays pure and unit-testable without `cloudflare:workers`; the shard
2412
+ * supplies the real resolver, tests a fake or `undefined`.
2413
+ */
2414
+ type CloudflareTracingResolver = () => CloudflareTracingLike | Promise<CloudflareTracingLike | undefined> | undefined;
2381
2415
  /** What {@link createTracer} needs from the shard to build a span. */
2382
2416
  interface TracerDeps {
2383
2417
  /** The trace this ctx's spans belong to. */
2384
2418
  anchor: TraceAnchor;
2385
2419
  /** Function path the spans are attributed to. */
2386
2420
  functionPath: string;
2421
+ /**
2422
+ * **Opt-in, EXPERIMENTAL, default off.** When `true` *and*
2423
+ * {@link TracerDeps.resolveCloudflareTracing} yields a working
2424
+ * `tracing.enterSpan`, each `ctx.trace` span is ALSO emitted as a Cloudflare
2425
+ * **custom span**, so it nests inside CF's native binding/fetch/handler trace
2426
+ * tree on the hosted path. This only ADDS a CF-side span — the recorded
2427
+ * {@link SpanEvent} (our `SpanBuffer`/`otlpSink`) is untouched and remains the
2428
+ * source of truth plus the local studio waterfall. The `enterSpan` call itself
2429
+ * is now workerd-validated as available and side-effect-free inside a Durable
2430
+ * Object; CF's exported parent-linking under sampling remains unverified. See
2431
+ * {@link createTracer} for the double-export and Durable-Object async-context
2432
+ * caveats.
2433
+ */
2434
+ fuseCloudflareSpans?: boolean;
2387
2435
  /** Hand a finished span to the buffer + sink. */
2388
2436
  record: (span: SpanEvent) => void;
2437
+ /**
2438
+ * Injected resolver for CF's `tracing` namespace (see
2439
+ * {@link CloudflareTracingResolver}). Only consulted when
2440
+ * {@link TracerDeps.fuseCloudflareSpans} is `true`, so the default path never
2441
+ * calls it.
2442
+ */
2443
+ resolveCloudflareTracing?: CloudflareTracingResolver;
2389
2444
  /** Shard key for single-shard calls; absent for the unnamed root DO. */
2390
2445
  shardKey: string | undefined;
2391
2446
  /** Read lazily — the acting user is resolved per span, not per ctx. */
@@ -2418,6 +2473,40 @@ interface MetricsDeps {
2418
2473
  * cleared in the dispatch `finally`, and a subscription re-run builds its ctx
2419
2474
  * during* the writing mutation's flush — so reading that shared field at span
2420
2475
  * time would file the re-run's spans under the mutation's trace.
2476
+ *
2477
+ * **Cloudflare custom-spans bridge (opt-in, EXPERIMENTAL).** When
2478
+ * `deps.fuseCloudflareSpans` is `true` and `deps.resolveCloudflareTracing` yields
2479
+ * a working `tracing.enterSpan` (`cloudflare:workers`, GA 2026-06-16), each span
2480
+ * body runs inside a CF custom span so our span nests under CF's native
2481
+ * binding/fetch/handler trace tree on the hosted path, and the finished span's
2482
+ * key attributes are mirrored onto it (gated on `span.isTraced`). Two deliberate
2483
+ * boundaries hold.
2484
+ *
2485
+ * **No double-export by default, and never a replacement.** The bridge only ADDS a
2486
+ * CF-side span; the recorded {@link SpanEvent} handed to `record` (our
2487
+ * `SpanBuffer`/`otlpSink`) is byte-for-byte the same as without the bridge and
2488
+ * stays the source of truth. It is off unless explicitly enabled precisely
2489
+ * because, once on, a deployment that ALSO ships our `otlpSink` to a collector
2490
+ * AND lets CF export its trace tree emits the same logical span down two
2491
+ * pipelines — an intentional, documented trade the operator opts into, not a
2492
+ * default.
2493
+ *
2494
+ * **DO async-context caveat (EXPERIMENTAL, partially workerd-validated).**
2495
+ * `tracing.enterSpan` is now confirmed to EXIST and RUN inside a real Durable
2496
+ * Object under `@cloudflare/vitest-pool-workers` (see
2497
+ * `__tests__/workerd/context-telemetry-cf-bridge.workerd.test.ts`): it resolves
2498
+ * from `cloudflare:workers`, its callback executes and returns the body value
2499
+ * without throwing, `span.isTraced` is a real boolean, and — the key additive
2500
+ * guarantee — our recorded {@link SpanEvent} tree (parent/child via the threaded
2501
+ * `parentSpanId`) is byte-for-byte identical with the bridge on vs off. What that
2502
+ * harness CANNOT prove is CF's own *exported* parent-linking: with no trace head
2503
+ * attached the run is unsampled (`isTraced === false`), so CF records nothing and
2504
+ * its span tree is not introspectable. So `enterSpan`'s ambient-span parent-linking
2505
+ * inside a DO stays unverified upstream, and this remains capability-probed: an
2506
+ * absent/undefined `tracing`, a missing `enterSpan`, or an off-CF/unsampled run all
2507
+ * resolve to `undefined`/no-op — exact prior behavior. If CF's ambient-span linkage
2508
+ * misbehaves in a DO, the worst case is a mis-parented CF span; our own recorded
2509
+ * waterfall is unaffected.
2421
2510
  */
2422
2511
  declare const createTracer: (deps: TracerDeps) => ContextTracer;
2423
2512
  /**
@@ -4324,6 +4413,20 @@ declare const subscriptionListDeltas: (previousJson: string, nextResult: unknown
4324
4413
  * `@lunora/runtime`'s `ObservabilitySink` without taking a dependency on it.
4325
4414
  */
4326
4415
  interface TelemetrySink {
4416
+ /**
4417
+ * **Opt-in, EXPERIMENTAL, default off.** When `true`, each `ctx.trace` span is
4418
+ * ALSO emitted as a Cloudflare **custom span** (`tracing.enterSpan` from
4419
+ * `cloudflare:workers`, GA 2026-06-16) so it nests inside CF's native trace
4420
+ * tree on the hosted path — capability-probed, and a safe no-op off-CF / on an
4421
+ * older compat date / when unsampled. This only ADDS a CF-side span; the
4422
+ * `onSpan` event below (our `SpanBuffer`/`otlpSink`) is unchanged and stays the
4423
+ * source of truth. The bridge is now workerd-validated as available and
4424
+ * side-effect-free inside a real DO (our recorded spans stay intact); CF's
4425
+ * exported parent-linking under sampling is still unverified, so it stays
4426
+ * EXPERIMENTAL. Mirror of `@lunora/runtime`'s `ObservabilitySink`
4427
+ * `fuseCloudflareTraces`; see {@link createTracer} for the double-export caveat.
4428
+ */
4429
+ fuseCloudflareTraces?: boolean;
4327
4430
  onLog?: (event: LogEventInput, context?: LogSinkContext) => void;
4328
4431
  onMetric?: (event: MetricEvent, context?: LogSinkContext) => void;
4329
4432
  onSpan?: (event: SpanEvent, context?: LogSinkContext) => void;
@@ -5921,6 +6024,9 @@ declare abstract class ShardDO {
5921
6024
  * `anchor` is the trace this ctx's spans belong to; omit it for a ctx with no
5922
6025
  * owning dispatch (an alarm, a subscription re-run) to mint a fresh anchor, so
5923
6026
  * `ctx.trace` still yields a coherent self-contained trace there.
6027
+ *
6028
+ * The Cloudflare custom-spans bridge is threaded here but stays off unless the
6029
+ * resolved sink sets `fuseCloudflareTraces` (see {@link resolveCloudflareTracing}).
5924
6030
  */
5925
6031
  protected makeTracer(functionPath: string, sink?: TelemetrySink, anchor?: TraceAnchor): ContextTracer;
5926
6032
  /**
package/dist/index.mjs CHANGED
@@ -3,7 +3,7 @@ export { AGGREGATE_SQL_FUNCTION, aggregateSqlFunction, matchesStaticWhere, norma
3
3
  export { aggregateTableName, coerceAggregateNumber, encodeAggregateKey, foldAggregateTally, readAggregateValue } from './packem_shared/aggregateTableName-CxNqY1Sl.mjs';
4
4
  export { CountRlsUnsupportedError, mergeWhere, planAggregateLookup, selectIndexForAggregate, selectIndexForCount, selectIndexForGroupBy } from './packem_shared/CountRlsUnsupportedError-BGxj0pgS.mjs';
5
5
  export { AUTH_METRICS_BUCKETS_TABLE, AUTH_METRICS_BUCKET_MS, AUTH_METRICS_BUCKET_RETENTION, AUTH_METRICS_TABLE, ensureAuthMetricsTables, readAuthMetrics, recordAuthEvent } from './packem_shared/AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
6
- export { c as createMetrics, a as createTracer, d as dispatchRootSpan } from './packem_shared/context-telemetry-BVSDl6PU.mjs';
6
+ export { c as createMetrics, a as createTracer, d as dispatchRootSpan } from './packem_shared/context-telemetry-CDpyil58.mjs';
7
7
  export { NotUniqueError, assertValidClientId, createShardCtxDb, normalizeIdStructurally } from './packem_shared/NotUniqueError-DbtlWwcG.mjs';
8
8
  export { DATA_MIGRATION_STATE_TABLE, readMigrationStatus, runDataMigration } from './packem_shared/DATA_MIGRATION_STATE_TABLE-CYwBpyTr.mjs';
9
9
  export { SCAN_DEP, createDependencyTracker, depKey } from './packem_shared/SCAN_DEP-DLJF8dsj.mjs';
@@ -28,7 +28,7 @@ export { RLS_UNWRAP_SYMBOL, RlsRequiredError, guardWriter } from './packem_share
28
28
  export { buildFtsMatch, ftsTableName, scoreDocument, stringifySearchText, tokenizeSearch } from './packem_shared/buildFtsMatch-BLEMawrp.mjs';
29
29
  export { M as MIN_ADMIN_TOKEN_LENGTH, a as MIN_AUTH_SECRET_LENGTH, b as buildSecurityAudit } from './packem_shared/security-audit-CucgBice.mjs';
30
30
  export { SESSION_DO_TTL_DEFAULT, SessionDO } from './packem_shared/SESSION_DO_TTL_DEFAULT-BnSKgVO4.mjs';
31
- export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-COEnnBhO.mjs';
31
+ export { ROOT_DO_SIZE_WARN_BYTES, ROOT_SHARD_NAME, ShardDO } from './packem_shared/ROOT_DO_SIZE_WARN_BYTES-B_zt4RQE.mjs';
32
32
  export { SHARD_REGISTRY_DO_NAME, ShardRegistryDO } from './packem_shared/SHARD_REGISTRY_DO_NAME-D99roc-r.mjs';
33
33
  export { MAX_SQL_ROWS, assertReadonly, runReadonlySql } from './packem_shared/MAX_SQL_ROWS-iFAA8FbD.mjs';
34
34
  export { createSystemReader } from './packem_shared/createSystemReader-D12eNH13.mjs';
@@ -2,7 +2,7 @@ import { LunoraError, toErrorBody } from '@lunora/errors';
2
2
  import { drizzle } from 'drizzle-orm/durable-sqlite';
3
3
  import { c as constantTimeEqual } from './constant-time-equal-BVRWZgES.mjs';
4
4
  import { j as jsonResponse } from './json-response-BdbtpOhm.mjs';
5
- import { n as normalizeLogFields, r as resolveTraceAnchor, p as parseTraceparent, a as createTracer, c as createMetrics, d as dispatchRootSpan } from './context-telemetry-BVSDl6PU.mjs';
5
+ import { n as normalizeLogFields, r as resolveTraceAnchor, p as parseTraceparent, a as createTracer, c as createMetrics, d as dispatchRootSpan } from './context-telemetry-CDpyil58.mjs';
6
6
  import { e as encodeWire, d as decodeWire } from './wire-codec-CzQc1pvf.mjs';
7
7
  import { parseExportShardArgs, parseImportShardArgs } from './exportShardRows-Dy3oFZ26.mjs';
8
8
  import { recordAuthEvent, readAuthMetrics } from './AUTH_METRICS_BUCKETS_TABLE-CiHHYeJi.mjs';
@@ -356,7 +356,11 @@ const runSql$3 = (sql, query, ...parameters) => {
356
356
  return runner.call(sql, query, ...parameters);
357
357
  };
358
358
  const bucketFloor = (ts) => Math.floor(ts / METRIC_HISTORY_BUCKET_MS) * METRIC_HISTORY_BUCKET_MS;
359
+ const ensuredHandles = /* @__PURE__ */ new WeakSet();
359
360
  const ensureMetricHistoryTable = (sql) => {
361
+ if (ensuredHandles.has(sql)) {
362
+ return;
363
+ }
360
364
  runSql$3(
361
365
  sql,
362
366
  `CREATE TABLE IF NOT EXISTS "${METRIC_HISTORY_TABLE}" (
@@ -377,12 +381,25 @@ const ensureMetricHistoryTable = (sql) => {
377
381
  PRIMARY KEY (series_key, bucket_ms)
378
382
  )`
379
383
  );
384
+ ensuredHandles.add(sql);
385
+ };
386
+ const KNOWN_BUCKETS_CAP = 4096;
387
+ const knownBuckets = /* @__PURE__ */ new WeakMap();
388
+ const knownBucketsFor = (sql) => {
389
+ let set = knownBuckets.get(sql);
390
+ if (set === void 0) {
391
+ set = /* @__PURE__ */ new Set();
392
+ knownBuckets.set(sql, set);
393
+ }
394
+ return set;
380
395
  };
381
396
  const recordMetricHistory = (sql, event, exemplarTraceId) => {
382
397
  ensureMetricHistoryTable(sql);
383
398
  const key = metricSeriesKey(event);
384
399
  const bucket = bucketFloor(event.ts);
385
- const bucketExists = runSql$3(sql, `SELECT 1 AS c FROM "${METRIC_HISTORY_TABLE}" WHERE series_key = ? AND bucket_ms = ? LIMIT 1`, key, bucket).toArray().length > 0;
400
+ const cache = knownBucketsFor(sql);
401
+ const cacheKey = `${key}\0${bucket.toString()}`;
402
+ const bucketExists = cache.has(cacheKey) || runSql$3(sql, `SELECT 1 AS c FROM "${METRIC_HISTORY_TABLE}" WHERE series_key = ? AND bucket_ms = ? LIMIT 1`, key, bucket).toArray().length > 0;
386
403
  if (!bucketExists) {
387
404
  const seriesTracked = runSql$3(sql, `SELECT 1 AS c FROM "${METRIC_HISTORY_TABLE}" WHERE series_key = ? LIMIT 1`, key).toArray().length > 0;
388
405
  if (!seriesTracked) {
@@ -434,6 +451,12 @@ const recordMetricHistory = (sql, event, exemplarTraceId) => {
434
451
  key
435
452
  );
436
453
  }
454
+ if (bucketExists && !cache.has(cacheKey)) {
455
+ if (cache.size >= KNOWN_BUCKETS_CAP) {
456
+ cache.clear();
457
+ }
458
+ cache.add(cacheKey);
459
+ }
437
460
  };
438
461
  const parseAttributes = (raw) => {
439
462
  if (raw === "" || raw === "{}") {
@@ -1968,6 +1991,21 @@ const findDanglingReferences = (sql, storageColumns, liveKeys) => {
1968
1991
  const WS_KEEPALIVE_PING = "lunora-ping";
1969
1992
  const WS_KEEPALIVE_PONG = "lunora-pong";
1970
1993
  const REQUIRE_EPHEMERAL_ENV_VALUES = /* @__PURE__ */ new Set(["1", "enabled", "on", "true", "yes"]);
1994
+ let cloudflareTracingResolved = false;
1995
+ let cloudflareTracing;
1996
+ const resolveCloudflareTracing = async () => {
1997
+ if (!cloudflareTracingResolved) {
1998
+ cloudflareTracingResolved = true;
1999
+ try {
2000
+ const cloudflareModule = await import('cloudflare:workers');
2001
+ const candidate = cloudflareModule.tracing;
2002
+ cloudflareTracing = candidate !== null && typeof candidate === "object" && typeof candidate.enterSpan === "function" ? candidate : void 0;
2003
+ } catch {
2004
+ cloudflareTracing = void 0;
2005
+ }
2006
+ }
2007
+ return cloudflareTracing;
2008
+ };
1971
2009
  const UNDELIVERED_BASELINE = "<undelivered>";
1972
2010
  const ROOT_DO_SIZE_WARN_BYTES = 1073741824;
1973
2011
  const CDC_RESUME_SCAN_LIMIT = 1e4;
@@ -4786,14 +4824,19 @@ class ShardDO {
4786
4824
  * `anchor` is the trace this ctx's spans belong to; omit it for a ctx with no
4787
4825
  * owning dispatch (an alarm, a subscription re-run) to mint a fresh anchor, so
4788
4826
  * `ctx.trace` still yields a coherent self-contained trace there.
4827
+ *
4828
+ * The Cloudflare custom-spans bridge is threaded here but stays off unless the
4829
+ * resolved sink sets `fuseCloudflareTraces` (see {@link resolveCloudflareTracing}).
4789
4830
  */
4790
4831
  makeTracer(functionPath, sink, anchor) {
4791
4832
  return createTracer({
4792
4833
  anchor: anchor ?? resolveTraceAnchor(void 0),
4834
+ fuseCloudflareSpans: sink?.fuseCloudflareTraces === true,
4793
4835
  functionPath,
4794
4836
  record: (span) => {
4795
4837
  this.recordSpan(span, sink);
4796
4838
  },
4839
+ resolveCloudflareTracing,
4797
4840
  shardKey: this.state.id?.name,
4798
4841
  userId: () => this.getCurrentUserId()
4799
4842
  });
@@ -63,8 +63,31 @@ const toErrorType = (error) => {
63
63
  return error instanceof Error ? error.constructor.name : "Error";
64
64
  };
65
65
 
66
+ const applyCloudflareSpanAttributes = (span, meta) => {
67
+ if (!span.isTraced) {
68
+ return;
69
+ }
70
+ span.setAttribute("lunora.function_path", meta.functionPath);
71
+ span.setAttribute("lunora.ok", meta.ok);
72
+ span.setAttribute("lunora.duration_ms", meta.durationMs);
73
+ if (meta.shardKey !== void 0) {
74
+ span.setAttribute("lunora.shard_key", meta.shardKey);
75
+ }
76
+ if (meta.userId !== void 0) {
77
+ span.setAttribute("lunora.user_id", meta.userId);
78
+ }
79
+ if (meta.error !== void 0) {
80
+ span.setAttribute("lunora.error.type", meta.error.type);
81
+ span.setAttribute("lunora.error.message", meta.error.message);
82
+ }
83
+ for (const [key, value] of Object.entries(meta.attributes)) {
84
+ if (typeof value === "boolean" || typeof value === "number" || typeof value === "string") {
85
+ span.setAttribute(`lunora.attr.${key}`, value);
86
+ }
87
+ }
88
+ };
66
89
  const createTracer = (deps) => {
67
- const { anchor, functionPath, record, shardKey, userId } = deps;
90
+ const { anchor, fuseCloudflareSpans, functionPath, record, resolveCloudflareTracing, shardKey, userId } = deps;
68
91
  const tracerFor = (parentSpanId) => async (name, function_, attributes) => {
69
92
  const spanId = otlpRandomHex(8);
70
93
  const startTs = Date.now();
@@ -78,39 +101,64 @@ const createTracer = (deps) => {
78
101
  Object.assign(collected, normalizeLogFields(fields));
79
102
  }
80
103
  };
81
- let ok = true;
82
- let error;
83
- try {
84
- return await function_(tracerFor(spanId), spanHandle);
85
- } catch (error_) {
86
- ok = false;
87
- error = {
88
- message: error_ instanceof Error ? error_.message : String(error_),
89
- // Prefer a LunoraError's stable `code` over the class name so
90
- // spans group by the same taxonomy the RPC spans use.
91
- type: toErrorType(error_)
92
- };
93
- throw error_;
94
- } finally {
104
+ const runRecorded = async (cfSpan) => {
105
+ let ok = true;
106
+ let error;
95
107
  try {
108
+ return await function_(tracerFor(spanId), spanHandle);
109
+ } catch (error_) {
110
+ ok = false;
111
+ error = {
112
+ message: error_ instanceof Error ? error_.message : String(error_),
113
+ // Prefer a LunoraError's stable `code` over the class name
114
+ // so spans group by the same taxonomy the RPC spans use.
115
+ type: toErrorType(error_)
116
+ };
117
+ throw error_;
118
+ } finally {
119
+ const durationMs = Date.now() - startTs;
120
+ const resolvedUserId = userId();
96
121
  const merged = { ...normalized, ...collected };
97
- record({
98
- ...Object.keys(merged).length === 0 ? {} : { attributes: merged },
99
- durationMs: Date.now() - startTs,
100
- ...error === void 0 ? {} : { error },
101
- functionPath,
102
- name,
103
- ok,
104
- parentSpanId,
105
- shardKey,
106
- spanId,
107
- startTs,
108
- traceId: anchor.traceId,
109
- userId: userId()
110
- });
111
- } catch {
122
+ try {
123
+ record({
124
+ ...Object.keys(merged).length === 0 ? {} : { attributes: merged },
125
+ durationMs,
126
+ ...error === void 0 ? {} : { error },
127
+ functionPath,
128
+ name,
129
+ ok,
130
+ parentSpanId,
131
+ shardKey,
132
+ spanId,
133
+ startTs,
134
+ traceId: anchor.traceId,
135
+ userId: resolvedUserId
136
+ });
137
+ } catch {
138
+ }
139
+ if (cfSpan !== void 0) {
140
+ try {
141
+ applyCloudflareSpanAttributes(cfSpan, {
142
+ attributes: merged,
143
+ durationMs,
144
+ error,
145
+ functionPath,
146
+ ok,
147
+ shardKey,
148
+ userId: resolvedUserId
149
+ });
150
+ } catch {
151
+ }
152
+ }
153
+ }
154
+ };
155
+ if (fuseCloudflareSpans === true && resolveCloudflareTracing !== void 0) {
156
+ const cfTracing = await resolveCloudflareTracing();
157
+ if (cfTracing !== void 0 && typeof cfTracing.enterSpan === "function") {
158
+ return await cfTracing.enterSpan(name, (span) => runRecorded(span));
112
159
  }
113
160
  }
161
+ return await runRecorded();
114
162
  };
115
163
  return tracerFor(anchor.rootSpanId);
116
164
  };
@@ -172,4 +220,4 @@ const dispatchRootSpan = (input) => {
172
220
  };
173
221
  };
174
222
 
175
- export { createTracer as a, createMetrics as c, dispatchRootSpan as d, normalizeLogFields as n, parseTraceparent as p, resolveTraceAnchor as r };
223
+ export { createTracer as a, applyCloudflareSpanAttributes as b, createMetrics as c, dispatchRootSpan as d, normalizeLogFields as n, parseTraceparent as p, resolveTraceAnchor as r };
@@ -0,0 +1 @@
1
+ export { b as applyCloudflareSpanAttributes, c as createMetrics, a as createTracer, d as dispatchRootSpan } from './context-telemetry-CDpyil58.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/do",
3
- "version": "1.0.0-alpha.40",
3
+ "version": "1.0.0-alpha.42",
4
4
  "description": "Lunora Durable Objects: ShardDO (SQLite, OCC, hibernated WebSocket subscriptions) and SessionDO",
5
5
  "keywords": [
6
6
  "cloudflare",
@@ -1 +0,0 @@
1
- export { c as createMetrics, a as createTracer, d as dispatchRootSpan } from './context-telemetry-BVSDl6PU.mjs';