@noy-db/by-peer 0.1.0-pre.3

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.
@@ -0,0 +1,236 @@
1
+ import { NoydbStore } from '@noy-db/hub';
2
+
3
+ /**
4
+ * `PeerChannel` — the minimal duplex message primitive used by the p2p
5
+ * NoydbStore wrapper. Any transport that can deliver UTF-8 strings
6
+ * reliably and in-order qualifies: WebRTC DataChannel, BroadcastChannel,
7
+ * MessagePort, WebSocket, even postMessage pairs.
8
+ *
9
+ * Keeping the transport abstract has three payoffs:
10
+ *
11
+ * 1. **Tests run without a WebRTC polyfill.** `pairInMemory()` returns
12
+ * two wired channels for conformance tests against `to-memory`.
13
+ * 2. **Consumers pick their signaling story.** Matrix rooms, QR codes,
14
+ * pastebin, Firebase Realtime DB — the handshake is out of scope.
15
+ * 3. **Future transports slot in cheaply.** WebTransport (HTTP/3),
16
+ * libp2p, Iroh, or a plain relay WebSocket become additional
17
+ * bindings without touching the RPC layer.
18
+ *
19
+ * @module
20
+ */
21
+ /**
22
+ * Minimal duplex message primitive.
23
+ *
24
+ * Implementations MUST deliver every `send` payload in order exactly
25
+ * once to every live `on('message')` subscriber. `close()` is best-effort
26
+ * — once called, further `send()` calls MAY throw and `on('close')` MUST
27
+ * fire once.
28
+ */
29
+ interface PeerChannel {
30
+ /** Enqueue a payload for delivery to the remote end. */
31
+ send(payload: string): void;
32
+ /** Subscribe to incoming payloads or lifecycle events. Returns unsubscribe. */
33
+ on(event: 'message', listener: (payload: string) => void): () => void;
34
+ on(event: 'close', listener: () => void): () => void;
35
+ /** Close the channel. Idempotent. */
36
+ close(): void;
37
+ /** True once the channel is ready for `send`. */
38
+ readonly isOpen: boolean;
39
+ }
40
+ /**
41
+ * Create a pair of in-memory `PeerChannel`s wired to each other.
42
+ * Intended for tests and multi-tab simulations inside a single process.
43
+ */
44
+ declare function pairInMemory(): [PeerChannel, PeerChannel];
45
+ /**
46
+ * Wrap a WebRTC `RTCDataChannel` as a `PeerChannel`.
47
+ *
48
+ * Browser-only — the caller is responsible for establishing the
49
+ * `RTCPeerConnection`, exchanging SDP offers/answers out of band, and
50
+ * passing the opened DataChannel here. When the remote peer is only
51
+ * reachable via TURN, the relay sees DTLS-wrapped ciphertext (noy-db
52
+ * already encrypts at rest, so even a TURN compromise leaks nothing).
53
+ */
54
+ declare function fromDataChannel(dc: RTCDataChannel): PeerChannel;
55
+
56
+ /**
57
+ * JSON-RPC protocol over a `PeerChannel`.
58
+ *
59
+ * Request shape:
60
+ * `{ t: 'req', id, method, args }`
61
+ * Response shape (success):
62
+ * `{ t: 'res', id, ok: true, result }`
63
+ * Response shape (error):
64
+ * `{ t: 'res', id, ok: false, error: { name, message, version? } }`
65
+ *
66
+ * Why not reuse msgpack/protobuf? The payloads are already base64-encoded
67
+ * ciphertext — further binary packing saves ~8-12% at a large dependency
68
+ * cost. JSON over UTF-8 is inspectable, fits the zero-dependency ethos,
69
+ * and WebRTC DataChannel string mode already frames for us.
70
+ *
71
+ * @module
72
+ */
73
+
74
+ /** Wire format discriminator for RPC messages. */
75
+ type RpcMessage = RpcRequest | RpcResponse;
76
+ interface RpcRequest {
77
+ readonly t: 'req';
78
+ readonly id: string;
79
+ readonly method: string;
80
+ readonly args: readonly unknown[];
81
+ }
82
+ interface RpcResponse {
83
+ readonly t: 'res';
84
+ readonly id: string;
85
+ readonly ok: boolean;
86
+ readonly result?: unknown;
87
+ readonly error?: {
88
+ name: string;
89
+ message: string;
90
+ version?: number;
91
+ };
92
+ }
93
+ /** Handler invoked when an RPC request arrives. Return value is serialized as `result`. */
94
+ type RpcHandler = (method: string, args: readonly unknown[]) => Promise<unknown>;
95
+ /** Options for a client-side RPC caller. */
96
+ interface RpcClientOptions {
97
+ /** Max milliseconds to wait for a response before rejecting. */
98
+ timeoutMs?: number;
99
+ }
100
+ /** Client: wrap a `PeerChannel` in a `call(method, args)` helper. */
101
+ declare function createRpcClient(channel: PeerChannel, opts?: RpcClientOptions): {
102
+ call<T = unknown>(method: string, args: readonly unknown[]): Promise<T>;
103
+ dispose(): void;
104
+ };
105
+ /** Server: dispatch incoming RPC requests through a handler. Returns a dispose fn. */
106
+ declare function serveRpc(channel: PeerChannel, handler: RpcHandler): () => void;
107
+
108
+ /**
109
+ * `peerStore()` — a `NoydbStore` backed by RPC calls over a `PeerChannel`.
110
+ *
111
+ * The local peer calls `get`/`put`/`delete`/… against this store as if
112
+ * it were any other backend; every call is serialized as an RPC request
113
+ * to the remote peer, which runs `servePeerStore()` to funnel the RPCs
114
+ * into its own local `NoydbStore`.
115
+ *
116
+ * Error re-hydration: the remote handler re-throws `ConflictError` with
117
+ * a `.version` field when a CAS check fails. The RPC layer carries
118
+ * `version` in the error envelope so the local caller can catch
119
+ * `ConflictError` with the same semantics as a direct store call.
120
+ *
121
+ * @module
122
+ */
123
+
124
+ interface PeerStoreOptions {
125
+ /** The duplex channel to the remote peer. */
126
+ readonly channel: PeerChannel;
127
+ /** Max ms to wait for any single RPC response. Default 30s. */
128
+ readonly timeoutMs?: number;
129
+ /** Optional display name used in diagnostics. Default `'by-peer'`. */
130
+ readonly name?: string;
131
+ }
132
+ /**
133
+ * Create a `NoydbStore` that forwards every operation to a remote peer
134
+ * over the supplied `PeerChannel`. The remote peer must be running
135
+ * `servePeerStore()` against its own local store.
136
+ */
137
+ declare function peerStore(opts: PeerStoreOptions): NoydbStore & {
138
+ dispose: () => void;
139
+ };
140
+
141
+ /**
142
+ * `servePeerStore()` — runs on the peer that owns the data. Listens on
143
+ * a `PeerChannel` for RPC requests from a remote `peerStore()` client
144
+ * and executes each one against the local `NoydbStore`.
145
+ *
146
+ * The 6 core methods plus the optional `ping` / `listSince` / `listPage`
147
+ * extensions are exposed. Unknown methods surface as a remote Error.
148
+ *
149
+ * @module
150
+ */
151
+
152
+ interface ServePeerStoreOptions {
153
+ /** The duplex channel from the remote peer. */
154
+ readonly channel: PeerChannel;
155
+ /** The local store to serve. */
156
+ readonly store: NoydbStore;
157
+ /**
158
+ * Optional method whitelist. When provided, any method not in the set
159
+ * is rejected with "method not allowed". Useful for read-only peers.
160
+ */
161
+ readonly allow?: ReadonlySet<string>;
162
+ }
163
+ /**
164
+ * Start serving the local store on the channel. Returns a dispose
165
+ * function that stops the RPC listener. The underlying channel is NOT
166
+ * closed by dispose — ownership stays with the caller.
167
+ */
168
+ declare function servePeerStore(opts: ServePeerStoreOptions): () => void;
169
+
170
+ /**
171
+ * WebRTC handshake helper — opinionated wrapper around
172
+ * `RTCPeerConnection` that produces a ready-to-use `PeerChannel`.
173
+ *
174
+ * The handshake is split into two halves so the caller can ferry the
175
+ * SDP blobs over whatever signaling channel they prefer (QR code,
176
+ * Matrix room, pastebin, Firebase, signed URL…). Signaling is
177
+ * intentionally out of scope — noy-db has no opinion on how peers
178
+ * discover each other, only on what flows once they do.
179
+ *
180
+ * ```ts
181
+ * // Peer A (initiator)
182
+ * const a = await createOffer({ iceServers })
183
+ * send(a.offer) // → signaling channel
184
+ * const answer = await receive() // ← signaling channel
185
+ * await a.accept(answer)
186
+ * const channel = await a.channel // ready PeerChannel
187
+ *
188
+ * // Peer B (responder)
189
+ * const offer = await receive() // ← signaling channel
190
+ * const b = await acceptOffer(offer, { iceServers })
191
+ * send(b.answer) // → signaling channel
192
+ * const channel = await b.channel // ready PeerChannel
193
+ * ```
194
+ *
195
+ * Browser-only. Node consumers who want to interconnect with browsers
196
+ * can plug `@roamhq/wrtc` into the global `RTCPeerConnection` slot; this
197
+ * module does not pull it in so the package has zero runtime deps.
198
+ *
199
+ * @module
200
+ */
201
+
202
+ type PeerConnection = RTCPeerConnection;
203
+ type SessionDescription = RTCSessionDescriptionInit;
204
+ interface WebRTCOptions {
205
+ /** Optional ICE servers (STUN / TURN). */
206
+ readonly iceServers?: RTCIceServer[];
207
+ /** Label for the `RTCDataChannel`. Default `'noydb'`. */
208
+ readonly label?: string;
209
+ }
210
+ interface Initiator {
211
+ readonly offer: SessionDescription;
212
+ /** Feed in the remote peer's SDP answer to complete the handshake. */
213
+ accept(answer: SessionDescription): Promise<void>;
214
+ /** Resolves with the opened `PeerChannel` once the DataChannel is live. */
215
+ readonly channel: Promise<PeerChannel>;
216
+ readonly connection: PeerConnection;
217
+ }
218
+ interface Responder {
219
+ readonly answer: SessionDescription;
220
+ /** Resolves with the opened `PeerChannel` once the DataChannel is live. */
221
+ readonly channel: Promise<PeerChannel>;
222
+ readonly connection: PeerConnection;
223
+ }
224
+ /**
225
+ * Build an offer as the initiating peer. Returns the SDP offer to send
226
+ * to the remote peer and a promise for the opened `PeerChannel`.
227
+ */
228
+ declare function createOffer(opts?: WebRTCOptions): Promise<Initiator>;
229
+ /**
230
+ * Accept an incoming offer as the responding peer. Returns the SDP
231
+ * answer to send back to the initiator and a promise for the opened
232
+ * `PeerChannel`.
233
+ */
234
+ declare function acceptOffer(offer: SessionDescription, opts?: WebRTCOptions): Promise<Responder>;
235
+
236
+ export { type Initiator, type PeerChannel, type PeerStoreOptions, type Responder, type RpcClientOptions, type RpcHandler, type RpcMessage, type RpcRequest, type RpcResponse, type ServePeerStoreOptions, type WebRTCOptions, acceptOffer, createOffer, createRpcClient, fromDataChannel, pairInMemory, peerStore, servePeerStore, serveRpc };
@@ -0,0 +1,236 @@
1
+ import { NoydbStore } from '@noy-db/hub';
2
+
3
+ /**
4
+ * `PeerChannel` — the minimal duplex message primitive used by the p2p
5
+ * NoydbStore wrapper. Any transport that can deliver UTF-8 strings
6
+ * reliably and in-order qualifies: WebRTC DataChannel, BroadcastChannel,
7
+ * MessagePort, WebSocket, even postMessage pairs.
8
+ *
9
+ * Keeping the transport abstract has three payoffs:
10
+ *
11
+ * 1. **Tests run without a WebRTC polyfill.** `pairInMemory()` returns
12
+ * two wired channels for conformance tests against `to-memory`.
13
+ * 2. **Consumers pick their signaling story.** Matrix rooms, QR codes,
14
+ * pastebin, Firebase Realtime DB — the handshake is out of scope.
15
+ * 3. **Future transports slot in cheaply.** WebTransport (HTTP/3),
16
+ * libp2p, Iroh, or a plain relay WebSocket become additional
17
+ * bindings without touching the RPC layer.
18
+ *
19
+ * @module
20
+ */
21
+ /**
22
+ * Minimal duplex message primitive.
23
+ *
24
+ * Implementations MUST deliver every `send` payload in order exactly
25
+ * once to every live `on('message')` subscriber. `close()` is best-effort
26
+ * — once called, further `send()` calls MAY throw and `on('close')` MUST
27
+ * fire once.
28
+ */
29
+ interface PeerChannel {
30
+ /** Enqueue a payload for delivery to the remote end. */
31
+ send(payload: string): void;
32
+ /** Subscribe to incoming payloads or lifecycle events. Returns unsubscribe. */
33
+ on(event: 'message', listener: (payload: string) => void): () => void;
34
+ on(event: 'close', listener: () => void): () => void;
35
+ /** Close the channel. Idempotent. */
36
+ close(): void;
37
+ /** True once the channel is ready for `send`. */
38
+ readonly isOpen: boolean;
39
+ }
40
+ /**
41
+ * Create a pair of in-memory `PeerChannel`s wired to each other.
42
+ * Intended for tests and multi-tab simulations inside a single process.
43
+ */
44
+ declare function pairInMemory(): [PeerChannel, PeerChannel];
45
+ /**
46
+ * Wrap a WebRTC `RTCDataChannel` as a `PeerChannel`.
47
+ *
48
+ * Browser-only — the caller is responsible for establishing the
49
+ * `RTCPeerConnection`, exchanging SDP offers/answers out of band, and
50
+ * passing the opened DataChannel here. When the remote peer is only
51
+ * reachable via TURN, the relay sees DTLS-wrapped ciphertext (noy-db
52
+ * already encrypts at rest, so even a TURN compromise leaks nothing).
53
+ */
54
+ declare function fromDataChannel(dc: RTCDataChannel): PeerChannel;
55
+
56
+ /**
57
+ * JSON-RPC protocol over a `PeerChannel`.
58
+ *
59
+ * Request shape:
60
+ * `{ t: 'req', id, method, args }`
61
+ * Response shape (success):
62
+ * `{ t: 'res', id, ok: true, result }`
63
+ * Response shape (error):
64
+ * `{ t: 'res', id, ok: false, error: { name, message, version? } }`
65
+ *
66
+ * Why not reuse msgpack/protobuf? The payloads are already base64-encoded
67
+ * ciphertext — further binary packing saves ~8-12% at a large dependency
68
+ * cost. JSON over UTF-8 is inspectable, fits the zero-dependency ethos,
69
+ * and WebRTC DataChannel string mode already frames for us.
70
+ *
71
+ * @module
72
+ */
73
+
74
+ /** Wire format discriminator for RPC messages. */
75
+ type RpcMessage = RpcRequest | RpcResponse;
76
+ interface RpcRequest {
77
+ readonly t: 'req';
78
+ readonly id: string;
79
+ readonly method: string;
80
+ readonly args: readonly unknown[];
81
+ }
82
+ interface RpcResponse {
83
+ readonly t: 'res';
84
+ readonly id: string;
85
+ readonly ok: boolean;
86
+ readonly result?: unknown;
87
+ readonly error?: {
88
+ name: string;
89
+ message: string;
90
+ version?: number;
91
+ };
92
+ }
93
+ /** Handler invoked when an RPC request arrives. Return value is serialized as `result`. */
94
+ type RpcHandler = (method: string, args: readonly unknown[]) => Promise<unknown>;
95
+ /** Options for a client-side RPC caller. */
96
+ interface RpcClientOptions {
97
+ /** Max milliseconds to wait for a response before rejecting. */
98
+ timeoutMs?: number;
99
+ }
100
+ /** Client: wrap a `PeerChannel` in a `call(method, args)` helper. */
101
+ declare function createRpcClient(channel: PeerChannel, opts?: RpcClientOptions): {
102
+ call<T = unknown>(method: string, args: readonly unknown[]): Promise<T>;
103
+ dispose(): void;
104
+ };
105
+ /** Server: dispatch incoming RPC requests through a handler. Returns a dispose fn. */
106
+ declare function serveRpc(channel: PeerChannel, handler: RpcHandler): () => void;
107
+
108
+ /**
109
+ * `peerStore()` — a `NoydbStore` backed by RPC calls over a `PeerChannel`.
110
+ *
111
+ * The local peer calls `get`/`put`/`delete`/… against this store as if
112
+ * it were any other backend; every call is serialized as an RPC request
113
+ * to the remote peer, which runs `servePeerStore()` to funnel the RPCs
114
+ * into its own local `NoydbStore`.
115
+ *
116
+ * Error re-hydration: the remote handler re-throws `ConflictError` with
117
+ * a `.version` field when a CAS check fails. The RPC layer carries
118
+ * `version` in the error envelope so the local caller can catch
119
+ * `ConflictError` with the same semantics as a direct store call.
120
+ *
121
+ * @module
122
+ */
123
+
124
+ interface PeerStoreOptions {
125
+ /** The duplex channel to the remote peer. */
126
+ readonly channel: PeerChannel;
127
+ /** Max ms to wait for any single RPC response. Default 30s. */
128
+ readonly timeoutMs?: number;
129
+ /** Optional display name used in diagnostics. Default `'by-peer'`. */
130
+ readonly name?: string;
131
+ }
132
+ /**
133
+ * Create a `NoydbStore` that forwards every operation to a remote peer
134
+ * over the supplied `PeerChannel`. The remote peer must be running
135
+ * `servePeerStore()` against its own local store.
136
+ */
137
+ declare function peerStore(opts: PeerStoreOptions): NoydbStore & {
138
+ dispose: () => void;
139
+ };
140
+
141
+ /**
142
+ * `servePeerStore()` — runs on the peer that owns the data. Listens on
143
+ * a `PeerChannel` for RPC requests from a remote `peerStore()` client
144
+ * and executes each one against the local `NoydbStore`.
145
+ *
146
+ * The 6 core methods plus the optional `ping` / `listSince` / `listPage`
147
+ * extensions are exposed. Unknown methods surface as a remote Error.
148
+ *
149
+ * @module
150
+ */
151
+
152
+ interface ServePeerStoreOptions {
153
+ /** The duplex channel from the remote peer. */
154
+ readonly channel: PeerChannel;
155
+ /** The local store to serve. */
156
+ readonly store: NoydbStore;
157
+ /**
158
+ * Optional method whitelist. When provided, any method not in the set
159
+ * is rejected with "method not allowed". Useful for read-only peers.
160
+ */
161
+ readonly allow?: ReadonlySet<string>;
162
+ }
163
+ /**
164
+ * Start serving the local store on the channel. Returns a dispose
165
+ * function that stops the RPC listener. The underlying channel is NOT
166
+ * closed by dispose — ownership stays with the caller.
167
+ */
168
+ declare function servePeerStore(opts: ServePeerStoreOptions): () => void;
169
+
170
+ /**
171
+ * WebRTC handshake helper — opinionated wrapper around
172
+ * `RTCPeerConnection` that produces a ready-to-use `PeerChannel`.
173
+ *
174
+ * The handshake is split into two halves so the caller can ferry the
175
+ * SDP blobs over whatever signaling channel they prefer (QR code,
176
+ * Matrix room, pastebin, Firebase, signed URL…). Signaling is
177
+ * intentionally out of scope — noy-db has no opinion on how peers
178
+ * discover each other, only on what flows once they do.
179
+ *
180
+ * ```ts
181
+ * // Peer A (initiator)
182
+ * const a = await createOffer({ iceServers })
183
+ * send(a.offer) // → signaling channel
184
+ * const answer = await receive() // ← signaling channel
185
+ * await a.accept(answer)
186
+ * const channel = await a.channel // ready PeerChannel
187
+ *
188
+ * // Peer B (responder)
189
+ * const offer = await receive() // ← signaling channel
190
+ * const b = await acceptOffer(offer, { iceServers })
191
+ * send(b.answer) // → signaling channel
192
+ * const channel = await b.channel // ready PeerChannel
193
+ * ```
194
+ *
195
+ * Browser-only. Node consumers who want to interconnect with browsers
196
+ * can plug `@roamhq/wrtc` into the global `RTCPeerConnection` slot; this
197
+ * module does not pull it in so the package has zero runtime deps.
198
+ *
199
+ * @module
200
+ */
201
+
202
+ type PeerConnection = RTCPeerConnection;
203
+ type SessionDescription = RTCSessionDescriptionInit;
204
+ interface WebRTCOptions {
205
+ /** Optional ICE servers (STUN / TURN). */
206
+ readonly iceServers?: RTCIceServer[];
207
+ /** Label for the `RTCDataChannel`. Default `'noydb'`. */
208
+ readonly label?: string;
209
+ }
210
+ interface Initiator {
211
+ readonly offer: SessionDescription;
212
+ /** Feed in the remote peer's SDP answer to complete the handshake. */
213
+ accept(answer: SessionDescription): Promise<void>;
214
+ /** Resolves with the opened `PeerChannel` once the DataChannel is live. */
215
+ readonly channel: Promise<PeerChannel>;
216
+ readonly connection: PeerConnection;
217
+ }
218
+ interface Responder {
219
+ readonly answer: SessionDescription;
220
+ /** Resolves with the opened `PeerChannel` once the DataChannel is live. */
221
+ readonly channel: Promise<PeerChannel>;
222
+ readonly connection: PeerConnection;
223
+ }
224
+ /**
225
+ * Build an offer as the initiating peer. Returns the SDP offer to send
226
+ * to the remote peer and a promise for the opened `PeerChannel`.
227
+ */
228
+ declare function createOffer(opts?: WebRTCOptions): Promise<Initiator>;
229
+ /**
230
+ * Accept an incoming offer as the responding peer. Returns the SDP
231
+ * answer to send back to the initiator and a promise for the opened
232
+ * `PeerChannel`.
233
+ */
234
+ declare function acceptOffer(offer: SessionDescription, opts?: WebRTCOptions): Promise<Responder>;
235
+
236
+ export { type Initiator, type PeerChannel, type PeerStoreOptions, type Responder, type RpcClientOptions, type RpcHandler, type RpcMessage, type RpcRequest, type RpcResponse, type ServePeerStoreOptions, type WebRTCOptions, acceptOffer, createOffer, createRpcClient, fromDataChannel, pairInMemory, peerStore, servePeerStore, serveRpc };