@razakalpha/convngx 0.2.4 → 0.2.5
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/fesm2022/razakalpha-convngx.mjs +767 -0
- package/fesm2022/razakalpha-convngx.mjs.map +1 -0
- package/index.d.ts +2678 -0
- package/package.json +26 -26
- package/ng-package.json +0 -7
- package/src/lib/auth/auth-client.provider.ts +0 -35
- package/src/lib/auth/convex-better-auth.provider.ts +0 -110
- package/src/lib/convex-angular.spec.ts +0 -23
- package/src/lib/convex-angular.ts +0 -15
- package/src/lib/core/convex-angular-client.ts +0 -351
- package/src/lib/core/helpers.ts +0 -60
- package/src/lib/core/inject-convex.token.ts +0 -12
- package/src/lib/core/types.ts +0 -14
- package/src/lib/resources/action.resource.ts +0 -153
- package/src/lib/resources/live.resource.ts +0 -149
- package/src/lib/resources/mutation.resource.ts +0 -171
- package/src/lib/setup/convex-angular.providers.ts +0 -66
- package/src/public-api.ts +0 -19
- package/tsconfig.lib.json +0 -20
- package/tsconfig.lib.prod.json +0 -11
- package/tsconfig.spec.json +0 -14
|
@@ -0,0 +1,767 @@
|
|
|
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
|
+
// Retry logic for transient failures (e.g., session not fully propagated)
|
|
383
|
+
let lastError;
|
|
384
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
385
|
+
try {
|
|
386
|
+
if (attempt > 0) {
|
|
387
|
+
// Wait before retry: 50ms, then 100ms
|
|
388
|
+
const delay = attempt * 50;
|
|
389
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
390
|
+
}
|
|
391
|
+
const { data } = await auth.convex.token();
|
|
392
|
+
return data?.token ?? null;
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
lastError = err;
|
|
396
|
+
// Only retry on "Failed to fetch" errors
|
|
397
|
+
if (attempt < 2 && err?.message === 'Failed to fetch') {
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
throw err;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
throw lastError;
|
|
404
|
+
};
|
|
405
|
+
client.setAuth(fetchAccessToken);
|
|
406
|
+
void client.warmAuth();
|
|
407
|
+
return client;
|
|
408
|
+
},
|
|
409
|
+
},
|
|
410
|
+
];
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Optional environment initializer to handle OTT (?ott=...) once and upgrade to a cookie session.
|
|
414
|
+
* Keep separate from main provider for explicit opt-in.
|
|
415
|
+
*/
|
|
416
|
+
function provideBetterAuthOttBootstrap() {
|
|
417
|
+
return provideEnvironmentInitializer(() => {
|
|
418
|
+
(async () => {
|
|
419
|
+
const auth = inject(AUTH_CLIENT);
|
|
420
|
+
const url = new URL(window.location.href);
|
|
421
|
+
const ott = url.searchParams.get('ott');
|
|
422
|
+
if (!ott)
|
|
423
|
+
return;
|
|
424
|
+
const result = await auth.crossDomain.oneTimeToken.verify({ token: ott });
|
|
425
|
+
const session = result?.data?.session;
|
|
426
|
+
if (session?.token) {
|
|
427
|
+
await auth.getSession({
|
|
428
|
+
fetchOptions: {
|
|
429
|
+
credentials: 'include',
|
|
430
|
+
headers: { Authorization: `Bearer ${session.token}` },
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
auth.updateSession();
|
|
434
|
+
}
|
|
435
|
+
url.searchParams.delete('ott');
|
|
436
|
+
window.history.replaceState({}, '', url.toString());
|
|
437
|
+
})();
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// convex-resource.ts
|
|
442
|
+
const DEFAULTS = { keep: 'last' };
|
|
443
|
+
const CONVEX_RESOURCE_OPTIONS = new InjectionToken('CONVEX_RESOURCE_OPTIONS', { factory: () => DEFAULTS });
|
|
444
|
+
function provideConvexResourceOptions(opts) {
|
|
445
|
+
return { provide: CONVEX_RESOURCE_OPTIONS, useValue: { ...DEFAULTS, ...opts } };
|
|
446
|
+
}
|
|
447
|
+
/** Impl */
|
|
448
|
+
function convexLiveResource(query, a, b) {
|
|
449
|
+
const convex = injectConvex();
|
|
450
|
+
const global = inject(CONVEX_RESOURCE_OPTIONS);
|
|
451
|
+
const keepMode = (typeof a === 'function' ? b : a)?.keep ?? global.keep;
|
|
452
|
+
const paramsFactory = typeof a === 'function' ? a : undefined;
|
|
453
|
+
const lastGlobal = signal(undefined, ...(ngDevMode ? [{ debugName: "lastGlobal" }] : []));
|
|
454
|
+
const argsSig = computed(() => {
|
|
455
|
+
if (!paramsFactory)
|
|
456
|
+
return {}; // no-args: always enabled
|
|
457
|
+
return paramsFactory();
|
|
458
|
+
}, ...(ngDevMode ? [{ debugName: "argsSig" }] : []));
|
|
459
|
+
// --- reload tagging ---
|
|
460
|
+
const reloadStamp = signal(0, ...(ngDevMode ? [{ debugName: "reloadStamp" }] : [])); // increments only when .reload() is called
|
|
461
|
+
let lastSeenReload = 0; // compared inside stream to detect reload
|
|
462
|
+
const request = computed(() => ({
|
|
463
|
+
args: argsSig(),
|
|
464
|
+
__r: reloadStamp(),
|
|
465
|
+
}), ...(ngDevMode ? [{ debugName: "request" }] : []));
|
|
466
|
+
const base = resource({
|
|
467
|
+
params: request,
|
|
468
|
+
stream: async ({ params, abortSignal }) => {
|
|
469
|
+
const s = signal({
|
|
470
|
+
value: keepMode === 'last' ? lastGlobal() : undefined,
|
|
471
|
+
}, ...(ngDevMode ? [{ debugName: "s" }] : []));
|
|
472
|
+
if (!params || !params.args)
|
|
473
|
+
return s; // gated
|
|
474
|
+
const isReload = params.__r !== lastSeenReload;
|
|
475
|
+
lastSeenReload = params.__r;
|
|
476
|
+
// One-shot fetch: ONLY on reload
|
|
477
|
+
if (isReload) {
|
|
478
|
+
convex
|
|
479
|
+
.query(query, params.args)
|
|
480
|
+
.then((next) => {
|
|
481
|
+
if (abortSignal.aborted)
|
|
482
|
+
return;
|
|
483
|
+
s.set({ value: next });
|
|
484
|
+
lastGlobal.set(next);
|
|
485
|
+
})
|
|
486
|
+
.catch((err) => {
|
|
487
|
+
if (abortSignal.aborted)
|
|
488
|
+
return;
|
|
489
|
+
s.set({ error: err });
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
// Live subscription (always)
|
|
493
|
+
const w = convex.watchQuery(query, params.args);
|
|
494
|
+
try {
|
|
495
|
+
const local = w.localQueryResult();
|
|
496
|
+
if (local !== undefined) {
|
|
497
|
+
s.set({ value: local });
|
|
498
|
+
lastGlobal.set(local);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
s.set({ error: err });
|
|
503
|
+
}
|
|
504
|
+
const off = w.onUpdate(() => {
|
|
505
|
+
try {
|
|
506
|
+
const next = w.localQueryResult();
|
|
507
|
+
s.set({ value: next });
|
|
508
|
+
lastGlobal.set(next);
|
|
509
|
+
}
|
|
510
|
+
catch (err) {
|
|
511
|
+
s.set({ error: err });
|
|
512
|
+
}
|
|
513
|
+
});
|
|
514
|
+
abortSignal.addEventListener('abort', () => off(), { once: true });
|
|
515
|
+
return s;
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
// Wrap to bump reloadStamp only when user calls reload()
|
|
519
|
+
const origReload = base.reload.bind(base);
|
|
520
|
+
const wrapped = {
|
|
521
|
+
...base,
|
|
522
|
+
reload: () => {
|
|
523
|
+
reloadStamp.set(reloadStamp() + 1);
|
|
524
|
+
return origReload();
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
return wrapped;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Single entry-point provider to wire Convex + Better Auth + resource defaults.
|
|
532
|
+
* Behavior:
|
|
533
|
+
* - If opts.authClient is provided, we use it (and verify required plugins).
|
|
534
|
+
* - Else we create a default Better Auth client using authBaseURL with required plugins.
|
|
535
|
+
*/
|
|
536
|
+
function provideConvexAngular(opts) {
|
|
537
|
+
const providers = [];
|
|
538
|
+
if (opts.authClient) {
|
|
539
|
+
providers.push({
|
|
540
|
+
provide: AUTH_CLIENT,
|
|
541
|
+
useFactory: () => {
|
|
542
|
+
const c = opts.authClient;
|
|
543
|
+
const hasConvex = typeof c.convex?.token === 'function';
|
|
544
|
+
const hasCross = typeof c.crossDomain?.oneTimeToken?.verify === 'function';
|
|
545
|
+
if (!hasConvex || !hasCross) {
|
|
546
|
+
throw new Error('Provided AUTH client is missing required plugins: convexClient() and crossDomainClient().');
|
|
547
|
+
}
|
|
548
|
+
return c;
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
// Default client with known plugin set (convex + crossDomain)
|
|
554
|
+
providers.push(provideAuthClient({
|
|
555
|
+
baseURL: opts.authBaseURL,
|
|
556
|
+
}));
|
|
557
|
+
}
|
|
558
|
+
providers.push(...provideConvexBetterAuth({
|
|
559
|
+
convexUrl: opts.convexUrl,
|
|
560
|
+
authSkewMs: opts.authSkewMs,
|
|
561
|
+
}));
|
|
562
|
+
if (opts.keep) {
|
|
563
|
+
providers.push(provideConvexResourceOptions({ keep: opts.keep }));
|
|
564
|
+
}
|
|
565
|
+
return providers;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// convex-mutation-resource.ts
|
|
569
|
+
/** Single impl */
|
|
570
|
+
function convexMutationResource(mutation, opts) {
|
|
571
|
+
const convex = injectConvex();
|
|
572
|
+
const isRunning = signal(false, ...(ngDevMode ? [{ debugName: "isRunning" }] : []));
|
|
573
|
+
const pending = new Map();
|
|
574
|
+
const inflight = signal(undefined, ...(ngDevMode ? [{ debugName: "inflight" }] : []));
|
|
575
|
+
const trigger = signal(undefined, ...(ngDevMode ? [{ debugName: "trigger" }] : []));
|
|
576
|
+
let seq = 0;
|
|
577
|
+
const state = resource({
|
|
578
|
+
params: computed(() => trigger()),
|
|
579
|
+
stream: async ({ params, abortSignal }) => {
|
|
580
|
+
const out = signal({
|
|
581
|
+
value: undefined,
|
|
582
|
+
}, ...(ngDevMode ? [{ debugName: "out" }] : []));
|
|
583
|
+
if (!params)
|
|
584
|
+
return out;
|
|
585
|
+
const done = () => {
|
|
586
|
+
// clean out waiter if still present
|
|
587
|
+
const w = pending.get(params.id);
|
|
588
|
+
if (w)
|
|
589
|
+
pending.delete(params.id);
|
|
590
|
+
};
|
|
591
|
+
const runOnce = async () => {
|
|
592
|
+
let attempt = 0;
|
|
593
|
+
const retries = opts?.retries ?? 0;
|
|
594
|
+
const delay = (n) => new Promise((r) => setTimeout(r, opts?.retryDelayMs?.(n) ?? 500 * n));
|
|
595
|
+
// NB: Convex mutations don’t support abort; we still observe abortSignal to stop emitting.
|
|
596
|
+
// We *do not* call mutation again on aborted; we just stop updating UI.
|
|
597
|
+
// If you choose mode: 'replace', older runs get superseded by newer trigger()s.
|
|
598
|
+
// That’s the main “cancellation” we can mimic here.
|
|
599
|
+
while (true) {
|
|
600
|
+
try {
|
|
601
|
+
const res = await convex.mutation(mutation, params.args, {
|
|
602
|
+
optimisticUpdate: opts?.optimisticUpdate,
|
|
603
|
+
});
|
|
604
|
+
if (!abortSignal.aborted) {
|
|
605
|
+
out.set({ value: res });
|
|
606
|
+
opts?.onSuccess?.(res);
|
|
607
|
+
pending.get(params.id)?.resolve(res);
|
|
608
|
+
}
|
|
609
|
+
done();
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
catch (e) {
|
|
613
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
614
|
+
if (!abortSignal.aborted) {
|
|
615
|
+
out.set({ error: err });
|
|
616
|
+
}
|
|
617
|
+
if (attempt >= retries) {
|
|
618
|
+
opts?.onError?.(err);
|
|
619
|
+
pending.get(params.id)?.reject(err);
|
|
620
|
+
done();
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
attempt++;
|
|
624
|
+
await delay(attempt);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
isRunning.set(true);
|
|
629
|
+
try {
|
|
630
|
+
const p = runOnce();
|
|
631
|
+
inflight.set(p);
|
|
632
|
+
await p;
|
|
633
|
+
}
|
|
634
|
+
finally {
|
|
635
|
+
if (!abortSignal.aborted)
|
|
636
|
+
isRunning.set(false);
|
|
637
|
+
if (inflight() && (await inflight()) === undefined)
|
|
638
|
+
inflight.set(undefined);
|
|
639
|
+
}
|
|
640
|
+
return out;
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
const data = computed(() => state.value(), ...(ngDevMode ? [{ debugName: "data" }] : []));
|
|
644
|
+
const error = computed(() => state.error(), ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
645
|
+
const run = (async (args) => {
|
|
646
|
+
const job = { id: ++seq, args: (args ?? {}) };
|
|
647
|
+
if (opts?.mode === 'drop' && inflight()) {
|
|
648
|
+
// return the inflight promise if present (best-effort)
|
|
649
|
+
return inflight();
|
|
650
|
+
}
|
|
651
|
+
if (opts?.mode === 'queue' && inflight()) {
|
|
652
|
+
await inflight();
|
|
653
|
+
}
|
|
654
|
+
const promise = new Promise((resolve, reject) => {
|
|
655
|
+
pending.set(job.id, { resolve, reject });
|
|
656
|
+
});
|
|
657
|
+
// "replace": push new job; older job’s UI will be superseded by the next emission
|
|
658
|
+
trigger.set(job);
|
|
659
|
+
return promise;
|
|
660
|
+
});
|
|
661
|
+
const reset = () => {
|
|
662
|
+
// clear UI state; does not affect in-flight promise
|
|
663
|
+
state.reset?.(); // safe if Angular adds reset later; otherwise ignore
|
|
664
|
+
};
|
|
665
|
+
return { run, state, data, error, isRunning: isRunning.asReadonly(), reset };
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// convex-action-resource.ts
|
|
669
|
+
/** Single impl */
|
|
670
|
+
function convexActionResource(action, opts) {
|
|
671
|
+
const convex = injectConvex();
|
|
672
|
+
const isRunning = signal(false, ...(ngDevMode ? [{ debugName: "isRunning" }] : []));
|
|
673
|
+
const inflight = signal(undefined, ...(ngDevMode ? [{ debugName: "inflight" }] : []));
|
|
674
|
+
const pending = new Map();
|
|
675
|
+
const trigger = signal(undefined, ...(ngDevMode ? [{ debugName: "trigger" }] : []));
|
|
676
|
+
let seq = 0;
|
|
677
|
+
const state = resource({
|
|
678
|
+
params: computed(() => trigger()),
|
|
679
|
+
stream: async ({ params, abortSignal }) => {
|
|
680
|
+
const out = signal({
|
|
681
|
+
value: undefined,
|
|
682
|
+
}, ...(ngDevMode ? [{ debugName: "out" }] : []));
|
|
683
|
+
if (!params)
|
|
684
|
+
return out;
|
|
685
|
+
const done = () => {
|
|
686
|
+
const w = pending.get(params.id);
|
|
687
|
+
if (w)
|
|
688
|
+
pending.delete(params.id);
|
|
689
|
+
};
|
|
690
|
+
const runOnce = async () => {
|
|
691
|
+
let attempt = 0;
|
|
692
|
+
const retries = opts?.retries ?? 0;
|
|
693
|
+
const delay = (n) => new Promise((r) => setTimeout(r, opts?.retryDelayMs?.(n) ?? 500 * n));
|
|
694
|
+
while (true) {
|
|
695
|
+
try {
|
|
696
|
+
const res = await convex.action(action, params.args);
|
|
697
|
+
if (!abortSignal.aborted) {
|
|
698
|
+
out.set({ value: res });
|
|
699
|
+
opts?.onSuccess?.(res);
|
|
700
|
+
pending.get(params.id)?.resolve(res);
|
|
701
|
+
}
|
|
702
|
+
done();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
catch (e) {
|
|
706
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
707
|
+
if (!abortSignal.aborted)
|
|
708
|
+
out.set({ error: err });
|
|
709
|
+
if (attempt >= retries) {
|
|
710
|
+
opts?.onError?.(err);
|
|
711
|
+
pending.get(params.id)?.reject(err);
|
|
712
|
+
done();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
attempt++;
|
|
716
|
+
await delay(attempt);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
isRunning.set(true);
|
|
721
|
+
try {
|
|
722
|
+
const p = runOnce();
|
|
723
|
+
inflight.set(p);
|
|
724
|
+
await p;
|
|
725
|
+
}
|
|
726
|
+
finally {
|
|
727
|
+
if (!abortSignal.aborted)
|
|
728
|
+
isRunning.set(false);
|
|
729
|
+
if (inflight() && (await inflight()) === undefined)
|
|
730
|
+
inflight.set(undefined);
|
|
731
|
+
}
|
|
732
|
+
return out;
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
const data = computed(() => state.value(), ...(ngDevMode ? [{ debugName: "data" }] : []));
|
|
736
|
+
const error = computed(() => state.error(), ...(ngDevMode ? [{ debugName: "error" }] : []));
|
|
737
|
+
const run = (async (args) => {
|
|
738
|
+
const job = { id: ++seq, args: (args ?? {}) };
|
|
739
|
+
if (opts?.mode === 'drop' && inflight()) {
|
|
740
|
+
return inflight();
|
|
741
|
+
}
|
|
742
|
+
if (opts?.mode === 'queue' && inflight()) {
|
|
743
|
+
await inflight();
|
|
744
|
+
}
|
|
745
|
+
const promise = new Promise((resolve, reject) => {
|
|
746
|
+
pending.set(job.id, { resolve, reject });
|
|
747
|
+
});
|
|
748
|
+
trigger.set(job); // "replace" naturally supersedes older emissions
|
|
749
|
+
return promise;
|
|
750
|
+
});
|
|
751
|
+
const reset = () => {
|
|
752
|
+
state.reset?.();
|
|
753
|
+
};
|
|
754
|
+
return { run, state, data, error, isRunning: isRunning.asReadonly(), reset };
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/*
|
|
758
|
+
* Public API Surface of convex-angular
|
|
759
|
+
*/
|
|
760
|
+
// Core client and DI token
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Generated bundle index. Do not edit.
|
|
764
|
+
*/
|
|
765
|
+
|
|
766
|
+
export { AUTH_CLIENT, CONVEX, CONVEX_RESOURCE_OPTIONS, ConvexAngularClient, convexActionResource, convexLiveResource, convexMutationResource, injectConvex, provideAuthClient, provideBetterAuthOttBootstrap, provideConvexAngular, provideConvexBetterAuth, provideConvexResourceOptions };
|
|
767
|
+
//# sourceMappingURL=razakalpha-convngx.mjs.map
|