osra 0.5.4 → 0.5.6

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 CHANGED
@@ -3,16 +3,16 @@
3
3
  [![npm version](https://img.shields.io/npm/v/osra.svg)](https://www.npmjs.com/package/osra)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- osra is a zero-runtime-dependency TypeScript RPC library that connects two JavaScript contexts over any message channel. Both sides call `expose(value, { transport })` and each receives the other's value with live semantics: functions become callable async proxies, async generators stream with `for await`, streams keep backpressure, errors keep their subclasses, `AbortSignal`s propagate aborts. It works across Workers, SharedWorkers, windows/iframes, MessagePorts, WebSockets, web extensions, and anything else you can wrap in a custom `{ emit, receive }` pair degrading gracefully to a JSON-only mode on text channels.
6
+ osra is a zero-runtime-dependency TypeScript RPC library that connects two JavaScript contexts over any message channel. Both sides call `expose(value, { transport })` and each receives the other's value with live semantics: functions become callable async proxies, async generators stream with `for await`, streams keep backpressure, errors keep their subclasses, `AbortSignal`s propagate aborts. It works across Workers, SharedWorkers, windows/iframes, MessagePorts, WebSockets, web extensions, and anything else you can wrap in a custom `{ emit, receive }` pair, degrading gracefully to a JSON-only mode on text channels.
7
7
 
8
8
  ## Features
9
9
 
10
- - **Zero runtime dependencies** one ESM module
11
- - **Symmetric API** both sides call `expose()`; either side can pass functions, both can call
12
- - **Deep type support** functions, promises, async generators, `ReadableStream`/`WritableStream`, `MessagePort`, `AbortSignal`, `Error` subclasses, `Blob`/`File`, `Request`/`Response`, `Map`/`Set`, typed arrays, `BigInt`, `Symbol`, …
13
- - **JSON-mode degradation** the same value types work over text-only transports (WebSocket, extension messaging); `Date`, `Map`, typed arrays, even `NaN`/`±Infinity` survive
10
+ - **Zero runtime dependencies**: one ESM module
11
+ - **Symmetric API**: both sides call `expose()`; either side can pass functions, both can call
12
+ - **Deep type support**: functions, promises, async generators, `ReadableStream`/`WritableStream`, `MessagePort`, `AbortSignal`, `Error` subclasses, `Blob`/`File`, `Request`/`Response`, `Map`/`Set`, typed arrays, `BigInt`, `Symbol`, …
13
+ - **JSON-mode degradation**: the same value types work over text-only transports (WebSocket, extension messaging); `Date`, `Map`, typed arrays, even `NaN`/`±Infinity` survive
14
14
  - **`identity()`** for reference-preserving sends, **`transfer()`** for zero-copy moves
15
- - **Strict TypeScript** `Remote<T>` maps your API type across the wire; a compile-time `Capable` check rejects non-serializable values with the offending path pinpointed
15
+ - **Strict TypeScript**: `Remote<T>` maps your API type across the wire; a compile-time `Capable` check rejects non-serializable values with the offending path pinpointed
16
16
  - **Tested** on Chromium, Firefox, and WebKit via Playwright
17
17
 
18
18
  ## Install
@@ -66,19 +66,19 @@ for await (const n of await remote.streamData()) {
66
66
  }
67
67
  ```
68
68
 
69
- Both sides call `expose()` the returned promise resolves with the remote side's value once the handshake completes. A side that only serves (like the worker above) can ignore the returned promise.
69
+ Both sides call `expose()`; the returned promise resolves with the remote side's value once the handshake completes. A side that only serves (like the worker above) can ignore the returned promise.
70
70
 
71
71
  ### Options
72
72
 
73
73
  | Option | Default | Description |
74
74
  |---|---|---|
75
75
  | `transport` | required | The channel to communicate over (see [Transports](#transports)) |
76
- | `key` | `'__OSRA_DEFAULT_KEY__'` | Namespacing tag lets multiple independent osra connections share one channel. **Not authentication.** |
76
+ | `key` | `'__OSRA_DEFAULT_KEY__'` | Namespacing tag that lets multiple independent osra connections share one channel. **Not authentication.** |
77
77
  | `origin` | `'*'` | On window transports: sets the outbound `postMessage` target origin **and** filters inbound messages by `event.origin` |
78
- | `name` / `remoteName` | | Label your endpoint / only accept envelopes from a matching peer name |
79
- | `unregisterSignal` | | `AbortSignal` that tears the connection down (see [Lifecycle](#error-handling--lifecycle)) |
80
- | `uuid` / `remoteUuid` | random / | Pin instance uuids (`remoteUuid` is otherwise learned from the peer's announce); when both sides preset each other's `remoteUuid`, the announce handshake is skipped |
81
- | `revivableModules` | | `defaults => modules` function to add, drop, reorder, or override type-handling modules |
78
+ | `name` / `remoteName` | - | Label your endpoint / only accept envelopes from a matching peer name |
79
+ | `unregisterSignal` | - | `AbortSignal` that tears the connection down (see [Lifecycle](#error-handling--lifecycle)) |
80
+ | `uuid` / `remoteUuid` | random / - | Pin instance uuids (`remoteUuid` is otherwise learned from the peer's announce); when both sides preset each other's `remoteUuid`, the announce handshake is skipped |
81
+ | `revivableModules` | - | `defaults => modules` function to add, drop, reorder, or override type-handling modules |
82
82
 
83
83
  If multiple peers connect over the same transport, the returned promise resolves with the **first** peer's value; later peers still connect and can call your exposed value.
84
84
 
@@ -106,7 +106,7 @@ Transports are either **structured-clone** (Worker, Window, MessagePort, SharedW
106
106
  | `Blob` / `File` | ✅ | ✅ | revive as `Promise<Blob>` / `Promise<File>` (bytes fetched async) |
107
107
  | `Request` / `Response` / `Headers` | ✅ | ✅ | streamed bodies; `Request.signal` propagates; `Response.url`/`redirected` restored; opaque status-0 revives as `Response.error()` |
108
108
  | `Event` / `CustomEvent` | ✅ | ✅ | subclass fields beyond `detail` are dropped |
109
- | `EventTarget` | ✅ | ✅ | revives as a listener-only façade `add`/`removeEventListener` proxy to the source; you can't dispatch through it |
109
+ | `EventTarget` | ✅ | ✅ | revives as a listener-only façade: `add`/`removeEventListener` proxy to the source; you can't dispatch through it |
110
110
  | Other structured-clonables (`FileList`, `ImageData`, `DOMRect`, `CryptoKey`, …) | ✅ | ❌ | pass through structured clone untouched |
111
111
  | Transfer-only host objects (`OffscreenCanvas`, `MediaStreamTrack`, `RTCDataChannel`, …) | ✅ | ❌ | always moved to the peer |
112
112
  | `ImageBitmap`, `VideoFrame`, `AudioData` | ✅ | ❌ | copied by structured clone; wrap in `transfer()` to move |
@@ -116,7 +116,7 @@ Transports are either **structured-clone** (Worker, Window, MessagePort, SharedW
116
116
 
117
117
  ### Worker
118
118
 
119
- Pass the `Worker` on the page side and `globalThis` (the `DedicatedWorkerGlobalScope`) inside the worker see [Quick Start](#quick-start). The worker scope is detected at runtime but isn't part of the `Transport` type union, so cast it: `globalThis as unknown as Transport`.
119
+ Pass the `Worker` on the page side and `globalThis` (the `DedicatedWorkerGlobalScope`) inside the worker; see [Quick Start](#quick-start). The worker scope is detected at runtime but isn't part of the `Transport` type union, so cast it: `globalThis as unknown as Transport`.
120
120
 
121
121
  ### Window ↔ iframe
122
122
 
@@ -141,7 +141,7 @@ const remote = await expose<ParentApi>(iframeApi, {
141
141
 
142
142
  ### SharedWorker
143
143
 
144
- Pass the `SharedWorker` instance directly on the page side osra rides its `.port` internally. Inside the worker, expose per connected port:
144
+ Pass the `SharedWorker` instance directly on the page side; osra rides its `.port` internally. Inside the worker, expose per connected port:
145
145
 
146
146
  ```ts
147
147
  // page
@@ -162,7 +162,7 @@ globalThis.addEventListener('connect', event => {
162
162
 
163
163
  ### WebSocket
164
164
 
165
- JSON mode. You can `expose()` while the socket is still `CONNECTING` outbound envelopes queue until open. The other end is anything that relays frames to a peer also running osra:
165
+ JSON mode. You can `expose()` while the socket is still `CONNECTING`; outbound envelopes queue until open. The other end is anything that relays frames to a peer also running osra:
166
166
 
167
167
  ```ts
168
168
  const socket = new WebSocket('wss://relay.example.com')
@@ -171,7 +171,7 @@ const remote = await expose<PeerApi>(localApi, { transport: socket })
171
171
 
172
172
  ### Service worker
173
173
 
174
- A `ServiceWorker` can only emit and a `ServiceWorkerContainer` can only receive combine them as a custom pair:
174
+ A `ServiceWorker` can only emit and a `ServiceWorkerContainer` can only receive, so combine them as a custom pair:
175
175
 
176
176
  ```ts
177
177
  const registration = await navigator.serviceWorker.ready
@@ -197,7 +197,7 @@ browser.runtime.onConnect.addListener(port => {
197
197
  })
198
198
  ```
199
199
 
200
- If you accept `onConnectExternal`/`onMessageExternal`, validate senders yourself the `MessageContext` passed to custom receive listeners exposes `sender`.
200
+ If you accept `onConnectExternal`/`onMessageExternal`, validate senders yourself; the `MessageContext` passed to custom receive listeners exposes `sender`.
201
201
 
202
202
  ### Custom transports
203
203
 
@@ -219,11 +219,11 @@ const remote = await expose<PeerApi>(localApi, {
219
219
  })
220
220
  ```
221
221
 
222
- Custom transports **must be plain objects** prototype-based objects (e.g. Node `EventEmitter`s) with `emit` methods are deliberately not detected as custom transports.
222
+ Custom transports **must be plain objects**: prototype-based objects (e.g. Node `EventEmitter`s) with `emit` methods are deliberately not detected as custom transports.
223
223
 
224
224
  ## `identity()`
225
225
 
226
- `identity(value)` preserves reference identity across the connection: sending the same wrapped value twice revives as the same object on the peer, and when the peer wraps the revived object in `identity()` and sends it back, you receive your original reference (`===`). Without it, every send produces an independent copy including the return trip: a revived value passed back *bare* arrives as a fresh copy, so the returning side must re-wrap it.
226
+ `identity(value)` preserves reference identity across the connection: sending the same wrapped value twice revives as the same object on the peer, and when the peer wraps the revived object in `identity()` and sends it back, you receive your original reference (`===`). Without it, every send produces an independent copy, including the return trip: a revived value passed back *bare* arrives as a fresh copy, so the returning side must re-wrap it.
227
227
 
228
228
  ```ts
229
229
  import { expose, identity } from 'osra'
@@ -239,13 +239,13 @@ expose({
239
239
 
240
240
  ## `transfer()`
241
241
 
242
- `transfer(value)` opts a `Transferable` (`ArrayBuffer`, `MessagePort`, streams, `ImageBitmap`, `OffscreenCanvas`, …) into move semantics ownership transfers to the peer instead of copying. On JSON transports it silently degrades to a copy.
242
+ `transfer(value)` opts a `Transferable` (`ArrayBuffer`, `MessagePort`, streams, `ImageBitmap`, `OffscreenCanvas`, …) into move semantics: ownership transfers to the peer instead of copying. On JSON transports it silently degrades to a copy.
243
243
 
244
244
  ```ts
245
245
  import { transfer } from 'osra'
246
246
 
247
247
  const pixels = new ArrayBuffer(16_000_000)
248
- await remote.render(transfer(pixels)) // moved pixels is detached locally
248
+ await remote.render(transfer(pixels)) // moved - pixels is detached locally
249
249
  ```
250
250
 
251
251
  ## Error handling & lifecycle
@@ -255,7 +255,7 @@ await remote.render(transfer(pixels)) // moved — pixels is detached locally
255
255
  - Aborting `unregisterSignal`:
256
256
  - the pending `expose()` rejects with the abort reason,
257
257
  - a protocol `close` is sent to every connected peer and per-connection state is disposed,
258
- - pending RPC calls reject with `'osra: connection closed'` on **both** sides (the peer receiving `close` rejects its pending calls too),
258
+ - pending RPC calls reject with `'osra: connection closed'` on **both** sides (the peer receiving `close` rejects its pending calls too),
259
259
  - proxied streams on wire-routed channels (JSON transports) are cancelled/aborted with the same error.
260
260
  - Promises and streams riding real transferred `MessagePort`s on structured-clone transports live independently of the connection and survive its closure; wire-routed traffic does not.
261
261
  - After aborting, calling `expose()` again on the same transport performs a fresh handshake.
@@ -269,16 +269,16 @@ controller.abort(new Error('shutting down'))
269
269
  // pending rejects with 'osra: connection closed'
270
270
  ```
271
271
 
272
- **Trust model**: `key` is namespacing, not authentication. `origin` filters window messages in both directions set it whenever you talk across origins. Treat peers as semi-trusted: malformed payloads are handled, but DoS-hardening against hostile peers is not complete.
272
+ **Trust model**: `key` is namespacing, not authentication. `origin` filters window messages in both directions; set it whenever you talk across origins. Treat peers as semi-trusted: malformed payloads are handled, but DoS-hardening against hostile peers is not complete.
273
273
 
274
274
  ## Limitations
275
275
 
276
- - **Circular structures throw** a `TypeError` at send time break the cycle or restructure.
276
+ - **Circular structures throw** a `TypeError` at send time; break the cycle or restructure.
277
277
  - **Shared references duplicate**: two fields pointing at the same object arrive as two copies unless wrapped with `identity()`.
278
- - **Classes/prototypes are not preserved** values cross as plain data; a class instance's methods are not proxied. Expose plain objects and functions.
279
- - **Unclonable values** (`WeakMap`, `WeakSet`, exotic host objects) coerce to `{}` and fail the compile-time check.
280
- - **One-shot bodies**: sending the same `Request`/`Response`/`ReadableStream` twice fails the body locks at first send.
281
- - **Generic functions collapse** in `Remote<T>` mapped types can't preserve generic signatures.
278
+ - **Classes/prototypes are not preserved**: values cross as plain data; a class instance's methods are not proxied. Expose plain objects and functions.
279
+ - **Unclonable values** (`WeakMap`, `WeakSet`, exotic host objects) coerce to `{}` and fail the compile-time check.
280
+ - **One-shot bodies**: sending the same `Request`/`Response`/`ReadableStream` twice fails; the body locks at first send.
281
+ - **Generic functions collapse** in `Remote<T>`: mapped types can't preserve generic signatures.
282
282
  - **Multi-peer**: only the first peer's value is accessible through the returned promise.
283
283
  - **Everything is async**: sync return values still arrive as `Promise`s.
284
284
 
@@ -286,11 +286,11 @@ controller.abort(new Error('shutting down'))
286
286
 
287
287
  `Remote<T>` is what the other side sees: functions become `(...args) => Promise<Awaited<R>>`, `Blob` becomes `Promise<Blob>`, containers map recursively, platform objects revive as themselves.
288
288
 
289
- `expose()` validates the value you pass at compile time against `Capable` the union of everything serializable for the inferred transport (narrower on JSON transports). Failures pinpoint the offending path:
289
+ `expose()` validates the value you pass at compile time against `Capable`, the union of everything serializable for the inferred transport (narrower on JSON transports). Failures pinpoint the offending path:
290
290
 
291
291
  ```ts
292
292
  expose({ ok: async () => 1, cache: new WeakMap() }, { transport: worker })
293
- // type error: Value type must resolve to a Capable with `cache` identified as the bad field
293
+ // type error: Value type must resolve to a Capable, with `cache` identified as the bad field
294
294
  ```
295
295
 
296
296
  The published declarations require **TypeScript >= 5.9** with `strict` mode.
@@ -6,7 +6,7 @@ import type { TypedEventTarget } from '../utils/typed-event-target.js';
6
6
  export declare const normalizeTransport: (transport: Transport) => Transport;
7
7
  /** Resolves the final revivable module list. The user supplies a function
8
8
  * that takes the defaults and returns whatever ordering/composition they
9
- * want add modules, drop defaults, reorder, override per-type. When
9
+ * want - add modules, drop defaults, reorder, override per-type. When
10
10
  * omitted, the defaults are used as-is. */
11
11
  export declare const mergeRevivableModules: <TModules extends readonly RevivableModule[] = DefaultRevivableModules>(configure: ((defaults: DefaultRevivableModules) => TModules) | undefined) => TModules;
12
12
  export type ProtocolEventMap<TModules extends readonly RevivableModule[] = DefaultRevivableModules> = {
@@ -35,7 +35,7 @@ export type StartConnectionsOptions<TModules extends readonly RevivableModule[]
35
35
  origin?: string;
36
36
  unregisterSignal?: AbortSignal;
37
37
  /** Configure the revivable module list. Receives the defaults and
38
- * returns the final ordered list add modules, drop defaults, reorder,
38
+ * returns the final ordered list - add modules, drop defaults, reorder,
39
39
  * or override per-type as needed. */
40
40
  revivableModules?: (defaults: DefaultRevivableModules) => TModules;
41
41
  uuid?: Uuid;