@fairfox/polly 0.20.1 → 0.22.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/README.md +83 -3
- package/dist/cli/polly.js +21 -1
- package/dist/cli/polly.js.map +3 -3
- package/dist/src/background/index.js.map +7 -7
- package/dist/src/background/message-router.js.map +7 -7
- package/dist/src/elysia/index.d.ts +2 -0
- package/dist/src/elysia/index.js +177 -17
- package/dist/src/elysia/index.js.map +8 -5
- package/dist/src/elysia/peer-repo-plugin.d.ts +79 -0
- package/dist/src/elysia/signaling-server-plugin.d.ts +121 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.js +90 -1
- package/dist/src/index.js.map +15 -13
- package/dist/src/mesh.d.ts +29 -0
- package/dist/src/mesh.js +1502 -0
- package/dist/src/mesh.js.map +22 -0
- package/dist/src/peer.d.ts +29 -0
- package/dist/src/peer.js +928 -0
- package/dist/src/peer.js.map +20 -0
- package/dist/src/shared/adapters/index.js.map +6 -6
- package/dist/src/shared/lib/_client-only.d.ts +38 -0
- package/dist/src/shared/lib/access.d.ts +124 -0
- package/dist/src/shared/lib/blob-ref.d.ts +72 -0
- package/dist/src/shared/lib/context-helpers.js.map +7 -7
- package/dist/src/shared/lib/crdt-specialised.d.ts +129 -0
- package/dist/src/shared/lib/crdt-state.d.ts +86 -0
- package/dist/src/shared/lib/encryption.d.ts +117 -0
- package/dist/src/shared/lib/mesh-network-adapter.d.ts +130 -0
- package/dist/src/shared/lib/mesh-signaling-client.d.ts +85 -0
- package/dist/src/shared/lib/mesh-state.d.ts +102 -0
- package/dist/src/shared/lib/mesh-webrtc-adapter.d.ts +132 -0
- package/dist/src/shared/lib/message-bus.js.map +7 -7
- package/dist/src/shared/lib/migrate-primitive.d.ts +100 -0
- package/dist/src/shared/lib/pairing.d.ts +170 -0
- package/dist/src/shared/lib/peer-relay-adapter.d.ts +80 -0
- package/dist/src/shared/lib/peer-repo-server.d.ts +83 -0
- package/dist/src/shared/lib/peer-state.d.ts +117 -0
- package/dist/src/shared/lib/primitive-registry.d.ts +88 -0
- package/dist/src/shared/lib/resource.js.map +4 -4
- package/dist/src/shared/lib/revocation.d.ts +126 -0
- package/dist/src/shared/lib/schema-version.d.ts +129 -0
- package/dist/src/shared/lib/signing.d.ts +118 -0
- package/dist/src/shared/lib/state.js.map +4 -4
- package/dist/src/shared/state/app-state.js.map +5 -5
- package/dist/tools/init/src/cli.js.map +1 -1
- package/dist/tools/quality/src/cli.js +162 -0
- package/dist/tools/quality/src/cli.js.map +11 -0
- package/dist/tools/test/src/adapters/index.js.map +2 -2
- package/dist/tools/test/src/browser/harness.d.ts +80 -0
- package/dist/tools/test/src/browser/index.d.ts +32 -0
- package/dist/tools/test/src/browser/index.js +243 -0
- package/dist/tools/test/src/browser/index.js.map +10 -0
- package/dist/tools/test/src/browser/run.d.ts +26 -0
- package/dist/tools/test/src/index.js.map +2 -2
- package/dist/tools/verify/specs/tla/MeshState.cfg +21 -0
- package/dist/tools/verify/specs/tla/MeshState.tla +247 -0
- package/dist/tools/verify/specs/tla/PeerState.cfg +27 -0
- package/dist/tools/verify/specs/tla/PeerState.tla +238 -0
- package/dist/tools/verify/specs/tla/README.md +27 -3
- package/dist/tools/verify/src/cli.js.map +8 -8
- package/dist/tools/visualize/src/cli.js.map +7 -7
- package/package.json +51 -5
package/dist/src/elysia/index.js
CHANGED
|
@@ -44,9 +44,79 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
44
44
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
// src/elysia/plugin.ts
|
|
47
|
+
// src/elysia/peer-repo-plugin.ts
|
|
48
48
|
import { Elysia } from "elysia";
|
|
49
49
|
|
|
50
|
+
// src/shared/lib/peer-repo-server.ts
|
|
51
|
+
import { Repo } from "@automerge/automerge-repo";
|
|
52
|
+
import { WebSocketServerAdapter } from "@automerge/automerge-repo-network-websocket";
|
|
53
|
+
import { NodeFSStorageAdapter } from "@automerge/automerge-repo-storage-nodefs";
|
|
54
|
+
import * as ws from "ws";
|
|
55
|
+
async function createPeerRepoServer(options) {
|
|
56
|
+
const wss = await (options.webSocketServer ? Promise.resolve(options.webSocketServer) : new Promise((resolve, reject) => {
|
|
57
|
+
const created = new ws.WebSocketServer({
|
|
58
|
+
port: options.port,
|
|
59
|
+
...options.host !== undefined && { host: options.host }
|
|
60
|
+
}, () => resolve(created));
|
|
61
|
+
created.once("error", reject);
|
|
62
|
+
}));
|
|
63
|
+
const adapter = new WebSocketServerAdapter(wss);
|
|
64
|
+
const storage = new NodeFSStorageAdapter(options.storagePath);
|
|
65
|
+
const repo = new Repo({
|
|
66
|
+
network: [adapter],
|
|
67
|
+
storage
|
|
68
|
+
});
|
|
69
|
+
await repo.storageId();
|
|
70
|
+
return {
|
|
71
|
+
repo,
|
|
72
|
+
webSocketServer: wss,
|
|
73
|
+
adapter,
|
|
74
|
+
storage,
|
|
75
|
+
close: async () => {
|
|
76
|
+
for (const client of wss.clients) {
|
|
77
|
+
try {
|
|
78
|
+
client.terminate();
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
repo.shutdown();
|
|
82
|
+
try {
|
|
83
|
+
wss.close();
|
|
84
|
+
} catch {}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/elysia/peer-repo-plugin.ts
|
|
90
|
+
function peerRepo(options) {
|
|
91
|
+
let server;
|
|
92
|
+
const healthPath = options.healthPath === false ? null : options.healthPath ?? "/polly/peer/health";
|
|
93
|
+
let app = new Elysia({ name: "polly-peer-repo" }).decorate("pollyPeerRepo", null).decorate("pollyPeerServer", null).onStart(async () => {
|
|
94
|
+
server = await createPeerRepoServer(options);
|
|
95
|
+
app.decorate("pollyPeerRepo", server.repo);
|
|
96
|
+
app.decorate("pollyPeerServer", server);
|
|
97
|
+
}).onStop(async () => {
|
|
98
|
+
if (server) {
|
|
99
|
+
await server.close();
|
|
100
|
+
server = undefined;
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
if (healthPath) {
|
|
104
|
+
app = app.get(healthPath, () => {
|
|
105
|
+
if (!server) {
|
|
106
|
+
return { status: "starting", peers: 0 };
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
status: "running",
|
|
110
|
+
peers: server.repo.peers.length,
|
|
111
|
+
port: options.port
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return app;
|
|
116
|
+
}
|
|
117
|
+
// src/elysia/plugin.ts
|
|
118
|
+
import { Elysia as Elysia2 } from "elysia";
|
|
119
|
+
|
|
50
120
|
// src/core/clock.ts
|
|
51
121
|
function createLamportClock(contextId) {
|
|
52
122
|
let tick = 0;
|
|
@@ -121,19 +191,19 @@ function findMatchingConfig(configs, method, path) {
|
|
|
121
191
|
|
|
122
192
|
class BroadcastManager {
|
|
123
193
|
connections = new Map;
|
|
124
|
-
register(clientId,
|
|
125
|
-
this.connections.set(clientId,
|
|
194
|
+
register(clientId, ws2) {
|
|
195
|
+
this.connections.set(clientId, ws2);
|
|
126
196
|
}
|
|
127
197
|
unregister(clientId) {
|
|
128
198
|
this.connections.delete(clientId);
|
|
129
199
|
}
|
|
130
200
|
broadcast(message, filter) {
|
|
131
201
|
const payload = JSON.stringify(message);
|
|
132
|
-
for (const [clientId,
|
|
202
|
+
for (const [clientId, ws2] of this.connections.entries()) {
|
|
133
203
|
if (filter && !filter(clientId))
|
|
134
204
|
continue;
|
|
135
|
-
if (
|
|
136
|
-
|
|
205
|
+
if (ws2.readyState === 1) {
|
|
206
|
+
ws2.send(payload);
|
|
137
207
|
}
|
|
138
208
|
}
|
|
139
209
|
}
|
|
@@ -143,30 +213,30 @@ function polly(config = {}) {
|
|
|
143
213
|
const clock = createLamportClock("server");
|
|
144
214
|
const broadcaster = new BroadcastManager;
|
|
145
215
|
const clientStateByConnection = new Map;
|
|
146
|
-
const app = new
|
|
216
|
+
const app = new Elysia2({ name: "polly" }).decorate("pollyState", {
|
|
147
217
|
client: config.state?.client || {},
|
|
148
218
|
server: config.state?.server || {}
|
|
149
219
|
}).decorate("pollyClock", clock).decorate("pollyBroadcast", broadcaster).ws(config.websocketPath || "/polly/ws", {
|
|
150
|
-
open(
|
|
151
|
-
const clientId =
|
|
152
|
-
broadcaster.register(clientId,
|
|
153
|
-
|
|
220
|
+
open(ws2) {
|
|
221
|
+
const clientId = ws2.data.headers?.["x-client-id"] || crypto.randomUUID();
|
|
222
|
+
broadcaster.register(clientId, ws2.raw);
|
|
223
|
+
ws2.send(JSON.stringify({
|
|
154
224
|
type: "state-sync",
|
|
155
225
|
state: config.state?.client || {},
|
|
156
226
|
clock: clock.now()
|
|
157
227
|
}));
|
|
158
228
|
},
|
|
159
|
-
close(
|
|
160
|
-
const clientId =
|
|
229
|
+
close(ws2) {
|
|
230
|
+
const clientId = ws2.data.headers?.["x-client-id"];
|
|
161
231
|
if (clientId) {
|
|
162
232
|
broadcaster.unregister(clientId);
|
|
163
233
|
clientStateByConnection.delete(clientId);
|
|
164
234
|
}
|
|
165
235
|
},
|
|
166
|
-
message(
|
|
236
|
+
message(ws2, message) {
|
|
167
237
|
const data = JSON.parse(message);
|
|
168
238
|
if (data.type === "state-update") {
|
|
169
|
-
const clientId =
|
|
239
|
+
const clientId = ws2.data.headers?.["x-client-id"];
|
|
170
240
|
if (clientId) {
|
|
171
241
|
clientStateByConnection.set(clientId, data.state);
|
|
172
242
|
}
|
|
@@ -230,8 +300,98 @@ function polly(config = {}) {
|
|
|
230
300
|
});
|
|
231
301
|
return app;
|
|
232
302
|
}
|
|
303
|
+
// src/elysia/signaling-server-plugin.ts
|
|
304
|
+
import { Elysia as Elysia3 } from "elysia";
|
|
305
|
+
function signalingServer(options = {}) {
|
|
306
|
+
const path = options.path ?? "/polly/signaling";
|
|
307
|
+
const peerSockets = new Map;
|
|
308
|
+
const parseMessage = (raw) => {
|
|
309
|
+
try {
|
|
310
|
+
return typeof raw === "string" ? JSON.parse(raw) : raw;
|
|
311
|
+
} catch {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
const handleJoin = (ws2, peerId) => {
|
|
316
|
+
peerSockets.set(peerId, ws2);
|
|
317
|
+
const wsWithData = ws2;
|
|
318
|
+
wsWithData.data.peerId = peerId;
|
|
319
|
+
};
|
|
320
|
+
const sendUnknownTarget = (ws2, targetPeerId) => {
|
|
321
|
+
ws2.send({
|
|
322
|
+
type: "error",
|
|
323
|
+
reason: "unknown-target",
|
|
324
|
+
targetPeerId
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
const findOpenTarget = (targetPeerId) => {
|
|
328
|
+
const target = peerSockets.get(targetPeerId);
|
|
329
|
+
if (!target)
|
|
330
|
+
return;
|
|
331
|
+
const readyState = target.readyState;
|
|
332
|
+
const OPEN = 1;
|
|
333
|
+
if (readyState !== undefined && readyState !== OPEN) {
|
|
334
|
+
peerSockets.delete(targetPeerId);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
return target;
|
|
338
|
+
};
|
|
339
|
+
const handleSignal = (ws2, msg) => {
|
|
340
|
+
const wsWithData = ws2;
|
|
341
|
+
const senderId = wsWithData.data.peerId;
|
|
342
|
+
if (!senderId) {
|
|
343
|
+
wsWithData.send({ type: "error", reason: "not-joined" });
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const target = findOpenTarget(msg.targetPeerId);
|
|
347
|
+
if (!target) {
|
|
348
|
+
sendUnknownTarget(ws2, msg.targetPeerId);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const relayed = {
|
|
352
|
+
type: "signal",
|
|
353
|
+
peerId: senderId,
|
|
354
|
+
targetPeerId: msg.targetPeerId,
|
|
355
|
+
payload: msg.payload
|
|
356
|
+
};
|
|
357
|
+
try {
|
|
358
|
+
target.send(relayed);
|
|
359
|
+
} catch {
|
|
360
|
+
peerSockets.delete(msg.targetPeerId);
|
|
361
|
+
sendUnknownTarget(ws2, msg.targetPeerId);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
return new Elysia3().ws(path, {
|
|
365
|
+
message(ws2, raw) {
|
|
366
|
+
const msg = parseMessage(raw);
|
|
367
|
+
if (!msg) {
|
|
368
|
+
ws2.send({ type: "error", reason: "malformed" });
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (msg.type === "join") {
|
|
372
|
+
handleJoin(ws2, msg.peerId);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
if (msg.type === "signal") {
|
|
376
|
+
handleSignal(ws2, msg);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
ws2.send({ type: "error", reason: "malformed" });
|
|
380
|
+
},
|
|
381
|
+
close(ws2) {
|
|
382
|
+
const peerId = ws2.data.peerId;
|
|
383
|
+
if (peerId) {
|
|
384
|
+
if (peerSockets.get(peerId) === ws2) {
|
|
385
|
+
peerSockets.delete(peerId);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
}
|
|
233
391
|
export {
|
|
234
|
-
|
|
392
|
+
signalingServer,
|
|
393
|
+
polly,
|
|
394
|
+
peerRepo
|
|
235
395
|
};
|
|
236
396
|
|
|
237
|
-
//# debugId=
|
|
397
|
+
//# debugId=0BDEE51AC0A70CC764756E2164756E21
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["../src/elysia/plugin.ts", "../src/core/clock.ts", "../src/utils/function-serialization.ts"],
|
|
3
|
+
"sources": ["../src/elysia/peer-repo-plugin.ts", "../src/shared/lib/peer-repo-server.ts", "../src/elysia/plugin.ts", "../src/core/clock.ts", "../src/utils/function-serialization.ts", "../src/elysia/signaling-server-plugin.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"// @ts-nocheck - Optional peer dependencies (elysia, @
|
|
5
|
+
"// @ts-nocheck - Optional peer dependencies (elysia, @automerge/automerge-repo*)\n/**\n * peerRepo — Elysia plugin that runs a Polly peer-relay server alongside an\n * Elysia application and exposes the server's Repo on the Elysia context.\n *\n * The Phase 1 plan originally framed the relay server as an Elysia plugin\n * that registers a WebSocket route on Elysia's native socket layer. That\n * design does not work directly: Automerge's WebSocketServerAdapter requires\n * an `isomorphic-ws` WebSocketServer instance, and Elysia's WebSocket layer\n * is built on Bun's native WebSocket API, which is a different shape. The\n * realistic Phase 1 cut is therefore a *lifecycle* plugin: it runs\n * createPeerRepoServer on its own port during Elysia startup, decorates the\n * Elysia context so route handlers can reach the resulting Repo, and tears\n * the server down during Elysia shutdown. Authenticated WebSocket upgrades\n * via Elysia's hook system are a Phase 1.1 follow-up that requires either\n * a custom Bun-native NetworkAdapter or an upgrade-time auth bridge.\n *\n * @example\n * ```ts\n * import { Elysia } from \"elysia\";\n * import { peerRepo } from \"@fairfox/polly/elysia\";\n *\n * const app = new Elysia()\n * .use(peerRepo({ port: 3030, storagePath: \"./data/polly-peer\" }))\n * .get(\"/stats\", ({ pollyPeerRepo }) => {\n * return { peers: pollyPeerRepo.peers.length };\n * })\n * .listen(8080);\n * ```\n */\n\nimport { Elysia } from \"elysia\";\nimport {\n type CreatePeerRepoServerOptions,\n createPeerRepoServer,\n type PeerRepoServer,\n} from \"../shared/lib/peer-repo-server\";\n\nexport interface PeerRepoPluginOptions extends CreatePeerRepoServerOptions {\n /** Optional path for a health endpoint that returns peer count and storage\n * id. Set to false to skip the health route entirely. Defaults to\n * \"/polly/peer/health\". */\n healthPath?: string | false;\n}\n\n/**\n * Elysia plugin that boots a Polly peer-relay server alongside the host app\n * and decorates the context with `pollyPeerRepo` (the Repo) and\n * `pollyPeerServer` (the full {@link PeerRepoServer} for advanced use).\n *\n * The plugin starts the relay server during Elysia's onStart lifecycle and\n * shuts it down during onStop, so route handlers and cron jobs can use the\n * decorated Repo throughout the request lifetime without managing the\n * underlying transport themselves.\n */\nexport function peerRepo(options: PeerRepoPluginOptions) {\n // The plugin holds a single PeerRepoServer for the lifetime of the Elysia\n // app. It is created during onStart so the listening socket is bound and\n // ready before any request handler can reach it.\n let server: PeerRepoServer | undefined;\n\n const healthPath =\n options.healthPath === false ? null : (options.healthPath ?? \"/polly/peer/health\");\n\n let app = new Elysia({ name: \"polly-peer-repo\" })\n .decorate(\"pollyPeerRepo\", null as unknown as PeerRepoServer[\"repo\"] | null)\n .decorate(\"pollyPeerServer\", null as unknown as PeerRepoServer | null)\n .onStart(async () => {\n server = await createPeerRepoServer(options);\n // Decorate after construction so handlers see the live Repo. Elysia's\n // decorate is mutable in-place when called on the running instance.\n app.decorate(\"pollyPeerRepo\", server.repo);\n app.decorate(\"pollyPeerServer\", server);\n })\n .onStop(async () => {\n if (server) {\n await server.close();\n server = undefined;\n }\n });\n\n if (healthPath) {\n app = app.get(healthPath, () => {\n if (!server) {\n return { status: \"starting\", peers: 0 };\n }\n return {\n status: \"running\",\n peers: server.repo.peers.length,\n port: options.port,\n };\n });\n }\n\n return app;\n}\n",
|
|
6
|
+
"/**\n * peer-repo-server — Phase 1 server-side factory for the Polly peer-relay\n * transport. Constructs an Automerge-Repo `Repo` wired to a WebSocket server\n * and a NodeFS storage backend, ready to relay sync messages between\n * connected $peerState clients.\n *\n * The \"always-on peer\" role for $peerState lives here. The server holds a\n * full Automerge replica of every document, participates in the sync protocol\n * as an ordinary peer, and persists state to disk so the next process restart\n * picks up where the previous one left off. Server-side cron, HTTP handlers,\n * and other compute can open document handles on the returned Repo and mutate\n * them; mutations propagate to connected clients through the same sync\n * protocol that handles client-to-client traffic.\n *\n * The plan originally called this an \"Elysia plugin,\" but Automerge's\n * `WebSocketServerAdapter` requires an `isomorphic-ws` `WebSocketServer`\n * instance — not Elysia's native WebSocket — so the cleanest first cut is a\n * standalone factory that runs its own `ws` server. Elysia integration for\n * authenticated upgrades is a Phase 1.1 follow-up that wraps this factory.\n *\n * @example\n * ```ts\n * import { createPeerRepoServer } from \"@fairfox/polly\";\n *\n * const server = await createPeerRepoServer({\n * port: 3030,\n * storagePath: \"./data/polly-peer\",\n * });\n *\n * // Open a document handle on the server's Repo for cron or compute work.\n * const handle = server.repo.create({ counter: 0 });\n *\n * // On shutdown:\n * await server.close();\n * ```\n */\n\nimport { Repo } from \"@automerge/automerge-repo\";\nimport { WebSocketServerAdapter } from \"@automerge/automerge-repo-network-websocket\";\nimport { NodeFSStorageAdapter } from \"@automerge/automerge-repo-storage-nodefs\";\nimport * as ws from \"ws\";\n\n// `@types/ws` uses CJS `export = WebSocket` with WebSocketServer hanging off\n// the namespace. Under the project's bundler module resolution, the namespace\n// import gives us access to both the constructor and the type.\ntype WebSocketServer = ws.WebSocketServer;\n\nexport interface CreatePeerRepoServerOptions {\n /** Port to listen on. The factory creates its own `WebSocketServer` and\n * binds to this port. */\n port: number;\n /** Filesystem directory for the NodeFS storage adapter. The directory is\n * created on demand. Defaults to `./automerge-repo-data` (Automerge's own\n * default). */\n storagePath?: string;\n /** Hostname interface to bind to. Defaults to all interfaces. */\n host?: string;\n /** Override the `WebSocketServer` instance entirely. When provided, `port`\n * and `host` are ignored and the caller is responsible for the lifecycle.\n * Useful for tests that want to bind to a random port. */\n webSocketServer?: WebSocketServer;\n}\n\nexport interface PeerRepoServer {\n /** A configured Repo participating as the always-on peer. Server-side\n * cron and HTTP handlers can open document handles on this directly. */\n repo: Repo;\n /** The underlying WebSocket server. Exposed for advanced use such as\n * health checks or graceful shutdown coordination. */\n webSocketServer: WebSocketServer;\n /** The Automerge network adapter wrapping the WebSocket server. */\n adapter: WebSocketServerAdapter;\n /** The NodeFS storage adapter writing to {@link CreatePeerRepoServerOptions.storagePath}. */\n storage: NodeFSStorageAdapter;\n /** Tear down the server: disconnect peers, flush storage, close the\n * underlying WebSocket server. Returns a promise that resolves once the\n * tear-down is complete. */\n close: () => Promise<void>;\n}\n\n/**\n * Construct a Polly peer-relay server. Returns a Repo that participates as\n * the always-on peer, the underlying WebSocket server and storage adapter\n * for advanced use, and a close function for orderly shutdown.\n *\n * Applications typically call this once at startup, hold the returned\n * `repo` reference for cron and compute work, and wire the close function\n * into their process shutdown signal handlers.\n */\nexport async function createPeerRepoServer(\n options: CreatePeerRepoServerOptions\n): Promise<PeerRepoServer> {\n // Construct the WebSocket server first and wait until it is actually\n // listening before wiring up the Repo. Using the constructor callback\n // avoids the race where the 'listening' event fires before our listener\n // is attached (the callback form is reliable across Node and Bun).\n const wss = await (options.webSocketServer\n ? Promise.resolve(options.webSocketServer)\n : new Promise<WebSocketServer>((resolve, reject) => {\n const created: WebSocketServer = new ws.WebSocketServer(\n {\n port: options.port,\n ...(options.host !== undefined && { host: options.host }),\n },\n () => resolve(created)\n );\n created.once(\"error\", reject);\n }));\n\n // The cast bridges a @types/ws identity quirk: Automerge's adapter type\n // expects WebSocketServer with options.WebSocket typed via isomorphic-ws's\n // CJS-style namespace, and our direct `ws` import resolves through a\n // different path with the same runtime shape but a structurally distinct\n // TypeScript type. The runtime is identical, the cast names that fact.\n const adapter = new WebSocketServerAdapter(\n wss as unknown as ConstructorParameters<typeof WebSocketServerAdapter>[0]\n );\n const storage = new NodeFSStorageAdapter(options.storagePath);\n\n const repo = new Repo({\n network: [adapter],\n storage,\n });\n\n // Force the storage subsystem to finish initialising before returning. The\n // Repo constructor is synchronous, but its NetworkSubsystem holds back the\n // peer-metadata JOIN until storageSubsystem.id() resolves. If a client\n // connects before that resolution lands, the handshake stalls indefinitely.\n // Awaiting storageId() drains the relevant microtask chain and guarantees\n // the server is ready to accept peers when this factory returns.\n await repo.storageId();\n\n return {\n repo,\n webSocketServer: wss,\n adapter,\n storage,\n close: async () => {\n // Forcibly terminate any still-open client sockets before closing the\n // server, otherwise wss.close() can hang waiting for orderly drain when\n // a peer disappeared without a clean disconnect. We then fire the\n // server close without awaiting its callback — the underlying socket\n // is released immediately by terminate(), and waiting for the close\n // callback can hang under Bun even after every client is gone.\n for (const client of wss.clients) {\n try {\n client.terminate();\n } catch {\n // best effort\n }\n }\n void repo.shutdown();\n try {\n wss.close();\n } catch {\n // best effort\n }\n },\n };\n}\n",
|
|
7
|
+
"// @ts-nocheck - Optional peer dependencies (elysia, @elysiajs/eden)\nimport type { Signal } from \"@preact/signals-core\";\nimport { Elysia } from \"elysia\";\nimport { createLamportClock } from \"../core/clock\";\nimport { serializeFunction } from \"../utils/function-serialization\";\nimport type { PollyConfig, PollyResponseMetadata } from \"./types\";\n\n/**\n * Broadcast message sent to connected clients\n */\ninterface BroadcastMessage {\n type: \"effect\";\n path: string;\n method: string;\n result: unknown;\n clock: { tick: number; contextId: string };\n}\n\n/**\n * Minimal WebSocket interface for broadcasting\n */\ninterface MinimalWebSocket {\n readyState: number;\n send(data: string): void;\n}\n\n/**\n * Route pattern matcher\n * Supports:\n * - Exact match: 'POST /todos'\n * - Param match: 'GET /todos/:id'\n * - Wildcard: '/todos/*'\n */\nfunction matchRoute(pattern: string, method: string, path: string): boolean {\n // Split pattern into method + path or just path\n const hasMethod = pattern.includes(\" \");\n const patternMethod = hasMethod ? pattern.split(\" \")[0] : null;\n const patternPath = hasMethod ? pattern.split(\" \")[1] : pattern;\n\n // Check method\n if (patternMethod && patternMethod !== method) {\n return false;\n }\n\n // Check path\n const patternSegments = patternPath.split(\"/\").filter(Boolean);\n const pathSegments = path.split(\"/\").filter(Boolean);\n\n if (patternSegments.length !== pathSegments.length && !patternPath.includes(\"*\")) {\n return false;\n }\n\n for (let i = 0; i < patternSegments.length; i++) {\n const patternSeg = patternSegments[i];\n const pathSeg = pathSegments[i];\n\n if (patternSeg === \"*\") return true;\n if (patternSeg.startsWith(\":\")) continue; // Param match\n if (patternSeg !== pathSeg) return false;\n }\n\n return true;\n}\n\n/**\n * Find matching config for a route\n */\nfunction findMatchingConfig<T>(\n configs: Record<string, T> | undefined,\n method: string,\n path: string\n): T | undefined {\n if (!configs) return undefined;\n\n for (const [pattern, config] of Object.entries(configs)) {\n if (matchRoute(pattern, method, path)) {\n return config;\n }\n }\n\n return undefined;\n}\n\n/**\n * WebSocket broadcast manager\n */\nclass BroadcastManager {\n private connections = new Map<string, MinimalWebSocket>();\n\n register(clientId: string, ws: MinimalWebSocket) {\n this.connections.set(clientId, ws);\n }\n\n unregister(clientId: string) {\n this.connections.delete(clientId);\n }\n\n broadcast(message: BroadcastMessage, filter?: (clientId: string) => boolean) {\n const payload = JSON.stringify(message);\n\n for (const [clientId, ws] of this.connections.entries()) {\n if (filter && !filter(clientId)) continue;\n if (ws.readyState === 1) {\n // WebSocket.OPEN = 1\n ws.send(payload);\n }\n }\n }\n}\n\n/**\n * Main Polly Elysia plugin\n */\nexport function polly(config: PollyConfig = {}) {\n const isDev = process.env.NODE_ENV !== \"production\";\n const clock = createLamportClock(\"server\");\n const broadcaster = new BroadcastManager();\n const clientStateByConnection = new Map<string, Record<string, Signal<unknown>>>();\n\n const app = new Elysia({ name: \"polly\" })\n // Add state to context\n .decorate(\"pollyState\", {\n client: config.state?.client || {},\n server: config.state?.server || {},\n })\n .decorate(\"pollyClock\", clock)\n .decorate(\"pollyBroadcast\", broadcaster)\n\n // WebSocket endpoint for real-time updates\n .ws(config.websocketPath || \"/polly/ws\", {\n // @ts-expect-error - Elysia WebSocket types from optional peer dependency\n open(ws) {\n const clientId = ws.data.headers?.[\"x-client-id\"] || crypto.randomUUID();\n broadcaster.register(clientId, ws.raw);\n\n // Send initial state sync\n ws.send(\n JSON.stringify({\n type: \"state-sync\",\n state: config.state?.client || {},\n clock: clock.now(),\n })\n );\n },\n // @ts-expect-error - Elysia WebSocket types from optional peer dependency\n close(ws) {\n const clientId = ws.data.headers?.[\"x-client-id\"];\n if (clientId) {\n broadcaster.unregister(clientId);\n clientStateByConnection.delete(clientId);\n }\n },\n // @ts-expect-error - Elysia WebSocket types from optional peer dependency\n message(ws, message) {\n // Handle client state updates\n const data = JSON.parse(message as unknown as string);\n\n if (data.type === \"state-update\") {\n const clientId = ws.data.headers?.[\"x-client-id\"];\n if (clientId) {\n clientStateByConnection.set(clientId, data.state);\n }\n }\n },\n })\n\n // Authorization hook (runs before handler)\n // @ts-expect-error - Elysia context types from optional peer dependency\n .onBeforeHandle(async ({ request, pollyState, body, params }) => {\n const method = request.method;\n const path = new URL(request.url).pathname;\n\n const authHandler = findMatchingConfig(config.authorization, method, path);\n\n if (authHandler) {\n const allowed = await authHandler({\n state: pollyState,\n body,\n params,\n headers: Object.fromEntries(request.headers.entries()),\n });\n\n if (!allowed) {\n return new Response(\"Unauthorized\", { status: 403 });\n }\n }\n })\n\n // Response hook (runs after handler)\n // @ts-expect-error - Elysia context types from optional peer dependency\n .onAfterHandle(\n async ({ request, response, _pollyState, pollyClock, pollyBroadcast, _body, _params }) => {\n const method = request.method;\n const path = new URL(request.url).pathname;\n\n // Find matching effect config\n const effectConfig = findMatchingConfig(config.effects, method, path);\n\n // Tick clock\n pollyClock.tick();\n\n // If broadcast enabled, send to all connected clients\n // This works in both dev and prod for real-time updates\n if (effectConfig?.broadcast) {\n const broadcastMessage = {\n type: \"effect\",\n path,\n method,\n result: response,\n clock: pollyClock.now(),\n };\n\n if (effectConfig.broadcastFilter) {\n pollyBroadcast.broadcast(broadcastMessage, (clientId) => {\n const clientState = clientStateByConnection.get(clientId) || {};\n return effectConfig.broadcastFilter?.(clientState) ?? false;\n });\n } else {\n pollyBroadcast.broadcast(broadcastMessage);\n }\n }\n\n // In production, skip expensive metadata operations\n if (!isDev) {\n return response;\n }\n\n // DEV ONLY: Add Polly metadata to response for debugging/hot-reload\n const offlineConfig = findMatchingConfig(config.offline, method, path);\n const metadata: PollyResponseMetadata = {\n clientEffect: effectConfig\n ? {\n handler: serializeFunction(effectConfig.client),\n broadcast: effectConfig.broadcast || false,\n }\n : undefined,\n offline: offlineConfig,\n clock: pollyClock.now(),\n };\n\n // Attach metadata as header\n return new Response(JSON.stringify(response), {\n headers: {\n \"Content-Type\": \"application/json\",\n \"X-Polly-Metadata\": JSON.stringify(metadata),\n },\n });\n }\n );\n\n return app;\n}\n",
|
|
6
8
|
"/**\n * Lamport Clock Implementation\n *\n * Provides logical timestamps for distributed systems to establish\n * causal ordering of events across different contexts (client/server).\n *\n * Key properties:\n * - Each event increments the local clock\n * - When receiving a message, clock = max(local, received) + 1\n * - If A happens before B, then timestamp(A) < timestamp(B)\n *\n * References:\n * - Lamport, L. (1978). \"Time, Clocks, and the Ordering of Events in a Distributed System\"\n * - https://lamport.azurewebsites.net/pubs/time-clocks.pdf\n */\n\n/**\n * Lamport clock state\n */\nexport interface LamportClock {\n tick: number;\n contextId: string;\n}\n\n/**\n * Lamport clock with operations\n */\nexport interface LamportClockOps {\n /**\n * Get current clock value\n */\n now(): LamportClock;\n\n /**\n * Increment the clock (before sending a message or performing an action)\n */\n tick(): number;\n\n /**\n * Update clock when receiving a message\n * Sets clock to max(local, received) + 1\n */\n update(receivedClock: LamportClock): void;\n}\n\n/**\n * Create a Lamport clock for a specific context\n *\n * @param contextId - Unique identifier for this context (e.g., \"client\", \"server\", \"worker-1\")\n * @returns Clock operations\n *\n * @example\n * ```typescript\n * const serverClock = createLamportClock(\"server\");\n *\n * // Before sending a message\n * serverClock.tick();\n * const timestamp = serverClock.now();\n * send({ data: \"...\", clock: timestamp });\n *\n * // When receiving a message\n * onReceive((message) => {\n * serverClock.update(message.clock);\n * // Process message with updated clock\n * });\n * ```\n */\nexport function createLamportClock(contextId: string): LamportClockOps {\n let tick = 0;\n\n return {\n now(): LamportClock {\n return { tick, contextId };\n },\n\n tick(): number {\n tick += 1;\n return tick;\n },\n\n update(receivedClock: LamportClock): void {\n tick = Math.max(tick, receivedClock.tick) + 1;\n },\n };\n}\n",
|
|
7
|
-
"import serialize from \"serialize-javascript\";\n\n/**\n * Check if we're in development mode\n */\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/**\n * Serialize a function to send to client\n *\n * DEV ONLY: Used for hot reloading and debugging\n * PROD: No-op - client effects are baked into bundle at build time\n */\n// biome-ignore lint/complexity/noBannedTypes: Generic function serialization requires Function type\nexport function serializeFunction(fn: Function): string {\n if (!isDev) {\n // In production, return empty string - this won't be used\n return \"\";\n }\n\n return serialize(fn, { space: 0 });\n}\n\n/**\n * Deserialize a function received from server\n *\n * DEV ONLY: Eval serialized function source\n * PROD: Should never be called - effects come from bundle\n */\n// biome-ignore lint/complexity/noBannedTypes: Generic function deserialization requires Function type\nexport function deserializeFunction(serialized: string): Function {\n if (!isDev) {\n throw new Error(\n \"[Polly] deserializeFunction should not be called in production. \" +\n \"Client effects should be imported from your bundle.\"\n );\n }\n\n if (!serialized) {\n throw new Error(\"[Polly] Cannot deserialize empty function\");\n }\n\n // biome-ignore lint/security/noGlobalEval: Required for dev-mode function deserialization\n return eval(`(${serialized})`);\n}\n"
|
|
9
|
+
"import serialize from \"serialize-javascript\";\n\n/**\n * Check if we're in development mode\n */\nconst isDev = process.env.NODE_ENV !== \"production\";\n\n/**\n * Serialize a function to send to client\n *\n * DEV ONLY: Used for hot reloading and debugging\n * PROD: No-op - client effects are baked into bundle at build time\n */\n// biome-ignore lint/complexity/noBannedTypes: Generic function serialization requires Function type\nexport function serializeFunction(fn: Function): string {\n if (!isDev) {\n // In production, return empty string - this won't be used\n return \"\";\n }\n\n return serialize(fn, { space: 0 });\n}\n\n/**\n * Deserialize a function received from server\n *\n * DEV ONLY: Eval serialized function source\n * PROD: Should never be called - effects come from bundle\n */\n// biome-ignore lint/complexity/noBannedTypes: Generic function deserialization requires Function type\nexport function deserializeFunction(serialized: string): Function {\n if (!isDev) {\n throw new Error(\n \"[Polly] deserializeFunction should not be called in production. \" +\n \"Client effects should be imported from your bundle.\"\n );\n }\n\n if (!serialized) {\n throw new Error(\"[Polly] Cannot deserialize empty function\");\n }\n\n // biome-ignore lint/security/noGlobalEval: Required for dev-mode function deserialization\n return eval(`(${serialized})`);\n}\n",
|
|
10
|
+
"// @ts-nocheck - Optional peer dependencies (elysia, @elysiajs/eden)\n/**\n * signalingServer — Phase 2 Elysia plugin that exposes a stateless\n * WebSocket route for SDP/ICE relay between $meshState peers.\n *\n * The mesh transport is a star-of-data-channels: peers establish direct\n * WebRTC connections to each other and exchange document operations\n * peer-to-peer once those channels are open. WebRTC connection setup\n * needs an out-of-band channel for SDP offer/answer and ICE candidate\n * exchange, and that channel is what this plugin provides. The plugin\n * does not own any document state, does not hold any encryption keys,\n * and never inspects the contents of the messages it relays. It is a\n * pure pub-sub by peer id.\n *\n * Once two peers have completed the SDP exchange and opened a direct\n * data channel, the signalling server is no longer on the critical\n * path — the peers talk directly. The signalling server's role is\n * therefore intermittent: peers connect to it only during the brief\n * windows when they are establishing or re-establishing connections.\n *\n * Wire protocol:\n *\n * Client → server (join):\n * { type: \"join\", peerId: \"peer-alice\" }\n *\n * Client → server (signal to another peer):\n * { type: \"signal\", peerId: \"peer-alice\", targetPeerId: \"peer-bob\",\n * payload: { ... } }\n *\n * Server → client (delivered signal):\n * { type: \"signal\", peerId: \"peer-alice\", targetPeerId: \"peer-bob\",\n * payload: { ... } }\n *\n * Server → client (notification of unknown target):\n * { type: \"error\", reason: \"unknown-target\", targetPeerId: \"...\" }\n *\n * The `payload` is opaque to the signalling server — typically it\n * carries an SDP offer, SDP answer, or ICE candidate. Applications can\n * also use the relay for any other peer-to-peer message that needs an\n * intermediary, such as the initial handshake of a pairing flow.\n *\n * @example\n * ```ts\n * import { Elysia } from \"elysia\";\n * import { signalingServer } from \"@fairfox/polly/elysia\";\n *\n * const app = new Elysia()\n * .use(signalingServer({ path: \"/polly/signaling\" }))\n * .listen(8080);\n * ```\n */\n\nimport { Elysia } from \"elysia\";\n\n/** A signalling message. The `type` discriminates between join (peer\n * registration), signal (relayed message), and error (server response). */\nexport type SignalingMessage =\n | {\n type: \"join\";\n /** The peer registering itself with the signalling server. */\n peerId: string;\n }\n | {\n type: \"signal\";\n /** The peer sending the signal. */\n peerId: string;\n /** The peer the signal is being relayed to. */\n targetPeerId: string;\n /** Opaque payload, typically SDP or ICE. */\n payload: unknown;\n }\n | {\n type: \"error\";\n reason: \"unknown-target\" | \"not-joined\" | \"malformed\";\n targetPeerId?: string;\n };\n\nexport interface SignalingServerOptions {\n /** WebSocket route path. Defaults to \"/polly/signaling\". */\n path?: string;\n}\n\n/**\n * Construct the signalling-server Elysia plugin. The plugin keeps a\n * per-instance map of peer id → WebSocket connection so that incoming\n * \"signal\" messages can be routed to the right target socket. The map\n * is local to the plugin instance, not shared across processes; for\n * multi-instance deployments behind a load balancer, applications need\n * sticky connection routing or a shared backplane (Redis pub-sub or\n * similar). That is a follow-up.\n */\nexport function signalingServer(options: SignalingServerOptions = {}) {\n const path = options.path ?? \"/polly/signaling\";\n // Per-peer-id map of joined sockets. The inverse mapping is stored\n // directly on ws.data (a mutable property bag that Elysia preserves\n // across message callbacks for a given connection); the webrtc-p2p-chat\n // example in examples/ confirms this pattern is stable under Bun.\n const peerSockets = new Map<string, { send: (msg: unknown) => void }>();\n\n // Intentionally unnamed — Elysia deduplicates plugins by name, and each\n // signalingServer() call needs its own closure-captured peer map.\n const parseMessage = (raw: unknown): SignalingMessage | undefined => {\n try {\n return typeof raw === \"string\" ? JSON.parse(raw) : (raw as unknown as SignalingMessage);\n } catch {\n return undefined;\n }\n };\n\n const handleJoin = (ws: unknown, peerId: string): void => {\n peerSockets.set(peerId, ws as unknown as { send: (m: unknown) => void });\n const wsWithData = ws as unknown as { data: Record<string, unknown> };\n wsWithData.data.peerId = peerId;\n };\n\n const sendUnknownTarget = (ws: unknown, targetPeerId: string): void => {\n (ws as unknown as { send: (m: unknown) => void }).send({\n type: \"error\",\n reason: \"unknown-target\",\n targetPeerId,\n } as unknown as SignalingMessage);\n };\n\n /** Look up a target socket and confirm it is still open. */\n const findOpenTarget = (targetPeerId: string): { send: (msg: unknown) => void } | undefined => {\n const target = peerSockets.get(targetPeerId);\n if (!target) return undefined;\n const readyState = (target as unknown as { readyState?: number }).readyState;\n const OPEN = 1;\n if (readyState !== undefined && readyState !== OPEN) {\n peerSockets.delete(targetPeerId);\n return undefined;\n }\n return target;\n };\n\n const handleSignal = (ws: unknown, msg: Extract<SignalingMessage, { type: \"signal\" }>): void => {\n const wsWithData = ws as unknown as {\n data: Record<string, unknown>;\n send: (m: unknown) => void;\n };\n const senderId = wsWithData.data.peerId as unknown as string | undefined;\n if (!senderId) {\n wsWithData.send({ type: \"error\", reason: \"not-joined\" } as unknown as SignalingMessage);\n return;\n }\n const target = findOpenTarget(msg.targetPeerId);\n if (!target) {\n sendUnknownTarget(ws, msg.targetPeerId);\n return;\n }\n const relayed: SignalingMessage = {\n type: \"signal\",\n peerId: senderId,\n targetPeerId: msg.targetPeerId,\n payload: msg.payload,\n };\n try {\n target.send(relayed);\n } catch {\n peerSockets.delete(msg.targetPeerId);\n sendUnknownTarget(ws, msg.targetPeerId);\n }\n };\n\n return new Elysia().ws(path, {\n message(ws, raw) {\n const msg = parseMessage(raw);\n if (!msg) {\n ws.send({ type: \"error\", reason: \"malformed\" } as unknown as SignalingMessage);\n return;\n }\n if (msg.type === \"join\") {\n handleJoin(ws, msg.peerId);\n return;\n }\n if (msg.type === \"signal\") {\n handleSignal(ws, msg);\n return;\n }\n ws.send({ type: \"error\", reason: \"malformed\" } as unknown as SignalingMessage);\n },\n\n close(ws) {\n const peerId = (ws.data as unknown as Record<string, unknown>).peerId as unknown as\n | string\n | undefined;\n if (peerId) {\n // Only delete the entry if it still points at *this* socket, so a\n // stale close after a reconnect does not evict the new socket.\n if (peerSockets.get(peerId) === (ws as unknown as { send: (m: unknown) => void })) {\n peerSockets.delete(peerId);\n }\n }\n },\n });\n}\n"
|
|
8
11
|
],
|
|
9
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
10
|
-
"debugId": "
|
|
12
|
+
"mappings": "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",
|
|
13
|
+
"debugId": "0BDEE51AC0A70CC764756E2164756E21",
|
|
11
14
|
"names": []
|
|
12
15
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* peerRepo — Elysia plugin that runs a Polly peer-relay server alongside an
|
|
3
|
+
* Elysia application and exposes the server's Repo on the Elysia context.
|
|
4
|
+
*
|
|
5
|
+
* The Phase 1 plan originally framed the relay server as an Elysia plugin
|
|
6
|
+
* that registers a WebSocket route on Elysia's native socket layer. That
|
|
7
|
+
* design does not work directly: Automerge's WebSocketServerAdapter requires
|
|
8
|
+
* an `isomorphic-ws` WebSocketServer instance, and Elysia's WebSocket layer
|
|
9
|
+
* is built on Bun's native WebSocket API, which is a different shape. The
|
|
10
|
+
* realistic Phase 1 cut is therefore a *lifecycle* plugin: it runs
|
|
11
|
+
* createPeerRepoServer on its own port during Elysia startup, decorates the
|
|
12
|
+
* Elysia context so route handlers can reach the resulting Repo, and tears
|
|
13
|
+
* the server down during Elysia shutdown. Authenticated WebSocket upgrades
|
|
14
|
+
* via Elysia's hook system are a Phase 1.1 follow-up that requires either
|
|
15
|
+
* a custom Bun-native NetworkAdapter or an upgrade-time auth bridge.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* import { Elysia } from "elysia";
|
|
20
|
+
* import { peerRepo } from "@fairfox/polly/elysia";
|
|
21
|
+
*
|
|
22
|
+
* const app = new Elysia()
|
|
23
|
+
* .use(peerRepo({ port: 3030, storagePath: "./data/polly-peer" }))
|
|
24
|
+
* .get("/stats", ({ pollyPeerRepo }) => {
|
|
25
|
+
* return { peers: pollyPeerRepo.peers.length };
|
|
26
|
+
* })
|
|
27
|
+
* .listen(8080);
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
import { Elysia } from "elysia";
|
|
31
|
+
import { type CreatePeerRepoServerOptions, type PeerRepoServer } from "../shared/lib/peer-repo-server";
|
|
32
|
+
export interface PeerRepoPluginOptions extends CreatePeerRepoServerOptions {
|
|
33
|
+
/** Optional path for a health endpoint that returns peer count and storage
|
|
34
|
+
* id. Set to false to skip the health route entirely. Defaults to
|
|
35
|
+
* "/polly/peer/health". */
|
|
36
|
+
healthPath?: string | false;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Elysia plugin that boots a Polly peer-relay server alongside the host app
|
|
40
|
+
* and decorates the context with `pollyPeerRepo` (the Repo) and
|
|
41
|
+
* `pollyPeerServer` (the full {@link PeerRepoServer} for advanced use).
|
|
42
|
+
*
|
|
43
|
+
* The plugin starts the relay server during Elysia's onStart lifecycle and
|
|
44
|
+
* shuts it down during onStop, so route handlers and cron jobs can use the
|
|
45
|
+
* decorated Repo throughout the request lifetime without managing the
|
|
46
|
+
* underlying transport themselves.
|
|
47
|
+
*/
|
|
48
|
+
export declare function peerRepo(options: PeerRepoPluginOptions): Elysia<"", {
|
|
49
|
+
decorator: {
|
|
50
|
+
pollyPeerRepo: import("@automerge/automerge-repo").Repo | null;
|
|
51
|
+
} & {
|
|
52
|
+
pollyPeerServer: PeerRepoServer | null;
|
|
53
|
+
};
|
|
54
|
+
store: {};
|
|
55
|
+
derive: {};
|
|
56
|
+
resolve: {};
|
|
57
|
+
}, {
|
|
58
|
+
typebox: {};
|
|
59
|
+
error: {};
|
|
60
|
+
}, {
|
|
61
|
+
schema: {};
|
|
62
|
+
standaloneSchema: {};
|
|
63
|
+
macro: {};
|
|
64
|
+
macroFn: {};
|
|
65
|
+
parser: {};
|
|
66
|
+
response: {};
|
|
67
|
+
}, {}, {
|
|
68
|
+
derive: {};
|
|
69
|
+
resolve: {};
|
|
70
|
+
schema: {};
|
|
71
|
+
standaloneSchema: {};
|
|
72
|
+
response: {};
|
|
73
|
+
}, {
|
|
74
|
+
derive: {};
|
|
75
|
+
resolve: {};
|
|
76
|
+
schema: {};
|
|
77
|
+
standaloneSchema: {};
|
|
78
|
+
response: {};
|
|
79
|
+
}>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* signalingServer — Phase 2 Elysia plugin that exposes a stateless
|
|
3
|
+
* WebSocket route for SDP/ICE relay between $meshState peers.
|
|
4
|
+
*
|
|
5
|
+
* The mesh transport is a star-of-data-channels: peers establish direct
|
|
6
|
+
* WebRTC connections to each other and exchange document operations
|
|
7
|
+
* peer-to-peer once those channels are open. WebRTC connection setup
|
|
8
|
+
* needs an out-of-band channel for SDP offer/answer and ICE candidate
|
|
9
|
+
* exchange, and that channel is what this plugin provides. The plugin
|
|
10
|
+
* does not own any document state, does not hold any encryption keys,
|
|
11
|
+
* and never inspects the contents of the messages it relays. It is a
|
|
12
|
+
* pure pub-sub by peer id.
|
|
13
|
+
*
|
|
14
|
+
* Once two peers have completed the SDP exchange and opened a direct
|
|
15
|
+
* data channel, the signalling server is no longer on the critical
|
|
16
|
+
* path — the peers talk directly. The signalling server's role is
|
|
17
|
+
* therefore intermittent: peers connect to it only during the brief
|
|
18
|
+
* windows when they are establishing or re-establishing connections.
|
|
19
|
+
*
|
|
20
|
+
* Wire protocol:
|
|
21
|
+
*
|
|
22
|
+
* Client → server (join):
|
|
23
|
+
* { type: "join", peerId: "peer-alice" }
|
|
24
|
+
*
|
|
25
|
+
* Client → server (signal to another peer):
|
|
26
|
+
* { type: "signal", peerId: "peer-alice", targetPeerId: "peer-bob",
|
|
27
|
+
* payload: { ... } }
|
|
28
|
+
*
|
|
29
|
+
* Server → client (delivered signal):
|
|
30
|
+
* { type: "signal", peerId: "peer-alice", targetPeerId: "peer-bob",
|
|
31
|
+
* payload: { ... } }
|
|
32
|
+
*
|
|
33
|
+
* Server → client (notification of unknown target):
|
|
34
|
+
* { type: "error", reason: "unknown-target", targetPeerId: "..." }
|
|
35
|
+
*
|
|
36
|
+
* The `payload` is opaque to the signalling server — typically it
|
|
37
|
+
* carries an SDP offer, SDP answer, or ICE candidate. Applications can
|
|
38
|
+
* also use the relay for any other peer-to-peer message that needs an
|
|
39
|
+
* intermediary, such as the initial handshake of a pairing flow.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* import { Elysia } from "elysia";
|
|
44
|
+
* import { signalingServer } from "@fairfox/polly/elysia";
|
|
45
|
+
*
|
|
46
|
+
* const app = new Elysia()
|
|
47
|
+
* .use(signalingServer({ path: "/polly/signaling" }))
|
|
48
|
+
* .listen(8080);
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
import { Elysia } from "elysia";
|
|
52
|
+
/** A signalling message. The `type` discriminates between join (peer
|
|
53
|
+
* registration), signal (relayed message), and error (server response). */
|
|
54
|
+
export type SignalingMessage = {
|
|
55
|
+
type: "join";
|
|
56
|
+
/** The peer registering itself with the signalling server. */
|
|
57
|
+
peerId: string;
|
|
58
|
+
} | {
|
|
59
|
+
type: "signal";
|
|
60
|
+
/** The peer sending the signal. */
|
|
61
|
+
peerId: string;
|
|
62
|
+
/** The peer the signal is being relayed to. */
|
|
63
|
+
targetPeerId: string;
|
|
64
|
+
/** Opaque payload, typically SDP or ICE. */
|
|
65
|
+
payload: unknown;
|
|
66
|
+
} | {
|
|
67
|
+
type: "error";
|
|
68
|
+
reason: "unknown-target" | "not-joined" | "malformed";
|
|
69
|
+
targetPeerId?: string;
|
|
70
|
+
};
|
|
71
|
+
export interface SignalingServerOptions {
|
|
72
|
+
/** WebSocket route path. Defaults to "/polly/signaling". */
|
|
73
|
+
path?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Construct the signalling-server Elysia plugin. The plugin keeps a
|
|
77
|
+
* per-instance map of peer id → WebSocket connection so that incoming
|
|
78
|
+
* "signal" messages can be routed to the right target socket. The map
|
|
79
|
+
* is local to the plugin instance, not shared across processes; for
|
|
80
|
+
* multi-instance deployments behind a load balancer, applications need
|
|
81
|
+
* sticky connection routing or a shared backplane (Redis pub-sub or
|
|
82
|
+
* similar). That is a follow-up.
|
|
83
|
+
*/
|
|
84
|
+
export declare function signalingServer(options?: SignalingServerOptions): Elysia<"", {
|
|
85
|
+
decorator: {};
|
|
86
|
+
store: {};
|
|
87
|
+
derive: {};
|
|
88
|
+
resolve: {};
|
|
89
|
+
}, {
|
|
90
|
+
typebox: {};
|
|
91
|
+
error: {};
|
|
92
|
+
}, {
|
|
93
|
+
schema: {};
|
|
94
|
+
standaloneSchema: {};
|
|
95
|
+
macro: {};
|
|
96
|
+
macroFn: {};
|
|
97
|
+
parser: {};
|
|
98
|
+
response: {};
|
|
99
|
+
}, {
|
|
100
|
+
[x: string]: {
|
|
101
|
+
subscribe: {
|
|
102
|
+
body: {};
|
|
103
|
+
params: {};
|
|
104
|
+
query: {};
|
|
105
|
+
headers: {};
|
|
106
|
+
response: {};
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
}, {
|
|
110
|
+
derive: {};
|
|
111
|
+
resolve: {};
|
|
112
|
+
schema: {};
|
|
113
|
+
standaloneSchema: {};
|
|
114
|
+
response: {};
|
|
115
|
+
}, {
|
|
116
|
+
derive: {};
|
|
117
|
+
resolve: {};
|
|
118
|
+
schema: {};
|
|
119
|
+
standaloneSchema: {};
|
|
120
|
+
response: {};
|
|
121
|
+
}>;
|
package/dist/src/index.d.ts
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export type { ExtensionAdapters } from "./shared/adapters";
|
|
8
8
|
export { createChromeAdapters } from "./shared/adapters";
|
|
9
|
+
export type { Access, AccessPredicate, PeerIdentity } from "./shared/lib/access";
|
|
10
|
+
export { and, anyOfPeers, anyone, groupAccess, nobody, not, onlyPeer, or, ownerAccess, publicAccess, readOnlyExcept, } from "./shared/lib/access";
|
|
11
|
+
export type { BlobRef, CreateBlobRefArgs } from "./shared/lib/blob-ref";
|
|
12
|
+
export { computeBlobHash, createBlobRef, isBlobRef } from "./shared/lib/blob-ref";
|
|
9
13
|
export { checkPostconditions, checkPreconditions, clearConstraints, isRuntimeConstraintsEnabled, registerConstraint, registerConstraints, } from "./shared/lib/constraints";
|
|
10
14
|
export type { ContextConfig } from "./shared/lib/context-helpers";
|
|
11
15
|
export { createContext, runInContext } from "./shared/lib/context-helpers";
|