@oxy.so/core 1.6.1 → 1.7.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/LICENSE +675 -201
- package/NOTICE +7 -2
- package/dist/cjs/.tsbuildinfo +1 -1
- package/dist/cjs/HttpService.js +146 -23
- package/dist/cjs/crypto/registrationPow.js +53 -0
- package/dist/cjs/mixins/OxyServices.auth.js +6 -0
- package/dist/cjs/mixins/OxyServices.user.js +8 -1
- package/dist/cjs/utils/asyncUtils.js +50 -9
- package/dist/cjs/utils/errorUtils.js +53 -3
- package/dist/cjs/utils/requestUtils.js +96 -14
- package/dist/esm/.tsbuildinfo +1 -1
- package/dist/esm/HttpService.js +147 -24
- package/dist/esm/crypto/registrationPow.js +50 -0
- package/dist/esm/mixins/OxyServices.auth.js +8 -2
- package/dist/esm/mixins/OxyServices.identityBackup.js +1 -1
- package/dist/esm/mixins/OxyServices.user.js +8 -1
- package/dist/esm/utils/asyncUtils.js +50 -9
- package/dist/esm/utils/errorUtils.js +51 -3
- package/dist/esm/utils/requestUtils.js +96 -14
- package/dist/types/.tsbuildinfo +1 -1
- package/dist/types/HttpService.d.ts +19 -0
- package/dist/types/crypto/registrationPow.d.ts +22 -0
- package/dist/types/mixins/OxyServices.user.d.ts +10 -2
- package/dist/types/utils/asyncUtils.d.ts +30 -1
- package/dist/types/utils/errorUtils.d.ts +36 -0
- package/dist/types/utils/requestUtils.d.ts +2 -2
- package/package.json +2 -2
- package/src/HttpService.ts +181 -31
- package/src/__tests__/fixEsmImports.test.ts +132 -0
- package/src/__tests__/httpServiceAbort.test.ts +308 -0
- package/src/crypto/__tests__/registrationPow.test.ts +61 -0
- package/src/crypto/registrationPow.ts +60 -0
- package/src/mixins/OxyServices.auth.ts +7 -0
- package/src/mixins/OxyServices.user.ts +12 -1
- package/src/utils/__tests__/asyncUtils.test.ts +93 -1
- package/src/utils/__tests__/requestUtilsAbort.test.ts +155 -0
- package/src/utils/asyncUtils.ts +97 -9
- package/src/utils/errorUtils.ts +78 -3
- package/src/utils/requestUtils.ts +104 -13
|
@@ -3,6 +3,26 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Provides reusable components for request deduplication, queuing, and logging
|
|
5
5
|
*/
|
|
6
|
+
import { createCancelledError } from './errorUtils.js';
|
|
7
|
+
/**
|
|
8
|
+
* The error to reject an aborted request with.
|
|
9
|
+
*
|
|
10
|
+
* ALWAYS the SDK's own cancellation error, with the signal's `reason` preserved
|
|
11
|
+
* as `cause` rather than substituted for it. The tempting version —
|
|
12
|
+
* `reason ?? createCancelledError()` — is wrong twice: `controller.abort()`
|
|
13
|
+
* always populates `reason` with a default `DOMException`, so the fallback
|
|
14
|
+
* would never run, and that `DOMException` carries none of the fields the retry
|
|
15
|
+
* predicates and consumers key off (`code: 'CANCELLED'`, `cancelled: true`).
|
|
16
|
+
* A cancellation that is not recognisable as one is the whole defect.
|
|
17
|
+
*/
|
|
18
|
+
function abortReason(signal) {
|
|
19
|
+
const reason = signal.reason;
|
|
20
|
+
const error = createCancelledError('Request cancelled');
|
|
21
|
+
if (reason !== undefined) {
|
|
22
|
+
error.cause = reason;
|
|
23
|
+
}
|
|
24
|
+
return error;
|
|
25
|
+
}
|
|
6
26
|
/**
|
|
7
27
|
* Request deduplication - prevents duplicate concurrent requests
|
|
8
28
|
*
|
|
@@ -29,17 +49,39 @@ export class RequestDeduplicator {
|
|
|
29
49
|
* @param requestFn Function that returns a promise
|
|
30
50
|
* @returns Promise that will be shared if key already exists
|
|
31
51
|
*/
|
|
32
|
-
async deduplicate(key, requestFn) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
52
|
+
async deduplicate(key, requestFn, signal) {
|
|
53
|
+
if (signal?.aborted) {
|
|
54
|
+
throw abortReason(signal);
|
|
55
|
+
}
|
|
56
|
+
let promise = this.pendingRequests.get(key);
|
|
57
|
+
if (!promise) {
|
|
58
|
+
promise = requestFn().finally(() => {
|
|
59
|
+
this.pendingRequests.delete(key);
|
|
60
|
+
});
|
|
61
|
+
this.pendingRequests.set(key, promise);
|
|
36
62
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
63
|
+
// Callers SHARE the work but own their cancellation separately.
|
|
64
|
+
//
|
|
65
|
+
// Returning the shared promise directly meant one caller's abort rejected
|
|
66
|
+
// every other caller on the same key — including ones that never cancelled
|
|
67
|
+
// anything, and which had no way to tell that the failure was not theirs.
|
|
68
|
+
// Racing each caller's own signal against the shared work keeps the single
|
|
69
|
+
// in-flight request (the point of deduplication) while making cancellation
|
|
70
|
+
// per-caller. The shared promise is left running: another caller may still
|
|
71
|
+
// want it, and if nobody does its own abort domain ends it.
|
|
72
|
+
if (!signal) {
|
|
73
|
+
return promise;
|
|
74
|
+
}
|
|
75
|
+
// `promise` is already owned by the map's `finally`, so attach a no-op
|
|
76
|
+
// catch to the copy we race: without it, a rejection settled by the race's
|
|
77
|
+
// loser surfaces as an unhandled rejection.
|
|
78
|
+
const shared = promise;
|
|
79
|
+
shared.catch(() => { });
|
|
80
|
+
return new Promise((resolve, reject) => {
|
|
81
|
+
const onAbort = () => reject(abortReason(signal));
|
|
82
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
83
|
+
shared.then((value) => { signal.removeEventListener('abort', onAbort); resolve(value); }, (error) => { signal.removeEventListener('abort', onAbort); reject(error); });
|
|
40
84
|
});
|
|
41
|
-
this.pendingRequests.set(key, promise);
|
|
42
|
-
return promise;
|
|
43
85
|
}
|
|
44
86
|
/**
|
|
45
87
|
* Clear all pending requests
|
|
@@ -87,20 +129,60 @@ export class RequestQueue {
|
|
|
87
129
|
* @param requestFn Function that returns a promise
|
|
88
130
|
* @returns Promise that resolves when request completes
|
|
89
131
|
*/
|
|
90
|
-
async enqueue(requestFn) {
|
|
132
|
+
async enqueue(requestFn, signal) {
|
|
91
133
|
if (this.queue.length >= this.maxQueueSize) {
|
|
92
134
|
throw new Error('Request queue is full');
|
|
93
135
|
}
|
|
136
|
+
// A slot is the scarce resource — ten by default — so a request whose
|
|
137
|
+
// caller has already given up must never consume one. Without this, a
|
|
138
|
+
// burst of cancelled work (a search box being typed into, say) keeps every
|
|
139
|
+
// slot busy on results nobody will read, and the ONE request the user is
|
|
140
|
+
// waiting on queues behind them. This is the change that actually returns
|
|
141
|
+
// the slots; refusing to retry a cancellation only stops making more.
|
|
142
|
+
if (signal?.aborted) {
|
|
143
|
+
throw abortReason(signal);
|
|
144
|
+
}
|
|
94
145
|
return new Promise((resolve, reject) => {
|
|
95
|
-
|
|
146
|
+
let settled = false;
|
|
147
|
+
const finish = (fn) => {
|
|
148
|
+
if (settled)
|
|
149
|
+
return;
|
|
150
|
+
settled = true;
|
|
151
|
+
signal?.removeEventListener('abort', onAbort);
|
|
152
|
+
fn();
|
|
153
|
+
};
|
|
154
|
+
const entry = async () => {
|
|
155
|
+
// Re-checked at the moment of execution, not just at enqueue: a request
|
|
156
|
+
// can sit in the queue for as long as the slots stay busy, and the
|
|
157
|
+
// caller may well have moved on in the meantime.
|
|
158
|
+
if (signal?.aborted) {
|
|
159
|
+
finish(() => reject(abortReason(signal)));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
96
162
|
try {
|
|
97
163
|
const result = await requestFn();
|
|
98
|
-
resolve(result);
|
|
164
|
+
finish(() => resolve(result));
|
|
99
165
|
}
|
|
100
166
|
catch (error) {
|
|
101
|
-
reject(error);
|
|
167
|
+
finish(() => reject(error));
|
|
102
168
|
}
|
|
103
|
-
}
|
|
169
|
+
};
|
|
170
|
+
const abortingSignal = signal;
|
|
171
|
+
function onAbort() {
|
|
172
|
+
if (!abortingSignal)
|
|
173
|
+
return;
|
|
174
|
+
// Drop it from the queue if it has not started. An entry already
|
|
175
|
+
// running is left to its own abort domain — the queue must still see it
|
|
176
|
+
// finish, or `running` never decrements and the slot leaks for good.
|
|
177
|
+
const index = queueRef.indexOf(entry);
|
|
178
|
+
if (index !== -1) {
|
|
179
|
+
queueRef.splice(index, 1);
|
|
180
|
+
}
|
|
181
|
+
finish(() => reject(abortReason(abortingSignal)));
|
|
182
|
+
}
|
|
183
|
+
const queueRef = this.queue;
|
|
184
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
185
|
+
this.queue.push(entry);
|
|
104
186
|
this.process();
|
|
105
187
|
});
|
|
106
188
|
}
|