swarpc 0.9.0 → 0.11.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 +18 -0
- package/dist/client.d.ts +37 -9
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +83 -21
- package/dist/localstorage.d.ts +14 -0
- package/dist/localstorage.d.ts.map +1 -0
- package/dist/localstorage.js +39 -0
- package/dist/server.d.ts +7 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +70 -12
- package/dist/types.d.ts +63 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -4
- package/package.json +5 -5
- package/src/client.ts +115 -27
- package/src/localstorage.ts +46 -0
- package/src/server.ts +98 -12
- package/src/types.ts +16 -8
package/src/server.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* @mergeModuleWith <project>
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
/// <reference lib="webworker" />
|
|
6
7
|
import { type } from "arktype"
|
|
7
8
|
import { createLogger, type LogLevel } from "./log.js"
|
|
8
9
|
import {
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
Payload,
|
|
11
12
|
PayloadCore,
|
|
12
13
|
PayloadHeaderSchema,
|
|
14
|
+
PayloadInitializeSchema,
|
|
13
15
|
PayloadSchema,
|
|
14
16
|
ProcedureImplementation,
|
|
15
17
|
zImplementations,
|
|
@@ -17,6 +19,20 @@ import {
|
|
|
17
19
|
type ProceduresMap,
|
|
18
20
|
} from "./types.js"
|
|
19
21
|
import { findTransferables } from "./utils.js"
|
|
22
|
+
import { FauxLocalStorage } from "./localstorage.js"
|
|
23
|
+
|
|
24
|
+
class MockedWorkerGlobalScope {
|
|
25
|
+
constructor() {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const SharedWorkerGlobalScope =
|
|
29
|
+
globalThis.SharedWorkerGlobalScope ?? MockedWorkerGlobalScope
|
|
30
|
+
|
|
31
|
+
const DedicatedWorkerGlobalScope =
|
|
32
|
+
globalThis.DedicatedWorkerGlobalScope ?? MockedWorkerGlobalScope
|
|
33
|
+
|
|
34
|
+
const ServiceWorkerGlobalScope =
|
|
35
|
+
globalThis.ServiceWorkerGlobalScope ?? MockedWorkerGlobalScope
|
|
20
36
|
|
|
21
37
|
/**
|
|
22
38
|
* The sw&rpc server instance, which provides methods to register {@link ProcedureImplementation | procedure implementations},
|
|
@@ -25,7 +41,7 @@ import { findTransferables } from "./utils.js"
|
|
|
25
41
|
export type SwarpcServer<Procedures extends ProceduresMap> = {
|
|
26
42
|
[zProcedures]: Procedures
|
|
27
43
|
[zImplementations]: ImplementationsMap<Procedures>
|
|
28
|
-
start(
|
|
44
|
+
start(): Promise<void>
|
|
29
45
|
} & {
|
|
30
46
|
[F in keyof Procedures]: (
|
|
31
47
|
impl: ProcedureImplementation<
|
|
@@ -43,7 +59,9 @@ const abortedRequests = new Set<string>()
|
|
|
43
59
|
* Creates a sw&rpc server instance.
|
|
44
60
|
* @param procedures procedures the server will implement, see {@link ProceduresMap}
|
|
45
61
|
* @param options various options
|
|
46
|
-
* @param options.
|
|
62
|
+
* @param options.scope The worker scope to use, defaults to the `self` of the file where Server() is called.
|
|
63
|
+
* @param options.loglevel Maximum log level to use, defaults to "debug" (shows everything). "info" will not show debug messages, "warn" will only show warnings and errors, "error" will only show errors.
|
|
64
|
+
* @param options._scopeType @internal Don't touch, this is used in testing environments because the mock is subpar. Manually overrides worker scope type detection.
|
|
47
65
|
* @returns a SwarpcServer instance. Each property of the procedures map will be a method, that accepts a function implementing the procedure (see {@link ProcedureImplementation}). There is also .start(), to be called after implementing all procedures.
|
|
48
66
|
*
|
|
49
67
|
* An example of defining a server:
|
|
@@ -51,17 +69,49 @@ const abortedRequests = new Set<string>()
|
|
|
51
69
|
*/
|
|
52
70
|
export function Server<Procedures extends ProceduresMap>(
|
|
53
71
|
procedures: Procedures,
|
|
54
|
-
{
|
|
72
|
+
{
|
|
73
|
+
loglevel = "debug",
|
|
74
|
+
scope,
|
|
75
|
+
_scopeType,
|
|
76
|
+
}: {
|
|
77
|
+
scope?: WorkerGlobalScope
|
|
78
|
+
loglevel?: LogLevel
|
|
79
|
+
_scopeType?: "dedicated" | "shared" | "service"
|
|
80
|
+
} = {}
|
|
55
81
|
): SwarpcServer<Procedures> {
|
|
56
82
|
const l = createLogger("server", loglevel)
|
|
57
83
|
|
|
84
|
+
// If scope is not provided, use the global scope
|
|
85
|
+
// This function is meant to be used in a worker, so `self` is a WorkerGlobalScope
|
|
86
|
+
scope ??= self as WorkerGlobalScope
|
|
87
|
+
|
|
88
|
+
function scopeIsShared(
|
|
89
|
+
scope: WorkerGlobalScope
|
|
90
|
+
): scope is SharedWorkerGlobalScope {
|
|
91
|
+
return scope instanceof SharedWorkerGlobalScope || _scopeType === "shared"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function scopeIsDedicated(
|
|
95
|
+
scope: WorkerGlobalScope
|
|
96
|
+
): scope is DedicatedWorkerGlobalScope {
|
|
97
|
+
return (
|
|
98
|
+
scope instanceof DedicatedWorkerGlobalScope || _scopeType === "dedicated"
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function scopeIsService(
|
|
103
|
+
scope: WorkerGlobalScope
|
|
104
|
+
): scope is ServiceWorkerGlobalScope {
|
|
105
|
+
return scope instanceof ServiceWorkerGlobalScope || _scopeType === "service"
|
|
106
|
+
}
|
|
107
|
+
|
|
58
108
|
// Initialize the instance.
|
|
59
109
|
// Procedures and implementations are stored on properties with symbol keys,
|
|
60
110
|
// to avoid any conflicts with procedure names, and also discourage direct access to them.
|
|
61
111
|
const instance = {
|
|
62
112
|
[zProcedures]: procedures,
|
|
63
113
|
[zImplementations]: {} as ImplementationsMap<Procedures>,
|
|
64
|
-
start: (
|
|
114
|
+
start: async () => {},
|
|
65
115
|
} as SwarpcServer<Procedures>
|
|
66
116
|
|
|
67
117
|
// Set all implementation-setter methods
|
|
@@ -85,7 +135,16 @@ export function Server<Procedures extends ProceduresMap>(
|
|
|
85
135
|
}) as SwarpcServer<Procedures>[typeof functionName]
|
|
86
136
|
}
|
|
87
137
|
|
|
88
|
-
instance.start = (
|
|
138
|
+
instance.start = async () => {
|
|
139
|
+
const port = await new Promise<MessagePort | undefined>((resolve) => {
|
|
140
|
+
if (!scopeIsShared(scope)) return resolve(undefined)
|
|
141
|
+
l.debug(null, "Awaiting shared worker connection...")
|
|
142
|
+
scope.addEventListener("connect", ({ ports: [port] }) => {
|
|
143
|
+
l.debug(null, "Shared worker connected with port", port)
|
|
144
|
+
resolve(port)
|
|
145
|
+
})
|
|
146
|
+
})
|
|
147
|
+
|
|
89
148
|
// Used to post messages back to the client
|
|
90
149
|
const postMessage = async (
|
|
91
150
|
autotransfer: boolean,
|
|
@@ -93,17 +152,27 @@ export function Server<Procedures extends ProceduresMap>(
|
|
|
93
152
|
) => {
|
|
94
153
|
const transfer = autotransfer ? [] : findTransferables(data)
|
|
95
154
|
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
} else {
|
|
99
|
-
|
|
155
|
+
if (port) {
|
|
156
|
+
port.postMessage(data, { transfer })
|
|
157
|
+
} else if (scopeIsDedicated(scope)) {
|
|
158
|
+
scope.postMessage(data, { transfer })
|
|
159
|
+
} else if (scopeIsService(scope)) {
|
|
160
|
+
await scope.clients.matchAll().then((clients) => {
|
|
100
161
|
clients.forEach((client) => client.postMessage(data, { transfer }))
|
|
101
162
|
})
|
|
102
163
|
}
|
|
103
164
|
}
|
|
104
165
|
|
|
105
|
-
|
|
106
|
-
|
|
166
|
+
const listener = async (
|
|
167
|
+
event: MessageEvent<any> | ExtendableMessageEvent
|
|
168
|
+
): Promise<void> => {
|
|
169
|
+
if (PayloadInitializeSchema.allows(event.data)) {
|
|
170
|
+
const { localStorageData } = event.data
|
|
171
|
+
l.debug(null, "Setting up faux localStorage", localStorageData)
|
|
172
|
+
new FauxLocalStorage(localStorageData).register(scope)
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
107
176
|
// Decode the payload
|
|
108
177
|
const { requestId, functionName } = PayloadHeaderSchema(
|
|
109
178
|
type.enumerated(...Object.keys(procedures))
|
|
@@ -151,6 +220,9 @@ export function Server<Procedures extends ProceduresMap>(
|
|
|
151
220
|
schemas.success
|
|
152
221
|
).assert(event.data)
|
|
153
222
|
|
|
223
|
+
if ("localStorageData" in payload)
|
|
224
|
+
throw "Unreachable: #initialize request payload should've been handled already"
|
|
225
|
+
|
|
154
226
|
// Handle abortion requests (pro-choice ftw!!)
|
|
155
227
|
if (payload.abort) {
|
|
156
228
|
const controller = abortControllers.get(requestId)
|
|
@@ -206,7 +278,21 @@ export function Server<Procedures extends ProceduresMap>(
|
|
|
206
278
|
} finally {
|
|
207
279
|
abortedRequests.delete(requestId)
|
|
208
280
|
}
|
|
209
|
-
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Listen for messages from the client
|
|
284
|
+
if (scopeIsShared(scope)) {
|
|
285
|
+
if (!port) throw new Error("SharedWorker port not initialized")
|
|
286
|
+
console.log("Listening for shared worker messages on port", port)
|
|
287
|
+
port.addEventListener("message", listener)
|
|
288
|
+
port.start()
|
|
289
|
+
} else if (scopeIsDedicated(scope)) {
|
|
290
|
+
scope.addEventListener("message", listener)
|
|
291
|
+
} else if (scopeIsService(scope)) {
|
|
292
|
+
scope.addEventListener("message", listener)
|
|
293
|
+
} else {
|
|
294
|
+
throw new Error(`Unsupported worker scope ${scope}`)
|
|
295
|
+
}
|
|
210
296
|
}
|
|
211
297
|
|
|
212
298
|
return instance
|
package/src/types.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { type, type Type } from "arktype"
|
|
7
|
-
import {
|
|
7
|
+
import { RequestBoundLogger } from "./log.js"
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* A procedure declaration
|
|
@@ -51,7 +51,7 @@ export type CancelablePromise<T = unknown> = {
|
|
|
51
51
|
* Abort the request.
|
|
52
52
|
* @param reason The reason for cancelling the request.
|
|
53
53
|
*/
|
|
54
|
-
cancel: (reason: string) =>
|
|
54
|
+
cancel: (reason: string) => void
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/**
|
|
@@ -87,7 +87,7 @@ export type ProcedureImplementation<
|
|
|
87
87
|
|
|
88
88
|
/**
|
|
89
89
|
* Declarations of procedures by name.
|
|
90
|
-
*
|
|
90
|
+
*
|
|
91
91
|
* An example of declaring procedures:
|
|
92
92
|
* {@includeCode ../example/src/lib/procedures.ts}
|
|
93
93
|
*/
|
|
@@ -131,6 +131,14 @@ export type Hooks<Procedures extends ProceduresMap> = {
|
|
|
131
131
|
) => void
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
export const PayloadInitializeSchema = type({
|
|
135
|
+
by: '"sw&rpc"',
|
|
136
|
+
functionName: '"#initialize"',
|
|
137
|
+
localStorageData: "Record<string, unknown>",
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
export type PayloadInitialize = typeof PayloadInitializeSchema.infer
|
|
141
|
+
|
|
134
142
|
/**
|
|
135
143
|
* @source
|
|
136
144
|
*/
|
|
@@ -184,11 +192,11 @@ export type PayloadCore<
|
|
|
184
192
|
* @source
|
|
185
193
|
*/
|
|
186
194
|
export const PayloadSchema = type
|
|
187
|
-
.scope({ PayloadCoreSchema, PayloadHeaderSchema })
|
|
195
|
+
.scope({ PayloadCoreSchema, PayloadHeaderSchema, PayloadInitializeSchema })
|
|
188
196
|
.type("<Name extends string, I, P, S>", [
|
|
189
|
-
"PayloadHeaderSchema<Name>",
|
|
190
|
-
"
|
|
191
|
-
"
|
|
197
|
+
["PayloadHeaderSchema<Name>", "&", "PayloadCoreSchema<I, P, S>"],
|
|
198
|
+
"|",
|
|
199
|
+
"PayloadInitializeSchema",
|
|
192
200
|
])
|
|
193
201
|
|
|
194
202
|
/**
|
|
@@ -197,7 +205,7 @@ export const PayloadSchema = type
|
|
|
197
205
|
export type Payload<
|
|
198
206
|
PM extends ProceduresMap,
|
|
199
207
|
Name extends keyof PM = keyof PM,
|
|
200
|
-
> = PayloadHeader<PM, Name> & PayloadCore<PM, Name>
|
|
208
|
+
> = (PayloadHeader<PM, Name> & PayloadCore<PM, Name>) | PayloadInitialize
|
|
201
209
|
|
|
202
210
|
/**
|
|
203
211
|
* 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.
|