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.
Files changed (2) hide show
  1. package/README.md +377 -0
  2. 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
+ }