@auth-craft/client 0.2.1 → 0.3.1
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 +46 -0
- package/dist/core/core.d.ts +13 -0
- package/dist/core/core.d.ts.map +1 -1
- package/dist/core/core.js +4 -0
- package/dist/core/core.js.map +1 -1
- package/dist/transport/client.d.ts +22 -1
- package/dist/transport/client.d.ts.map +1 -1
- package/dist/transport/client.js +130 -99
- package/dist/transport/client.js.map +1 -1
- package/dist/transport/index.d.ts +5 -1
- package/dist/transport/index.d.ts.map +1 -1
- package/dist/transport/index.js +3 -0
- package/dist/transport/index.js.map +1 -1
- package/dist/transport/inline-worker.d.ts +6 -0
- package/dist/transport/inline-worker.d.ts.map +1 -1
- package/dist/transport/inline-worker.js +8 -1
- package/dist/transport/inline-worker.js.map +1 -1
- package/dist/transport/provider/storage/session-hint-channel.d.ts +34 -0
- package/dist/transport/provider/storage/session-hint-channel.d.ts.map +1 -0
- package/dist/transport/provider/storage/session-hint-channel.js +81 -0
- package/dist/transport/provider/storage/session-hint-channel.js.map +1 -0
- package/dist/transport/provider/storage/session-hint.d.ts +65 -0
- package/dist/transport/provider/storage/session-hint.d.ts.map +1 -0
- package/dist/transport/provider/storage/session-hint.js +117 -0
- package/dist/transport/provider/storage/session-hint.js.map +1 -0
- package/dist/transport/types.d.ts +52 -4
- package/dist/transport/types.d.ts.map +1 -1
- package/dist/transport/worker.d.ts +7 -1
- package/dist/transport/worker.d.ts.map +1 -1
- package/dist/transport/worker.js +123 -0
- package/dist/transport/worker.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -126,6 +126,11 @@ interface AuthCraftClientConfig {
|
|
|
126
126
|
* 'system' = admin, 'tenant' = tenant-scoped, omit/'customer' = default.
|
|
127
127
|
*/
|
|
128
128
|
context?: 'customer' | 'tenant' | 'system'
|
|
129
|
+
/**
|
|
130
|
+
* Skip the boot-time refresh for visitors who never logged in (default false).
|
|
131
|
+
* See "Skipping the pointless boot refresh" below.
|
|
132
|
+
*/
|
|
133
|
+
sessionHint?: boolean
|
|
129
134
|
/**
|
|
130
135
|
* Subscribe to session state changes (login, logout, refresh, expiry).
|
|
131
136
|
* Fires after `init()`. Bridge it into app state or trigger auto-logout
|
|
@@ -135,6 +140,47 @@ interface AuthCraftClientConfig {
|
|
|
135
140
|
}
|
|
136
141
|
```
|
|
137
142
|
|
|
143
|
+
### Skipping the pointless boot refresh
|
|
144
|
+
|
|
145
|
+
The refresh token is an httpOnly cookie the worker cannot read, so after every
|
|
146
|
+
reload the worker has no idea whether a session exists — the only way to find out
|
|
147
|
+
is to try `POST /auth/refresh`. For someone who never logged in, that attempt can
|
|
148
|
+
never succeed, yet it is paid on **every** page load, and the app has to wait for
|
|
149
|
+
it to fail before it can render "logged out".
|
|
150
|
+
|
|
151
|
+
`sessionHint: true` makes the worker remember the answer:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const client = createAuthCraftClient({
|
|
155
|
+
baseUrl: 'https://auth.example.com',
|
|
156
|
+
strategy: 'cookie-auth',
|
|
157
|
+
allowedDomains: ['auth.example.com'],
|
|
158
|
+
sessionHint: true, // don't try refresh if nobody ever logged in here
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
- **login succeeds** → the hint is written (IndexedDB; the worker has no
|
|
163
|
+
`localStorage`).
|
|
164
|
+
- **logout**, or a refresh the server rejects → the hint is cleared.
|
|
165
|
+
- **next reload with no hint** → auth-requiring calls resolve immediately as
|
|
166
|
+
`NOT_AUTHENTICATED` with **zero HTTP requests**. Public calls (login, register,
|
|
167
|
+
password reset, …) are unaffected.
|
|
168
|
+
|
|
169
|
+
The hint is one non-secret bit and is never trusted for authorization — it can
|
|
170
|
+
only skip a request that was going to fail anyway. It self-corrects in both
|
|
171
|
+
directions: a rejected refresh clears it, and a login or logout in another tab is
|
|
172
|
+
broadcast to every open tab (via `BroadcastChannel`), so a tab that booted
|
|
173
|
+
"anonymous" starts attempting refresh again without a reload. If IndexedDB is
|
|
174
|
+
unavailable or faulting, the hint **fails open** — the client simply behaves as it
|
|
175
|
+
does today.
|
|
176
|
+
|
|
177
|
+
The storage key is derived from `context`, so a customer login on the same origin
|
|
178
|
+
never implies a `tenant` or `system` session.
|
|
179
|
+
|
|
180
|
+
> Handle `NOT_AUTHENTICATED` as "show the login screen". If your app currently
|
|
181
|
+
> distinguishes "refresh failed" from "never logged in", both now arrive as the
|
|
182
|
+
> same error code — the difference is only that the second costs no request.
|
|
183
|
+
|
|
138
184
|
### Transport: worker vs inline
|
|
139
185
|
|
|
140
186
|
By default the client runs its token/refresh loop in a **Web Worker**, so the
|
package/dist/core/core.d.ts
CHANGED
|
@@ -49,6 +49,19 @@ export interface ClientCoreConfig {
|
|
|
49
49
|
transport?: 'worker' | 'inline';
|
|
50
50
|
/** Auth scope → `X-Auth-Context` header. */
|
|
51
51
|
context?: 'customer' | 'tenant' | 'system';
|
|
52
|
+
/**
|
|
53
|
+
* Skip the boot-time `POST /auth/refresh` for visitors who never logged in.
|
|
54
|
+
*
|
|
55
|
+
* Off by default. When `true`, the worker remembers (in IndexedDB) that a
|
|
56
|
+
* session was established here and, on a later reload, fails auth-requiring
|
|
57
|
+
* calls fast with NOT_AUTHENTICATED instead of spending a refresh that cannot
|
|
58
|
+
* succeed. The hint is cleared whenever the server reports no session and is
|
|
59
|
+
* synced across tabs, so it only ever skips a request that would have failed.
|
|
60
|
+
*
|
|
61
|
+
* The storage key is derived from `context`, so a customer login on the same
|
|
62
|
+
* origin never implies a tenant or system session.
|
|
63
|
+
*/
|
|
64
|
+
sessionHint?: boolean;
|
|
52
65
|
/** Session state changes (login/logout/refresh/expiry). Fires after init(). */
|
|
53
66
|
onAuthStateChanged?: (state: TransportAuthResult) => void;
|
|
54
67
|
}
|
package/dist/core/core.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,UAAU,IAAI,mBAAmB,EACvC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAe,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAE9D;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACpG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACnG,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACrG,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;CACvG;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,aAAa,GAAG,WAAW,CAAC;IACtC,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;IAC7B;;;;OAIG;IACH,SAAS,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAChC,4CAA4C;IAC5C,OAAO,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC3C,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;CAC3D;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,EAAE,CAAiC;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAE1C,YAAY,MAAM,EAAE,gBAAgB,EAEnC;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"core.d.ts","sourceRoot":"","sources":["../../src/core/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,+BAA+B,CAAC;AAC9D,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,UAAU,IAAI,mBAAmB,EACvC,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAe,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAE9D;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,IAAI,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACpG,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACnG,KAAK,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;IACrG,MAAM,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;CACvG;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,aAAa,GAAG,WAAW,CAAC;IACtC,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;IAC7B;;;;OAIG;IACH,SAAS,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAChC,4CAA4C;IAC5C,OAAO,CAAC,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,CAAC;IAC3C;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,CAAC;CAC3D;AAED,qBAAa,UAAU;IACrB,OAAO,CAAC,EAAE,CAAiC;IAC3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAE1C,YAAY,MAAM,EAAE,gBAAgB,EAEnC;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA6B1B;IAED;;;OAGG;IACH,KAAK,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,cAAc,CAAC,CAAC,CAAC,CAIxE;IAED;;;OAGG;IACH,IAAI,GAAG,IAAI,YAAY,CAqBtB;IAED,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAG7B;IAED,2EAA2E;IAC3E,YAAY,IAAI,gBAAgB,CAG/B;IAED,OAAO,IAAI,IAAI,CAGd;CACF;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAIpF;AAED,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAC7E,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/core/core.js
CHANGED
|
@@ -43,6 +43,10 @@ export class ClientCore {
|
|
|
43
43
|
...(headers ? { defaultHeaders: headers } : {}),
|
|
44
44
|
...(this.config.workerFactory ? { workerFactory: this.config.workerFactory } : {}),
|
|
45
45
|
...(this.config.transport ? { transport: this.config.transport } : {}),
|
|
46
|
+
// Per-scope key: each auth context tracks its own session independently.
|
|
47
|
+
...(this.config.sessionHint
|
|
48
|
+
? { sessionHint: { enabled: true, key: `sessionHint:${this.config.context ?? 'customer'}` } }
|
|
49
|
+
: {}),
|
|
46
50
|
});
|
|
47
51
|
await this.fg.whenReady();
|
|
48
52
|
if (this.config.onAuthStateChanged) {
|
package/dist/core/core.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/core/core.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,OAAO,EACL,YAAY,GAIb,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,WAAW,EAAuB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"core.js","sourceRoot":"","sources":["../../src/core/core.ts"],"names":[],"mappings":"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"}
|
|
@@ -16,6 +16,10 @@ export declare class FetchGuardClient {
|
|
|
16
16
|
private isReady;
|
|
17
17
|
private requestQueue;
|
|
18
18
|
private activeRequests;
|
|
19
|
+
/** Ids posted to the worker that still hold a concurrency slot */
|
|
20
|
+
private readonly inFlight;
|
|
21
|
+
/** Round-trip timers (queue wait + worker), keyed by message id */
|
|
22
|
+
private readonly requestTimers;
|
|
19
23
|
private readonly maxConcurrent;
|
|
20
24
|
private readonly maxQueueSize;
|
|
21
25
|
private readonly setupTimeout;
|
|
@@ -112,6 +116,20 @@ export declare class FetchGuardClient {
|
|
|
112
116
|
* Cancel a pending request by ID
|
|
113
117
|
*/
|
|
114
118
|
cancel(id: string): void;
|
|
119
|
+
/**
|
|
120
|
+
* Round-trip timeout fired: abort the worker's fetch (if it was sent),
|
|
121
|
+
* free the slot and settle the caller with REQUEST_TIMEOUT.
|
|
122
|
+
*/
|
|
123
|
+
private timeoutRequest;
|
|
124
|
+
private armRequestTimer;
|
|
125
|
+
private clearRequestTimer;
|
|
126
|
+
/**
|
|
127
|
+
* End a message's life — the single exit for every path (answer, error,
|
|
128
|
+
* cancel, timeout, send failure). Clears its timer, drops it from the queue
|
|
129
|
+
* if not sent yet, gives back its concurrency slot exactly once, and removes
|
|
130
|
+
* its bookkeeping. Returns the pending entry if the caller still waits.
|
|
131
|
+
*/
|
|
132
|
+
private finish;
|
|
115
133
|
/**
|
|
116
134
|
* Convenience methods
|
|
117
135
|
*/
|
|
@@ -219,7 +237,10 @@ export declare class FetchGuardClient {
|
|
|
219
237
|
}>>;
|
|
220
238
|
/**
|
|
221
239
|
* Send message through queue system
|
|
222
|
-
* All messages go through queue for sequential processing
|
|
240
|
+
* All messages go through queue for sequential processing.
|
|
241
|
+
* Resolves once the message is posted to the worker (or dropped from the
|
|
242
|
+
* queue by finish()); the answer arrives via handleWorkerMessage. Timeouts
|
|
243
|
+
* are the caller's (armRequestTimer), not the queue's.
|
|
223
244
|
* @param transferables - Optional Transferable objects for zero-copy postMessage
|
|
224
245
|
*/
|
|
225
246
|
private sendMessageQueued;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/transport/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAOvD,OAAO,KAAK,EACV,UAAU,EAGV,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EAMtB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/transport/client.ts"],"names":[],"mappings":"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"}
|
package/dist/transport/client.js
CHANGED
|
@@ -11,8 +11,10 @@ const DEFAULT_MAX_CONCURRENT = 6;
|
|
|
11
11
|
const DEFAULT_MAX_QUEUE_SIZE = 1000;
|
|
12
12
|
/** Default setup timeout (ms) */
|
|
13
13
|
const DEFAULT_SETUP_TIMEOUT = 10000;
|
|
14
|
-
/**
|
|
15
|
-
const
|
|
14
|
+
/** Round-trip timeout for AUTH_CALL messages (ms) */
|
|
15
|
+
const AUTH_CALL_TIMEOUT = 15000;
|
|
16
|
+
/** Round-trip timeout for PING messages (ms) */
|
|
17
|
+
const PING_TIMEOUT = 5000;
|
|
16
18
|
/**
|
|
17
19
|
* FetchGuard Client - main interface cho việc gọi API thông qua Web Worker
|
|
18
20
|
*/
|
|
@@ -31,6 +33,10 @@ export class FetchGuardClient {
|
|
|
31
33
|
isReady = false;
|
|
32
34
|
requestQueue = [];
|
|
33
35
|
activeRequests = 0;
|
|
36
|
+
/** Ids posted to the worker that still hold a concurrency slot */
|
|
37
|
+
inFlight = new Set();
|
|
38
|
+
/** Round-trip timers (queue wait + worker), keyed by message id */
|
|
39
|
+
requestTimers = new Map();
|
|
34
40
|
maxConcurrent;
|
|
35
41
|
maxQueueSize;
|
|
36
42
|
setupTimeout;
|
|
@@ -48,7 +54,7 @@ export class FetchGuardClient {
|
|
|
48
54
|
this.maxConcurrent = options.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
49
55
|
this.maxQueueSize = options.maxQueueSize ?? DEFAULT_MAX_QUEUE_SIZE;
|
|
50
56
|
this.setupTimeout = options.setupTimeout ?? DEFAULT_SETUP_TIMEOUT;
|
|
51
|
-
this.requestTimeout = options.requestTimeout
|
|
57
|
+
this.requestTimeout = options.requestTimeout;
|
|
52
58
|
this.debug = options.debug;
|
|
53
59
|
this.retry = options.retry;
|
|
54
60
|
this.dedupe = options.dedupe;
|
|
@@ -79,6 +85,7 @@ export class FetchGuardClient {
|
|
|
79
85
|
allowedDomains: options.allowedDomains || [],
|
|
80
86
|
refreshEarlyMs: options.refreshEarlyMs ?? DEFAULT_REFRESH_EARLY_MS,
|
|
81
87
|
defaultHeaders: options.defaultHeaders || {},
|
|
88
|
+
...(options.sessionHint ? { sessionHint: options.sessionHint } : {}),
|
|
82
89
|
};
|
|
83
90
|
// Serialize provider config based on type
|
|
84
91
|
let providerConfig = null;
|
|
@@ -126,15 +133,11 @@ export class FetchGuardClient {
|
|
|
126
133
|
if (type === MSG.FETCH_RESULT) {
|
|
127
134
|
// FETCH_RESULT contains FetchEnvelope (raw HTTP response)
|
|
128
135
|
// Worker doesn't judge HTTP status - client receives envelope as-is
|
|
129
|
-
|
|
130
|
-
|
|
136
|
+
// A late answer (after cancel/timeout) only gives back its slot
|
|
137
|
+
const entry = this.finish(id);
|
|
138
|
+
if (!entry)
|
|
131
139
|
return;
|
|
132
|
-
const url =
|
|
133
|
-
const timing = this.requestTimings.get(id);
|
|
134
|
-
this.pendingRequests.delete(id);
|
|
135
|
-
this.requestUrls.delete(id);
|
|
136
|
-
this.requestTimings.delete(id);
|
|
137
|
-
this.onRequestComplete();
|
|
140
|
+
const { request, url, timing } = entry;
|
|
138
141
|
// Calculate metrics
|
|
139
142
|
const metrics = this.calculateMetrics(timing);
|
|
140
143
|
// Debug hook: onResponse
|
|
@@ -146,15 +149,10 @@ export class FetchGuardClient {
|
|
|
146
149
|
}
|
|
147
150
|
if (type === MSG.FETCH_ERROR) {
|
|
148
151
|
// Network/timeout/cancel errors (no HTTP response)
|
|
149
|
-
const
|
|
150
|
-
if (!
|
|
152
|
+
const entry = this.finish(id);
|
|
153
|
+
if (!entry)
|
|
151
154
|
return;
|
|
152
|
-
const url =
|
|
153
|
-
const timing = this.requestTimings.get(id);
|
|
154
|
-
this.pendingRequests.delete(id);
|
|
155
|
-
this.requestUrls.delete(id);
|
|
156
|
-
this.requestTimings.delete(id);
|
|
157
|
-
this.onRequestComplete();
|
|
155
|
+
const { request, url, timing } = entry;
|
|
158
156
|
const errorMessage = String(payload?.error || 'Network error');
|
|
159
157
|
// Calculate metrics
|
|
160
158
|
const metrics = this.calculateMetrics(timing);
|
|
@@ -166,12 +164,10 @@ export class FetchGuardClient {
|
|
|
166
164
|
return;
|
|
167
165
|
}
|
|
168
166
|
if (type === MSG.ERROR) {
|
|
169
|
-
const
|
|
170
|
-
if (!
|
|
167
|
+
const entry = this.finish(id);
|
|
168
|
+
if (!entry)
|
|
171
169
|
return;
|
|
172
|
-
|
|
173
|
-
this.onRequestComplete();
|
|
174
|
-
request.resolve(err(payload.errors, payload.meta));
|
|
170
|
+
entry.request.resolve(err(payload.errors, payload.meta));
|
|
175
171
|
return;
|
|
176
172
|
}
|
|
177
173
|
if (type === MSG.SETUP_ERROR) {
|
|
@@ -199,12 +195,7 @@ export class FetchGuardClient {
|
|
|
199
195
|
return;
|
|
200
196
|
}
|
|
201
197
|
if (type === MSG.PONG) {
|
|
202
|
-
|
|
203
|
-
if (request) {
|
|
204
|
-
this.pendingRequests.delete(id);
|
|
205
|
-
this.onRequestComplete();
|
|
206
|
-
request.resolve(ok({ timestamp: payload?.timestamp }));
|
|
207
|
-
}
|
|
198
|
+
this.finish(id)?.request.resolve(ok({ timestamp: payload?.timestamp }));
|
|
208
199
|
return;
|
|
209
200
|
}
|
|
210
201
|
if (type === MSG.AUTH_STATE_CHANGED) {
|
|
@@ -213,12 +204,7 @@ export class FetchGuardClient {
|
|
|
213
204
|
return;
|
|
214
205
|
}
|
|
215
206
|
if (type === MSG.AUTH_CALL_RESULT) {
|
|
216
|
-
|
|
217
|
-
if (request) {
|
|
218
|
-
this.pendingRequests.delete(id);
|
|
219
|
-
this.onRequestComplete();
|
|
220
|
-
request.resolve(ok(payload)); // payload is AuthResult
|
|
221
|
-
}
|
|
207
|
+
this.finish(id)?.request.resolve(ok(payload)); // payload is AuthResult
|
|
222
208
|
return;
|
|
223
209
|
}
|
|
224
210
|
if (type === MSG.TOKEN_REFRESHED) {
|
|
@@ -234,8 +220,10 @@ export class FetchGuardClient {
|
|
|
234
220
|
console.error('Worker error:', error);
|
|
235
221
|
// Debug hook: onWorkerError
|
|
236
222
|
this.debug?.onWorkerError?.(error);
|
|
237
|
-
// Reject all pending requests
|
|
238
|
-
|
|
223
|
+
// Reject all pending requests (in-flight ones keep their slot until the
|
|
224
|
+
// worker answers or never — late answers release it in finish())
|
|
225
|
+
for (const [id, request] of this.pendingRequests) {
|
|
226
|
+
this.clearRequestTimer(id);
|
|
239
227
|
request.reject(new Error(`Worker error: ${error.message}`));
|
|
240
228
|
}
|
|
241
229
|
this.pendingRequests.clear();
|
|
@@ -492,6 +480,7 @@ export class FetchGuardClient {
|
|
|
492
480
|
*/
|
|
493
481
|
fetchWithId(url, options = {}) {
|
|
494
482
|
const id = this.generateMessageId();
|
|
483
|
+
const { timeout, ...sendOptions } = options;
|
|
495
484
|
// The Promise is resolved/rejected via pendingRequests (worker callback).
|
|
496
485
|
// The executor stays synchronous (no async); the serialize-and-send work
|
|
497
486
|
// runs separately and routes failures through the same reject path.
|
|
@@ -507,16 +496,19 @@ export class FetchGuardClient {
|
|
|
507
496
|
// Debug hook: onRequest
|
|
508
497
|
this.debug?.onRequest?.(url, options);
|
|
509
498
|
});
|
|
499
|
+
// Round-trip timeout, if any: covers serialization, queue wait and the
|
|
500
|
+
// worker's fetch. Default is none — large uploads are legitimately slow.
|
|
501
|
+
this.armRequestTimer(id, timeout ?? this.requestTimeout);
|
|
510
502
|
// Serialize (FormData/Headers) then send — async, but kept out of the
|
|
511
503
|
// Promise executor (noAsyncPromiseExecutor). Failures reject via the
|
|
512
504
|
// pending request registered above.
|
|
513
505
|
void (async () => {
|
|
514
506
|
try {
|
|
515
|
-
const serializedOptions = { ...
|
|
507
|
+
const serializedOptions = { ...sendOptions };
|
|
516
508
|
let transferables;
|
|
517
509
|
// Serialize FormData body before sending to worker
|
|
518
|
-
if (
|
|
519
|
-
const { data, transferables: formDataTransferables } = await serializeFormData(
|
|
510
|
+
if (sendOptions.body && isFormData(sendOptions.body)) {
|
|
511
|
+
const { data, transferables: formDataTransferables } = await serializeFormData(sendOptions.body);
|
|
520
512
|
// SerializedFormData will be deserialized back to FormData in worker
|
|
521
513
|
serializedOptions.body = data;
|
|
522
514
|
// ArrayBuffers for zero-copy transfer
|
|
@@ -525,24 +517,21 @@ export class FetchGuardClient {
|
|
|
525
517
|
}
|
|
526
518
|
}
|
|
527
519
|
// Serialize Headers object to plain object (Headers cannot be cloned)
|
|
528
|
-
if (
|
|
520
|
+
if (sendOptions.headers instanceof Headers) {
|
|
529
521
|
const plainHeaders = {};
|
|
530
|
-
|
|
522
|
+
sendOptions.headers.forEach((value, key) => {
|
|
531
523
|
plainHeaders[key] = value;
|
|
532
524
|
});
|
|
533
525
|
serializedOptions.headers = plainHeaders;
|
|
534
526
|
}
|
|
527
|
+
// Cancelled or timed out while serializing: don't send it at all
|
|
528
|
+
if (!this.pendingRequests.has(id))
|
|
529
|
+
return;
|
|
535
530
|
const message = { id, type: MSG.FETCH, payload: { url, options: serializedOptions } };
|
|
536
|
-
await this.sendMessageQueued(message,
|
|
531
|
+
await this.sendMessageQueued(message, transferables);
|
|
537
532
|
}
|
|
538
533
|
catch (error) {
|
|
539
|
-
|
|
540
|
-
if (request) {
|
|
541
|
-
this.pendingRequests.delete(id);
|
|
542
|
-
this.requestUrls.delete(id);
|
|
543
|
-
this.requestTimings.delete(id);
|
|
544
|
-
request.reject(error instanceof Error ? error : new Error(String(error)));
|
|
545
|
-
}
|
|
534
|
+
this.finish(id)?.request.reject(error instanceof Error ? error : new Error(String(error)));
|
|
546
535
|
}
|
|
547
536
|
})();
|
|
548
537
|
const cancel = () => this.cancel(id);
|
|
@@ -552,23 +541,70 @@ export class FetchGuardClient {
|
|
|
552
541
|
* Cancel a pending request by ID
|
|
553
542
|
*/
|
|
554
543
|
cancel(id) {
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
this.requestTimings.delete(id);
|
|
544
|
+
const sent = this.inFlight.has(id);
|
|
545
|
+
const entry = this.finish(id);
|
|
546
|
+
if (!entry)
|
|
547
|
+
return;
|
|
548
|
+
// Still queued: finish() dropped it from the queue, nothing to abort
|
|
549
|
+
if (sent)
|
|
562
550
|
this.worker.postMessage({ id, type: MSG.CANCEL });
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
551
|
+
// Debug hook: onError for cancelled request
|
|
552
|
+
if (this.debug?.onError && entry.url) {
|
|
553
|
+
this.debug.onError(entry.url, { code: 'REQUEST_CANCELLED', message: 'Request cancelled' }, this.calculateMetrics(entry.timing));
|
|
554
|
+
}
|
|
555
|
+
entry.request.resolve(err(RequestErrors.Cancelled()));
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* Round-trip timeout fired: abort the worker's fetch (if it was sent),
|
|
559
|
+
* free the slot and settle the caller with REQUEST_TIMEOUT.
|
|
560
|
+
*/
|
|
561
|
+
timeoutRequest(id) {
|
|
562
|
+
const sent = this.inFlight.has(id);
|
|
563
|
+
const entry = this.finish(id);
|
|
564
|
+
if (sent)
|
|
565
|
+
this.worker.postMessage({ id, type: MSG.CANCEL });
|
|
566
|
+
if (!entry)
|
|
567
|
+
return;
|
|
568
|
+
if (this.debug?.onError && entry.url) {
|
|
569
|
+
this.debug.onError(entry.url, { code: 'REQUEST_TIMEOUT', message: 'Request timed out' }, this.calculateMetrics(entry.timing));
|
|
570
|
+
}
|
|
571
|
+
entry.request.resolve(err(RequestErrors.Timeout()));
|
|
572
|
+
}
|
|
573
|
+
armRequestTimer(id, timeoutMs) {
|
|
574
|
+
if (timeoutMs === undefined || timeoutMs <= 0)
|
|
575
|
+
return;
|
|
576
|
+
this.requestTimers.set(id, setTimeout(() => this.timeoutRequest(id), timeoutMs));
|
|
577
|
+
}
|
|
578
|
+
clearRequestTimer(id) {
|
|
579
|
+
const timer = this.requestTimers.get(id);
|
|
580
|
+
if (timer !== undefined) {
|
|
581
|
+
clearTimeout(timer);
|
|
582
|
+
this.requestTimers.delete(id);
|
|
570
583
|
}
|
|
571
584
|
}
|
|
585
|
+
/**
|
|
586
|
+
* End a message's life — the single exit for every path (answer, error,
|
|
587
|
+
* cancel, timeout, send failure). Clears its timer, drops it from the queue
|
|
588
|
+
* if not sent yet, gives back its concurrency slot exactly once, and removes
|
|
589
|
+
* its bookkeeping. Returns the pending entry if the caller still waits.
|
|
590
|
+
*/
|
|
591
|
+
finish(id) {
|
|
592
|
+
this.clearRequestTimer(id);
|
|
593
|
+
const index = this.requestQueue.findIndex((item) => item.id === id);
|
|
594
|
+
if (index !== -1) {
|
|
595
|
+
const [item] = this.requestQueue.splice(index, 1);
|
|
596
|
+
item?.resolve();
|
|
597
|
+
}
|
|
598
|
+
if (this.inFlight.delete(id))
|
|
599
|
+
this.onRequestComplete();
|
|
600
|
+
const request = this.pendingRequests.get(id);
|
|
601
|
+
const url = this.requestUrls.get(id);
|
|
602
|
+
const timing = this.requestTimings.get(id);
|
|
603
|
+
this.pendingRequests.delete(id);
|
|
604
|
+
this.requestUrls.delete(id);
|
|
605
|
+
this.requestTimings.delete(id);
|
|
606
|
+
return request ? { request, url, timing } : undefined;
|
|
607
|
+
}
|
|
572
608
|
/**
|
|
573
609
|
* Convenience methods
|
|
574
610
|
*/
|
|
@@ -701,12 +737,9 @@ export class FetchGuardClient {
|
|
|
701
737
|
resolve: (r) => resolve(r),
|
|
702
738
|
reject: (e) => reject(e),
|
|
703
739
|
});
|
|
704
|
-
this.
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
this.pendingRequests.delete(id);
|
|
708
|
-
request.reject(error);
|
|
709
|
-
}
|
|
740
|
+
this.armRequestTimer(id, AUTH_CALL_TIMEOUT);
|
|
741
|
+
this.sendMessageQueued(message).catch((error) => {
|
|
742
|
+
this.finish(id)?.request.reject(error);
|
|
710
743
|
});
|
|
711
744
|
});
|
|
712
745
|
}
|
|
@@ -831,44 +864,33 @@ export class FetchGuardClient {
|
|
|
831
864
|
resolve: (r) => resolve(r),
|
|
832
865
|
reject: (e) => reject(e),
|
|
833
866
|
});
|
|
834
|
-
this.
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
this.pendingRequests.delete(id);
|
|
838
|
-
request.reject(error);
|
|
839
|
-
}
|
|
867
|
+
this.armRequestTimer(id, PING_TIMEOUT);
|
|
868
|
+
this.sendMessageQueued(message).catch((error) => {
|
|
869
|
+
this.finish(id)?.request.reject(error);
|
|
840
870
|
});
|
|
841
871
|
});
|
|
842
872
|
}
|
|
843
873
|
/**
|
|
844
874
|
* Send message through queue system
|
|
845
|
-
* All messages go through queue for sequential processing
|
|
875
|
+
* All messages go through queue for sequential processing.
|
|
876
|
+
* Resolves once the message is posted to the worker (or dropped from the
|
|
877
|
+
* queue by finish()); the answer arrives via handleWorkerMessage. Timeouts
|
|
878
|
+
* are the caller's (armRequestTimer), not the queue's.
|
|
846
879
|
* @param transferables - Optional Transferable objects for zero-copy postMessage
|
|
847
880
|
*/
|
|
848
|
-
sendMessageQueued(message,
|
|
881
|
+
sendMessageQueued(message, transferables) {
|
|
849
882
|
return new Promise((resolve, reject) => {
|
|
850
883
|
// Check queue size limit to prevent memory leak
|
|
851
884
|
if (this.requestQueue.length >= this.maxQueueSize) {
|
|
852
885
|
reject(err(RequestErrors.QueueFull({ size: this.requestQueue.length, maxSize: this.maxQueueSize })));
|
|
853
886
|
return;
|
|
854
887
|
}
|
|
855
|
-
const timeout = setTimeout(() => {
|
|
856
|
-
const index = this.requestQueue.findIndex((item) => item.id === message.id);
|
|
857
|
-
if (index !== -1) {
|
|
858
|
-
this.requestQueue.splice(index, 1);
|
|
859
|
-
}
|
|
860
|
-
this.pendingRequests.delete(message.id);
|
|
861
|
-
this.requestUrls.delete(message.id);
|
|
862
|
-
this.requestTimings.delete(message.id);
|
|
863
|
-
reject(err(RequestErrors.Timeout()));
|
|
864
|
-
}, timeoutMs);
|
|
865
888
|
const queueItem = {
|
|
866
889
|
id: message.id,
|
|
867
890
|
message,
|
|
868
891
|
transferables,
|
|
869
|
-
resolve
|
|
892
|
+
resolve,
|
|
870
893
|
reject,
|
|
871
|
-
timeout,
|
|
872
894
|
};
|
|
873
895
|
this.requestQueue.push(queueItem);
|
|
874
896
|
this.processQueue();
|
|
@@ -890,6 +912,7 @@ export class FetchGuardClient {
|
|
|
890
912
|
if (!item)
|
|
891
913
|
continue;
|
|
892
914
|
this.activeRequests++;
|
|
915
|
+
this.inFlight.add(item.id);
|
|
893
916
|
// Update timing: mark when request is actually sent to worker
|
|
894
917
|
const timing = this.requestTimings.get(item.id);
|
|
895
918
|
if (timing) {
|
|
@@ -903,12 +926,12 @@ export class FetchGuardClient {
|
|
|
903
926
|
else {
|
|
904
927
|
this.worker.postMessage(item.message);
|
|
905
928
|
}
|
|
906
|
-
// Note:
|
|
907
|
-
|
|
929
|
+
// Note: the slot is given back by finish() (answer, cancel, timeout)
|
|
930
|
+
item.resolve();
|
|
908
931
|
}
|
|
909
932
|
catch (error) {
|
|
933
|
+
this.inFlight.delete(item.id);
|
|
910
934
|
this.activeRequests--;
|
|
911
|
-
clearTimeout(item.timeout);
|
|
912
935
|
item.reject(error instanceof Error ? error : new Error(String(error)));
|
|
913
936
|
// Continue processing queue after error
|
|
914
937
|
this.processQueue();
|
|
@@ -928,14 +951,22 @@ export class FetchGuardClient {
|
|
|
928
951
|
*/
|
|
929
952
|
destroy() {
|
|
930
953
|
this.worker.terminate();
|
|
954
|
+
for (const timer of this.requestTimers.values())
|
|
955
|
+
clearTimeout(timer);
|
|
956
|
+
this.requestTimers.clear();
|
|
957
|
+
const queue = this.requestQueue;
|
|
958
|
+
this.requestQueue = [];
|
|
959
|
+
for (const item of queue)
|
|
960
|
+
item.resolve();
|
|
961
|
+
// Settle every waiting caller — the worker is gone, no answer will come
|
|
962
|
+
for (const request of this.pendingRequests.values()) {
|
|
963
|
+
request.reject(new Error('Client destroyed'));
|
|
964
|
+
}
|
|
931
965
|
this.pendingRequests.clear();
|
|
932
966
|
this.requestUrls.clear();
|
|
933
967
|
this.requestTimings.clear();
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
item.reject(new Error('Client destroyed'));
|
|
937
|
-
}
|
|
938
|
-
this.requestQueue = [];
|
|
968
|
+
this.inFlight.clear();
|
|
969
|
+
this.activeRequests = 0;
|
|
939
970
|
}
|
|
940
971
|
}
|
|
941
972
|
/**
|