supertalk 0.0.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 +377 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
# Supertalk
|
|
2
|
+
|
|
3
|
+
A type-safe, unified communication library for Web Workers, Iframes, and Node.js
|
|
4
|
+
worker threads.
|
|
5
|
+
|
|
6
|
+
**Supertalk turns workers' low-level message passing into a high-level,
|
|
7
|
+
type-safe RPC layer—so you can call methods, pass callbacks, and await promises
|
|
8
|
+
as if they were local.**
|
|
9
|
+
|
|
10
|
+
Supertalk is built to be a joy to use and deploy:
|
|
11
|
+
|
|
12
|
+
- **Type-safe:** Your IDE knows exactly what's proxied vs cloned
|
|
13
|
+
- **Ergonomic:** Callbacks, promises, and classes just work
|
|
14
|
+
- **Bidirectional:** The same types and patterns work in both directions
|
|
15
|
+
- **Fast & small:** ~2.3 kB brotli-compressed, zero dependencies
|
|
16
|
+
- **Composable & extendable:** Non-global configuration, nested objects,
|
|
17
|
+
services are just classes, composable transport handlers
|
|
18
|
+
- **Standard modules:** ESM-only, no CommonJS
|
|
19
|
+
- **Modern JavaScript:** Published as ES2024, targeting current browsers and
|
|
20
|
+
Node.js 20+
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install supertalk
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
This package re-exports `@supertalk/core`. As additional packages are added to
|
|
29
|
+
the Supertalk ecosystem, they may be included here as well.
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
**worker.ts** (exposed side):
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import {expose} from 'supertalk';
|
|
37
|
+
|
|
38
|
+
const service = {
|
|
39
|
+
add(a: number, b: number): number {
|
|
40
|
+
return a + b;
|
|
41
|
+
},
|
|
42
|
+
async fetchData(url: string): Promise<string> {
|
|
43
|
+
const res = await fetch(url);
|
|
44
|
+
return res.text();
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
expose(service, self);
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**main.ts** (wrapped side):
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import {wrap} from 'supertalk';
|
|
55
|
+
|
|
56
|
+
const worker = new Worker('./worker.ts');
|
|
57
|
+
const remote = await wrap<typeof service>(worker);
|
|
58
|
+
|
|
59
|
+
// Methods become async
|
|
60
|
+
const result = await remote.add(1, 2); // 3
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Core Concepts
|
|
64
|
+
|
|
65
|
+
### Requests and Responses
|
|
66
|
+
|
|
67
|
+
Most cross-worker communication follows a request/response pattern, but
|
|
68
|
+
`postMessage()` only sends one-way messages. Matching responses to requests is
|
|
69
|
+
left as an exercise to the developer. Supertalk builds a request/response
|
|
70
|
+
protocol on top of `postMessage()`, so you can call methods and await results
|
|
71
|
+
naturally.
|
|
72
|
+
|
|
73
|
+
### Clones vs Proxies
|
|
74
|
+
|
|
75
|
+
`postMessage()` copies payloads via the structured clone algorithm, which only
|
|
76
|
+
supports a limited set of types. Functions are completely unsupported, and class
|
|
77
|
+
instances lose their prototypes—so things like Promises don't survive the trip.
|
|
78
|
+
|
|
79
|
+
Supertalk addresses this by _proxying_ values that can't be cloned. A proxied
|
|
80
|
+
object stays on its original side; the other side gets a lightweight proxy that
|
|
81
|
+
forwards calls back. This is how functions, promises, and class instances work
|
|
82
|
+
across the message boundary.
|
|
83
|
+
|
|
84
|
+
### Shallow vs Deep Proxying
|
|
85
|
+
|
|
86
|
+
By default, Supertalk only proxies objects passed directly to or returned from
|
|
87
|
+
method calls. This keeps messages fast. If you need proxies nested anywhere in a
|
|
88
|
+
payload, set `nestedProxies: true` to traverse the full object graph.
|
|
89
|
+
|
|
90
|
+
## Core Features
|
|
91
|
+
|
|
92
|
+
### Automatic proxying of functions and promises
|
|
93
|
+
|
|
94
|
+
Functions and promises passed as **top-level arguments or return values** are
|
|
95
|
+
always proxied:
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// Exposed side
|
|
99
|
+
const service = {
|
|
100
|
+
forEach(items: number[], callback: (item: number) => void) {
|
|
101
|
+
for (const item of items) {
|
|
102
|
+
callback(item); // Calls back to wrapped side
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// Wrapped side
|
|
110
|
+
await remote.forEach([1, 2, 3], (item) => {
|
|
111
|
+
console.log(item); // Runs locally
|
|
112
|
+
});
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
All functions are transformed into promise-returning async functions on the
|
|
116
|
+
wrapped side. Proxies are released from memory when they're no longer in use by
|
|
117
|
+
the wrapped side.
|
|
118
|
+
|
|
119
|
+
### Object proxying with `proxy()`
|
|
120
|
+
|
|
121
|
+
Objects are cloned by default. This works well for immutable data objects, but
|
|
122
|
+
not for all cases. The `proxy()` function marks an object as needing to be
|
|
123
|
+
proxied instead of cloned.
|
|
124
|
+
|
|
125
|
+
**Use `proxy()` when returning:**
|
|
126
|
+
|
|
127
|
+
1. **Mutable objects** — The remote side should see updates
|
|
128
|
+
2. **Large graphs** — Avoid cloning expensive data structures
|
|
129
|
+
3. **Class instances with methods** — Preserve the prototype API
|
|
130
|
+
|
|
131
|
+
`worker.ts`:
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
import {expose, proxy} from 'supertalk';
|
|
135
|
+
|
|
136
|
+
export class Widget {
|
|
137
|
+
count = 42;
|
|
138
|
+
sayHello() {
|
|
139
|
+
return 'hello';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
expose(
|
|
144
|
+
{
|
|
145
|
+
createWidget() {
|
|
146
|
+
return proxy(new Widget()); // Explicitly proxied
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
self,
|
|
150
|
+
);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`main.ts`:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
const service = await wrap(worker);
|
|
157
|
+
const widget = await service.createWidget();
|
|
158
|
+
|
|
159
|
+
// Instance and prototype APIs are available asynchronously
|
|
160
|
+
const count = await widget.count;
|
|
161
|
+
const hello = await widget.sayHello();
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Nested Objects & Proxies
|
|
165
|
+
|
|
166
|
+
Supertalk supports two modes for handling nested values:
|
|
167
|
+
|
|
168
|
+
- **Shallow mode** (default): Maximum performance, only top-level function
|
|
169
|
+
arguments and return values are proxied. Nested functions/promises fail with
|
|
170
|
+
`DataCloneError`.
|
|
171
|
+
- **Nested mode** (`nestedProxies: true`): Full payload traversal. Functions and
|
|
172
|
+
promises anywhere in the graph are auto-proxied.
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
// On exposed side
|
|
176
|
+
expose(service, self, {nestedProxies: true});
|
|
177
|
+
|
|
178
|
+
const service = {
|
|
179
|
+
getData() {
|
|
180
|
+
return {
|
|
181
|
+
process: (x) => x * 2, // auto-proxied (function)
|
|
182
|
+
widget: proxy(new Widget()), // explicitly proxied (class)
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// On wrapped side
|
|
188
|
+
const remote = await wrap<typeof service>(worker, {nestedProxies: true});
|
|
189
|
+
const data = await remote.getData();
|
|
190
|
+
await data.process(5); // 10
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Transferables with `transfer()`
|
|
194
|
+
|
|
195
|
+
Use `transfer()` to mark values like `ArrayBuffer`, `MessagePort`, or streams to
|
|
196
|
+
be transferred rather than cloned.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
import {transfer} from 'supertalk';
|
|
200
|
+
|
|
201
|
+
const service = {
|
|
202
|
+
getBuffer(): ArrayBuffer {
|
|
203
|
+
const buf = new ArrayBuffer(1024);
|
|
204
|
+
return transfer(buf); // Zero-copy transfer
|
|
205
|
+
},
|
|
206
|
+
};
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### Value Handling Reference
|
|
210
|
+
|
|
211
|
+
When values cross the communication boundary:
|
|
212
|
+
|
|
213
|
+
| Value Type | Shallow Mode | Nested Mode |
|
|
214
|
+
| ------------------------------------------------ | ------------ | ----------- |
|
|
215
|
+
| Primitives | ✅ Cloned | ✅ Cloned |
|
|
216
|
+
| Plain objects `{...}` | ✅ Cloned | ✅ Cloned |
|
|
217
|
+
| Arrays | ✅ Cloned | ✅ Cloned |
|
|
218
|
+
| Functions (top-level) | 🛜 Proxied | 🛜 Proxied |
|
|
219
|
+
| Functions (nested) | ❌ Error | 🛜 Proxied |
|
|
220
|
+
| Promises (top-level return) | 🛜 Proxied | 🛜 Proxied |
|
|
221
|
+
| Promises (nested) | ❌ Error | 🛜 Proxied |
|
|
222
|
+
| `proxy()` wrapped values | 🛜 Proxied | 🛜 Proxied |
|
|
223
|
+
| `proxy()` wrapped values (nested) | ❌ Error | 🛜 Proxied |
|
|
224
|
+
| Class instances & objects with custom prototypes | ⚠️ Cloned\* | ⚠️ Cloned\* |
|
|
225
|
+
| Other structured cloneable objects | ✅ Cloned | ✅ Cloned |
|
|
226
|
+
|
|
227
|
+
\* _Class instances are cloned via structured clone (losing methods) unless
|
|
228
|
+
wrapped in `proxy()`._
|
|
229
|
+
|
|
230
|
+
## API Reference
|
|
231
|
+
|
|
232
|
+
### Core Functions
|
|
233
|
+
|
|
234
|
+
#### `expose(target, endpoint, options?)`
|
|
235
|
+
|
|
236
|
+
Exposes an object or function to the other side.
|
|
237
|
+
|
|
238
|
+
- `target`: The service object or function to expose.
|
|
239
|
+
- `endpoint`: The `Worker`, `MessagePort`, `Window`, or compatible interface.
|
|
240
|
+
- `options`: Connection options (see below).
|
|
241
|
+
|
|
242
|
+
#### `wrap<T>(endpoint, options?)`
|
|
243
|
+
|
|
244
|
+
Connects to an exposed service and returns a proxy.
|
|
245
|
+
|
|
246
|
+
- `endpoint`: The `Worker`, `MessagePort`, `Window`, or compatible interface.
|
|
247
|
+
- `options`: Connection options (see below).
|
|
248
|
+
- Returns: `Promise<Remote<T>>`
|
|
249
|
+
|
|
250
|
+
#### `proxy(value)`
|
|
251
|
+
|
|
252
|
+
Marks an object to be proxied rather than cloned. Use this for:
|
|
253
|
+
|
|
254
|
+
- Class instances with methods
|
|
255
|
+
- Mutable objects that should be shared
|
|
256
|
+
- Large objects to avoid cloning costs
|
|
257
|
+
|
|
258
|
+
#### `transfer(value, transferables?)`
|
|
259
|
+
|
|
260
|
+
Marks a value to be transferred (zero-copy) rather than cloned.
|
|
261
|
+
|
|
262
|
+
- `value`: The value to send (e.g. `ArrayBuffer`, `MessagePort`).
|
|
263
|
+
- `transferables`: Optional array of transferables. If omitted, `value` is
|
|
264
|
+
assumed to be the transferable.
|
|
265
|
+
|
|
266
|
+
### Options
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
interface Options {
|
|
270
|
+
/** Enable nested proxy handling (default: false) */
|
|
271
|
+
nestedProxies?: boolean;
|
|
272
|
+
/** Enable debug mode for better error messages */
|
|
273
|
+
debug?: boolean;
|
|
274
|
+
/** Custom handlers for serializing/deserializing specific types */
|
|
275
|
+
handlers?: Array<Handler>;
|
|
276
|
+
}
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Handlers
|
|
280
|
+
|
|
281
|
+
Handlers provide pluggable serialization for custom types or streams.
|
|
282
|
+
|
|
283
|
+
**Stream Handler Example:**
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
import {streamHandler} from 'supertalk/handlers/streams.js';
|
|
287
|
+
|
|
288
|
+
expose(service, self, {handlers: [streamHandler]});
|
|
289
|
+
const remote = await wrap<typeof service>(worker, {handlers: [streamHandler]});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Custom Handlers:** You can create handlers for types like `Map`, `Set`, or
|
|
293
|
+
domain objects. See `Handler`, `ToWireContext`, and `FromWireContext` types.
|
|
294
|
+
|
|
295
|
+
### TypeScript Types
|
|
296
|
+
|
|
297
|
+
#### `Remote<T>`
|
|
298
|
+
|
|
299
|
+
The primary type for service proxies. Transforms all methods to async.
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
const remote = await wrap<MyService>(worker);
|
|
303
|
+
// remote has type Remote<MyService>
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
#### `RemoteProxy<T>`
|
|
307
|
+
|
|
308
|
+
What you receive when the other side sends a `LocalProxy<T>`. All property and
|
|
309
|
+
method access becomes async.
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
// RemoteProxy<Counter>:
|
|
313
|
+
// {
|
|
314
|
+
// count: Promise<number>;
|
|
315
|
+
// increment: () => Promise<number>;
|
|
316
|
+
// }
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
## Advanced Usage
|
|
320
|
+
|
|
321
|
+
### Debugging
|
|
322
|
+
|
|
323
|
+
Debugging `DataCloneError` can be tricky. Supertalk provides a `debug` option
|
|
324
|
+
that traverses your data before sending and throws a `NonCloneableError` with
|
|
325
|
+
the exact path to the problematic value.
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
const remote = await wrap<Service>(endpoint, {debug: true});
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Memory Management
|
|
332
|
+
|
|
333
|
+
Proxied objects are tracked with registries on both sides.
|
|
334
|
+
|
|
335
|
+
- **Source side**: Holds strong references until released.
|
|
336
|
+
- **Consumer side**: Holds weak references; when GC'd, notifies source to
|
|
337
|
+
release.
|
|
338
|
+
|
|
339
|
+
## Ecosystem
|
|
340
|
+
|
|
341
|
+
This package re-exports `@supertalk/core`. Additional packages are available for
|
|
342
|
+
extended functionality:
|
|
343
|
+
|
|
344
|
+
| Package | Description |
|
|
345
|
+
| ---------------------------------------------------------------------- | ------------------------------------------------ |
|
|
346
|
+
| [@supertalk/signals](https://www.npmjs.com/package/@supertalk/signals) | TC39 Signals integration for reactive state sync |
|
|
347
|
+
|
|
348
|
+
## Why Supertalk?
|
|
349
|
+
|
|
350
|
+
Workers are great for offloading work, but the raw `postMessage` API is
|
|
351
|
+
difficult:
|
|
352
|
+
|
|
353
|
+
- No built-in request/response (one-way only)
|
|
354
|
+
- No error propagation
|
|
355
|
+
- No functions or promises (DataCloneError)
|
|
356
|
+
- Manual lifetime management
|
|
357
|
+
- Manual transfer lists
|
|
358
|
+
|
|
359
|
+
Supertalk handles all of this: RPC layer, transparent proxying, automatic
|
|
360
|
+
lifetime management, and deep traversal.
|
|
361
|
+
|
|
362
|
+
### Comparison to Comlink
|
|
363
|
+
|
|
364
|
+
Supertalk is inspired by [Comlink](https://github.com/GoogleChromeLabs/comlink)
|
|
365
|
+
but improves on it:
|
|
366
|
+
|
|
367
|
+
- **Automatic proxying:** Functions/promises are auto-proxied without special
|
|
368
|
+
wrappers
|
|
369
|
+
- **Nested support:** `nestedProxies` mode allows proxies anywhere in the
|
|
370
|
+
payload
|
|
371
|
+
- **Debug mode:** Reports exactly where non-serializable values are
|
|
372
|
+
- **Symmetric:** Both ends use the same internal architecture
|
|
373
|
+
- **Type-safe:** Better TypeScript inference for what's proxied vs cloned
|
|
374
|
+
|
|
375
|
+
## License
|
|
376
|
+
|
|
377
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "supertalk",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Type-safe client/server communication for workers, iframes, and RPC",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./index.d.ts",
|
|
9
|
+
"default": "./index.js"
|
|
10
|
+
}
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"index.js",
|
|
14
|
+
"index.d.ts",
|
|
15
|
+
"index.d.ts.map",
|
|
16
|
+
"index.js.map"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "wireit"
|
|
20
|
+
},
|
|
21
|
+
"wireit": {
|
|
22
|
+
"build": {
|
|
23
|
+
"command": "tsc --build --pretty",
|
|
24
|
+
"dependencies": [
|
|
25
|
+
"../core:build"
|
|
26
|
+
],
|
|
27
|
+
"files": [
|
|
28
|
+
"src/**/*.ts",
|
|
29
|
+
"tsconfig.json",
|
|
30
|
+
"../../tsconfig.base.json"
|
|
31
|
+
],
|
|
32
|
+
"output": [
|
|
33
|
+
"index.*",
|
|
34
|
+
".tsbuildinfo"
|
|
35
|
+
],
|
|
36
|
+
"clean": "if-file-deleted"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"rpc",
|
|
41
|
+
"worker",
|
|
42
|
+
"iframe",
|
|
43
|
+
"postmessage",
|
|
44
|
+
"proxy",
|
|
45
|
+
"comlink",
|
|
46
|
+
"typescript"
|
|
47
|
+
],
|
|
48
|
+
"author": "Justin Fagnani",
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"homepage": "https://github.com/justinfagnani/supertalk#readme",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/justinfagnani/supertalk.git",
|
|
54
|
+
"directory": "packages/supertalk"
|
|
55
|
+
},
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/justinfagnani/supertalk/issues"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"@supertalk/core": "0.0.1"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"typescript": "^5.7.2"
|
|
64
|
+
}
|
|
65
|
+
}
|