@senzops/apm-node 1.2.6 → 1.2.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@senzops/apm-node",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
4
4
  "description": "Universal APM SDK for Senzor",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -5,17 +5,41 @@ interface IStorage<T> {
5
5
  getStore(): T | undefined;
6
6
  }
7
7
 
8
+ /**
9
+ * Async-safe fallback when AsyncLocalStorage is unavailable.
10
+ *
11
+ * Not concurrency-safe across truly parallel requests in a single isolate,
12
+ * but correct for sequential and async/await patterns (e.g. Cloudflare Workers
13
+ * where each request gets its own execution context).
14
+ */
8
15
  class NaiveStorage<T> implements IStorage<T> {
9
16
  private store: T | undefined;
10
17
 
11
18
  run<R>(store: T, callback: (...args: any[]) => R, ...args: any[]): R {
12
19
  const prev = this.store;
13
20
  this.store = store;
21
+
22
+ let result: R;
14
23
  try {
15
- return callback(...args);
16
- } finally {
24
+ result = callback(...args);
25
+ } catch (err) {
17
26
  this.store = prev;
27
+ throw err;
28
+ }
29
+
30
+ // If the callback returned a thenable (async handler), defer the restore
31
+ // until the promise settles so Context.current() works across awaits.
32
+ if (result != null && typeof (result as any).then === 'function') {
33
+ const promise = (result as any).then(
34
+ (val: any) => { this.store = prev; return val; },
35
+ (err: any) => { this.store = prev; throw err; }
36
+ );
37
+ return promise as R;
18
38
  }
39
+
40
+ // Sync callback — restore immediately.
41
+ this.store = prev;
42
+ return result;
19
43
  }
20
44
 
21
45
  getStore(): T | undefined {
@@ -23,29 +47,67 @@ class NaiveStorage<T> implements IStorage<T> {
23
47
  }
24
48
  }
25
49
 
26
- const resolveStorage = <T>(): IStorage<T> => {
50
+ /**
51
+ * Resolve the best available async context storage.
52
+ *
53
+ * Uses lazy re-resolution: if the initial attempt (at module evaluation time)
54
+ * falls back to NaiveStorage, subsequent calls to resolveStorage() will retry.
55
+ * This handles runtimes where AsyncLocalStorage becomes available after module
56
+ * init (e.g. some Workers configurations).
57
+ */
58
+ const tryResolveALS = <T>(): IStorage<T> | null => {
59
+ // 1. Check globalThis (Cloudflare Workers nodejs_compat_v2, Bun, Deno)
27
60
  if (typeof globalThis !== 'undefined' && (globalThis as any).AsyncLocalStorage) {
28
61
  return new (globalThis as any).AsyncLocalStorage();
29
62
  }
30
63
 
64
+ // 2. Node.js CJS require
31
65
  try {
32
66
  if (typeof require !== 'undefined') {
33
- const { AsyncLocalStorage } = require('node:async_hooks');
34
- if (AsyncLocalStorage) return new AsyncLocalStorage();
67
+ const mod = require('node:async_hooks');
68
+ if (mod?.AsyncLocalStorage) return new mod.AsyncLocalStorage();
35
69
  }
36
70
  } catch {}
37
71
 
38
72
  try {
39
73
  if (typeof require !== 'undefined') {
40
- const { AsyncLocalStorage } = require('async_hooks');
41
- if (AsyncLocalStorage) return new AsyncLocalStorage();
74
+ const mod = require('async_hooks');
75
+ if (mod?.AsyncLocalStorage) return new mod.AsyncLocalStorage();
42
76
  }
43
77
  } catch {}
44
78
 
45
- return new NaiveStorage<T>();
79
+ return null;
46
80
  };
47
81
 
48
- export const storage = resolveStorage<ActiveTrace>();
82
+ class LazyStorage<T> implements IStorage<T> {
83
+ private inner: IStorage<T>;
84
+ private resolved = false;
85
+
86
+ constructor() {
87
+ this.inner = tryResolveALS<T>() || new NaiveStorage<T>();
88
+ this.resolved = !(this.inner instanceof NaiveStorage);
89
+ }
90
+
91
+ private ensureResolved() {
92
+ if (this.resolved) return;
93
+ const als = tryResolveALS<T>();
94
+ if (als) {
95
+ this.inner = als;
96
+ this.resolved = true;
97
+ }
98
+ }
99
+
100
+ run<R>(store: T, callback: (...args: any[]) => R, ...args: any[]): R {
101
+ this.ensureResolved();
102
+ return this.inner.run(store, callback, ...args);
103
+ }
104
+
105
+ getStore(): T | undefined {
106
+ return this.inner.getStore();
107
+ }
108
+ }
109
+
110
+ export const storage = new LazyStorage<ActiveTrace>();
49
111
 
50
112
  export const Context = {
51
113
  run: <T>(trace: ActiveTrace, fn: () => T): T => {
@@ -22,7 +22,8 @@ export class Transport {
22
22
  private taskErrorQueue: SenzorError[] = [];
23
23
  private taskLogQueue: SenzorLog[] = [];
24
24
 
25
- private timer: NodeJS.Timeout | null = null;
25
+ private timer: ReturnType<typeof setInterval> | null = null;
26
+ private timerStarted = false;
26
27
  private apmEndpoint: string;
27
28
  private taskEndpoint: string;
28
29
  private isFlushing = false;
@@ -38,15 +39,26 @@ export class Transport {
38
39
  ? baseEndpoint.replace('/apm', '/task')
39
40
  : `${baseEndpoint}/api/ingest/task`;
40
41
 
41
- if (typeof setInterval !== 'undefined') {
42
- this.timer = setInterval(
43
- () => void this.flush(),
44
- config.flushInterval || 10000
45
- );
46
- if (this.timer && typeof this.timer.unref === 'function') {
47
- this.timer.unref();
42
+ // Timer and shutdown flush are deferred to first enqueue.
43
+ // Cloudflare Workers forbids setInterval / process access in global scope,
44
+ // and init() may be called at module evaluation time (e.g. Nitro plugins).
45
+ }
46
+
47
+ private ensureTimer() {
48
+ if (this.timerStarted) return;
49
+ this.timerStarted = true;
50
+
51
+ try {
52
+ if (typeof setInterval !== 'undefined') {
53
+ this.timer = setInterval(
54
+ () => void this.flush(),
55
+ this.config.flushInterval || 10000
56
+ );
57
+ if (this.timer && typeof (this.timer as any).unref === 'function') {
58
+ (this.timer as any).unref();
59
+ }
48
60
  }
49
- }
61
+ } catch {}
50
62
 
51
63
  this.installShutdownFlush();
52
64
  }
@@ -78,6 +90,7 @@ export class Transport {
78
90
  }
79
91
 
80
92
  private enqueue<T>(queue: T[], item: T) {
93
+ this.ensureTimer();
81
94
  queue.push(item);
82
95
 
83
96
  const maxQueueSize = this.config.maxQueueSize ?? 10000;