@razakalpha/convngx 0.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.
@@ -0,0 +1,747 @@
1
+ import { BaseConvexClient, ConvexHttpClient } from 'convex/browser';
2
+ import { getFunctionName } from 'convex/server';
3
+ import { InjectionToken, inject, provideEnvironmentInitializer, signal, computed, resource } from '@angular/core';
4
+ import { createAuthClient } from 'better-auth/client';
5
+ import { convexClient, crossDomainClient } from '@convex-dev/better-auth/client/plugins';
6
+
7
+ /**
8
+ * Internal helpers for ConvexAngularClient.
9
+ * NOTE: Pure refactor; functionality unchanged.
10
+ */
11
+ const AUTH_KEY = 'convex:jwt';
12
+ const AUTH_CH = 'convex-auth';
13
+ /** Decode JWT exp claim (ms). Returns 0 if invalid. */
14
+ function jwtExpMs(jwt) {
15
+ try {
16
+ const payload = JSON.parse(atob(jwt.split('.')[1] || ''));
17
+ return typeof payload?.exp === 'number' ? payload.exp * 1000 : 0;
18
+ }
19
+ catch {
20
+ return 0;
21
+ }
22
+ }
23
+ /** Canonical JSON stringify for stable de-dupe keys. */
24
+ function stableStringify(input) {
25
+ const seen = new WeakSet();
26
+ const norm = (v) => {
27
+ if (v === null || typeof v !== 'object')
28
+ return v;
29
+ if (seen.has(v))
30
+ return v;
31
+ seen.add(v);
32
+ if (Array.isArray(v))
33
+ return v.map(norm);
34
+ return Object.keys(v)
35
+ .sort()
36
+ .reduce((acc, k) => {
37
+ acc[k] = norm(v[k]);
38
+ return acc;
39
+ }, {});
40
+ };
41
+ return JSON.stringify(norm(input));
42
+ }
43
+ /** sessionStorage helpers (some browsers may throw on access) */
44
+ const safeSession = {
45
+ get: (k) => {
46
+ try {
47
+ return typeof sessionStorage !== 'undefined' ? sessionStorage.getItem(k) : null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ },
53
+ set: (k, v) => {
54
+ try {
55
+ if (typeof sessionStorage !== 'undefined')
56
+ sessionStorage.setItem(k, v);
57
+ }
58
+ catch {
59
+ /* no-op */
60
+ }
61
+ },
62
+ del: (k) => {
63
+ try {
64
+ if (typeof sessionStorage !== 'undefined')
65
+ sessionStorage.removeItem(k);
66
+ }
67
+ catch {
68
+ /* no-op */
69
+ }
70
+ },
71
+ };
72
+
73
+ /**
74
+ * ConvexAngularClient
75
+ * - Wraps Convex BaseConvexClient + ConvexHttpClient
76
+ * - Integrates Better Auth via pluggable fetcher (setAuth)
77
+ * - Caches JWT in sessionStorage with BroadcastChannel sync
78
+ * - Auto-refreshes auth token ahead of expiry with jitter
79
+ * - Provides:
80
+ * - watchQuery: live subscription with localQueryResult + onUpdate
81
+ * - query: HTTP one-shot with in-flight de-dupe + 401 retry
82
+ * - mutation: supports optimisticUpdate passthrough
83
+ * - action: HTTP call with 401 retry
84
+ * - Does not change any runtime behavior – documentation and structure only.
85
+ */
86
+ // src/app/convex-angular-client.ts
87
+ /* helpers moved to ./helpers (no behavior change) */
88
+ const bc = typeof BroadcastChannel !== 'undefined'
89
+ ? new BroadcastChannel(AUTH_CH)
90
+ : null;
91
+ class ConvexAngularClient {
92
+ authListeners = new Set();
93
+ lastSnap;
94
+ // ==== internals ====
95
+ emitAuth() {
96
+ const snap = this.getAuthSnapshot();
97
+ // de-dupe emissions
98
+ if (this.lastSnap &&
99
+ this.lastSnap.isAuthenticated === snap.isAuthenticated &&
100
+ this.lastSnap.token === snap.token) {
101
+ return;
102
+ }
103
+ this.lastSnap = snap;
104
+ for (const cb of this.authListeners)
105
+ cb(snap);
106
+ }
107
+ base;
108
+ http;
109
+ byToken = new Map();
110
+ fetchToken;
111
+ inflightToken;
112
+ token;
113
+ refreshTimer;
114
+ inflightHttp = new Map();
115
+ authLocked = false;
116
+ skewMs;
117
+ constructor(url, opts) {
118
+ this.skewMs = opts?.authSkewMs ?? 30_000;
119
+ this.base = new BaseConvexClient(url, (updatedTokens) => {
120
+ for (const t of updatedTokens) {
121
+ const e = this.byToken.get(t);
122
+ if (!e)
123
+ continue;
124
+ for (const cb of e.listeners)
125
+ cb();
126
+ }
127
+ }, opts);
128
+ this.http = new ConvexHttpClient(url);
129
+ // pick up cached token
130
+ const raw = safeSession.get(AUTH_KEY);
131
+ if (raw) {
132
+ try {
133
+ const t = JSON.parse(raw);
134
+ if (t?.value && t?.exp && Date.now() < t.exp - this.skewMs) {
135
+ this.token = t;
136
+ this.http.setAuth(t.value);
137
+ this.scheduleRefresh();
138
+ }
139
+ }
140
+ catch { }
141
+ }
142
+ // 3) BroadcastChannel: keep as-is (clear only on 'clear', set on 'set')
143
+ bc?.addEventListener('message', (ev) => {
144
+ const d = ev.data;
145
+ if (d?.type === 'set') {
146
+ if (this.authLocked)
147
+ return; // ignore while logging out
148
+ this.applyToken(d.token?.value ?? null);
149
+ }
150
+ else if (d?.type === 'clear') {
151
+ this.applyToken(null);
152
+ }
153
+ });
154
+ // visibility/network pokes
155
+ const poke = () => {
156
+ if (!this.freshTokenInCache())
157
+ void this.getToken(true);
158
+ };
159
+ if (typeof window !== 'undefined') {
160
+ window.addEventListener('online', poke);
161
+ document.addEventListener?.('visibilitychange', () => {
162
+ if (document.visibilityState === 'visible')
163
+ poke();
164
+ });
165
+ }
166
+ }
167
+ setAuth(fetcher) {
168
+ this.fetchToken = fetcher;
169
+ this.base.setAuth(async ({ forceRefreshToken }) => (this.authLocked ? null : this.getToken(forceRefreshToken)), () => {
170
+ // identity changed (connect/reconnect). Do NOT clear token here.
171
+ // If we're truly logged out, next HTTP/WS use will 401 and we clear then.
172
+ this.inflightToken = undefined;
173
+ this.emitAuth(); // soft notify, no flip to false
174
+ });
175
+ }
176
+ /** Optional: call once on app start */
177
+ async warmAuth() {
178
+ await this.getToken(true);
179
+ }
180
+ freshTokenInCache() {
181
+ if (!this.token)
182
+ return null;
183
+ return Date.now() < this.token.exp - this.skewMs ? this.token.value : null;
184
+ }
185
+ // ==== public auth helpers ====
186
+ onAuth(cb) {
187
+ cb(this.getAuthSnapshot());
188
+ this.authListeners.add(cb);
189
+ return () => this.authListeners.delete(cb);
190
+ }
191
+ logoutLocal(lock = true) {
192
+ if (lock)
193
+ this.authLocked = true;
194
+ this.inflightToken = undefined;
195
+ this.applyToken(null); // this is the ONLY place we hard clear locally
196
+ }
197
+ // 5) snapshot uses token presence (not freshness)
198
+ getAuthSnapshot() {
199
+ return {
200
+ isAuthenticated: !!this.token?.value,
201
+ token: this.token?.value ?? null,
202
+ exp: this.token?.exp,
203
+ };
204
+ }
205
+ /** Allow re-auth then fetch a fresh token (use after successful sign-in) */
206
+ async refreshAuth() {
207
+ this.authLocked = false;
208
+ await this.getToken(true);
209
+ }
210
+ // 4) applyToken: always emit after mutation (you already fixed this)
211
+ applyToken(token) {
212
+ if (!token) {
213
+ this.token = undefined;
214
+ this.http.clearAuth();
215
+ safeSession.del(AUTH_KEY);
216
+ bc?.postMessage({ type: 'clear' });
217
+ if (this.refreshTimer)
218
+ window.clearTimeout(this.refreshTimer);
219
+ }
220
+ else {
221
+ if (this.authLocked) {
222
+ this.emitAuth();
223
+ return;
224
+ }
225
+ const exp = jwtExpMs(token);
226
+ this.token = { value: token, exp };
227
+ this.http.setAuth(token);
228
+ safeSession.set(AUTH_KEY, JSON.stringify(this.token));
229
+ bc?.postMessage({ type: 'set', token: this.token });
230
+ this.scheduleRefresh();
231
+ }
232
+ this.emitAuth();
233
+ }
234
+ scheduleRefresh() {
235
+ if (!this.token)
236
+ return;
237
+ if (this.refreshTimer)
238
+ window.clearTimeout(this.refreshTimer);
239
+ const jitter = 2_000 + Math.floor(Math.random() * 2_000);
240
+ const due = Math.max(0, this.token.exp - this.skewMs - Date.now() - jitter);
241
+ this.refreshTimer = window.setTimeout(() => {
242
+ void this.getToken(true);
243
+ }, due);
244
+ }
245
+ async getToken(force) {
246
+ if (!this.fetchToken || this.authLocked)
247
+ return null; // 🔒 deny any token
248
+ const cached = this.freshTokenInCache();
249
+ if (!force && cached)
250
+ return cached;
251
+ if (!this.inflightToken) {
252
+ this.inflightToken = (async () => {
253
+ const t = await this.fetchToken({ forceRefreshToken: true });
254
+ // ignore token if we got locked while waiting
255
+ if (this.authLocked) {
256
+ this.emitAuth(); // ensure listeners see current (likely false)
257
+ return null;
258
+ }
259
+ this.applyToken(t ?? null);
260
+ return t ?? null;
261
+ })().finally(() => setTimeout(() => (this.inflightToken = undefined), 0));
262
+ }
263
+ return await this.inflightToken;
264
+ }
265
+ async ensureHttpAuth() {
266
+ await this.getToken(false);
267
+ }
268
+ // ——— live query ———
269
+ watchQuery(q, args) {
270
+ const name = getFunctionName(q);
271
+ const valueArgs = (args ?? {});
272
+ const { queryToken, unsubscribe } = this.base.subscribe(name, valueArgs);
273
+ const entry = { name, args: valueArgs, listeners: new Set(), unsubscribe };
274
+ this.byToken.set(queryToken, entry);
275
+ return {
276
+ localQueryResult: () => this.base.localQueryResult(name, valueArgs),
277
+ onUpdate: (cb) => {
278
+ entry.listeners.add(cb);
279
+ return () => entry.listeners.delete(cb);
280
+ },
281
+ unsubscribe: () => {
282
+ entry.unsubscribe();
283
+ this.byToken.delete(queryToken);
284
+ },
285
+ };
286
+ }
287
+ // ——— mutation ———
288
+ async mutation(m, args, opts) {
289
+ const name = getFunctionName(m);
290
+ return this.base.mutation(name, args, opts?.optimisticUpdate ? { optimisticUpdate: opts.optimisticUpdate } : undefined);
291
+ }
292
+ async action(a, args) {
293
+ await this.ensureHttpAuth();
294
+ const call = () => this.http.action(a, ...(args ? [args] : []));
295
+ try {
296
+ return await call();
297
+ }
298
+ catch (e) {
299
+ const status = e?.status ?? e?.response?.status;
300
+ if (this.fetchToken && status === 401) {
301
+ await this.getToken(true);
302
+ return await call();
303
+ }
304
+ throw e;
305
+ }
306
+ }
307
+ async query(q, args) {
308
+ await this.ensureHttpAuth();
309
+ const name = getFunctionName(q);
310
+ const key = name + ':' + (args ? stableStringify(args) : '');
311
+ if (!this.inflightHttp.has(key)) {
312
+ this.inflightHttp.set(key, (async () => {
313
+ try {
314
+ return await this.http.query(q, ...(args ? [args] : []));
315
+ }
316
+ catch (e) {
317
+ const status = e?.status ?? e?.response?.status;
318
+ if (this.fetchToken && status === 401) {
319
+ await this.getToken(true);
320
+ return await this.http.query(q, ...(args ? [args] : []));
321
+ }
322
+ throw e;
323
+ }
324
+ finally {
325
+ setTimeout(() => this.inflightHttp.delete(key), 0);
326
+ }
327
+ })());
328
+ }
329
+ return this.inflightHttp.get(key);
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Injection token and helper for accessing the Convex client from DI.
335
+ * Keep this tiny and stable — many helpers rely on it.
336
+ */
337
+ /** DI token for the configured ConvexAngularClient instance */
338
+ const CONVEX = new InjectionToken('CONVEX');
339
+ /** Convenience helper to inject the Convex client */
340
+ const injectConvex = () => inject(CONVEX);
341
+
342
+ /**
343
+ * Un-typed DI token. We don't re-export types; callers who create the client
344
+ * get full type safety from better-auth directly.
345
+ */
346
+ const AUTH_CLIENT = new InjectionToken('AUTH_CLIENT');
347
+ /** Provide a configured Better Auth client via DI (default convex+crossDomain plugins) */
348
+ function provideAuthClient(opts) {
349
+ return {
350
+ provide: AUTH_CLIENT,
351
+ useFactory: () => createAuthClient({
352
+ baseURL: opts.baseURL,
353
+ plugins: [convexClient(), crossDomainClient()],
354
+ fetchOptions: { credentials: 'include', ...(opts.fetchOptions ?? {}) },
355
+ }),
356
+ };
357
+ }
358
+
359
+ /**
360
+ * Convex + Better Auth providers.
361
+ * - provideConvexBetterAuth: Registers a ConvexAngularClient in DI and wires Better Auth token fetching.
362
+ * - provideBetterAuthOttBootstrap: Optional environment initializer to handle cross-domain one-time-token.
363
+ *
364
+ * No functional changes—cleaned comments and added docs.
365
+ */
366
+ /**
367
+ * Registers the Convex client in DI and connects it to Better Auth for JWT retrieval.
368
+ * Consumers still need to provide the Better Auth HTTP client via provideAuthClient().
369
+ */
370
+ function provideConvexBetterAuth(opts) {
371
+ return [
372
+ {
373
+ provide: CONVEX,
374
+ useFactory: () => {
375
+ const client = new ConvexAngularClient(opts.convexUrl, {
376
+ authSkewMs: opts.authSkewMs ?? 45_000,
377
+ });
378
+ const auth = inject(AUTH_CLIENT);
379
+ const fetchAccessToken = async ({ forceRefreshToken }) => {
380
+ if (!forceRefreshToken)
381
+ return null;
382
+ const { data } = await auth.convex.token();
383
+ return data?.token ?? null;
384
+ };
385
+ client.setAuth(fetchAccessToken);
386
+ void client.warmAuth();
387
+ return client;
388
+ },
389
+ },
390
+ ];
391
+ }
392
+ /**
393
+ * Optional environment initializer to handle OTT (?ott=...) once and upgrade to a cookie session.
394
+ * Keep separate from main provider for explicit opt-in.
395
+ */
396
+ function provideBetterAuthOttBootstrap() {
397
+ return provideEnvironmentInitializer(() => {
398
+ (async () => {
399
+ const auth = inject(AUTH_CLIENT);
400
+ const url = new URL(window.location.href);
401
+ const ott = url.searchParams.get('ott');
402
+ if (!ott)
403
+ return;
404
+ const result = await auth.crossDomain.oneTimeToken.verify({ token: ott });
405
+ const session = result?.data?.session;
406
+ if (session?.token) {
407
+ await auth.getSession({
408
+ fetchOptions: {
409
+ credentials: 'include',
410
+ headers: { Authorization: `Bearer ${session.token}` },
411
+ },
412
+ });
413
+ auth.updateSession();
414
+ }
415
+ url.searchParams.delete('ott');
416
+ window.history.replaceState({}, '', url.toString());
417
+ })();
418
+ });
419
+ }
420
+
421
+ // convex-resource.ts
422
+ const DEFAULTS = { keep: 'last' };
423
+ const CONVEX_RESOURCE_OPTIONS = new InjectionToken('CONVEX_RESOURCE_OPTIONS', { factory: () => DEFAULTS });
424
+ function provideConvexResourceOptions(opts) {
425
+ return { provide: CONVEX_RESOURCE_OPTIONS, useValue: { ...DEFAULTS, ...opts } };
426
+ }
427
+ /** Impl */
428
+ function convexLiveResource(query, a, b) {
429
+ const convex = injectConvex();
430
+ const global = inject(CONVEX_RESOURCE_OPTIONS);
431
+ const keepMode = (typeof a === 'function' ? b : a)?.keep ?? global.keep;
432
+ const paramsFactory = typeof a === 'function' ? a : undefined;
433
+ const lastGlobal = signal(undefined, ...(ngDevMode ? [{ debugName: "lastGlobal" }] : []));
434
+ const argsSig = computed(() => {
435
+ if (!paramsFactory)
436
+ return {}; // no-args: always enabled
437
+ return paramsFactory();
438
+ }, ...(ngDevMode ? [{ debugName: "argsSig" }] : []));
439
+ // --- reload tagging ---
440
+ const reloadStamp = signal(0, ...(ngDevMode ? [{ debugName: "reloadStamp" }] : [])); // increments only when .reload() is called
441
+ let lastSeenReload = 0; // compared inside stream to detect reload
442
+ const request = computed(() => ({
443
+ args: argsSig(),
444
+ __r: reloadStamp(),
445
+ }), ...(ngDevMode ? [{ debugName: "request" }] : []));
446
+ const base = resource({
447
+ params: request,
448
+ stream: async ({ params, abortSignal }) => {
449
+ const s = signal({
450
+ value: keepMode === 'last' ? lastGlobal() : undefined,
451
+ }, ...(ngDevMode ? [{ debugName: "s" }] : []));
452
+ if (!params || !params.args)
453
+ return s; // gated
454
+ const isReload = params.__r !== lastSeenReload;
455
+ lastSeenReload = params.__r;
456
+ // One-shot fetch: ONLY on reload
457
+ if (isReload) {
458
+ convex
459
+ .query(query, params.args)
460
+ .then((next) => {
461
+ if (abortSignal.aborted)
462
+ return;
463
+ s.set({ value: next });
464
+ lastGlobal.set(next);
465
+ })
466
+ .catch((err) => {
467
+ if (abortSignal.aborted)
468
+ return;
469
+ s.set({ error: err });
470
+ });
471
+ }
472
+ // Live subscription (always)
473
+ const w = convex.watchQuery(query, params.args);
474
+ try {
475
+ const local = w.localQueryResult();
476
+ if (local !== undefined) {
477
+ s.set({ value: local });
478
+ lastGlobal.set(local);
479
+ }
480
+ }
481
+ catch (err) {
482
+ s.set({ error: err });
483
+ }
484
+ const off = w.onUpdate(() => {
485
+ try {
486
+ const next = w.localQueryResult();
487
+ s.set({ value: next });
488
+ lastGlobal.set(next);
489
+ }
490
+ catch (err) {
491
+ s.set({ error: err });
492
+ }
493
+ });
494
+ abortSignal.addEventListener('abort', () => off(), { once: true });
495
+ return s;
496
+ },
497
+ });
498
+ // Wrap to bump reloadStamp only when user calls reload()
499
+ const origReload = base.reload.bind(base);
500
+ const wrapped = {
501
+ ...base,
502
+ reload: () => {
503
+ reloadStamp.set(reloadStamp() + 1);
504
+ return origReload();
505
+ },
506
+ };
507
+ return wrapped;
508
+ }
509
+
510
+ /**
511
+ * Single entry-point provider to wire Convex + Better Auth + resource defaults.
512
+ * Behavior:
513
+ * - If opts.authClient is provided, we use it (and verify required plugins).
514
+ * - Else we create a default Better Auth client using authBaseURL with required plugins.
515
+ */
516
+ function provideConvexAngular(opts) {
517
+ const providers = [];
518
+ if (opts.authClient) {
519
+ providers.push({
520
+ provide: AUTH_CLIENT,
521
+ useFactory: () => {
522
+ const c = opts.authClient;
523
+ const hasConvex = typeof c.convex?.token === 'function';
524
+ const hasCross = typeof c.crossDomain?.oneTimeToken?.verify === 'function';
525
+ if (!hasConvex || !hasCross) {
526
+ throw new Error('Provided AUTH client is missing required plugins: convexClient() and crossDomainClient().');
527
+ }
528
+ return c;
529
+ },
530
+ });
531
+ }
532
+ else {
533
+ // Default client with known plugin set (convex + crossDomain)
534
+ providers.push(provideAuthClient({
535
+ baseURL: opts.authBaseURL,
536
+ }));
537
+ }
538
+ providers.push(...provideConvexBetterAuth({
539
+ convexUrl: opts.convexUrl,
540
+ authSkewMs: opts.authSkewMs,
541
+ }));
542
+ if (opts.keep) {
543
+ providers.push(provideConvexResourceOptions({ keep: opts.keep }));
544
+ }
545
+ return providers;
546
+ }
547
+
548
+ // convex-mutation-resource.ts
549
+ /** Single impl */
550
+ function convexMutationResource(mutation, opts) {
551
+ const convex = injectConvex();
552
+ const isRunning = signal(false, ...(ngDevMode ? [{ debugName: "isRunning" }] : []));
553
+ const pending = new Map();
554
+ const inflight = signal(undefined, ...(ngDevMode ? [{ debugName: "inflight" }] : []));
555
+ const trigger = signal(undefined, ...(ngDevMode ? [{ debugName: "trigger" }] : []));
556
+ let seq = 0;
557
+ const state = resource({
558
+ params: computed(() => trigger()),
559
+ stream: async ({ params, abortSignal }) => {
560
+ const out = signal({
561
+ value: undefined,
562
+ }, ...(ngDevMode ? [{ debugName: "out" }] : []));
563
+ if (!params)
564
+ return out;
565
+ const done = () => {
566
+ // clean out waiter if still present
567
+ const w = pending.get(params.id);
568
+ if (w)
569
+ pending.delete(params.id);
570
+ };
571
+ const runOnce = async () => {
572
+ let attempt = 0;
573
+ const retries = opts?.retries ?? 0;
574
+ const delay = (n) => new Promise((r) => setTimeout(r, opts?.retryDelayMs?.(n) ?? 500 * n));
575
+ // NB: Convex mutations don’t support abort; we still observe abortSignal to stop emitting.
576
+ // We *do not* call mutation again on aborted; we just stop updating UI.
577
+ // If you choose mode: 'replace', older runs get superseded by newer trigger()s.
578
+ // That’s the main “cancellation” we can mimic here.
579
+ while (true) {
580
+ try {
581
+ const res = await convex.mutation(mutation, params.args, {
582
+ optimisticUpdate: opts?.optimisticUpdate,
583
+ });
584
+ if (!abortSignal.aborted) {
585
+ out.set({ value: res });
586
+ opts?.onSuccess?.(res);
587
+ pending.get(params.id)?.resolve(res);
588
+ }
589
+ done();
590
+ return;
591
+ }
592
+ catch (e) {
593
+ const err = e instanceof Error ? e : new Error(String(e));
594
+ if (!abortSignal.aborted) {
595
+ out.set({ error: err });
596
+ }
597
+ if (attempt >= retries) {
598
+ opts?.onError?.(err);
599
+ pending.get(params.id)?.reject(err);
600
+ done();
601
+ return;
602
+ }
603
+ attempt++;
604
+ await delay(attempt);
605
+ }
606
+ }
607
+ };
608
+ isRunning.set(true);
609
+ try {
610
+ const p = runOnce();
611
+ inflight.set(p);
612
+ await p;
613
+ }
614
+ finally {
615
+ if (!abortSignal.aborted)
616
+ isRunning.set(false);
617
+ if (inflight() && (await inflight()) === undefined)
618
+ inflight.set(undefined);
619
+ }
620
+ return out;
621
+ },
622
+ });
623
+ const data = computed(() => state.value(), ...(ngDevMode ? [{ debugName: "data" }] : []));
624
+ const error = computed(() => state.error(), ...(ngDevMode ? [{ debugName: "error" }] : []));
625
+ const run = (async (args) => {
626
+ const job = { id: ++seq, args: (args ?? {}) };
627
+ if (opts?.mode === 'drop' && inflight()) {
628
+ // return the inflight promise if present (best-effort)
629
+ return inflight();
630
+ }
631
+ if (opts?.mode === 'queue' && inflight()) {
632
+ await inflight();
633
+ }
634
+ const promise = new Promise((resolve, reject) => {
635
+ pending.set(job.id, { resolve, reject });
636
+ });
637
+ // "replace": push new job; older job’s UI will be superseded by the next emission
638
+ trigger.set(job);
639
+ return promise;
640
+ });
641
+ const reset = () => {
642
+ // clear UI state; does not affect in-flight promise
643
+ state.reset?.(); // safe if Angular adds reset later; otherwise ignore
644
+ };
645
+ return { run, state, data, error, isRunning: isRunning.asReadonly(), reset };
646
+ }
647
+
648
+ // convex-action-resource.ts
649
+ /** Single impl */
650
+ function convexActionResource(action, opts) {
651
+ const convex = injectConvex();
652
+ const isRunning = signal(false, ...(ngDevMode ? [{ debugName: "isRunning" }] : []));
653
+ const inflight = signal(undefined, ...(ngDevMode ? [{ debugName: "inflight" }] : []));
654
+ const pending = new Map();
655
+ const trigger = signal(undefined, ...(ngDevMode ? [{ debugName: "trigger" }] : []));
656
+ let seq = 0;
657
+ const state = resource({
658
+ params: computed(() => trigger()),
659
+ stream: async ({ params, abortSignal }) => {
660
+ const out = signal({
661
+ value: undefined,
662
+ }, ...(ngDevMode ? [{ debugName: "out" }] : []));
663
+ if (!params)
664
+ return out;
665
+ const done = () => {
666
+ const w = pending.get(params.id);
667
+ if (w)
668
+ pending.delete(params.id);
669
+ };
670
+ const runOnce = async () => {
671
+ let attempt = 0;
672
+ const retries = opts?.retries ?? 0;
673
+ const delay = (n) => new Promise((r) => setTimeout(r, opts?.retryDelayMs?.(n) ?? 500 * n));
674
+ while (true) {
675
+ try {
676
+ const res = await convex.action(action, params.args);
677
+ if (!abortSignal.aborted) {
678
+ out.set({ value: res });
679
+ opts?.onSuccess?.(res);
680
+ pending.get(params.id)?.resolve(res);
681
+ }
682
+ done();
683
+ return;
684
+ }
685
+ catch (e) {
686
+ const err = e instanceof Error ? e : new Error(String(e));
687
+ if (!abortSignal.aborted)
688
+ out.set({ error: err });
689
+ if (attempt >= retries) {
690
+ opts?.onError?.(err);
691
+ pending.get(params.id)?.reject(err);
692
+ done();
693
+ return;
694
+ }
695
+ attempt++;
696
+ await delay(attempt);
697
+ }
698
+ }
699
+ };
700
+ isRunning.set(true);
701
+ try {
702
+ const p = runOnce();
703
+ inflight.set(p);
704
+ await p;
705
+ }
706
+ finally {
707
+ if (!abortSignal.aborted)
708
+ isRunning.set(false);
709
+ if (inflight() && (await inflight()) === undefined)
710
+ inflight.set(undefined);
711
+ }
712
+ return out;
713
+ },
714
+ });
715
+ const data = computed(() => state.value(), ...(ngDevMode ? [{ debugName: "data" }] : []));
716
+ const error = computed(() => state.error(), ...(ngDevMode ? [{ debugName: "error" }] : []));
717
+ const run = (async (args) => {
718
+ const job = { id: ++seq, args: (args ?? {}) };
719
+ if (opts?.mode === 'drop' && inflight()) {
720
+ return inflight();
721
+ }
722
+ if (opts?.mode === 'queue' && inflight()) {
723
+ await inflight();
724
+ }
725
+ const promise = new Promise((resolve, reject) => {
726
+ pending.set(job.id, { resolve, reject });
727
+ });
728
+ trigger.set(job); // "replace" naturally supersedes older emissions
729
+ return promise;
730
+ });
731
+ const reset = () => {
732
+ state.reset?.();
733
+ };
734
+ return { run, state, data, error, isRunning: isRunning.asReadonly(), reset };
735
+ }
736
+
737
+ /*
738
+ * Public API Surface of convex-angular
739
+ */
740
+ // Core client and DI token
741
+
742
+ /**
743
+ * Generated bundle index. Do not edit.
744
+ */
745
+
746
+ export { AUTH_CLIENT, CONVEX, CONVEX_RESOURCE_OPTIONS, ConvexAngularClient, convexActionResource, convexLiveResource, convexMutationResource, injectConvex, provideAuthClient, provideBetterAuthOttBootstrap, provideConvexAngular, provideConvexBetterAuth, provideConvexResourceOptions };
747
+ //# sourceMappingURL=alpha-convngx.mjs.map