@signap/signals-angular 2.2.0-dev.20260930000246.gd4b7f02 → 2.2.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/README.md CHANGED
@@ -71,7 +71,8 @@ round-trip — no separate ack/lookup transition.
71
71
  | field | type | when populated |
72
72
  |-------------|------------------------------------------|----------------------------------------|
73
73
  | `data` | `Signal<IdentifyResult \| undefined>` | once `agent.identify()` resolves |
74
- | `isLoading` | `Signal<boolean>` | true between call time and `data` |
74
+ | `isLoading` | `Signal<boolean>` | true between call time and `data`; false while held |
75
+ | `isHeld` | `Signal<boolean>` | true while `enabled()` is `false` |
75
76
  | `error` | `Signal<Error \| null>` | last failure (load / identify) |
76
77
  | `refetch` | `() => Promise<void>` | re-run `agent.identify()` |
77
78
 
@@ -82,6 +83,19 @@ SSR-safe: `provideSignals()` gates `SignalsAgent.load()` behind
82
83
  `isPlatformBrowser(PLATFORM_ID)`, so it never runs during Angular Universal /
83
84
  prerender server rendering.
84
85
 
86
+ ## Holding collection
87
+
88
+ ```ts
89
+ const enabled = signal(false);
90
+ provideSignals({ apiKey: "pk_live_...", endpoint: "https://ingest.example" }, { enabled });
91
+ ```
92
+
93
+ While `enabled()` is `false` the client does not load the agent: the SDK registers no event listener, runs no collector and sends no request, and `injectVisitorData()` returns `isHeld() === true`. Setting it to `true` loads the agent and runs the first identify, exactly as if `enabled` had never been set. Setting it back to `false` stops the SDK's interaction listeners for the whole page and discards any identify result still in flight; a request that has already been sent is not recalled. Without the second argument, nothing is held. `SignalsClient` also exposes the same two transitions as `release()` and `hold()`.
94
+
95
+ Holding or releasing does not delete anything Signap has already received.
96
+
97
+ The hold applies to the agent this client loads. A copy of the SDK loaded another way, for example the CDN script, is not held.
98
+
85
99
  ## Endpoint (required)
86
100
 
87
101
  `provideSignals()` takes the underlying `AgentLoadOptions`. `endpoint` is
package/dist/index.cjs CHANGED
@@ -36,34 +36,109 @@ var import_signals_web = require("@signap/signals-web");
36
36
  var SignalsClient = class {
37
37
  agentState = (0, import_core.signal)(null);
38
38
  loadErrorState = (0, import_core.signal)(null);
39
+ heldState;
39
40
  /** Resolved agent, or `null` until `SignalsAgent.load()` settles. */
40
41
  agent = this.agentState.asReadonly();
41
42
  /** Non-null if `SignalsAgent.load()` rejected. */
42
43
  loadError = this.loadErrorState.asReadonly();
44
+ /** True while held (`provideSignals(opts, { enabled })` with `enabled()` false): nothing is loaded. */
45
+ held;
43
46
  /**
44
- * Resolves once `load()` has settled (agent set OR loadError set). Lets
45
- * `injectVisitorData()` await readiness without an Angular `effect`, so the
46
- * auto-identify fires exactly once regardless of change-detection timing.
47
+ * Resolves once the FIRST `load()` has settled (agent set OR loadError set), or
48
+ * at once outside a browser. While the client is held before its first release,
49
+ * it stays pending.
47
50
  */
48
51
  ready;
52
+ opts;
53
+ browser;
54
+ resolveReady = () => {
55
+ };
56
+ started = false;
57
+ loaded = null;
58
+ // Bumped on every withdrawal; a load() that settles under an older generation is discarded.
59
+ generation = 0;
60
+ listeners = /* @__PURE__ */ new Set();
49
61
  /**
50
- * @param opts forwarded verbatim to `SignalsAgent.load()`.
51
- * @param browser whether we're running in a browser; `load()` only fires
52
- * client-side so SSR / prerender never touches
53
- * window/document/WebCrypto (the Angular analog of the Vue
54
- * plugin's `typeof window` guard).
62
+ * @param opts forwarded verbatim to `SignalsAgent.load()`.
63
+ * @param browser whether we're running in a browser; `load()` only fires
64
+ * client-side so SSR / prerender never touches
65
+ * window/document/WebCrypto (the Angular analog of the Vue
66
+ * plugin's `typeof window` guard).
67
+ * @param initiallyHeld start held (issue 2894): do not load until `release()`.
55
68
  */
56
- constructor(opts, browser) {
69
+ constructor(opts, browser, initiallyHeld = false) {
70
+ this.opts = opts;
71
+ this.browser = browser;
72
+ this.heldState = (0, import_core.signal)(initiallyHeld);
73
+ this.held = this.heldState.asReadonly();
74
+ this.ready = new Promise((resolve) => {
75
+ this.resolveReady = resolve;
76
+ });
57
77
  if (!browser) {
58
- this.ready = Promise.resolve();
78
+ this.resolveReady();
59
79
  return;
60
80
  }
61
- this.ready = import_signals_web.SignalsAgent.load(opts).then((loaded) => {
81
+ if (!initiallyHeld) this.release();
82
+ }
83
+ /**
84
+ * Leave the held state: load the agent once (a no-op if already released). What
85
+ * `provideSignals()` calls when its `enabled` signal becomes true.
86
+ */
87
+ release() {
88
+ this.heldState.set(false);
89
+ if (!this.browser || this.started) return;
90
+ this.started = true;
91
+ const mine = this.generation;
92
+ import_signals_web.SignalsAgent.load(this.opts).then((loaded) => {
93
+ if (mine !== this.generation) {
94
+ if (!this.started) loaded.stopObservers();
95
+ return;
96
+ }
97
+ this.loaded = loaded;
62
98
  this.agentState.set(loaded);
99
+ this.settled();
63
100
  }).catch((err) => {
101
+ if (mine !== this.generation) return;
64
102
  this.loadErrorState.set(err instanceof Error ? err : new Error(String(err)));
103
+ this.settled();
65
104
  });
66
105
  }
106
+ /**
107
+ * Enter the held state. After a release this stops the SDK's interaction
108
+ * listeners (page-wide, for this copy of the SDK), drops the agent and discards
109
+ * a load still in flight; a request already sent is not recalled, and nothing
110
+ * already received is deleted. Before any release it only marks the client held.
111
+ */
112
+ hold() {
113
+ this.heldState.set(true);
114
+ if (!this.started) {
115
+ this.notify();
116
+ return;
117
+ }
118
+ this.started = false;
119
+ this.generation++;
120
+ this.loaded?.stopObservers();
121
+ this.loaded = null;
122
+ this.agentState.set(null);
123
+ this.loadErrorState.set(null);
124
+ this.notify();
125
+ }
126
+ /**
127
+ * @internal Called after every state change (a load settling, a hold). Lets
128
+ * `injectVisitorData()` re-run without an Angular `effect`, so the auto-identify
129
+ * fires exactly once per release regardless of change-detection timing.
130
+ */
131
+ onChange(listener) {
132
+ this.listeners.add(listener);
133
+ return () => this.listeners.delete(listener);
134
+ }
135
+ settled() {
136
+ this.resolveReady();
137
+ this.notify();
138
+ }
139
+ notify() {
140
+ for (const l of [...this.listeners]) l();
141
+ }
67
142
  };
68
143
 
69
144
  // src/provide-signals.ts
@@ -75,11 +150,21 @@ var import_core2 = require("@angular/core");
75
150
  var SIGNALS_CLIENT = new import_core2.InjectionToken("signap.signals.client");
76
151
 
77
152
  // src/provide-signals.ts
78
- function provideSignals(opts) {
153
+ function provideSignals(opts, config = {}) {
79
154
  return (0, import_core3.makeEnvironmentProviders)([
80
155
  {
81
156
  provide: SIGNALS_CLIENT,
82
- useFactory: () => new SignalsClient(opts, (0, import_common.isPlatformBrowser)((0, import_core3.inject)(import_core3.PLATFORM_ID)))
157
+ useFactory: () => {
158
+ const browser = (0, import_common.isPlatformBrowser)((0, import_core3.inject)(import_core3.PLATFORM_ID));
159
+ const { enabled } = config;
160
+ if (!enabled) return new SignalsClient(opts, browser);
161
+ const client = new SignalsClient(opts, browser, !(0, import_core3.untracked)(enabled));
162
+ (0, import_core3.effect)(() => {
163
+ const on = enabled();
164
+ (0, import_core3.untracked)(() => on ? client.release() : client.hold());
165
+ });
166
+ return client;
167
+ }
83
168
  }
84
169
  ]);
85
170
  }
@@ -96,10 +181,30 @@ function injectVisitorData(opts) {
96
181
  }
97
182
  const data = (0, import_core4.signal)(void 0);
98
183
  const error = (0, import_core4.signal)(null);
99
- const isLoading = (0, import_core4.signal)(client.agent() === null && client.loadError() === null);
184
+ const isLoading = (0, import_core4.signal)(
185
+ !client.held() && client.agent() === null && client.loadError() === null
186
+ );
187
+ const isHeld = (0, import_core4.computed)(() => client.held());
100
188
  let token = { cancelled: false };
189
+ let started = false;
190
+ const onChange = () => {
191
+ if (client.held()) {
192
+ token.cancelled = true;
193
+ started = false;
194
+ data.set(void 0);
195
+ error.set(null);
196
+ isLoading.set(false);
197
+ return;
198
+ }
199
+ if (started) return;
200
+ if (client.agent() === null && client.loadError() === null) return;
201
+ started = true;
202
+ void run();
203
+ };
204
+ const unsubscribe = client.onChange(onChange);
101
205
  (0, import_core4.inject)(import_core4.DestroyRef).onDestroy(() => {
102
206
  token.cancelled = true;
207
+ unsubscribe();
103
208
  });
104
209
  const run = async () => {
105
210
  const agent = client.agent();
@@ -128,8 +233,8 @@ function injectVisitorData(opts) {
128
233
  if (!my.cancelled) isLoading.set(false);
129
234
  }
130
235
  };
131
- void client.ready.then(() => run());
132
- return { data, isLoading, error, refetch: run };
236
+ void Promise.resolve().then(onChange);
237
+ return { data, isLoading, isHeld, error, refetch: run };
133
238
  }
134
239
  // Annotate the CommonJS export names for ESM import in node:
135
240
  0 && (module.exports = {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/provide-signals.ts","../src/tokens.ts","../src/inject-visitor-data.ts"],"sourcesContent":["export {\n type AgentLoadOptions,\n type IdentifyOptions,\n type IdentifyResult,\n SdkError,\n type SdkErrorCode,\n VERSION as WEB_SDK_VERSION,\n} from \"@signap/signals-web\";\n\nexport { SignalsClient } from \"./client\";\nexport { provideSignals } from \"./provide-signals\";\nexport { SIGNALS_CLIENT } from \"./tokens\";\nexport {\n injectVisitorData,\n type InjectVisitorDataOptions,\n type InjectVisitorDataResult,\n} from \"./inject-visitor-data\";\n","import { type Signal, signal } from \"@angular/core\";\nimport { type AgentLoadOptions, SignalsAgent } from \"@signap/signals-web\";\n\n/**\n * Holds a single lazily-loaded SignalsAgent and exposes its readiness as Angular\n * signals — the framework-native analog of the Vue wrapper's provide/inject refs\n * and the React wrapper's context. `agent()` is `null` until `SignalsAgent.load()`\n * settles; `loadError()` is non-null if it rejected (e.g. an invalid apiKey).\n *\n * Deliberately NOT `@Injectable`: a decorator-free class keeps this package\n * AOT-safe under a plain tsup build (no ngtsc / ng-packagr partial-Ivy metadata\n * step). It is wired into DI by the factory in `provideSignals()`.\n */\nexport class SignalsClient {\n private readonly agentState = signal<SignalsAgent | null>(null);\n private readonly loadErrorState = signal<Error | null>(null);\n\n /** Resolved agent, or `null` until `SignalsAgent.load()` settles. */\n readonly agent: Signal<SignalsAgent | null> = this.agentState.asReadonly();\n /** Non-null if `SignalsAgent.load()` rejected. */\n readonly loadError: Signal<Error | null> = this.loadErrorState.asReadonly();\n /**\n * Resolves once `load()` has settled (agent set OR loadError set). Lets\n * `injectVisitorData()` await readiness without an Angular `effect`, so the\n * auto-identify fires exactly once regardless of change-detection timing.\n */\n readonly ready: Promise<void>;\n\n /**\n * @param opts forwarded verbatim to `SignalsAgent.load()`.\n * @param browser whether we're running in a browser; `load()` only fires\n * client-side so SSR / prerender never touches\n * window/document/WebCrypto (the Angular analog of the Vue\n * plugin's `typeof window` guard).\n */\n constructor(opts: AgentLoadOptions, browser: boolean) {\n if (!browser) {\n this.ready = Promise.resolve();\n return;\n }\n this.ready = SignalsAgent.load(opts)\n .then((loaded) => {\n this.agentState.set(loaded);\n })\n .catch((err: unknown) => {\n this.loadErrorState.set(err instanceof Error ? err : new Error(String(err)));\n });\n }\n}\n","import { isPlatformBrowser } from \"@angular/common\";\nimport {\n type EnvironmentProviders,\n PLATFORM_ID,\n inject,\n makeEnvironmentProviders,\n} from \"@angular/core\";\nimport type { AgentLoadOptions } from \"@signap/signals-web\";\nimport { SignalsClient } from \"./client\";\nimport { SIGNALS_CLIENT } from \"./tokens\";\n\n/**\n * Registers a single app-wide SignalsAgent — the idiomatic standalone-Angular\n * entry point, analogous to React's `<SignalsProvider>` and Vue's\n * `app.use(SignalsPlugin)`:\n *\n * ```ts\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideSignals({ apiKey: \"pk_live_...\", endpoint: \"https://ingest.example\" }),\n * ],\n * });\n * ```\n *\n * SSR-safe: the factory injects `PLATFORM_ID` and only lets the client call\n * `SignalsAgent.load()` in a browser, so a server render / prerender never loads\n * the agent.\n *\n * `opts` is snapshotted here at bootstrap; to swap apiKey/endpoint, re-bootstrap\n * (same snapshot-at-init contract as the React/Vue wrappers).\n */\nexport function provideSignals(opts: AgentLoadOptions): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: SIGNALS_CLIENT,\n useFactory: () => new SignalsClient(opts, isPlatformBrowser(inject(PLATFORM_ID))),\n },\n ]);\n}\n","import { InjectionToken } from \"@angular/core\";\nimport type { SignalsClient } from \"./client\";\n\n/**\n * DI token the app-wide {@link SignalsClient} is registered under (by\n * `provideSignals()`) and that `injectVisitorData()` resolves. Most consumers\n * don't inject this directly — call `injectVisitorData()`. It is exported for\n * advanced use (e.g. `inject(SIGNALS_CLIENT).agent()` to read the raw agent).\n */\nexport const SIGNALS_CLIENT = new InjectionToken<SignalsClient>(\"signap.signals.client\");\n","import { DestroyRef, type Signal, assertInInjectionContext, inject, signal } from \"@angular/core\";\nimport type { IdentifyOptions, IdentifyResult } from \"@signap/signals-web\";\nimport { SIGNALS_CLIENT } from \"./tokens\";\n\nexport type InjectVisitorDataOptions = IdentifyOptions;\n\nexport interface InjectVisitorDataResult {\n /**\n * Resolved visitor from `agent.identify()`. Under ADR §A2 the call is a single\n * synchronous round-trip — `visitorId`/`confidence`/`identifiedAt` arrive\n * together, no polling. A `Signal`, so read it as `data()` (in a template or a\n * `computed`/`effect`).\n */\n data: Signal<IdentifyResult | undefined>;\n /** True between call time and the identify() resolution (or `error`). */\n isLoading: Signal<boolean>;\n error: Signal<Error | null>;\n /** Re-run `agent.identify()` (e.g. from a button click). */\n refetch: () => Promise<void>;\n}\n\n/**\n * Auto-identifies once the provider's agent is ready, then exposes the resolved\n * visitor as Angular signals — the framework-native analog of React's\n * `useVisitorData()` hook and Vue's `useVisitorData()` composable.\n *\n * Must be called in an injection context (a component/directive constructor or a\n * field initializer, or inside `runInInjectionContext`) below `provideSignals()`.\n */\nexport function injectVisitorData(opts?: InjectVisitorDataOptions): InjectVisitorDataResult {\n assertInInjectionContext(injectVisitorData);\n\n const client = inject(SIGNALS_CLIENT, { optional: true });\n if (!client) {\n throw new Error(\n \"[@signap/signals-angular] injectVisitorData() requires provideSignals() in the application (or component) providers\",\n );\n }\n\n const data = signal<IdentifyResult | undefined>(undefined);\n const error = signal<Error | null>(null);\n // Loading until the agent (or a load error) settles.\n const isLoading = signal(client.agent() === null && client.loadError() === null);\n\n // Supersede token so a slow response from a call that refetch() replaced (or a\n // response arriving after teardown) is discarded.\n let token = { cancelled: false };\n inject(DestroyRef).onDestroy(() => {\n token.cancelled = true;\n });\n\n const run = async (): Promise<void> => {\n const agent = client.agent();\n if (!agent) {\n const loadError = client.loadError();\n if (loadError) {\n error.set(loadError);\n isLoading.set(false);\n }\n return;\n }\n token.cancelled = true;\n const my = { cancelled: false };\n token = my;\n isLoading.set(true);\n error.set(null);\n data.set(undefined);\n try {\n const res = await agent.identify(opts);\n if (my.cancelled) return;\n data.set(res);\n } catch (err) {\n if (my.cancelled) return;\n error.set(err instanceof Error ? err : new Error(String(err)));\n } finally {\n if (!my.cancelled) isLoading.set(false);\n }\n };\n\n // Auto-run exactly once, after load() settles. `ready` resolves whether load()\n // succeeded or failed; even if it already resolved, `.then` defers to a\n // microtask so the returned signals are wired up before the first run().\n void client.ready.then(() => run());\n\n return { data, isLoading, error, refetch: run };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAAAA,sBAOO;;;ACPP,kBAAoC;AACpC,yBAAoD;AAY7C,IAAM,gBAAN,MAAoB;AAAA,EACR,iBAAa,oBAA4B,IAAI;AAAA,EAC7C,qBAAiB,oBAAqB,IAAI;AAAA;AAAA,EAGlD,QAAqC,KAAK,WAAW,WAAW;AAAA;AAAA,EAEhE,YAAkC,KAAK,eAAe,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAST,YAAY,MAAwB,SAAkB;AACpD,QAAI,CAAC,SAAS;AACZ,WAAK,QAAQ,QAAQ,QAAQ;AAC7B;AAAA,IACF;AACA,SAAK,QAAQ,gCAAa,KAAK,IAAI,EAChC,KAAK,CAAC,WAAW;AAChB,WAAK,WAAW,IAAI,MAAM;AAAA,IAC5B,CAAC,EACA,MAAM,CAAC,QAAiB;AACvB,WAAK,eAAe,IAAI,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAC7E,CAAC;AAAA,EACL;AACF;;;AChDA,oBAAkC;AAClC,IAAAC,eAKO;;;ACNP,IAAAC,eAA+B;AASxB,IAAM,iBAAiB,IAAI,4BAA8B,uBAAuB;;;ADsBhF,SAAS,eAAe,MAA8C;AAC3E,aAAO,uCAAyB;AAAA,IAC9B;AAAA,MACE,SAAS;AAAA,MACT,YAAY,MAAM,IAAI,cAAc,UAAM,qCAAkB,qBAAO,wBAAW,CAAC,CAAC;AAAA,IAClF;AAAA,EACF,CAAC;AACH;;;AEtCA,IAAAC,eAAkF;AA6B3E,SAAS,kBAAkB,MAA0D;AAC1F,6CAAyB,iBAAiB;AAE1C,QAAM,aAAS,qBAAO,gBAAgB,EAAE,UAAU,KAAK,CAAC;AACxD,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,WAAO,qBAAmC,MAAS;AACzD,QAAM,YAAQ,qBAAqB,IAAI;AAEvC,QAAM,gBAAY,qBAAO,OAAO,MAAM,MAAM,QAAQ,OAAO,UAAU,MAAM,IAAI;AAI/E,MAAI,QAAQ,EAAE,WAAW,MAAM;AAC/B,2BAAO,uBAAU,EAAE,UAAU,MAAM;AACjC,UAAM,YAAY;AAAA,EACpB,CAAC;AAED,QAAM,MAAM,YAA2B;AACrC,UAAM,QAAQ,OAAO,MAAM;AAC3B,QAAI,CAAC,OAAO;AACV,YAAM,YAAY,OAAO,UAAU;AACnC,UAAI,WAAW;AACb,cAAM,IAAI,SAAS;AACnB,kBAAU,IAAI,KAAK;AAAA,MACrB;AACA;AAAA,IACF;AACA,UAAM,YAAY;AAClB,UAAM,KAAK,EAAE,WAAW,MAAM;AAC9B,YAAQ;AACR,cAAU,IAAI,IAAI;AAClB,UAAM,IAAI,IAAI;AACd,SAAK,IAAI,MAAS;AAClB,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,SAAS,IAAI;AACrC,UAAI,GAAG,UAAW;AAClB,WAAK,IAAI,GAAG;AAAA,IACd,SAAS,KAAK;AACZ,UAAI,GAAG,UAAW;AAClB,YAAM,IAAI,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAC/D,UAAE;AACA,UAAI,CAAC,GAAG,UAAW,WAAU,IAAI,KAAK;AAAA,IACxC;AAAA,EACF;AAKA,OAAK,OAAO,MAAM,KAAK,MAAM,IAAI,CAAC;AAElC,SAAO,EAAE,MAAM,WAAW,OAAO,SAAS,IAAI;AAChD;","names":["import_signals_web","import_core","import_core","import_core"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/provide-signals.ts","../src/tokens.ts","../src/inject-visitor-data.ts"],"sourcesContent":["export {\n type AgentLoadOptions,\n type IdentifyOptions,\n type IdentifyResult,\n SdkError,\n type SdkErrorCode,\n VERSION as WEB_SDK_VERSION,\n} from \"@signap/signals-web\";\n\nexport { SignalsClient } from \"./client\";\nexport { provideSignals, type SignalsConfig } from \"./provide-signals\";\nexport { SIGNALS_CLIENT } from \"./tokens\";\nexport {\n injectVisitorData,\n type InjectVisitorDataOptions,\n type InjectVisitorDataResult,\n} from \"./inject-visitor-data\";\n","import { type Signal, type WritableSignal, signal } from \"@angular/core\";\nimport { type AgentLoadOptions, SignalsAgent } from \"@signap/signals-web\";\n\n/**\n * Holds a single lazily-loaded SignalsAgent and exposes its readiness as Angular\n * signals — the framework-native analog of the Vue wrapper's provide/inject refs\n * and the React wrapper's context. `agent()` is `null` until `SignalsAgent.load()`\n * settles; `loadError()` is non-null if it rejected (e.g. an invalid apiKey).\n *\n * Deliberately NOT `@Injectable`: a decorator-free class keeps this package\n * AOT-safe under a plain tsup build (no ngtsc / ng-packagr partial-Ivy metadata\n * step). It is wired into DI by the factory in `provideSignals()`.\n */\nexport class SignalsClient {\n private readonly agentState = signal<SignalsAgent | null>(null);\n private readonly loadErrorState = signal<Error | null>(null);\n private readonly heldState: WritableSignal<boolean>;\n\n /** Resolved agent, or `null` until `SignalsAgent.load()` settles. */\n readonly agent: Signal<SignalsAgent | null> = this.agentState.asReadonly();\n /** Non-null if `SignalsAgent.load()` rejected. */\n readonly loadError: Signal<Error | null> = this.loadErrorState.asReadonly();\n /** True while held (`provideSignals(opts, { enabled })` with `enabled()` false): nothing is loaded. */\n readonly held: Signal<boolean>;\n /**\n * Resolves once the FIRST `load()` has settled (agent set OR loadError set), or\n * at once outside a browser. While the client is held before its first release,\n * it stays pending.\n */\n readonly ready: Promise<void>;\n\n private readonly opts: AgentLoadOptions;\n private readonly browser: boolean;\n private resolveReady: () => void = () => {};\n private started = false;\n private loaded: SignalsAgent | null = null;\n // Bumped on every withdrawal; a load() that settles under an older generation is discarded.\n private generation = 0;\n private readonly listeners = new Set<() => void>();\n\n /**\n * @param opts forwarded verbatim to `SignalsAgent.load()`.\n * @param browser whether we're running in a browser; `load()` only fires\n * client-side so SSR / prerender never touches\n * window/document/WebCrypto (the Angular analog of the Vue\n * plugin's `typeof window` guard).\n * @param initiallyHeld start held (issue 2894): do not load until `release()`.\n */\n constructor(opts: AgentLoadOptions, browser: boolean, initiallyHeld = false) {\n this.opts = opts;\n this.browser = browser;\n this.heldState = signal(initiallyHeld);\n this.held = this.heldState.asReadonly();\n this.ready = new Promise<void>((resolve) => {\n this.resolveReady = resolve;\n });\n if (!browser) {\n this.resolveReady();\n return;\n }\n if (!initiallyHeld) this.release();\n }\n\n /**\n * Leave the held state: load the agent once (a no-op if already released). What\n * `provideSignals()` calls when its `enabled` signal becomes true.\n */\n release(): void {\n this.heldState.set(false);\n if (!this.browser || this.started) return;\n this.started = true;\n const mine = this.generation;\n SignalsAgent.load(this.opts)\n .then((loaded) => {\n if (mine !== this.generation) {\n // Withdrawn while in flight. load() started the observers synchronously;\n // stop them, unless a newer release is running them now (page-wide singletons).\n if (!this.started) loaded.stopObservers();\n return;\n }\n this.loaded = loaded;\n this.agentState.set(loaded);\n this.settled();\n })\n .catch((err: unknown) => {\n if (mine !== this.generation) return;\n this.loadErrorState.set(err instanceof Error ? err : new Error(String(err)));\n this.settled();\n });\n }\n\n /**\n * Enter the held state. After a release this stops the SDK's interaction\n * listeners (page-wide, for this copy of the SDK), drops the agent and discards\n * a load still in flight; a request already sent is not recalled, and nothing\n * already received is deleted. Before any release it only marks the client held.\n */\n hold(): void {\n this.heldState.set(true);\n if (!this.started) {\n this.notify();\n return;\n }\n this.started = false;\n this.generation++;\n this.loaded?.stopObservers();\n this.loaded = null;\n this.agentState.set(null);\n this.loadErrorState.set(null);\n this.notify();\n }\n\n /**\n * @internal Called after every state change (a load settling, a hold). Lets\n * `injectVisitorData()` re-run without an Angular `effect`, so the auto-identify\n * fires exactly once per release regardless of change-detection timing.\n */\n onChange(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n private settled(): void {\n this.resolveReady();\n this.notify();\n }\n\n private notify(): void {\n for (const l of [...this.listeners]) l();\n }\n}\n","import { isPlatformBrowser } from \"@angular/common\";\nimport {\n type EnvironmentProviders,\n PLATFORM_ID,\n type Signal,\n effect,\n inject,\n makeEnvironmentProviders,\n untracked,\n} from \"@angular/core\";\nimport type { AgentLoadOptions } from \"@signap/signals-web\";\nimport { SignalsClient } from \"./client\";\nimport { SIGNALS_CLIENT } from \"./tokens\";\n\n/**\n * Registers a single app-wide SignalsAgent — the idiomatic standalone-Angular\n * entry point, analogous to React's `<SignalsProvider>` and Vue's\n * `app.use(SignalsPlugin)`:\n *\n * ```ts\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideSignals({ apiKey: \"pk_live_...\", endpoint: \"https://ingest.example\" }),\n * ],\n * });\n * ```\n *\n * SSR-safe: the factory injects `PLATFORM_ID` and only lets the client call\n * `SignalsAgent.load()` in a browser, so a server render / prerender never loads\n * the agent.\n *\n * `opts` is snapshotted here at bootstrap; to swap apiKey/endpoint, re-bootstrap\n * (same snapshot-at-init contract as the React/Vue wrappers).\n */\nexport function provideSignals(\n opts: AgentLoadOptions,\n config: SignalsConfig = {},\n): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: SIGNALS_CLIENT,\n useFactory: () => {\n const browser = isPlatformBrowser(inject(PLATFORM_ID));\n const { enabled } = config;\n if (!enabled) return new SignalsClient(opts, browser);\n const client = new SignalsClient(opts, browser, !untracked(enabled));\n // The factory runs in an injection context, so the effect lives as long as\n // the environment injector. The client's writes are untracked so they are\n // not writes \"inside\" the effect (Angular 17/18 reject those by default).\n effect(() => {\n const on = enabled();\n untracked(() => (on ? client.release() : client.hold()));\n });\n return client;\n },\n },\n ]);\n}\n\n/** Second argument of `provideSignals()`. */\nexport interface SignalsConfig {\n /**\n * While this signal is `false` the client is held: it does not load the agent, so\n * the SDK registers no event listener, runs no collector and sends no request, and\n * `injectVisitorData()` returns `isHeld() === true`. When it becomes `true` the\n * agent loads and the first identify runs, as if `enabled` had never been set.\n * When it goes back to `false` the SDK's interaction listeners stop (page-wide, for\n * this copy of the SDK) and any identify result still in flight is discarded; a\n * request already sent is not recalled, and nothing already received is deleted.\n * Omitted = never held.\n */\n enabled?: Signal<boolean>;\n}\n","import { InjectionToken } from \"@angular/core\";\nimport type { SignalsClient } from \"./client\";\n\n/**\n * DI token the app-wide {@link SignalsClient} is registered under (by\n * `provideSignals()`) and that `injectVisitorData()` resolves. Most consumers\n * don't inject this directly — call `injectVisitorData()`. It is exported for\n * advanced use (e.g. `inject(SIGNALS_CLIENT).agent()` to read the raw agent).\n */\nexport const SIGNALS_CLIENT = new InjectionToken<SignalsClient>(\"signap.signals.client\");\n","import {\n DestroyRef,\n type Signal,\n assertInInjectionContext,\n computed,\n inject,\n signal,\n} from \"@angular/core\";\nimport type { IdentifyOptions, IdentifyResult } from \"@signap/signals-web\";\nimport { SIGNALS_CLIENT } from \"./tokens\";\n\nexport type InjectVisitorDataOptions = IdentifyOptions;\n\nexport interface InjectVisitorDataResult {\n /**\n * Resolved visitor from `agent.identify()`. Under ADR §A2 the call is a single\n * synchronous round-trip — `visitorId`/`confidence`/`identifiedAt` arrive\n * together, no polling. A `Signal`, so read it as `data()` (in a template or a\n * `computed`/`effect`).\n */\n data: Signal<IdentifyResult | undefined>;\n /** True between call time and the identify() resolution (or `error`). False while held. */\n isLoading: Signal<boolean>;\n /**\n * True while `provideSignals(opts, { enabled })` is held. Nothing is loaded or sent,\n * `data()` is `undefined` and `refetch()` does nothing. Always false without `enabled`.\n */\n isHeld: Signal<boolean>;\n error: Signal<Error | null>;\n /** Re-run `agent.identify()` (e.g. from a button click). */\n refetch: () => Promise<void>;\n}\n\n/**\n * Auto-identifies once the provider's agent is ready, then exposes the resolved\n * visitor as Angular signals — the framework-native analog of React's\n * `useVisitorData()` hook and Vue's `useVisitorData()` composable.\n *\n * Must be called in an injection context (a component/directive constructor or a\n * field initializer, or inside `runInInjectionContext`) below `provideSignals()`.\n */\nexport function injectVisitorData(opts?: InjectVisitorDataOptions): InjectVisitorDataResult {\n assertInInjectionContext(injectVisitorData);\n\n const client = inject(SIGNALS_CLIENT, { optional: true });\n if (!client) {\n throw new Error(\n \"[@signap/signals-angular] injectVisitorData() requires provideSignals() in the application (or component) providers\",\n );\n }\n\n const data = signal<IdentifyResult | undefined>(undefined);\n const error = signal<Error | null>(null);\n // Loading until the agent (or a load error) settles — unless held.\n const isLoading = signal(\n !client.held() && client.agent() === null && client.loadError() === null,\n );\n const isHeld = computed(() => client.held());\n\n // Supersede token so a slow response from a call that refetch() replaced (or a\n // response arriving after teardown) is discarded.\n let token = { cancelled: false };\n let started = false;\n const onChange = (): void => {\n if (client.held()) {\n // Held (issue 2894): discard any identify in flight and any previous result, and\n // re-arm so a later release identifies once, like a fresh call.\n token.cancelled = true;\n started = false;\n data.set(undefined);\n error.set(null);\n isLoading.set(false);\n return;\n }\n if (started) return;\n if (client.agent() === null && client.loadError() === null) return; // not settled yet\n started = true;\n void run();\n };\n const unsubscribe = client.onChange(onChange);\n inject(DestroyRef).onDestroy(() => {\n token.cancelled = true;\n unsubscribe();\n });\n\n const run = async (): Promise<void> => {\n const agent = client.agent();\n if (!agent) {\n const loadError = client.loadError();\n if (loadError) {\n error.set(loadError);\n isLoading.set(false);\n }\n return;\n }\n token.cancelled = true;\n const my = { cancelled: false };\n token = my;\n isLoading.set(true);\n error.set(null);\n data.set(undefined);\n try {\n const res = await agent.identify(opts);\n if (my.cancelled) return;\n data.set(res);\n } catch (err) {\n if (my.cancelled) return;\n error.set(err instanceof Error ? err : new Error(String(err)));\n } finally {\n if (!my.cancelled) isLoading.set(false);\n }\n };\n\n // Auto-run once per release, after load() settles. The client calls onChange on\n // every settle and hold; the first look is deferred to a microtask (as the old\n // `ready.then` was) so the returned signals are wired up before the first run().\n void Promise.resolve().then(onChange);\n\n return { data, isLoading, isHeld, error, refetch: run };\n}\n"],"mappings":"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","names":["import_signals_web","import_core","import_core","import_core"]}
package/dist/index.d.cts CHANGED
@@ -15,24 +15,55 @@ import { Signal, EnvironmentProviders, InjectionToken } from '@angular/core';
15
15
  declare class SignalsClient {
16
16
  private readonly agentState;
17
17
  private readonly loadErrorState;
18
+ private readonly heldState;
18
19
  /** Resolved agent, or `null` until `SignalsAgent.load()` settles. */
19
20
  readonly agent: Signal<SignalsAgent | null>;
20
21
  /** Non-null if `SignalsAgent.load()` rejected. */
21
22
  readonly loadError: Signal<Error | null>;
23
+ /** True while held (`provideSignals(opts, { enabled })` with `enabled()` false): nothing is loaded. */
24
+ readonly held: Signal<boolean>;
22
25
  /**
23
- * Resolves once `load()` has settled (agent set OR loadError set). Lets
24
- * `injectVisitorData()` await readiness without an Angular `effect`, so the
25
- * auto-identify fires exactly once regardless of change-detection timing.
26
+ * Resolves once the FIRST `load()` has settled (agent set OR loadError set), or
27
+ * at once outside a browser. While the client is held before its first release,
28
+ * it stays pending.
26
29
  */
27
30
  readonly ready: Promise<void>;
31
+ private readonly opts;
32
+ private readonly browser;
33
+ private resolveReady;
34
+ private started;
35
+ private loaded;
36
+ private generation;
37
+ private readonly listeners;
28
38
  /**
29
- * @param opts forwarded verbatim to `SignalsAgent.load()`.
30
- * @param browser whether we're running in a browser; `load()` only fires
31
- * client-side so SSR / prerender never touches
32
- * window/document/WebCrypto (the Angular analog of the Vue
33
- * plugin's `typeof window` guard).
39
+ * @param opts forwarded verbatim to `SignalsAgent.load()`.
40
+ * @param browser whether we're running in a browser; `load()` only fires
41
+ * client-side so SSR / prerender never touches
42
+ * window/document/WebCrypto (the Angular analog of the Vue
43
+ * plugin's `typeof window` guard).
44
+ * @param initiallyHeld start held (issue 2894): do not load until `release()`.
34
45
  */
35
- constructor(opts: AgentLoadOptions, browser: boolean);
46
+ constructor(opts: AgentLoadOptions, browser: boolean, initiallyHeld?: boolean);
47
+ /**
48
+ * Leave the held state: load the agent once (a no-op if already released). What
49
+ * `provideSignals()` calls when its `enabled` signal becomes true.
50
+ */
51
+ release(): void;
52
+ /**
53
+ * Enter the held state. After a release this stops the SDK's interaction
54
+ * listeners (page-wide, for this copy of the SDK), drops the agent and discards
55
+ * a load still in flight; a request already sent is not recalled, and nothing
56
+ * already received is deleted. Before any release it only marks the client held.
57
+ */
58
+ hold(): void;
59
+ /**
60
+ * @internal Called after every state change (a load settling, a hold). Lets
61
+ * `injectVisitorData()` re-run without an Angular `effect`, so the auto-identify
62
+ * fires exactly once per release regardless of change-detection timing.
63
+ */
64
+ onChange(listener: () => void): () => void;
65
+ private settled;
66
+ private notify;
36
67
  }
37
68
 
38
69
  /**
@@ -55,7 +86,21 @@ declare class SignalsClient {
55
86
  * `opts` is snapshotted here at bootstrap; to swap apiKey/endpoint, re-bootstrap
56
87
  * (same snapshot-at-init contract as the React/Vue wrappers).
57
88
  */
58
- declare function provideSignals(opts: AgentLoadOptions): EnvironmentProviders;
89
+ declare function provideSignals(opts: AgentLoadOptions, config?: SignalsConfig): EnvironmentProviders;
90
+ /** Second argument of `provideSignals()`. */
91
+ interface SignalsConfig {
92
+ /**
93
+ * While this signal is `false` the client is held: it does not load the agent, so
94
+ * the SDK registers no event listener, runs no collector and sends no request, and
95
+ * `injectVisitorData()` returns `isHeld() === true`. When it becomes `true` the
96
+ * agent loads and the first identify runs, as if `enabled` had never been set.
97
+ * When it goes back to `false` the SDK's interaction listeners stop (page-wide, for
98
+ * this copy of the SDK) and any identify result still in flight is discarded; a
99
+ * request already sent is not recalled, and nothing already received is deleted.
100
+ * Omitted = never held.
101
+ */
102
+ enabled?: Signal<boolean>;
103
+ }
59
104
 
60
105
  /**
61
106
  * DI token the app-wide {@link SignalsClient} is registered under (by
@@ -74,8 +119,13 @@ interface InjectVisitorDataResult {
74
119
  * `computed`/`effect`).
75
120
  */
76
121
  data: Signal<IdentifyResult | undefined>;
77
- /** True between call time and the identify() resolution (or `error`). */
122
+ /** True between call time and the identify() resolution (or `error`). False while held. */
78
123
  isLoading: Signal<boolean>;
124
+ /**
125
+ * True while `provideSignals(opts, { enabled })` is held. Nothing is loaded or sent,
126
+ * `data()` is `undefined` and `refetch()` does nothing. Always false without `enabled`.
127
+ */
128
+ isHeld: Signal<boolean>;
79
129
  error: Signal<Error | null>;
80
130
  /** Re-run `agent.identify()` (e.g. from a button click). */
81
131
  refetch: () => Promise<void>;
@@ -90,4 +140,4 @@ interface InjectVisitorDataResult {
90
140
  */
91
141
  declare function injectVisitorData(opts?: InjectVisitorDataOptions): InjectVisitorDataResult;
92
142
 
93
- export { type InjectVisitorDataOptions, type InjectVisitorDataResult, SIGNALS_CLIENT, SignalsClient, injectVisitorData, provideSignals };
143
+ export { type InjectVisitorDataOptions, type InjectVisitorDataResult, SIGNALS_CLIENT, SignalsClient, type SignalsConfig, injectVisitorData, provideSignals };
package/dist/index.d.ts CHANGED
@@ -15,24 +15,55 @@ import { Signal, EnvironmentProviders, InjectionToken } from '@angular/core';
15
15
  declare class SignalsClient {
16
16
  private readonly agentState;
17
17
  private readonly loadErrorState;
18
+ private readonly heldState;
18
19
  /** Resolved agent, or `null` until `SignalsAgent.load()` settles. */
19
20
  readonly agent: Signal<SignalsAgent | null>;
20
21
  /** Non-null if `SignalsAgent.load()` rejected. */
21
22
  readonly loadError: Signal<Error | null>;
23
+ /** True while held (`provideSignals(opts, { enabled })` with `enabled()` false): nothing is loaded. */
24
+ readonly held: Signal<boolean>;
22
25
  /**
23
- * Resolves once `load()` has settled (agent set OR loadError set). Lets
24
- * `injectVisitorData()` await readiness without an Angular `effect`, so the
25
- * auto-identify fires exactly once regardless of change-detection timing.
26
+ * Resolves once the FIRST `load()` has settled (agent set OR loadError set), or
27
+ * at once outside a browser. While the client is held before its first release,
28
+ * it stays pending.
26
29
  */
27
30
  readonly ready: Promise<void>;
31
+ private readonly opts;
32
+ private readonly browser;
33
+ private resolveReady;
34
+ private started;
35
+ private loaded;
36
+ private generation;
37
+ private readonly listeners;
28
38
  /**
29
- * @param opts forwarded verbatim to `SignalsAgent.load()`.
30
- * @param browser whether we're running in a browser; `load()` only fires
31
- * client-side so SSR / prerender never touches
32
- * window/document/WebCrypto (the Angular analog of the Vue
33
- * plugin's `typeof window` guard).
39
+ * @param opts forwarded verbatim to `SignalsAgent.load()`.
40
+ * @param browser whether we're running in a browser; `load()` only fires
41
+ * client-side so SSR / prerender never touches
42
+ * window/document/WebCrypto (the Angular analog of the Vue
43
+ * plugin's `typeof window` guard).
44
+ * @param initiallyHeld start held (issue 2894): do not load until `release()`.
34
45
  */
35
- constructor(opts: AgentLoadOptions, browser: boolean);
46
+ constructor(opts: AgentLoadOptions, browser: boolean, initiallyHeld?: boolean);
47
+ /**
48
+ * Leave the held state: load the agent once (a no-op if already released). What
49
+ * `provideSignals()` calls when its `enabled` signal becomes true.
50
+ */
51
+ release(): void;
52
+ /**
53
+ * Enter the held state. After a release this stops the SDK's interaction
54
+ * listeners (page-wide, for this copy of the SDK), drops the agent and discards
55
+ * a load still in flight; a request already sent is not recalled, and nothing
56
+ * already received is deleted. Before any release it only marks the client held.
57
+ */
58
+ hold(): void;
59
+ /**
60
+ * @internal Called after every state change (a load settling, a hold). Lets
61
+ * `injectVisitorData()` re-run without an Angular `effect`, so the auto-identify
62
+ * fires exactly once per release regardless of change-detection timing.
63
+ */
64
+ onChange(listener: () => void): () => void;
65
+ private settled;
66
+ private notify;
36
67
  }
37
68
 
38
69
  /**
@@ -55,7 +86,21 @@ declare class SignalsClient {
55
86
  * `opts` is snapshotted here at bootstrap; to swap apiKey/endpoint, re-bootstrap
56
87
  * (same snapshot-at-init contract as the React/Vue wrappers).
57
88
  */
58
- declare function provideSignals(opts: AgentLoadOptions): EnvironmentProviders;
89
+ declare function provideSignals(opts: AgentLoadOptions, config?: SignalsConfig): EnvironmentProviders;
90
+ /** Second argument of `provideSignals()`. */
91
+ interface SignalsConfig {
92
+ /**
93
+ * While this signal is `false` the client is held: it does not load the agent, so
94
+ * the SDK registers no event listener, runs no collector and sends no request, and
95
+ * `injectVisitorData()` returns `isHeld() === true`. When it becomes `true` the
96
+ * agent loads and the first identify runs, as if `enabled` had never been set.
97
+ * When it goes back to `false` the SDK's interaction listeners stop (page-wide, for
98
+ * this copy of the SDK) and any identify result still in flight is discarded; a
99
+ * request already sent is not recalled, and nothing already received is deleted.
100
+ * Omitted = never held.
101
+ */
102
+ enabled?: Signal<boolean>;
103
+ }
59
104
 
60
105
  /**
61
106
  * DI token the app-wide {@link SignalsClient} is registered under (by
@@ -74,8 +119,13 @@ interface InjectVisitorDataResult {
74
119
  * `computed`/`effect`).
75
120
  */
76
121
  data: Signal<IdentifyResult | undefined>;
77
- /** True between call time and the identify() resolution (or `error`). */
122
+ /** True between call time and the identify() resolution (or `error`). False while held. */
78
123
  isLoading: Signal<boolean>;
124
+ /**
125
+ * True while `provideSignals(opts, { enabled })` is held. Nothing is loaded or sent,
126
+ * `data()` is `undefined` and `refetch()` does nothing. Always false without `enabled`.
127
+ */
128
+ isHeld: Signal<boolean>;
79
129
  error: Signal<Error | null>;
80
130
  /** Re-run `agent.identify()` (e.g. from a button click). */
81
131
  refetch: () => Promise<void>;
@@ -90,4 +140,4 @@ interface InjectVisitorDataResult {
90
140
  */
91
141
  declare function injectVisitorData(opts?: InjectVisitorDataOptions): InjectVisitorDataResult;
92
142
 
93
- export { type InjectVisitorDataOptions, type InjectVisitorDataResult, SIGNALS_CLIENT, SignalsClient, injectVisitorData, provideSignals };
143
+ export { type InjectVisitorDataOptions, type InjectVisitorDataResult, SIGNALS_CLIENT, SignalsClient, type SignalsConfig, injectVisitorData, provideSignals };
package/dist/index.js CHANGED
@@ -10,42 +10,119 @@ import { SignalsAgent } from "@signap/signals-web";
10
10
  var SignalsClient = class {
11
11
  agentState = signal(null);
12
12
  loadErrorState = signal(null);
13
+ heldState;
13
14
  /** Resolved agent, or `null` until `SignalsAgent.load()` settles. */
14
15
  agent = this.agentState.asReadonly();
15
16
  /** Non-null if `SignalsAgent.load()` rejected. */
16
17
  loadError = this.loadErrorState.asReadonly();
18
+ /** True while held (`provideSignals(opts, { enabled })` with `enabled()` false): nothing is loaded. */
19
+ held;
17
20
  /**
18
- * Resolves once `load()` has settled (agent set OR loadError set). Lets
19
- * `injectVisitorData()` await readiness without an Angular `effect`, so the
20
- * auto-identify fires exactly once regardless of change-detection timing.
21
+ * Resolves once the FIRST `load()` has settled (agent set OR loadError set), or
22
+ * at once outside a browser. While the client is held before its first release,
23
+ * it stays pending.
21
24
  */
22
25
  ready;
26
+ opts;
27
+ browser;
28
+ resolveReady = () => {
29
+ };
30
+ started = false;
31
+ loaded = null;
32
+ // Bumped on every withdrawal; a load() that settles under an older generation is discarded.
33
+ generation = 0;
34
+ listeners = /* @__PURE__ */ new Set();
23
35
  /**
24
- * @param opts forwarded verbatim to `SignalsAgent.load()`.
25
- * @param browser whether we're running in a browser; `load()` only fires
26
- * client-side so SSR / prerender never touches
27
- * window/document/WebCrypto (the Angular analog of the Vue
28
- * plugin's `typeof window` guard).
36
+ * @param opts forwarded verbatim to `SignalsAgent.load()`.
37
+ * @param browser whether we're running in a browser; `load()` only fires
38
+ * client-side so SSR / prerender never touches
39
+ * window/document/WebCrypto (the Angular analog of the Vue
40
+ * plugin's `typeof window` guard).
41
+ * @param initiallyHeld start held (issue 2894): do not load until `release()`.
29
42
  */
30
- constructor(opts, browser) {
43
+ constructor(opts, browser, initiallyHeld = false) {
44
+ this.opts = opts;
45
+ this.browser = browser;
46
+ this.heldState = signal(initiallyHeld);
47
+ this.held = this.heldState.asReadonly();
48
+ this.ready = new Promise((resolve) => {
49
+ this.resolveReady = resolve;
50
+ });
31
51
  if (!browser) {
32
- this.ready = Promise.resolve();
52
+ this.resolveReady();
33
53
  return;
34
54
  }
35
- this.ready = SignalsAgent.load(opts).then((loaded) => {
55
+ if (!initiallyHeld) this.release();
56
+ }
57
+ /**
58
+ * Leave the held state: load the agent once (a no-op if already released). What
59
+ * `provideSignals()` calls when its `enabled` signal becomes true.
60
+ */
61
+ release() {
62
+ this.heldState.set(false);
63
+ if (!this.browser || this.started) return;
64
+ this.started = true;
65
+ const mine = this.generation;
66
+ SignalsAgent.load(this.opts).then((loaded) => {
67
+ if (mine !== this.generation) {
68
+ if (!this.started) loaded.stopObservers();
69
+ return;
70
+ }
71
+ this.loaded = loaded;
36
72
  this.agentState.set(loaded);
73
+ this.settled();
37
74
  }).catch((err) => {
75
+ if (mine !== this.generation) return;
38
76
  this.loadErrorState.set(err instanceof Error ? err : new Error(String(err)));
77
+ this.settled();
39
78
  });
40
79
  }
80
+ /**
81
+ * Enter the held state. After a release this stops the SDK's interaction
82
+ * listeners (page-wide, for this copy of the SDK), drops the agent and discards
83
+ * a load still in flight; a request already sent is not recalled, and nothing
84
+ * already received is deleted. Before any release it only marks the client held.
85
+ */
86
+ hold() {
87
+ this.heldState.set(true);
88
+ if (!this.started) {
89
+ this.notify();
90
+ return;
91
+ }
92
+ this.started = false;
93
+ this.generation++;
94
+ this.loaded?.stopObservers();
95
+ this.loaded = null;
96
+ this.agentState.set(null);
97
+ this.loadErrorState.set(null);
98
+ this.notify();
99
+ }
100
+ /**
101
+ * @internal Called after every state change (a load settling, a hold). Lets
102
+ * `injectVisitorData()` re-run without an Angular `effect`, so the auto-identify
103
+ * fires exactly once per release regardless of change-detection timing.
104
+ */
105
+ onChange(listener) {
106
+ this.listeners.add(listener);
107
+ return () => this.listeners.delete(listener);
108
+ }
109
+ settled() {
110
+ this.resolveReady();
111
+ this.notify();
112
+ }
113
+ notify() {
114
+ for (const l of [...this.listeners]) l();
115
+ }
41
116
  };
42
117
 
43
118
  // src/provide-signals.ts
44
119
  import { isPlatformBrowser } from "@angular/common";
45
120
  import {
46
121
  PLATFORM_ID,
122
+ effect,
47
123
  inject,
48
- makeEnvironmentProviders
124
+ makeEnvironmentProviders,
125
+ untracked
49
126
  } from "@angular/core";
50
127
 
51
128
  // src/tokens.ts
@@ -53,17 +130,33 @@ import { InjectionToken } from "@angular/core";
53
130
  var SIGNALS_CLIENT = new InjectionToken("signap.signals.client");
54
131
 
55
132
  // src/provide-signals.ts
56
- function provideSignals(opts) {
133
+ function provideSignals(opts, config = {}) {
57
134
  return makeEnvironmentProviders([
58
135
  {
59
136
  provide: SIGNALS_CLIENT,
60
- useFactory: () => new SignalsClient(opts, isPlatformBrowser(inject(PLATFORM_ID)))
137
+ useFactory: () => {
138
+ const browser = isPlatformBrowser(inject(PLATFORM_ID));
139
+ const { enabled } = config;
140
+ if (!enabled) return new SignalsClient(opts, browser);
141
+ const client = new SignalsClient(opts, browser, !untracked(enabled));
142
+ effect(() => {
143
+ const on = enabled();
144
+ untracked(() => on ? client.release() : client.hold());
145
+ });
146
+ return client;
147
+ }
61
148
  }
62
149
  ]);
63
150
  }
64
151
 
65
152
  // src/inject-visitor-data.ts
66
- import { DestroyRef, assertInInjectionContext, inject as inject2, signal as signal2 } from "@angular/core";
153
+ import {
154
+ DestroyRef,
155
+ assertInInjectionContext,
156
+ computed,
157
+ inject as inject2,
158
+ signal as signal2
159
+ } from "@angular/core";
67
160
  function injectVisitorData(opts) {
68
161
  assertInInjectionContext(injectVisitorData);
69
162
  const client = inject2(SIGNALS_CLIENT, { optional: true });
@@ -74,10 +167,30 @@ function injectVisitorData(opts) {
74
167
  }
75
168
  const data = signal2(void 0);
76
169
  const error = signal2(null);
77
- const isLoading = signal2(client.agent() === null && client.loadError() === null);
170
+ const isLoading = signal2(
171
+ !client.held() && client.agent() === null && client.loadError() === null
172
+ );
173
+ const isHeld = computed(() => client.held());
78
174
  let token = { cancelled: false };
175
+ let started = false;
176
+ const onChange = () => {
177
+ if (client.held()) {
178
+ token.cancelled = true;
179
+ started = false;
180
+ data.set(void 0);
181
+ error.set(null);
182
+ isLoading.set(false);
183
+ return;
184
+ }
185
+ if (started) return;
186
+ if (client.agent() === null && client.loadError() === null) return;
187
+ started = true;
188
+ void run();
189
+ };
190
+ const unsubscribe = client.onChange(onChange);
79
191
  inject2(DestroyRef).onDestroy(() => {
80
192
  token.cancelled = true;
193
+ unsubscribe();
81
194
  });
82
195
  const run = async () => {
83
196
  const agent = client.agent();
@@ -106,8 +219,8 @@ function injectVisitorData(opts) {
106
219
  if (!my.cancelled) isLoading.set(false);
107
220
  }
108
221
  };
109
- void client.ready.then(() => run());
110
- return { data, isLoading, error, refetch: run };
222
+ void Promise.resolve().then(onChange);
223
+ return { data, isLoading, isHeld, error, refetch: run };
111
224
  }
112
225
  export {
113
226
  SIGNALS_CLIENT,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/provide-signals.ts","../src/tokens.ts","../src/inject-visitor-data.ts"],"sourcesContent":["export {\n type AgentLoadOptions,\n type IdentifyOptions,\n type IdentifyResult,\n SdkError,\n type SdkErrorCode,\n VERSION as WEB_SDK_VERSION,\n} from \"@signap/signals-web\";\n\nexport { SignalsClient } from \"./client\";\nexport { provideSignals } from \"./provide-signals\";\nexport { SIGNALS_CLIENT } from \"./tokens\";\nexport {\n injectVisitorData,\n type InjectVisitorDataOptions,\n type InjectVisitorDataResult,\n} from \"./inject-visitor-data\";\n","import { type Signal, signal } from \"@angular/core\";\nimport { type AgentLoadOptions, SignalsAgent } from \"@signap/signals-web\";\n\n/**\n * Holds a single lazily-loaded SignalsAgent and exposes its readiness as Angular\n * signals — the framework-native analog of the Vue wrapper's provide/inject refs\n * and the React wrapper's context. `agent()` is `null` until `SignalsAgent.load()`\n * settles; `loadError()` is non-null if it rejected (e.g. an invalid apiKey).\n *\n * Deliberately NOT `@Injectable`: a decorator-free class keeps this package\n * AOT-safe under a plain tsup build (no ngtsc / ng-packagr partial-Ivy metadata\n * step). It is wired into DI by the factory in `provideSignals()`.\n */\nexport class SignalsClient {\n private readonly agentState = signal<SignalsAgent | null>(null);\n private readonly loadErrorState = signal<Error | null>(null);\n\n /** Resolved agent, or `null` until `SignalsAgent.load()` settles. */\n readonly agent: Signal<SignalsAgent | null> = this.agentState.asReadonly();\n /** Non-null if `SignalsAgent.load()` rejected. */\n readonly loadError: Signal<Error | null> = this.loadErrorState.asReadonly();\n /**\n * Resolves once `load()` has settled (agent set OR loadError set). Lets\n * `injectVisitorData()` await readiness without an Angular `effect`, so the\n * auto-identify fires exactly once regardless of change-detection timing.\n */\n readonly ready: Promise<void>;\n\n /**\n * @param opts forwarded verbatim to `SignalsAgent.load()`.\n * @param browser whether we're running in a browser; `load()` only fires\n * client-side so SSR / prerender never touches\n * window/document/WebCrypto (the Angular analog of the Vue\n * plugin's `typeof window` guard).\n */\n constructor(opts: AgentLoadOptions, browser: boolean) {\n if (!browser) {\n this.ready = Promise.resolve();\n return;\n }\n this.ready = SignalsAgent.load(opts)\n .then((loaded) => {\n this.agentState.set(loaded);\n })\n .catch((err: unknown) => {\n this.loadErrorState.set(err instanceof Error ? err : new Error(String(err)));\n });\n }\n}\n","import { isPlatformBrowser } from \"@angular/common\";\nimport {\n type EnvironmentProviders,\n PLATFORM_ID,\n inject,\n makeEnvironmentProviders,\n} from \"@angular/core\";\nimport type { AgentLoadOptions } from \"@signap/signals-web\";\nimport { SignalsClient } from \"./client\";\nimport { SIGNALS_CLIENT } from \"./tokens\";\n\n/**\n * Registers a single app-wide SignalsAgent — the idiomatic standalone-Angular\n * entry point, analogous to React's `<SignalsProvider>` and Vue's\n * `app.use(SignalsPlugin)`:\n *\n * ```ts\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideSignals({ apiKey: \"pk_live_...\", endpoint: \"https://ingest.example\" }),\n * ],\n * });\n * ```\n *\n * SSR-safe: the factory injects `PLATFORM_ID` and only lets the client call\n * `SignalsAgent.load()` in a browser, so a server render / prerender never loads\n * the agent.\n *\n * `opts` is snapshotted here at bootstrap; to swap apiKey/endpoint, re-bootstrap\n * (same snapshot-at-init contract as the React/Vue wrappers).\n */\nexport function provideSignals(opts: AgentLoadOptions): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: SIGNALS_CLIENT,\n useFactory: () => new SignalsClient(opts, isPlatformBrowser(inject(PLATFORM_ID))),\n },\n ]);\n}\n","import { InjectionToken } from \"@angular/core\";\nimport type { SignalsClient } from \"./client\";\n\n/**\n * DI token the app-wide {@link SignalsClient} is registered under (by\n * `provideSignals()`) and that `injectVisitorData()` resolves. Most consumers\n * don't inject this directly — call `injectVisitorData()`. It is exported for\n * advanced use (e.g. `inject(SIGNALS_CLIENT).agent()` to read the raw agent).\n */\nexport const SIGNALS_CLIENT = new InjectionToken<SignalsClient>(\"signap.signals.client\");\n","import { DestroyRef, type Signal, assertInInjectionContext, inject, signal } from \"@angular/core\";\nimport type { IdentifyOptions, IdentifyResult } from \"@signap/signals-web\";\nimport { SIGNALS_CLIENT } from \"./tokens\";\n\nexport type InjectVisitorDataOptions = IdentifyOptions;\n\nexport interface InjectVisitorDataResult {\n /**\n * Resolved visitor from `agent.identify()`. Under ADR §A2 the call is a single\n * synchronous round-trip — `visitorId`/`confidence`/`identifiedAt` arrive\n * together, no polling. A `Signal`, so read it as `data()` (in a template or a\n * `computed`/`effect`).\n */\n data: Signal<IdentifyResult | undefined>;\n /** True between call time and the identify() resolution (or `error`). */\n isLoading: Signal<boolean>;\n error: Signal<Error | null>;\n /** Re-run `agent.identify()` (e.g. from a button click). */\n refetch: () => Promise<void>;\n}\n\n/**\n * Auto-identifies once the provider's agent is ready, then exposes the resolved\n * visitor as Angular signals — the framework-native analog of React's\n * `useVisitorData()` hook and Vue's `useVisitorData()` composable.\n *\n * Must be called in an injection context (a component/directive constructor or a\n * field initializer, or inside `runInInjectionContext`) below `provideSignals()`.\n */\nexport function injectVisitorData(opts?: InjectVisitorDataOptions): InjectVisitorDataResult {\n assertInInjectionContext(injectVisitorData);\n\n const client = inject(SIGNALS_CLIENT, { optional: true });\n if (!client) {\n throw new Error(\n \"[@signap/signals-angular] injectVisitorData() requires provideSignals() in the application (or component) providers\",\n );\n }\n\n const data = signal<IdentifyResult | undefined>(undefined);\n const error = signal<Error | null>(null);\n // Loading until the agent (or a load error) settles.\n const isLoading = signal(client.agent() === null && client.loadError() === null);\n\n // Supersede token so a slow response from a call that refetch() replaced (or a\n // response arriving after teardown) is discarded.\n let token = { cancelled: false };\n inject(DestroyRef).onDestroy(() => {\n token.cancelled = true;\n });\n\n const run = async (): Promise<void> => {\n const agent = client.agent();\n if (!agent) {\n const loadError = client.loadError();\n if (loadError) {\n error.set(loadError);\n isLoading.set(false);\n }\n return;\n }\n token.cancelled = true;\n const my = { cancelled: false };\n token = my;\n isLoading.set(true);\n error.set(null);\n data.set(undefined);\n try {\n const res = await agent.identify(opts);\n if (my.cancelled) return;\n data.set(res);\n } catch (err) {\n if (my.cancelled) return;\n error.set(err instanceof Error ? err : new Error(String(err)));\n } finally {\n if (!my.cancelled) isLoading.set(false);\n }\n };\n\n // Auto-run exactly once, after load() settles. `ready` resolves whether load()\n // succeeded or failed; even if it already resolved, `.then` defers to a\n // microtask so the returned signals are wired up before the first run().\n void client.ready.then(() => run());\n\n return { data, isLoading, error, refetch: run };\n}\n"],"mappings":";AAAA;AAAA,EAIE;AAAA,EAEW;AAAA,OACN;;;ACPP,SAAsB,cAAc;AACpC,SAAgC,oBAAoB;AAY7C,IAAM,gBAAN,MAAoB;AAAA,EACR,aAAa,OAA4B,IAAI;AAAA,EAC7C,iBAAiB,OAAqB,IAAI;AAAA;AAAA,EAGlD,QAAqC,KAAK,WAAW,WAAW;AAAA;AAAA,EAEhE,YAAkC,KAAK,eAAe,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMjE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAST,YAAY,MAAwB,SAAkB;AACpD,QAAI,CAAC,SAAS;AACZ,WAAK,QAAQ,QAAQ,QAAQ;AAC7B;AAAA,IACF;AACA,SAAK,QAAQ,aAAa,KAAK,IAAI,EAChC,KAAK,CAAC,WAAW;AAChB,WAAK,WAAW,IAAI,MAAM;AAAA,IAC5B,CAAC,EACA,MAAM,CAAC,QAAiB;AACvB,WAAK,eAAe,IAAI,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAC7E,CAAC;AAAA,EACL;AACF;;;AChDA,SAAS,yBAAyB;AAClC;AAAA,EAEE;AAAA,EACA;AAAA,EACA;AAAA,OACK;;;ACNP,SAAS,sBAAsB;AASxB,IAAM,iBAAiB,IAAI,eAA8B,uBAAuB;;;ADsBhF,SAAS,eAAe,MAA8C;AAC3E,SAAO,yBAAyB;AAAA,IAC9B;AAAA,MACE,SAAS;AAAA,MACT,YAAY,MAAM,IAAI,cAAc,MAAM,kBAAkB,OAAO,WAAW,CAAC,CAAC;AAAA,IAClF;AAAA,EACF,CAAC;AACH;;;AEtCA,SAAS,YAAyB,0BAA0B,UAAAA,SAAQ,UAAAC,eAAc;AA6B3E,SAAS,kBAAkB,MAA0D;AAC1F,2BAAyB,iBAAiB;AAE1C,QAAM,SAASC,QAAO,gBAAgB,EAAE,UAAU,KAAK,CAAC;AACxD,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,QAAM,OAAOC,QAAmC,MAAS;AACzD,QAAM,QAAQA,QAAqB,IAAI;AAEvC,QAAM,YAAYA,QAAO,OAAO,MAAM,MAAM,QAAQ,OAAO,UAAU,MAAM,IAAI;AAI/E,MAAI,QAAQ,EAAE,WAAW,MAAM;AAC/B,EAAAD,QAAO,UAAU,EAAE,UAAU,MAAM;AACjC,UAAM,YAAY;AAAA,EACpB,CAAC;AAED,QAAM,MAAM,YAA2B;AACrC,UAAM,QAAQ,OAAO,MAAM;AAC3B,QAAI,CAAC,OAAO;AACV,YAAM,YAAY,OAAO,UAAU;AACnC,UAAI,WAAW;AACb,cAAM,IAAI,SAAS;AACnB,kBAAU,IAAI,KAAK;AAAA,MACrB;AACA;AAAA,IACF;AACA,UAAM,YAAY;AAClB,UAAM,KAAK,EAAE,WAAW,MAAM;AAC9B,YAAQ;AACR,cAAU,IAAI,IAAI;AAClB,UAAM,IAAI,IAAI;AACd,SAAK,IAAI,MAAS;AAClB,QAAI;AACF,YAAM,MAAM,MAAM,MAAM,SAAS,IAAI;AACrC,UAAI,GAAG,UAAW;AAClB,WAAK,IAAI,GAAG;AAAA,IACd,SAAS,KAAK;AACZ,UAAI,GAAG,UAAW;AAClB,YAAM,IAAI,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;AAAA,IAC/D,UAAE;AACA,UAAI,CAAC,GAAG,UAAW,WAAU,IAAI,KAAK;AAAA,IACxC;AAAA,EACF;AAKA,OAAK,OAAO,MAAM,KAAK,MAAM,IAAI,CAAC;AAElC,SAAO,EAAE,MAAM,WAAW,OAAO,SAAS,IAAI;AAChD;","names":["inject","signal","inject","signal"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/client.ts","../src/provide-signals.ts","../src/tokens.ts","../src/inject-visitor-data.ts"],"sourcesContent":["export {\n type AgentLoadOptions,\n type IdentifyOptions,\n type IdentifyResult,\n SdkError,\n type SdkErrorCode,\n VERSION as WEB_SDK_VERSION,\n} from \"@signap/signals-web\";\n\nexport { SignalsClient } from \"./client\";\nexport { provideSignals, type SignalsConfig } from \"./provide-signals\";\nexport { SIGNALS_CLIENT } from \"./tokens\";\nexport {\n injectVisitorData,\n type InjectVisitorDataOptions,\n type InjectVisitorDataResult,\n} from \"./inject-visitor-data\";\n","import { type Signal, type WritableSignal, signal } from \"@angular/core\";\nimport { type AgentLoadOptions, SignalsAgent } from \"@signap/signals-web\";\n\n/**\n * Holds a single lazily-loaded SignalsAgent and exposes its readiness as Angular\n * signals — the framework-native analog of the Vue wrapper's provide/inject refs\n * and the React wrapper's context. `agent()` is `null` until `SignalsAgent.load()`\n * settles; `loadError()` is non-null if it rejected (e.g. an invalid apiKey).\n *\n * Deliberately NOT `@Injectable`: a decorator-free class keeps this package\n * AOT-safe under a plain tsup build (no ngtsc / ng-packagr partial-Ivy metadata\n * step). It is wired into DI by the factory in `provideSignals()`.\n */\nexport class SignalsClient {\n private readonly agentState = signal<SignalsAgent | null>(null);\n private readonly loadErrorState = signal<Error | null>(null);\n private readonly heldState: WritableSignal<boolean>;\n\n /** Resolved agent, or `null` until `SignalsAgent.load()` settles. */\n readonly agent: Signal<SignalsAgent | null> = this.agentState.asReadonly();\n /** Non-null if `SignalsAgent.load()` rejected. */\n readonly loadError: Signal<Error | null> = this.loadErrorState.asReadonly();\n /** True while held (`provideSignals(opts, { enabled })` with `enabled()` false): nothing is loaded. */\n readonly held: Signal<boolean>;\n /**\n * Resolves once the FIRST `load()` has settled (agent set OR loadError set), or\n * at once outside a browser. While the client is held before its first release,\n * it stays pending.\n */\n readonly ready: Promise<void>;\n\n private readonly opts: AgentLoadOptions;\n private readonly browser: boolean;\n private resolveReady: () => void = () => {};\n private started = false;\n private loaded: SignalsAgent | null = null;\n // Bumped on every withdrawal; a load() that settles under an older generation is discarded.\n private generation = 0;\n private readonly listeners = new Set<() => void>();\n\n /**\n * @param opts forwarded verbatim to `SignalsAgent.load()`.\n * @param browser whether we're running in a browser; `load()` only fires\n * client-side so SSR / prerender never touches\n * window/document/WebCrypto (the Angular analog of the Vue\n * plugin's `typeof window` guard).\n * @param initiallyHeld start held (issue 2894): do not load until `release()`.\n */\n constructor(opts: AgentLoadOptions, browser: boolean, initiallyHeld = false) {\n this.opts = opts;\n this.browser = browser;\n this.heldState = signal(initiallyHeld);\n this.held = this.heldState.asReadonly();\n this.ready = new Promise<void>((resolve) => {\n this.resolveReady = resolve;\n });\n if (!browser) {\n this.resolveReady();\n return;\n }\n if (!initiallyHeld) this.release();\n }\n\n /**\n * Leave the held state: load the agent once (a no-op if already released). What\n * `provideSignals()` calls when its `enabled` signal becomes true.\n */\n release(): void {\n this.heldState.set(false);\n if (!this.browser || this.started) return;\n this.started = true;\n const mine = this.generation;\n SignalsAgent.load(this.opts)\n .then((loaded) => {\n if (mine !== this.generation) {\n // Withdrawn while in flight. load() started the observers synchronously;\n // stop them, unless a newer release is running them now (page-wide singletons).\n if (!this.started) loaded.stopObservers();\n return;\n }\n this.loaded = loaded;\n this.agentState.set(loaded);\n this.settled();\n })\n .catch((err: unknown) => {\n if (mine !== this.generation) return;\n this.loadErrorState.set(err instanceof Error ? err : new Error(String(err)));\n this.settled();\n });\n }\n\n /**\n * Enter the held state. After a release this stops the SDK's interaction\n * listeners (page-wide, for this copy of the SDK), drops the agent and discards\n * a load still in flight; a request already sent is not recalled, and nothing\n * already received is deleted. Before any release it only marks the client held.\n */\n hold(): void {\n this.heldState.set(true);\n if (!this.started) {\n this.notify();\n return;\n }\n this.started = false;\n this.generation++;\n this.loaded?.stopObservers();\n this.loaded = null;\n this.agentState.set(null);\n this.loadErrorState.set(null);\n this.notify();\n }\n\n /**\n * @internal Called after every state change (a load settling, a hold). Lets\n * `injectVisitorData()` re-run without an Angular `effect`, so the auto-identify\n * fires exactly once per release regardless of change-detection timing.\n */\n onChange(listener: () => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n\n private settled(): void {\n this.resolveReady();\n this.notify();\n }\n\n private notify(): void {\n for (const l of [...this.listeners]) l();\n }\n}\n","import { isPlatformBrowser } from \"@angular/common\";\nimport {\n type EnvironmentProviders,\n PLATFORM_ID,\n type Signal,\n effect,\n inject,\n makeEnvironmentProviders,\n untracked,\n} from \"@angular/core\";\nimport type { AgentLoadOptions } from \"@signap/signals-web\";\nimport { SignalsClient } from \"./client\";\nimport { SIGNALS_CLIENT } from \"./tokens\";\n\n/**\n * Registers a single app-wide SignalsAgent — the idiomatic standalone-Angular\n * entry point, analogous to React's `<SignalsProvider>` and Vue's\n * `app.use(SignalsPlugin)`:\n *\n * ```ts\n * bootstrapApplication(AppComponent, {\n * providers: [\n * provideSignals({ apiKey: \"pk_live_...\", endpoint: \"https://ingest.example\" }),\n * ],\n * });\n * ```\n *\n * SSR-safe: the factory injects `PLATFORM_ID` and only lets the client call\n * `SignalsAgent.load()` in a browser, so a server render / prerender never loads\n * the agent.\n *\n * `opts` is snapshotted here at bootstrap; to swap apiKey/endpoint, re-bootstrap\n * (same snapshot-at-init contract as the React/Vue wrappers).\n */\nexport function provideSignals(\n opts: AgentLoadOptions,\n config: SignalsConfig = {},\n): EnvironmentProviders {\n return makeEnvironmentProviders([\n {\n provide: SIGNALS_CLIENT,\n useFactory: () => {\n const browser = isPlatformBrowser(inject(PLATFORM_ID));\n const { enabled } = config;\n if (!enabled) return new SignalsClient(opts, browser);\n const client = new SignalsClient(opts, browser, !untracked(enabled));\n // The factory runs in an injection context, so the effect lives as long as\n // the environment injector. The client's writes are untracked so they are\n // not writes \"inside\" the effect (Angular 17/18 reject those by default).\n effect(() => {\n const on = enabled();\n untracked(() => (on ? client.release() : client.hold()));\n });\n return client;\n },\n },\n ]);\n}\n\n/** Second argument of `provideSignals()`. */\nexport interface SignalsConfig {\n /**\n * While this signal is `false` the client is held: it does not load the agent, so\n * the SDK registers no event listener, runs no collector and sends no request, and\n * `injectVisitorData()` returns `isHeld() === true`. When it becomes `true` the\n * agent loads and the first identify runs, as if `enabled` had never been set.\n * When it goes back to `false` the SDK's interaction listeners stop (page-wide, for\n * this copy of the SDK) and any identify result still in flight is discarded; a\n * request already sent is not recalled, and nothing already received is deleted.\n * Omitted = never held.\n */\n enabled?: Signal<boolean>;\n}\n","import { InjectionToken } from \"@angular/core\";\nimport type { SignalsClient } from \"./client\";\n\n/**\n * DI token the app-wide {@link SignalsClient} is registered under (by\n * `provideSignals()`) and that `injectVisitorData()` resolves. Most consumers\n * don't inject this directly — call `injectVisitorData()`. It is exported for\n * advanced use (e.g. `inject(SIGNALS_CLIENT).agent()` to read the raw agent).\n */\nexport const SIGNALS_CLIENT = new InjectionToken<SignalsClient>(\"signap.signals.client\");\n","import {\n DestroyRef,\n type Signal,\n assertInInjectionContext,\n computed,\n inject,\n signal,\n} from \"@angular/core\";\nimport type { IdentifyOptions, IdentifyResult } from \"@signap/signals-web\";\nimport { SIGNALS_CLIENT } from \"./tokens\";\n\nexport type InjectVisitorDataOptions = IdentifyOptions;\n\nexport interface InjectVisitorDataResult {\n /**\n * Resolved visitor from `agent.identify()`. Under ADR §A2 the call is a single\n * synchronous round-trip — `visitorId`/`confidence`/`identifiedAt` arrive\n * together, no polling. A `Signal`, so read it as `data()` (in a template or a\n * `computed`/`effect`).\n */\n data: Signal<IdentifyResult | undefined>;\n /** True between call time and the identify() resolution (or `error`). False while held. */\n isLoading: Signal<boolean>;\n /**\n * True while `provideSignals(opts, { enabled })` is held. Nothing is loaded or sent,\n * `data()` is `undefined` and `refetch()` does nothing. Always false without `enabled`.\n */\n isHeld: Signal<boolean>;\n error: Signal<Error | null>;\n /** Re-run `agent.identify()` (e.g. from a button click). */\n refetch: () => Promise<void>;\n}\n\n/**\n * Auto-identifies once the provider's agent is ready, then exposes the resolved\n * visitor as Angular signals — the framework-native analog of React's\n * `useVisitorData()` hook and Vue's `useVisitorData()` composable.\n *\n * Must be called in an injection context (a component/directive constructor or a\n * field initializer, or inside `runInInjectionContext`) below `provideSignals()`.\n */\nexport function injectVisitorData(opts?: InjectVisitorDataOptions): InjectVisitorDataResult {\n assertInInjectionContext(injectVisitorData);\n\n const client = inject(SIGNALS_CLIENT, { optional: true });\n if (!client) {\n throw new Error(\n \"[@signap/signals-angular] injectVisitorData() requires provideSignals() in the application (or component) providers\",\n );\n }\n\n const data = signal<IdentifyResult | undefined>(undefined);\n const error = signal<Error | null>(null);\n // Loading until the agent (or a load error) settles — unless held.\n const isLoading = signal(\n !client.held() && client.agent() === null && client.loadError() === null,\n );\n const isHeld = computed(() => client.held());\n\n // Supersede token so a slow response from a call that refetch() replaced (or a\n // response arriving after teardown) is discarded.\n let token = { cancelled: false };\n let started = false;\n const onChange = (): void => {\n if (client.held()) {\n // Held (issue 2894): discard any identify in flight and any previous result, and\n // re-arm so a later release identifies once, like a fresh call.\n token.cancelled = true;\n started = false;\n data.set(undefined);\n error.set(null);\n isLoading.set(false);\n return;\n }\n if (started) return;\n if (client.agent() === null && client.loadError() === null) return; // not settled yet\n started = true;\n void run();\n };\n const unsubscribe = client.onChange(onChange);\n inject(DestroyRef).onDestroy(() => {\n token.cancelled = true;\n unsubscribe();\n });\n\n const run = async (): Promise<void> => {\n const agent = client.agent();\n if (!agent) {\n const loadError = client.loadError();\n if (loadError) {\n error.set(loadError);\n isLoading.set(false);\n }\n return;\n }\n token.cancelled = true;\n const my = { cancelled: false };\n token = my;\n isLoading.set(true);\n error.set(null);\n data.set(undefined);\n try {\n const res = await agent.identify(opts);\n if (my.cancelled) return;\n data.set(res);\n } catch (err) {\n if (my.cancelled) return;\n error.set(err instanceof Error ? err : new Error(String(err)));\n } finally {\n if (!my.cancelled) isLoading.set(false);\n }\n };\n\n // Auto-run once per release, after load() settles. The client calls onChange on\n // every settle and hold; the first look is deferred to a microtask (as the old\n // `ready.then` was) so the returned signals are wired up before the first run().\n void Promise.resolve().then(onChange);\n\n return { data, isLoading, isHeld, error, refetch: run };\n}\n"],"mappings":"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","names":["inject","signal","inject","signal"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signap/signals-angular",
3
- "version": "2.2.0-dev.20260930000246.gd4b7f02",
3
+ "version": "2.2.0",
4
4
  "description": "Angular provider + inject() helper wrapper for @signap/signals-web",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -22,7 +22,7 @@
22
22
  "@angular/core": ">=17 <21"
23
23
  },
24
24
  "dependencies": {
25
- "@signap/signals-web": "2.3.0-dev.20260930000246.gd4b7f02"
25
+ "@signap/signals-web": "^2.3.0"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@angular/common": "^19.0.0",