swarpc 0.17.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +44 -0
- package/dist/client.d.ts +8 -1
- package/dist/client.js +90 -1
- package/dist/types.d.ts +13 -1
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -254,6 +254,50 @@ setTimeout(() => cancel().then(() => console.warn("Took too long!!")), 5_000);
|
|
|
254
254
|
await request;
|
|
255
255
|
```
|
|
256
256
|
|
|
257
|
+
### Call in "once" mode
|
|
258
|
+
|
|
259
|
+
The "once" mode allows you to automatically cancel any previous ongoing call before running a new one. This is useful for scenarios like search-as-you-type, where you only care about the latest request.
|
|
260
|
+
|
|
261
|
+
#### Method-scoped once mode
|
|
262
|
+
|
|
263
|
+
Cancel any previous call of the same method:
|
|
264
|
+
|
|
265
|
+
```js
|
|
266
|
+
// If any previous call of searchIMDb is ongoing, it gets cancelled beforehand
|
|
267
|
+
const result = await swarpc.searchIMDb.once({ query });
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
#### Method-scoped once mode with key
|
|
271
|
+
|
|
272
|
+
Cancel any previous call of the same method with the same key:
|
|
273
|
+
|
|
274
|
+
```js
|
|
275
|
+
// If any previous call of searchIMDb with "foo" as the key is ongoing,
|
|
276
|
+
// it gets cancelled beforehand
|
|
277
|
+
const result = await swarpc.searchIMDb.onceBy("foo", { query });
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
This allows multiple concurrent calls with different keys:
|
|
281
|
+
|
|
282
|
+
```js
|
|
283
|
+
// These two calls can run concurrently
|
|
284
|
+
const result1 = await swarpc.searchIMDb.onceBy("search-bar", {
|
|
285
|
+
query: "action",
|
|
286
|
+
});
|
|
287
|
+
const result2 = await swarpc.searchIMDb.onceBy("sidebar", { query: "comedy" });
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
#### Global once mode
|
|
291
|
+
|
|
292
|
+
Cancel any ongoing call with the same global key, across all methods:
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
// Any call from ANY procedure with "global-search" key gets cancelled beforehand
|
|
296
|
+
const result = await swarpc.onceBy("global-search").searchIMDb({ query });
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
This is useful when you want to ensure only one operation of a certain type is running at a time, regardless of which procedure is being called.
|
|
300
|
+
|
|
257
301
|
### Polyfill a `localStorage` for the Server to access
|
|
258
302
|
|
|
259
303
|
You might call third-party code that accesses on `localStorage` from within your procedures.
|
package/dist/client.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* @mergeModuleWith <project>
|
|
4
4
|
*/
|
|
5
5
|
import { type LogLevel } from "./log.js";
|
|
6
|
-
import { ClientMethod, Hooks, WorkerConstructor, zProcedures, type ProceduresMap } from "./types.js";
|
|
6
|
+
import { ClientMethod, ClientMethodCallable, Hooks, WorkerConstructor, zProcedures, type ProceduresMap } from "./types.js";
|
|
7
7
|
/**
|
|
8
8
|
* The sw&rpc client instance, which provides {@link ClientMethod | methods to call procedures}.
|
|
9
9
|
* Each property of the procedures map will be a method, that accepts an input, an optional onProgress callback and an optional request ID.
|
|
@@ -11,6 +11,13 @@ import { ClientMethod, Hooks, WorkerConstructor, zProcedures, type ProceduresMap
|
|
|
11
11
|
*/
|
|
12
12
|
export type SwarpcClient<Procedures extends ProceduresMap> = {
|
|
13
13
|
[zProcedures]: Procedures;
|
|
14
|
+
/**
|
|
15
|
+
* Create a proxy that cancels any ongoing call with the given global key before running new calls.
|
|
16
|
+
* Usage: `await swarpc.onceBy("global-key").myMethod(...)`
|
|
17
|
+
*/
|
|
18
|
+
onceBy: (key: string) => {
|
|
19
|
+
[F in keyof Procedures]: ClientMethodCallable<Procedures[F]>;
|
|
20
|
+
};
|
|
14
21
|
} & {
|
|
15
22
|
[F in keyof Procedures]: ClientMethod<Procedures[F]>;
|
|
16
23
|
};
|
package/dist/client.js
CHANGED
|
@@ -3,6 +3,10 @@ import { makeNodeId, nodeIdOrSW, whoToSendTo } from "./nodes.js";
|
|
|
3
3
|
import { zProcedures, } from "./types.js";
|
|
4
4
|
import { findTransferables } from "./utils.js";
|
|
5
5
|
const pendingRequests = new Map();
|
|
6
|
+
const onceByMethod = new Map();
|
|
7
|
+
const onceByMethodAndKey = new Map();
|
|
8
|
+
const onceByGlobalKey = new Map();
|
|
9
|
+
const emptyProgressCallback = () => { };
|
|
6
10
|
let _clientListenerStarted = new Set();
|
|
7
11
|
export function Client(procedures, { worker, nodes: nodeCount, loglevel = "debug", restartListener = false, hooks = {}, localStorage = {}, } = {}) {
|
|
8
12
|
const l = createLogger("client", loglevel);
|
|
@@ -24,6 +28,23 @@ export function Client(procedures, { worker, nodes: nodeCount, loglevel = "debug
|
|
|
24
28
|
}
|
|
25
29
|
l.info(null, `Started ${nodeCount} node${nodeCount > 1 ? "s" : ""}`, Object.keys(nodes));
|
|
26
30
|
}
|
|
31
|
+
const cancelRequest = (requestId, reason, functionName) => {
|
|
32
|
+
const pending = pendingRequests.get(requestId);
|
|
33
|
+
if (!pending)
|
|
34
|
+
return;
|
|
35
|
+
const nodeId = pending.nodeId;
|
|
36
|
+
const l = createLogger("client", loglevel, nodeIdOrSW(nodeId), requestId);
|
|
37
|
+
l.debug(requestId, `Cancelling ${functionName} with`, reason);
|
|
38
|
+
pending.reject(new Error(reason));
|
|
39
|
+
postMessageSync(l, nodeId ? nodes?.[nodeId] : undefined, {
|
|
40
|
+
by: "sw&rpc",
|
|
41
|
+
requestId,
|
|
42
|
+
functionName,
|
|
43
|
+
abort: { reason },
|
|
44
|
+
});
|
|
45
|
+
pendingRequests.delete(requestId);
|
|
46
|
+
};
|
|
47
|
+
const runProcedureFunctions = new Map();
|
|
27
48
|
for (const functionName of Object.keys(procedures)) {
|
|
28
49
|
if (typeof functionName !== "string") {
|
|
29
50
|
throw new Error(`[SWARPC Client] Invalid function name, don't use symbols`);
|
|
@@ -43,7 +64,7 @@ export function Client(procedures, { worker, nodes: nodeCount, loglevel = "debug
|
|
|
43
64
|
functionName,
|
|
44
65
|
}, options);
|
|
45
66
|
};
|
|
46
|
-
const _runProcedure = async (input, onProgress =
|
|
67
|
+
const _runProcedure = async (input, onProgress = emptyProgressCallback, reqid, nodeId) => {
|
|
47
68
|
const validation = procedures[functionName].input["~standard"].validate(input);
|
|
48
69
|
if (validation instanceof Promise)
|
|
49
70
|
throw new Error("Validations must not be async");
|
|
@@ -70,6 +91,7 @@ export function Client(procedures, { worker, nodes: nodeCount, loglevel = "debug
|
|
|
70
91
|
.catch(reject);
|
|
71
92
|
});
|
|
72
93
|
};
|
|
94
|
+
runProcedureFunctions.set(functionName, _runProcedure);
|
|
73
95
|
instance[functionName] = _runProcedure;
|
|
74
96
|
instance[functionName].broadcast = async (input, onProgresses, nodesCount) => {
|
|
75
97
|
let nodesToUse = [undefined];
|
|
@@ -111,7 +133,74 @@ export function Client(procedures, { worker, nodes: nodeCount, loglevel = "debug
|
|
|
111
133
|
},
|
|
112
134
|
};
|
|
113
135
|
};
|
|
136
|
+
instance[functionName].once = async (input, onProgress) => {
|
|
137
|
+
const previousRequestId = onceByMethod.get(functionName);
|
|
138
|
+
if (previousRequestId) {
|
|
139
|
+
cancelRequest(previousRequestId, "Cancelled by .once() call", functionName);
|
|
140
|
+
onceByMethod.delete(functionName);
|
|
141
|
+
}
|
|
142
|
+
const requestId = makeRequestId();
|
|
143
|
+
onceByMethod.set(functionName, requestId);
|
|
144
|
+
try {
|
|
145
|
+
return await _runProcedure(input, onProgress, requestId);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
if (onceByMethod.get(functionName) === requestId) {
|
|
149
|
+
onceByMethod.delete(functionName);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
instance[functionName].onceBy = async (key, input, onProgress) => {
|
|
154
|
+
const trackingKey = `${functionName}:${key}`;
|
|
155
|
+
const previousRequestId = onceByMethodAndKey.get(trackingKey);
|
|
156
|
+
if (previousRequestId) {
|
|
157
|
+
cancelRequest(previousRequestId, `Cancelled by .onceBy("${key}") call`, functionName);
|
|
158
|
+
onceByMethodAndKey.delete(trackingKey);
|
|
159
|
+
}
|
|
160
|
+
const requestId = makeRequestId();
|
|
161
|
+
onceByMethodAndKey.set(trackingKey, requestId);
|
|
162
|
+
try {
|
|
163
|
+
return await _runProcedure(input, onProgress, requestId);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
if (onceByMethodAndKey.get(trackingKey) === requestId) {
|
|
167
|
+
onceByMethodAndKey.delete(trackingKey);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
};
|
|
114
171
|
}
|
|
172
|
+
instance.onceBy = (globalKey) => {
|
|
173
|
+
const proxy = {};
|
|
174
|
+
for (const functionName of Object.keys(procedures)) {
|
|
175
|
+
if (typeof functionName !== "string")
|
|
176
|
+
continue;
|
|
177
|
+
proxy[functionName] = async (input, onProgress) => {
|
|
178
|
+
const previousRequestId = onceByGlobalKey.get(globalKey);
|
|
179
|
+
if (previousRequestId) {
|
|
180
|
+
const pending = pendingRequests.get(previousRequestId);
|
|
181
|
+
if (pending) {
|
|
182
|
+
cancelRequest(previousRequestId, `Cancelled by global onceBy("${globalKey}") call`, pending.functionName);
|
|
183
|
+
}
|
|
184
|
+
onceByGlobalKey.delete(globalKey);
|
|
185
|
+
}
|
|
186
|
+
const requestId = makeRequestId();
|
|
187
|
+
onceByGlobalKey.set(globalKey, requestId);
|
|
188
|
+
const _runProcedure = runProcedureFunctions.get(functionName);
|
|
189
|
+
if (!_runProcedure) {
|
|
190
|
+
throw new Error(`No procedure found for ${functionName}`);
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
return await _runProcedure(input, onProgress ?? emptyProgressCallback, requestId);
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
if (onceByGlobalKey.get(globalKey) === requestId) {
|
|
197
|
+
onceByGlobalKey.delete(globalKey);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return proxy;
|
|
203
|
+
};
|
|
115
204
|
return instance;
|
|
116
205
|
}
|
|
117
206
|
async function postMessage(ctx, message, options) {
|
package/dist/types.d.ts
CHANGED
|
@@ -122,10 +122,14 @@ export type PayloadCore<PM extends ProceduresMap, Name extends keyof PM = keyof
|
|
|
122
122
|
message: string;
|
|
123
123
|
};
|
|
124
124
|
};
|
|
125
|
+
/**
|
|
126
|
+
* The callable function signature for a client method
|
|
127
|
+
*/
|
|
128
|
+
export type ClientMethodCallable<P extends Procedure<Schema, Schema, Schema>> = (input: Schema.InferInput<P["input"]>, onProgress?: (progress: Schema.InferOutput<P["progress"]>) => void) => Promise<Schema.InferOutput<P["success"]>>;
|
|
125
129
|
/**
|
|
126
130
|
* A procedure's corresponding method on the client instance -- used to call the procedure. If you want to be able to cancel the request, you can use the `cancelable` method instead of running the procedure directly.
|
|
127
131
|
*/
|
|
128
|
-
export type ClientMethod<P extends Procedure<Schema, Schema, Schema>> =
|
|
132
|
+
export type ClientMethod<P extends Procedure<Schema, Schema, Schema>> = ClientMethodCallable<P> & {
|
|
129
133
|
/**
|
|
130
134
|
* A method that returns a `CancelablePromise`. Cancel it by calling `.cancel(reason)` on it, and wait for the request to resolve by awaiting the `request` property on the returned object.
|
|
131
135
|
*/
|
|
@@ -142,6 +146,14 @@ export type ClientMethod<P extends Procedure<Schema, Schema, Schema>> = ((input:
|
|
|
142
146
|
nodes?: number) => Promise<Array<PromiseSettledResult<Schema.InferOutput<P["success"]>> & {
|
|
143
147
|
node: string;
|
|
144
148
|
}>>;
|
|
149
|
+
/**
|
|
150
|
+
* Call the procedure, cancelling any previous ongoing call of this procedure beforehand.
|
|
151
|
+
*/
|
|
152
|
+
once: (input: Schema.InferInput<P["input"]>, onProgress?: (progress: Schema.InferOutput<P["progress"]>) => void) => Promise<Schema.InferOutput<P["success"]>>;
|
|
153
|
+
/**
|
|
154
|
+
* Call the procedure with a concurrency key, cancelling any previous ongoing call of this procedure with the same key beforehand.
|
|
155
|
+
*/
|
|
156
|
+
onceBy: (key: string, input: Schema.InferInput<P["input"]>, onProgress?: (progress: Schema.InferOutput<P["progress"]>) => void) => Promise<Schema.InferOutput<P["success"]>>;
|
|
145
157
|
};
|
|
146
158
|
export type WorkerConstructor<T extends Worker | SharedWorker = Worker | SharedWorker> = {
|
|
147
159
|
new (opts?: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swarpc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Full type-safe RPC library for service worker -- move things off of the UI thread with ease!",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"service-workers",
|
|
@@ -50,33 +50,33 @@
|
|
|
50
50
|
"@playwright/test": "^1.57.0",
|
|
51
51
|
"@size-limit/esbuild-why": "^12.0.0",
|
|
52
52
|
"@size-limit/preset-small-lib": "^12.0.0",
|
|
53
|
-
"@vitest/web-worker": "^4.0.
|
|
54
|
-
"arktype": "^2.1.
|
|
53
|
+
"@vitest/web-worker": "^4.0.16",
|
|
54
|
+
"arktype": "^2.1.29",
|
|
55
55
|
"date-fns": "^4.1.0",
|
|
56
56
|
"husky": "^9.1.7",
|
|
57
57
|
"kacl": "^1.1.1",
|
|
58
|
-
"knip": "^5.
|
|
58
|
+
"knip": "^5.80.0",
|
|
59
59
|
"lint-staged": "^16.2.7",
|
|
60
60
|
"nodemon": "^3.1.11",
|
|
61
|
-
"oxlint": "^1.
|
|
61
|
+
"oxlint": "^1.37.0",
|
|
62
62
|
"pkg-pr-new": "^0.0.62",
|
|
63
63
|
"prettier": "^3.7.4",
|
|
64
64
|
"sirv-cli": "^3.0.1",
|
|
65
65
|
"size-limit": "^12.0.0",
|
|
66
66
|
"typedoc": "^0.28.15",
|
|
67
67
|
"typedoc-material-theme": "^1.4.1",
|
|
68
|
-
"typedoc-plugin-dt-links": "^2.0.
|
|
68
|
+
"typedoc-plugin-dt-links": "^2.0.36",
|
|
69
69
|
"typedoc-plugin-extras": "^4.0.1",
|
|
70
70
|
"typedoc-plugin-inline-sources": "^1.3.0",
|
|
71
71
|
"typedoc-plugin-mdn-links": "^5.0.10",
|
|
72
72
|
"typedoc-plugin-redirect": "^1.2.1",
|
|
73
73
|
"typescript": "^5.9.3",
|
|
74
|
-
"vite": "^7.
|
|
75
|
-
"vitest": "^4.0.
|
|
74
|
+
"vite": "^7.3.0",
|
|
75
|
+
"vitest": "^4.0.16"
|
|
76
76
|
},
|
|
77
77
|
"volta": {
|
|
78
|
-
"node": "24.
|
|
79
|
-
"npm": "11.
|
|
78
|
+
"node": "24.12.0",
|
|
79
|
+
"npm": "11.7.0"
|
|
80
80
|
},
|
|
81
81
|
"lint-staged": {
|
|
82
82
|
"*.{ts,js,md,json,yaml,yml}": [
|