@jcubic/mitty 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jakub T. Jankiewicz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,298 @@
1
+ <h1 align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://github.com/jcubic/mitty/blob/master/.github/logo-dark.svg?raw=true" />
4
+ <source media="(prefers-color-scheme: light)" srcset="https://github.com/jcubic/mitty/blob/master/.github/logo-light.svg?raw=true" />
5
+ <img alt="Mitty Logo" src="https://github.com/jcubic/mitty/blob/master/.github/logo-light.svg?raw=true" height="500"/>
6
+ </picture>
7
+ </h1>
8
+
9
+ <div align="center">
10
+
11
+ [![npm version](https://img.shields.io/npm/v/@jcubic/mitty.svg)](https://www.npmjs.com/package/@jcubic/mitty)
12
+ [![github repo](https://img.shields.io/badge/github-repo-orange?logo=github)](https://github.com/jcubic/mitty)
13
+ [![CI](https://github.com/jcubic/mitty/actions/workflows/ci.yml/badge.svg)](https://github.com/jcubic/mitty/actions/workflows/ci.yml)
14
+ [![Coverage Status](https://coveralls.io/repos/github/jcubic/mitty/badge.svg)](https://coveralls.io/github/jcubic/mitty)
15
+ [![LICENSE MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jcubic/mitty/blob/master/LICENSE)
16
+
17
+ </div>
18
+
19
+ Use objects that only exist on the main thread — DOM nodes, jQuery objects, class
20
+ instances with methods — from inside a Web Worker.
21
+
22
+ A worker has no DOM, and `postMessage` cannot carry a DOM node, a jQuery object, or
23
+ anything else that isn't structured-cloneable. Mitty leaves the real object on the main
24
+ thread and puts a proxy in the worker. The proxy records property accesses and calls
25
+ without touching the channel; the whole chain is replayed on the main thread when you
26
+ await it.
27
+
28
+ ```js
29
+ // inside a worker — no DOM here
30
+ const $ = require('$');
31
+ await $('#list').find('li').first().text(); // one message, not four
32
+ ```
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ npm install @jcubic/mitty
38
+ ```
39
+
40
+ Or from a CDN, for a `<script>` tag or `importScripts()` — the build exposes a `Mitty`
41
+ global:
42
+
43
+ ```html
44
+ <script src="https://cdn.jsdelivr.net/npm/@jcubic/mitty"></script>
45
+ ```
46
+
47
+ ## Quick start
48
+
49
+ ### Main thread
50
+
51
+ ```js
52
+ import { Host } from '@jcubic/mitty';
53
+
54
+ const modules = {
55
+ $: () => jQuery,
56
+ term: () => $('.terminal').terminal(),
57
+ };
58
+
59
+ const channel = new BroadcastChannel('my-app');
60
+
61
+ const host = new Host({
62
+ channel,
63
+ resolve(name) {
64
+ if (Object.hasOwn(modules, name)) {
65
+ return modules[name]();
66
+ }
67
+ return null;
68
+ },
69
+ serialize(value) {
70
+ // a jQuery object wraps DOM nodes and cannot cross the channel —
71
+ // hand out a handle the worker can call methods on instead
72
+ if (value instanceof $.fn.init) {
73
+ return this.remote(value);
74
+ }
75
+ return value;
76
+ },
77
+ });
78
+
79
+ const worker = new Worker('./worker.js');
80
+ ```
81
+
82
+ Only the names your `resolve()` answers are reachable. Everything else on the page stays
83
+ invisible to the worker.
84
+
85
+ ### Worker
86
+
87
+ With an `import` statement, in a worker started with `{ type: 'module' }`:
88
+
89
+ ```js
90
+ import { connect } from '@jcubic/mitty';
91
+
92
+ const { require } = connect(new BroadcastChannel('my-app'));
93
+
94
+ const $ = require('$');
95
+ await $('.terminal').terminal().echo('Hello from a worker');
96
+ ```
97
+
98
+ Or with `importScripts()`, in a classic worker:
99
+
100
+ ```js
101
+ importScripts('https://cdn.jsdelivr.net/npm/@jcubic/mitty');
102
+
103
+ const { require } = Mitty.connect(new BroadcastChannel('my-app'));
104
+ ```
105
+
106
+ ## One channel per worker
107
+
108
+ Both ends take a channel rather than creating one, so you decide what the two sides talk
109
+ over and how many conversations there are. Request ids are per connection and start at 1,
110
+ so two workers sharing a channel name will see each other's replies and resolve the wrong
111
+ calls. Give every worker its own channel:
112
+
113
+ ```js
114
+ let seq = 0;
115
+
116
+ function spawn(url) {
117
+ const name = `my-app-${++seq}`;
118
+ const host = new Host({ channel: new BroadcastChannel(name), resolve, serialize });
119
+ const worker = new Worker(url);
120
+ worker.postMessage({ channel: name }); // tell the worker which one to join
121
+ return { host, worker };
122
+ }
123
+ ```
124
+
125
+ A channel is never closed by this library — whoever created it owns it.
126
+
127
+ ## How a chain becomes one message
128
+
129
+ `$('#list').find('li').first().text()` does not talk to the main thread four times. Each
130
+ step appends to a list of operations held in the worker:
131
+
132
+ ```js
133
+ [
134
+ { type: 'call', args: ['#list'] },
135
+ { type: 'get', key: 'find' },
136
+ { type: 'call', args: ['li'] },
137
+ // ...
138
+ ];
139
+ ```
140
+
141
+ Nothing is sent until the chain is awaited — that is the only point at which the proxy
142
+ becomes a real thenable. The host then walks the whole list against the resolved module
143
+ and sends back the final value.
144
+
145
+ A consequence worth knowing: a proxy you have not awaited is not a promise, and a bare
146
+ handle is not one either. Both are inert until you chain something onto them and await.
147
+
148
+ ## Handles and memory
149
+
150
+ Anything `serialize()` turns into a handle with `this.remote(value)` is kept alive on the
151
+ main thread until it is released. There is no automatic collection — a handle is a plain
152
+ integer on the wire, and the host cannot see when the worker drops its proxy.
153
+
154
+ Release explicitly when you are done:
155
+
156
+ ```js
157
+ const list = await $('#list');
158
+ // ... use it ...
159
+ release(list);
160
+ ```
161
+
162
+ If you would rather tie it to garbage collection, a `FinalizationRegistry` can do it —
163
+ but the callback must not capture the proxy, or the proxy will never become unreachable
164
+ and the callback will never run. Register the numeric id instead, which is what
165
+ `handle()` is for:
166
+
167
+ ```js
168
+ const client = connect(channel);
169
+ const registry = new FinalizationRegistry(id => client.release(id));
170
+
171
+ function tracked(remote) {
172
+ registry.register(remote, client.handle(remote));
173
+ return remote;
174
+ }
175
+
176
+ const list = tracked(await $('#list'));
177
+ // when `list` becomes unreachable, the host is told to drop it
178
+ ```
179
+
180
+ Collection timing is not guaranteed, so this is a safety net rather than a replacement
181
+ for releasing things you know you are finished with.
182
+
183
+ ## Callbacks
184
+
185
+ A function passed from the worker stays in the worker. The host receives a stub that
186
+ returns a promise; calling it runs the real function back in the worker:
187
+
188
+ ```js
189
+ // worker
190
+ const result = await require('util').map([1, 2, 3], n => n * 2);
191
+ // -> [2, 4, 6], with the doubling done in the worker
192
+ ```
193
+
194
+ Arguments are trimmed to the callback's declared arity, because callers like jQuery pass
195
+ extras (event objects, indexes) that usually cannot be serialized. Declare the parameters
196
+ you actually want.
197
+
198
+ ## Errors
199
+
200
+ Errors cross the channel with their `name`, `message` and the host's `stack`, and arrive
201
+ as real `Error` instances:
202
+
203
+ ```js
204
+ try {
205
+ await $('#list').nosuchmethod();
206
+ } catch (error) {
207
+ error instanceof Error; // true
208
+ error.message; // "mitty: $().nosuchmethod is not a function"
209
+ }
210
+ ```
211
+
212
+ ## API
213
+
214
+ ### `new Host(options)`
215
+
216
+ | option | type | description |
217
+ | ------------- | -------------------- | -------------------------------------------------------------------------------------------- |
218
+ | `channel` | `Channel` | Transport to listen on. Required. Never closed by mitty. |
219
+ | `resolve` | `(name) => unknown` | Turns a `require()` name into a value. Return `null`/`undefined` for unknown. May be async. |
220
+ | `serialize` | `(value) => unknown` | Called for every outgoing value. Return `this.remote(value)` for anything JSON cannot carry. |
221
+ | `unserialize` | `(value) => unknown` | Called for every incoming value. |
222
+
223
+ `serialize` and `unserialize` are called with the host as `this`.
224
+
225
+ - **`host.remote(value)`** — register `value` and return a handle marker. Call this from
226
+ `serialize()`.
227
+ - **`host.release(handle)`** — drop a handle, by marker or by id. Returns `false` if it
228
+ was already gone.
229
+ - **`host.close()`** — stop listening and forget every handle.
230
+
231
+ ### `connect(channel)`
232
+
233
+ Returns a client:
234
+
235
+ - **`require(name)`** — a proxy rooted at whatever the host's `resolve()` returns.
236
+ - **`handle(remote)`** — the numeric id behind a handle proxy.
237
+ - **`release(remote)`** — tell the host to drop it, by proxy or by id.
238
+ - **`close()`** — stop listening. The channel stays open.
239
+
240
+ ### `Channel`
241
+
242
+ Any object with these three members works — a `BroadcastChannel`, a `MessagePort`
243
+ wrapper, or a stub in tests:
244
+
245
+ ```ts
246
+ interface Channel {
247
+ postMessage(message: string): void;
248
+ addEventListener(type: 'message', listener: (event: { data: string }) => void): void;
249
+ removeEventListener(type: 'message', listener: (event: { data: string }) => void): void;
250
+ }
251
+ ```
252
+
253
+ Messages on the wire are JSON strings, so a channel only has to carry text.
254
+
255
+ ## Limitations
256
+
257
+ - **Values must survive JSON.** Anything else needs a handle via `serialize()`. Circular
258
+ structures fail the call that would return them.
259
+ - **`{ type, data }` is reserved.** Functions, handles and errors travel as objects of
260
+ that exact shape. An application object that looks identical would be mistaken for one.
261
+ - **Functions returned from the host are dropped**, as they would be by `JSON.stringify`.
262
+ Expose them through a handle instead.
263
+ - **Handles are not garbage collected** — see [Handles and memory](#handles-and-memory).
264
+
265
+ ## Example
266
+
267
+ A runnable page that drives jQuery from a worker, in both the `importScripts` and
268
+ `import` styles, lives in
269
+ [`example/`](https://github.com/jcubic/mitty/tree/master/example):
270
+
271
+ ```bash
272
+ npm install
273
+ npm run build
274
+ npx serve .
275
+ ```
276
+
277
+ then open `/example/`.
278
+
279
+ ## Development
280
+
281
+ ```bash
282
+ npm test # unit tests
283
+ npm run coverage # tests with coverage
284
+ npm run lint # eslint
285
+ npm run build # ESM + IIFE + .d.ts into dist/
286
+ ```
287
+
288
+ ## Origin
289
+
290
+ The first idea for the mechanism was created for [Hacking Cafe](https://hacking.cafe), a Unix-like environment in the browser. It was then extracted into an NPM library. The name and logo were based on a fictional "Life" magazine worker named [Waleter Mitty](https://en.wikipedia.org/wiki/Walter_Mitty) from the movie ["The Secret Life of Walter Mitty"](<https://en.wikipedia.org/wiki/The_Secret_Life_of_Walter_Mitty_(2013_film)>).
291
+
292
+ A similar RPC mechanism was created in [Wayne library](https://github.com/jcubic/wayne).
293
+
294
+ ## License
295
+
296
+ Copyright (c) 2026 [Jakub T. Jankiewicz](https://jakub.jankiewicz.org/)
297
+
298
+ Released under the MIT License. See [LICENSE](https://github.com/jcubic/mitty/blob/master/LICENSE) for details.
@@ -0,0 +1,71 @@
1
+ interface ChannelEvent {
2
+ data: string;
3
+ }
4
+ type ChannelListener = (event: ChannelEvent) => void;
5
+ interface Channel {
6
+ postMessage(message: string): void;
7
+ addEventListener(type: 'message', listener: ChannelListener): void;
8
+ removeEventListener(type: 'message', listener: ChannelListener): void;
9
+ }
10
+ interface FunctionMarker {
11
+ type: 'function';
12
+ data: [id: number, length: number];
13
+ }
14
+ interface ObjectMarker {
15
+ type: 'object';
16
+ data: [id: number];
17
+ }
18
+ interface ErrorMarker {
19
+ type: 'error';
20
+ data: [name: string, message: string, stack: string | null];
21
+ }
22
+ type Marker = FunctionMarker | ObjectMarker | ErrorMarker;
23
+ type Op = {
24
+ type: 'get';
25
+ key: string;
26
+ } | {
27
+ type: 'call';
28
+ args: unknown[];
29
+ };
30
+ interface Remote {
31
+ (...args: any[]): Remote;
32
+ then<R1 = any, R2 = never>(onfulfilled?: ((value: any) => R1 | PromiseLike<R1>) | null, onrejected?: ((reason: any) => R2 | PromiseLike<R2>) | null): Promise<R1 | R2>;
33
+ [key: string]: any;
34
+ }
35
+ interface Client {
36
+ require(name: string): Remote;
37
+ handle(remote: unknown): number;
38
+ release(remote: unknown | number): void;
39
+ close(): void;
40
+ }
41
+
42
+ interface HostOptions {
43
+ channel: Channel;
44
+ resolve(name: string): unknown;
45
+ serialize?(this: Host, value: unknown): unknown;
46
+ unserialize?(this: Host, value: unknown): unknown;
47
+ }
48
+ declare class Host {
49
+ private _options;
50
+ private _channel;
51
+ private _listener;
52
+ private _objects;
53
+ private _object_id;
54
+ private _pending;
55
+ private _call_id;
56
+ constructor(options: HostOptions);
57
+ remote(value: unknown): ObjectMarker;
58
+ release(handle: ObjectMarker | number): boolean;
59
+ close(): void;
60
+ private _post;
61
+ private _serialize;
62
+ private _unserialize;
63
+ private _callback;
64
+ private _on_message;
65
+ private _invoke;
66
+ private _root;
67
+ }
68
+
69
+ declare function connect(channel: Channel): Client;
70
+
71
+ export { type Channel, type ChannelEvent, type ChannelListener, type Client, type ErrorMarker, type FunctionMarker, Host, type HostOptions, type Marker, type ObjectMarker, type Op, type Remote, connect };