@super-line/server 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,13 +1,8 @@
1
- import "./chunk-MLKGABMK.js";
2
-
3
1
  // src/index.ts
4
2
  import {
5
3
  jsonSerializer,
6
4
  validate,
7
- SuperLineError,
8
- INSPECTOR_ROLE,
9
- classifyContract,
10
- eventPayload
5
+ SuperLineError
11
6
  } from "@super-line/core";
12
7
 
13
8
  // src/conn.ts
@@ -185,26 +180,82 @@ var TOPIC = "t:";
185
180
  var CONN = "c:";
186
181
  var USER = "u:";
187
182
  var REPLY = "reply:";
188
- var INSPECT = "i:";
189
- var inspectorEncoder = new TextEncoder();
190
- function encodedByteSize(encoded) {
191
- return typeof encoded === "string" ? inspectorEncoder.encode(encoded).length : encoded.byteLength;
192
- }
183
+ var PLUGIN = "x:";
193
184
  var STORE = "s:";
194
185
  var SERVER_ORIGIN = "server";
195
186
  function createSuperLineServer(contract, opts) {
196
187
  const c = contract;
197
188
  const serializer = opts.serializer ?? jsonSerializer;
198
189
  const adapter = opts.adapter ?? createInMemoryAdapter();
199
- const inspectorEnabled = !!opts.inspector;
200
- const inspectorRedact = new Set(
201
- opts.inspector && typeof opts.inspector === "object" ? opts.inspector.redact ?? [] : []
202
- );
203
190
  const storeMap = opts.stores ?? {};
191
+ const plugins = opts.plugins ?? [];
192
+ const pluginNames = /* @__PURE__ */ new Set();
193
+ for (const p of plugins) {
194
+ if (pluginNames.has(p.name)) throw new Error(`Duplicate plugin name: ${p.name}`);
195
+ pluginNames.add(p.name);
196
+ }
197
+ for (const p of plugins) {
198
+ if (!p.stores) continue;
199
+ for (const [name, store] of Object.entries(p.stores)) {
200
+ if (name in storeMap)
201
+ throw new Error(`Plugin '${p.name}' store '${name}' collides with an existing store of that name`);
202
+ storeMap[name] = store;
203
+ }
204
+ }
205
+ const reserved = [];
206
+ for (const p of plugins) {
207
+ if (!p.connection) continue;
208
+ const { role, subprotocol, match } = p.connection;
209
+ if (reserved.some((r) => r.role === role) || role in c.roles)
210
+ throw new Error(`Plugin '${p.name}' reserved role '${role}' collides with an existing role`);
211
+ reserved.push({ role, subprotocol, match });
212
+ }
213
+ const reservedRoles = new Set(reserved.map((r) => r.role));
214
+ const reservedServing = /* @__PURE__ */ new Map();
215
+ const errorHandlers = [];
216
+ if (opts.onError) errorHandlers.push(opts.onError);
217
+ for (const p of plugins) if (p.onError) errorHandlers.push(p.onError);
218
+ function fireError(error, info) {
219
+ for (const handler of errorHandlers) {
220
+ try {
221
+ handler(error, info);
222
+ } catch {
223
+ }
224
+ }
225
+ }
226
+ const connectionHooks = [];
227
+ if (opts.onConnection) connectionHooks.push(opts.onConnection);
228
+ for (const p of plugins) if (p.onConnection) connectionHooks.push(p.onConnection);
229
+ function fireConnection(conn, ctx) {
230
+ for (const handler of connectionHooks) {
231
+ try {
232
+ handler(conn, ctx);
233
+ } catch (err) {
234
+ fireError(err, { kind: "connect", name: "onConnection", conn });
235
+ }
236
+ }
237
+ }
238
+ const disconnectHooks = [];
239
+ if (opts.onDisconnect) disconnectHooks.push(opts.onDisconnect);
240
+ for (const p of plugins) if (p.onDisconnect) disconnectHooks.push(p.onDisconnect);
241
+ function fireDisconnect(conn, ctx, code) {
242
+ for (const handler of disconnectHooks) {
243
+ try {
244
+ handler(conn, ctx, code);
245
+ } catch (err) {
246
+ fireError(err, { kind: "disconnect", name: "onDisconnect", conn });
247
+ }
248
+ }
249
+ }
250
+ const middlewareChain = [
251
+ ...opts.use ?? [],
252
+ ...plugins.flatMap((p) => p.use ?? [])
253
+ ];
204
254
  const conns = /* @__PURE__ */ new Set();
205
- const inspectorConns = /* @__PURE__ */ new Set();
255
+ const reservedConns = /* @__PURE__ */ new Set();
206
256
  const members = /* @__PURE__ */ new Map();
207
257
  const busListeners = /* @__PURE__ */ new Map();
258
+ const pluginChannels = /* @__PURE__ */ new Map();
208
259
  const instanceId = randomUUID();
209
260
  const envNodeName = typeof process !== "undefined" ? process.env.SUPER_LINE_NODE_NAME : void 0;
210
261
  const nodeName = opts.nodeName ?? envNodeName ?? instanceId.slice(0, 8);
@@ -248,6 +299,10 @@ function createSuperLineServer(contract, opts) {
248
299
  handleStoreRelay(channel, payload);
249
300
  return;
250
301
  }
302
+ if (channel.startsWith(PLUGIN)) {
303
+ handlePluginChannel(channel, payload);
304
+ return;
305
+ }
251
306
  const set = members.get(channel);
252
307
  if (set) for (const conn of set) conn.sendRaw(payload);
253
308
  const busSet = busListeners.get(channel);
@@ -307,9 +362,9 @@ function createSuperLineServer(contract, opts) {
307
362
  } else {
308
363
  env = { i: r.corrId, ok: false, code: result.code, m: result.m, d: result.d };
309
364
  }
310
- if (inspectorEnabled)
311
- emitInspectorEvent(
312
- env.ok ? { type: "msg.serverReply", target: conn.id, name: r.name, ok: true, output: safeSnapshot(env.d), reqId: r.corrId } : {
365
+ if (taps.length)
366
+ emitTap(
367
+ env.ok ? { type: "msg.serverReply", target: conn.id, name: r.name, ok: true, output: env.d, reqId: r.corrId } : {
313
368
  type: "msg.serverReply",
314
369
  target: conn.id,
315
370
  name: r.name,
@@ -338,25 +393,26 @@ function createSuperLineServer(contract, opts) {
338
393
  }, hb.interval ?? 3e4);
339
394
  hbTimer.unref?.();
340
395
  }
341
- function emitInspectorEvent(event) {
342
- if (!inspectorEnabled) return;
343
- const payload = eventPayload(event);
344
- const envelope = {
345
- event,
346
- ts: Date.now(),
347
- originNodeId: instanceId,
348
- byteSize: payload === void 0 ? void 0 : encodedByteSize(serializer.encode(payload))
349
- };
350
- void adapter.publish(INSPECT + "events", serializer.encode({ t: "pub", c: "events", d: envelope }));
396
+ const taps = [];
397
+ function emitTap(event) {
398
+ if (!taps.length) return;
399
+ for (const tap of taps) {
400
+ try {
401
+ tap(event);
402
+ } catch (err) {
403
+ fireError(err, { kind: "event", name: event.type });
404
+ }
405
+ }
351
406
  }
407
+ for (const p of plugins) if (p.onEvent) taps.push(p.onEvent);
352
408
  function joinChannel(conn, channel) {
353
409
  conn.channels.add(channel);
354
410
  if (channel.startsWith(ROOM)) {
355
411
  const room2 = channel.slice(ROOM.length);
356
412
  void adapter.presence?.addRoom(conn.id, room2);
357
- emitInspectorEvent({ type: "room.add", connId: conn.id, room: room2 });
413
+ emitTap({ type: "room.add", connId: conn.id, room: room2 });
358
414
  } else if (channel.startsWith(TOPIC)) {
359
- emitInspectorEvent({ type: "topic.sub", connId: conn.id, topic: channel.slice(channel.indexOf(":", TOPIC.length) + 1) });
415
+ emitTap({ type: "topic.sub", connId: conn.id, topic: channel.slice(channel.indexOf(":", TOPIC.length) + 1) });
360
416
  }
361
417
  const set = members.get(channel);
362
418
  if (set) {
@@ -375,9 +431,9 @@ function createSuperLineServer(contract, opts) {
375
431
  if (channel.startsWith(ROOM)) {
376
432
  const room2 = channel.slice(ROOM.length);
377
433
  void adapter.presence?.removeRoom(conn.id, room2);
378
- emitInspectorEvent({ type: "room.remove", connId: conn.id, room: room2 });
434
+ emitTap({ type: "room.remove", connId: conn.id, room: room2 });
379
435
  } else if (channel.startsWith(TOPIC)) {
380
- emitInspectorEvent({ type: "topic.unsub", connId: conn.id, topic: channel.slice(channel.indexOf(":", TOPIC.length) + 1) });
436
+ emitTap({ type: "topic.unsub", connId: conn.id, topic: channel.slice(channel.indexOf(":", TOPIC.length) + 1) });
381
437
  }
382
438
  if (set.size === 0) {
383
439
  members.delete(channel);
@@ -406,128 +462,47 @@ function createSuperLineServer(contract, opts) {
406
462
  }
407
463
  return out;
408
464
  }
409
- async function onInspectorFrame(conn, frame) {
410
- if (frame.t === "req") await handleInspectorReq(conn, frame);
411
- else if (frame.t === "sub") await handleInspectorSub(conn, frame);
412
- else if (frame.t === "unsub") leaveChannel(conn, INSPECT + "events");
413
- }
414
- async function handleInspectorSub(conn, frame) {
415
- if (frame.c !== "events") {
416
- conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown topic: ${frame.c}` });
417
- return;
418
- }
419
- await joinChannel(conn, INSPECT + "events");
420
- conn.send({ t: "res", i: frame.i, d: null });
421
- }
422
- async function handleInspectorReq(conn, frame) {
423
- const handler = inspectorHandlers[frame.m];
424
- if (!handler) {
425
- conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown message: ${frame.m}` });
426
- return;
427
- }
428
- try {
429
- const output = await handler(frame.d, conn);
430
- conn.send({ t: "res", i: frame.i, d: output });
431
- } catch (err) {
432
- const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
433
- conn.send({ t: "err", i: frame.i, code: e.code, m: e.message, d: e.data });
434
- }
435
- }
436
- async function buildInspectedContract() {
437
- let toJsonSchema;
438
- try {
439
- const mod = await import("./dist-S566F7HE.js");
440
- toJsonSchema = mod.toJsonSchema;
441
- } catch {
442
- return classifyContract(c);
443
- }
444
- const schemas = /* @__PURE__ */ new Set();
445
- classifyContract(c, (s) => {
446
- schemas.add(s);
447
- return void 0;
448
- });
449
- const converted = /* @__PURE__ */ new Map();
450
- await Promise.all(
451
- [...schemas].map(
452
- (s) => toJsonSchema(s).then(
453
- (j) => {
454
- converted.set(s, j);
455
- },
456
- () => {
457
- }
458
- // unsupported vendor / missing per-vendor converter -> structure-only for this entry
459
- )
460
- )
461
- );
462
- return classifyContract(c, (s) => converted.get(s));
463
- }
464
- function safeSnapshot(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
465
- if (value === null) return null;
466
- const t = typeof value;
467
- if (t === "bigint") return `${value.toString()}n`;
468
- if (t === "function") return "[Function]";
469
- if (t === "symbol") return value.toString();
470
- if (t !== "object") return value;
471
- const obj = value;
472
- if (obj instanceof Date) return obj.toISOString();
473
- if (seen.has(obj)) return "[Circular]";
474
- if (depth >= 6) return "[MaxDepth]";
475
- seen.add(obj);
476
- try {
477
- if (Array.isArray(obj)) return obj.slice(0, 1e3).map((v) => safeSnapshot(v, depth + 1, seen));
478
- const ctor = Object.getPrototypeOf(obj)?.constructor?.name;
479
- const out = {};
480
- if (ctor && ctor !== "Object") out["#type"] = ctor;
481
- for (const [k, v] of Object.entries(obj)) {
482
- out[k] = inspectorRedact.has(k) ? "[Redacted]" : safeSnapshot(v, depth + 1, seen);
465
+ const reservedBridges = /* @__PURE__ */ new WeakMap();
466
+ async function onReservedFrame(conn, frame) {
467
+ const serving = reservedServing.get(conn.role);
468
+ if (!serving) return;
469
+ if (frame.t === "req") {
470
+ const handler = serving.handlers[frame.m];
471
+ if (!handler) {
472
+ conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown message: ${frame.m}` });
473
+ return;
483
474
  }
484
- return out;
485
- } finally {
486
- seen.delete(obj);
487
- }
488
- }
489
- const inspectorHandlers = {
490
- getContract: () => buildInspectedContract(),
491
- getTopology: async () => presenceOrThrow().topology(),
492
- listConnections: async () => presenceOrThrow().list(),
493
- getNode: async () => ({ nodeId: instanceId, nodeName, rooms: localRooms(), topics: localTopics() }),
494
- getConn: async (input) => {
495
- const id = input?.id;
496
- if (!id) throw new SuperLineError("BAD_REQUEST", "getConn requires an id");
497
- const local = [...conns].find((cn) => cn.id === id);
498
- if (local) {
499
- return {
500
- descriptor: buildDescriptor(local),
501
- ctx: safeSnapshot(local.ctx),
502
- data: safeSnapshot(local.data),
503
- ctxAvailable: true
504
- };
475
+ try {
476
+ conn.send({ t: "res", i: frame.i, d: await handler(frame.d, conn) });
477
+ } catch (err) {
478
+ const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
479
+ conn.send({ t: "err", i: frame.i, code: e.code, m: e.message, d: e.data });
505
480
  }
506
- const remote = await presenceOrThrow().get(id);
507
- if (!remote) throw new SuperLineError("NOT_FOUND", `Unknown connection: ${id}`);
508
- return { descriptor: remote, ctxAvailable: false };
509
- },
510
- listStores: async () => Object.entries(storeMap).map(([name, store]) => ({ name, model: store.model })),
511
- listResources: async (input) => {
512
- const store = storeMap[input.store];
513
- if (!store) throw new SuperLineError("NOT_FOUND", `Unknown store: ${input.store}`);
514
- return store.list();
515
- },
516
- readResource: async (input) => {
517
- const { store: name, id } = input;
518
- const store = storeMap[name];
519
- if (!store) throw new SuperLineError("NOT_FOUND", `Unknown store: ${name}`);
520
- const resource = await store.read(id);
521
- if (!resource) throw new SuperLineError("NOT_FOUND", `No resource: ${name}/${id}`);
522
- let data = resource.data;
523
- if (store.open) {
524
- const replica = store.open(id);
525
- data = replica.getSnapshot();
526
- replica.close();
481
+ } else if (frame.t === "sub") {
482
+ if (!isSubscribeTopic(serving.connection.contract, frame.c)) {
483
+ conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown topic: ${frame.c}` });
484
+ return;
527
485
  }
528
- return { data: safeSnapshot(data), accessRules: resource.accessRules };
486
+ let bridges = reservedBridges.get(conn);
487
+ if (!bridges) {
488
+ bridges = /* @__PURE__ */ new Map();
489
+ reservedBridges.set(conn, bridges);
490
+ }
491
+ bridges.get(frame.c)?.();
492
+ bridges.set(frame.c, serving.ctx.channel(frame.c).subscribe((data) => conn.send({ t: "pub", c: frame.c, d: data })));
493
+ conn.send({ t: "res", i: frame.i, d: null });
494
+ } else if (frame.t === "unsub") {
495
+ const bridges = reservedBridges.get(conn);
496
+ bridges?.get(frame.c)?.();
497
+ bridges?.delete(frame.c);
529
498
  }
530
- };
499
+ }
500
+ function isSubscribeTopic(contract2, name) {
501
+ const isTopicDef = (def) => !!def && typeof def === "object" && "subscribe" in def && def.subscribe === true;
502
+ if (isTopicDef(contract2.shared?.serverToClient?.[name])) return true;
503
+ for (const role of Object.keys(contract2.roles)) if (isTopicDef(contract2.roles[role]?.serverToClient?.[name])) return true;
504
+ return false;
505
+ }
531
506
  const authHook = async (handshake) => {
532
507
  const auth = await opts.authenticate(handshake);
533
508
  return { role: auth.role, ctx: auth.ctx, transport: handshake.transport };
@@ -535,11 +510,7 @@ function createSuperLineServer(contract, opts) {
535
510
  function acceptConn(raw, auth) {
536
511
  const role = auth.role;
537
512
  const ctx = auth.ctx;
538
- const inspector = role === INSPECTOR_ROLE;
539
- if (inspector && !inspectorEnabled) {
540
- raw.close();
541
- return;
542
- }
513
+ const isReserved = reservedRoles.has(role);
543
514
  const connId = randomUUID();
544
515
  const conn = new Conn(
545
516
  raw,
@@ -547,17 +518,22 @@ function createSuperLineServer(contract, opts) {
547
518
  role,
548
519
  ctx,
549
520
  serializer,
550
- inspectorEnabled ? (event, data) => emitInspectorEvent({ type: "msg.event", target: connId, name: event, data: safeSnapshot(data) }) : void 0
521
+ taps.length ? (event, data) => emitTap({ type: "msg.event", target: connId, name: event, data }) : void 0
551
522
  );
552
523
  conn.transport = auth.transport;
553
524
  conn.principal = resolvePrincipal(conn, opts.identify);
554
525
  raw.onMessage((bytes) => {
555
526
  void onMessage(conn, bytes);
556
527
  });
557
- if (inspector) {
558
- inspectorConns.add(conn);
528
+ if (isReserved) {
529
+ reservedConns.add(conn);
559
530
  raw.onClose(() => {
560
- inspectorConns.delete(conn);
531
+ reservedConns.delete(conn);
532
+ const bridges = reservedBridges.get(conn);
533
+ if (bridges) {
534
+ for (const off of bridges.values()) off();
535
+ reservedBridges.delete(conn);
536
+ }
561
537
  for (const channel of conn.channels) leaveChannel(conn, channel);
562
538
  });
563
539
  return;
@@ -568,18 +544,18 @@ function createSuperLineServer(contract, opts) {
568
544
  for (const channel of conn.channels) leaveChannel(conn, channel);
569
545
  void adapter.presence?.del(conn.id);
570
546
  const goneUserId = opts.identify?.(conn);
571
- emitInspectorEvent({
547
+ emitTap({
572
548
  type: "disconnect",
573
549
  connId: conn.id,
574
550
  nodeId: instanceId,
575
551
  ...goneUserId !== void 0 ? { userId: goneUserId } : {}
576
552
  });
577
- opts.onDisconnect?.(conn, ctx, code);
553
+ fireDisconnect(conn, ctx, code);
578
554
  });
579
- opts.onConnection?.(conn, ctx);
555
+ fireConnection(conn, ctx);
580
556
  const descriptor = buildDescriptor(conn);
581
557
  void adapter.presence?.set(descriptor);
582
- emitInspectorEvent({ type: "connect", descriptor });
558
+ emitTap({ type: "connect", descriptor });
583
559
  void joinChannel(conn, CONN + conn.id);
584
560
  const uid = opts.identify?.(conn);
585
561
  if (uid !== void 0) void joinChannel(conn, USER + uid);
@@ -591,8 +567,8 @@ function createSuperLineServer(contract, opts) {
591
567
  } catch {
592
568
  return;
593
569
  }
594
- if (inspectorConns.has(conn)) {
595
- await onInspectorFrame(conn, frame);
570
+ if (reservedConns.has(conn)) {
571
+ await onReservedFrame(conn, frame);
596
572
  return;
597
573
  }
598
574
  if (frame.t === "req") await handleReq(conn, frame);
@@ -614,10 +590,10 @@ function createSuperLineServer(contract, opts) {
614
590
  }
615
591
  }
616
592
  for (const transport of opts.transports) {
617
- void transport.start({ authenticate: authHook, onConnection: acceptConn });
593
+ void transport.start({ authenticate: authHook, onConnection: acceptConn, reserved });
618
594
  }
619
595
  function runMiddleware(info, terminal) {
620
- const chain = opts.use ?? [];
596
+ const chain = middlewareChain;
621
597
  let last = -1;
622
598
  const dispatch = (idx) => {
623
599
  if (idx <= last) return Promise.reject(new Error("next() called multiple times"));
@@ -636,11 +612,11 @@ function createSuperLineServer(contract, opts) {
636
612
  responded = true;
637
613
  });
638
614
  } catch (err) {
639
- opts.onError?.(err, info);
615
+ fireError(err, info);
640
616
  const e = err instanceof SuperLineError ? err : new SuperLineError("INTERNAL", "Internal server error");
641
617
  if (!responded) conn.send({ t: "err", i: id, code: e.code, m: e.message, d: e.data });
642
- if (inspectorEnabled && info.kind === "request")
643
- emitInspectorEvent({
618
+ if (taps.length && info.kind === "request")
619
+ emitTap({
644
620
  type: "msg.response",
645
621
  connId: conn.id,
646
622
  name: info.name,
@@ -652,30 +628,30 @@ function createSuperLineServer(contract, opts) {
652
628
  }
653
629
  async function handleReq(conn, frame) {
654
630
  const def = c.roles[conn.role]?.clientToServer?.[frame.m] ?? c.shared?.clientToServer?.[frame.m];
655
- const handler = impl[conn.role]?.[frame.m] ?? impl.shared?.[frame.m];
631
+ const handler = impl[conn.role]?.[frame.m] ?? impl.shared?.[frame.m] ?? pluginHandlers[frame.m];
656
632
  if (!def || !handler) {
657
633
  conn.send({ t: "err", i: frame.i, code: "NOT_FOUND", m: `Unknown message: ${frame.m}` });
658
634
  return;
659
635
  }
660
636
  await dispatchOp(conn, frame.i, { kind: "request", name: frame.m, conn }, async () => {
661
637
  const input = await validate(def.input, frame.d);
662
- if (inspectorEnabled)
663
- emitInspectorEvent({
638
+ if (taps.length)
639
+ emitTap({
664
640
  type: "msg.request",
665
641
  connId: conn.id,
666
642
  role: conn.role,
667
643
  name: frame.m,
668
- input: safeSnapshot(input),
644
+ input,
669
645
  reqId: frame.i
670
646
  });
671
647
  const output = await handler(input, conn.ctx, conn);
672
- if (inspectorEnabled)
673
- emitInspectorEvent({
648
+ if (taps.length)
649
+ emitTap({
674
650
  type: "msg.response",
675
651
  connId: conn.id,
676
652
  name: frame.m,
677
653
  ok: true,
678
- output: safeSnapshot(output),
654
+ output,
679
655
  reqId: frame.i
680
656
  });
681
657
  conn.send({ t: "res", i: frame.i, d: output });
@@ -711,7 +687,7 @@ function createSuperLineServer(contract, opts) {
711
687
  if (!resource.accessRules[principal]?.read)
712
688
  throw new SuperLineError("FORBIDDEN", `Read denied: ${frame.n}/${frame.id}`);
713
689
  await joinChannel(conn, STORE + frame.n + ":" + frame.id);
714
- if (inspectorEnabled) emitInspectorEvent({ type: "store.subscribe", connId: conn.id, store: frame.n, id: frame.id });
690
+ if (taps.length) emitTap({ type: "store.subscribe", connId: conn.id, store: frame.n, id: frame.id });
715
691
  conn.send({ t: "res", i: frame.i, d: resource.data });
716
692
  });
717
693
  }
@@ -737,14 +713,14 @@ function createSuperLineServer(contract, opts) {
737
713
  if (!resource.accessRules[principal]?.write)
738
714
  throw new SuperLineError("FORBIDDEN", `Write denied: ${frame.n}/${frame.id}`);
739
715
  await store.apply({ id: frame.id, update: frame.u, origin: frame.o });
740
- if (inspectorEnabled)
741
- emitInspectorEvent({
716
+ if (taps.length)
717
+ emitTap({
742
718
  type: "store.write",
743
719
  store: frame.n,
744
720
  id: frame.id,
745
721
  origin: frame.o,
746
722
  connId: conn.id,
747
- data: safeSnapshot(frame.u)
723
+ data: frame.u
748
724
  });
749
725
  conn.send({ t: "res", i: frame.i, d: null });
750
726
  });
@@ -752,7 +728,7 @@ function createSuperLineServer(contract, opts) {
752
728
  function handleStoreClose(conn, frame) {
753
729
  if (!storeMap[frame.n]) return;
754
730
  leaveChannel(conn, STORE + frame.n + ":" + frame.id);
755
- if (inspectorEnabled) emitInspectorEvent({ type: "store.unsubscribe", connId: conn.id, store: frame.n, id: frame.id });
731
+ if (taps.length) emitTap({ type: "store.unsubscribe", connId: conn.id, store: frame.n, id: frame.id });
756
732
  }
757
733
  for (const [name, store] of Object.entries(storeMap)) {
758
734
  const isSelf = store.clustering === "self";
@@ -813,8 +789,8 @@ function createSuperLineServer(contract, opts) {
813
789
  leaveChannel(conn, channel);
814
790
  },
815
791
  broadcast(event, data) {
816
- if (inspectorEnabled)
817
- emitInspectorEvent({ type: "msg.broadcast", room: name, name: String(event), data: safeSnapshot(data) });
792
+ if (taps.length)
793
+ emitTap({ type: "msg.broadcast", room: name, name: String(event), data });
818
794
  void adapter.publish(channel, serializer.encode({ t: "evt", e: String(event), d: data }));
819
795
  },
820
796
  get size() {
@@ -827,7 +803,7 @@ function createSuperLineServer(contract, opts) {
827
803
  }
828
804
  function publishTo(ns, name, data) {
829
805
  const channel = TOPIC + ns + ":" + name;
830
- if (inspectorEnabled) emitInspectorEvent({ type: "msg.publish", topic: name, data: safeSnapshot(data) });
806
+ if (taps.length) emitTap({ type: "msg.publish", topic: name, data });
831
807
  const busSet = busListeners.get(channel);
832
808
  if (busSet) for (const cb of busSet) callBus(cb, data, instanceId, name);
833
809
  void adapter.publish(channel, serializer.encode({ t: "pub", c: name, d: data, i: instanceId }));
@@ -836,7 +812,7 @@ function createSuperLineServer(contract, opts) {
836
812
  try {
837
813
  cb(data, { from });
838
814
  } catch (err) {
839
- opts.onError?.(err, { kind: "event", name });
815
+ fireError(err, { kind: "event", name });
840
816
  }
841
817
  }
842
818
  function deliverBus(payload, set) {
@@ -857,22 +833,62 @@ function createSuperLineServer(contract, opts) {
857
833
  try {
858
834
  data = await validate(schema, frame.d);
859
835
  } catch (err) {
860
- opts.onError?.(err, { kind: "event", name });
836
+ fireError(err, { kind: "event", name });
861
837
  return;
862
838
  }
863
839
  }
864
840
  for (const cb of set) callBus(cb, data, from, name);
865
841
  })();
866
842
  }
843
+ function handlePluginChannel(channel, payload) {
844
+ const set = pluginChannels.get(channel);
845
+ if (!set) return;
846
+ let frame;
847
+ try {
848
+ frame = serializer.decode(payload);
849
+ } catch {
850
+ return;
851
+ }
852
+ if (frame.i === instanceId) return;
853
+ for (const cb of set) callBus(cb, frame.d, frame.i, channel);
854
+ }
855
+ function pluginChannel(pluginName, name) {
856
+ const channel = PLUGIN + pluginName + ":" + name;
857
+ return {
858
+ publish(data) {
859
+ const set = pluginChannels.get(channel);
860
+ if (set) for (const cb of set) callBus(cb, data, instanceId, channel);
861
+ void adapter.publish(channel, serializer.encode({ i: instanceId, d: data }));
862
+ },
863
+ subscribe(handler) {
864
+ let set = pluginChannels.get(channel);
865
+ if (!set) {
866
+ set = /* @__PURE__ */ new Set();
867
+ pluginChannels.set(channel, set);
868
+ void adapter.subscribe(channel);
869
+ }
870
+ set.add(handler);
871
+ return () => {
872
+ const current = pluginChannels.get(channel);
873
+ if (!current) return;
874
+ current.delete(handler);
875
+ if (current.size === 0) {
876
+ pluginChannels.delete(channel);
877
+ void adapter.unsubscribe(channel);
878
+ }
879
+ };
880
+ }
881
+ };
882
+ }
867
883
  function personalTarget(channel) {
868
884
  return {
869
885
  emit(event, data) {
870
- if (inspectorEnabled)
871
- emitInspectorEvent({
886
+ if (taps.length)
887
+ emitTap({
872
888
  type: "msg.event",
873
889
  target: channel.slice(channel.indexOf(":") + 1),
874
890
  name: event,
875
- data: safeSnapshot(data)
891
+ data
876
892
  });
877
893
  const env = { p: "emit", f: { t: "evt", e: event, d: data } };
878
894
  void adapter.publish(channel, serializer.encode(env));
@@ -901,8 +917,8 @@ function createSuperLineServer(contract, opts) {
901
917
  },
902
918
  { once: true }
903
919
  );
904
- if (inspectorEnabled)
905
- emitInspectorEvent({ type: "msg.serverRequest", target: id, name, input: safeSnapshot(input), reqId });
920
+ if (taps.length)
921
+ emitTap({ type: "msg.serverRequest", target: id, name, input, reqId });
906
922
  const env = { p: "req", o: instanceId, i: reqId, m: name, d: input };
907
923
  void adapter.publish(CONN + id, serializer.encode(env));
908
924
  });
@@ -917,7 +933,7 @@ function createSuperLineServer(contract, opts) {
917
933
  storeApi[name] = {
918
934
  async create(id, data, accessRules) {
919
935
  await store.create(id, data, accessRules);
920
- if (inspectorEnabled) emitInspectorEvent({ type: "store.create", store: name, id });
936
+ if (taps.length) emitTap({ type: "store.create", store: name, id });
921
937
  },
922
938
  read(id) {
923
939
  return Promise.resolve(store.read(id));
@@ -925,20 +941,19 @@ function createSuperLineServer(contract, opts) {
925
941
  async write(id, data, opts2) {
926
942
  const origin = opts2?.origin ?? SERVER_ORIGIN;
927
943
  await store.apply({ id, update: data, origin });
928
- if (inspectorEnabled)
929
- emitInspectorEvent({ type: "store.write", store: name, id, origin, data: safeSnapshot(data) });
944
+ if (taps.length) emitTap({ type: "store.write", store: name, id, origin, data });
930
945
  },
931
946
  async grant(id, principal, perms) {
932
947
  const r = await readOrThrow(id);
933
948
  await store.setAccess(id, { ...r.accessRules, [principal]: perms });
934
- if (inspectorEnabled) emitInspectorEvent({ type: "store.grant", store: name, id, principal, perms });
949
+ if (taps.length) emitTap({ type: "store.grant", store: name, id, principal, perms });
935
950
  },
936
951
  async revoke(id, principal) {
937
952
  const r = await readOrThrow(id);
938
953
  const next = { ...r.accessRules };
939
954
  delete next[principal];
940
955
  await store.setAccess(id, next);
941
- if (inspectorEnabled) emitInspectorEvent({ type: "store.revoke", store: name, id, principal });
956
+ if (taps.length) emitTap({ type: "store.revoke", store: name, id, principal });
942
957
  },
943
958
  async delete(id) {
944
959
  await store.delete(id);
@@ -947,18 +962,21 @@ function createSuperLineServer(contract, opts) {
947
962
  STORE + name + ":" + id,
948
963
  serializer.encode({ t: "sdel", n: name, id, nd: instanceId })
949
964
  );
950
- if (inspectorEnabled) emitInspectorEvent({ type: "store.delete", store: name, id });
965
+ if (taps.length) emitTap({ type: "store.delete", store: name, id });
951
966
  },
952
- list() {
953
- return Promise.resolve(store.list());
967
+ list(opts2) {
968
+ return Promise.resolve(store.list(opts2));
969
+ },
970
+ searchPrincipals(opts2) {
971
+ return Promise.resolve(store.searchPrincipals(opts2));
954
972
  },
955
973
  open(id, openOpts) {
956
974
  if (!store.open)
957
975
  throw new SuperLineError("UNSUPPORTED", `Store ${name} does not support reactive open()`);
958
976
  const replica = store.open(id, openOpts);
959
- if (!inspectorEnabled) return replica;
977
+ if (!taps.length) return replica;
960
978
  const origin = openOpts?.origin ?? SERVER_ORIGIN;
961
- const emit = (data) => emitInspectorEvent({ type: "store.write", store: name, id, origin, data: safeSnapshot(data) });
979
+ const emit = (data) => emitTap({ type: "store.write", store: name, id, origin, data });
962
980
  return {
963
981
  ...replica,
964
982
  set: (d) => {
@@ -977,6 +995,8 @@ function createSuperLineServer(contract, opts) {
977
995
  }
978
996
  };
979
997
  }
998
+ const pluginDisposers = [];
999
+ const pluginHandlers = {};
980
1000
  const api = {
981
1001
  nodeId: instanceId,
982
1002
  nodeName,
@@ -1024,7 +1044,24 @@ function createSuperLineServer(contract, opts) {
1024
1044
  return { emit: t.emit, disconnect: t.close };
1025
1045
  },
1026
1046
  implement(handlers) {
1027
- impl = handlers;
1047
+ const map = handlers;
1048
+ const missing = [];
1049
+ const duplicate = [];
1050
+ const checkBlock = (block, defs) => {
1051
+ if (!defs) return;
1052
+ for (const key of Object.keys(defs)) {
1053
+ const inImpl = !!map[block]?.[key];
1054
+ const inPlugin = key in pluginHandlers;
1055
+ if (inImpl && inPlugin) duplicate.push(`${block}.${key}`);
1056
+ else if (!inImpl && !inPlugin) missing.push(`${block}.${key}`);
1057
+ }
1058
+ };
1059
+ checkBlock("shared", c.shared?.clientToServer);
1060
+ for (const role of Object.keys(c.roles)) checkBlock(role, c.roles[role]?.clientToServer);
1061
+ if (duplicate.length)
1062
+ throw new Error(`implement: these keys are also handled by a plugin \u2014 remove them: ${duplicate.join(", ")}`);
1063
+ if (missing.length) throw new Error(`implement: missing handler(s) for: ${missing.join(", ")}`);
1064
+ impl = map;
1028
1065
  return api;
1029
1066
  },
1030
1067
  room,
@@ -1066,14 +1103,89 @@ function createSuperLineServer(contract, opts) {
1066
1103
  async close() {
1067
1104
  if (closing) return;
1068
1105
  closing = true;
1106
+ for (const dispose of pluginDisposers) {
1107
+ try {
1108
+ dispose();
1109
+ } catch {
1110
+ }
1111
+ }
1069
1112
  if (hbTimer) clearInterval(hbTimer);
1070
1113
  for (const conn of conns) conn.close();
1071
- for (const conn of inspectorConns) conn.close();
1114
+ for (const conn of reservedConns) conn.close();
1072
1115
  await adapter.presence?.clearNode(instanceId);
1073
1116
  await adapter.close?.();
1074
1117
  for (const transport of opts.transports) await transport.stop();
1075
1118
  }
1076
1119
  };
1120
+ function makePluginContext(pluginName) {
1121
+ return {
1122
+ nodeId: instanceId,
1123
+ nodeName,
1124
+ instanceId,
1125
+ serializer,
1126
+ contract: c,
1127
+ get conns() {
1128
+ return [...conns];
1129
+ },
1130
+ get local() {
1131
+ return api.local;
1132
+ },
1133
+ cluster: api.cluster,
1134
+ isOnline: (userId) => api.isOnline(userId),
1135
+ publish: (topic, data) => publishTo("shared", topic, data),
1136
+ subscribe: (topic, handler) => api.subscribe(topic, handler),
1137
+ toConn: (id) => {
1138
+ const t = api.toConn(id);
1139
+ return { emit: (event, data) => t.emit(event, data), close: t.close };
1140
+ },
1141
+ toUser: (userId) => {
1142
+ const t = api.toUser(userId);
1143
+ return { emit: (event, data) => t.emit(event, data), disconnect: t.disconnect };
1144
+ },
1145
+ room: (name) => {
1146
+ const r = room(name);
1147
+ return {
1148
+ add: (conn) => r.add(conn),
1149
+ remove: (conn) => r.remove(conn),
1150
+ broadcast: (event, data) => r.broadcast(event, data),
1151
+ get size() {
1152
+ return r.size;
1153
+ },
1154
+ get connections() {
1155
+ return r.connections;
1156
+ }
1157
+ };
1158
+ },
1159
+ store: (name) => api.store(name),
1160
+ storeInfos: () => Object.entries(storeMap).map(([name, store]) => ({ name, model: store.model })),
1161
+ describe: (conn) => buildDescriptor(conn),
1162
+ connectionById: (id) => Promise.resolve(presenceOrThrow().get(id)),
1163
+ channel: (name) => pluginChannel(pluginName, name)
1164
+ };
1165
+ }
1166
+ const contractRequestKeys = new Set(Object.keys(c.shared?.clientToServer ?? {}));
1167
+ for (const role of Object.keys(c.roles))
1168
+ for (const k of Object.keys(c.roles[role]?.clientToServer ?? {})) contractRequestKeys.add(k);
1169
+ for (const p of plugins) {
1170
+ const ctx = makePluginContext(p.name);
1171
+ if (p.handlers) {
1172
+ for (const [key, fn] of Object.entries(p.handlers(ctx))) {
1173
+ if (!contractRequestKeys.has(key))
1174
+ throw new Error(`Plugin '${p.name}' handles '${key}', which the contract has no request for \u2014 did you forget to merge its surface?`);
1175
+ if (key in pluginHandlers)
1176
+ throw new Error(`Plugin handler collision on '${key}' (already provided by another plugin)`);
1177
+ pluginHandlers[key] = fn;
1178
+ }
1179
+ }
1180
+ if (p.connection)
1181
+ reservedServing.set(p.connection.role, {
1182
+ connection: p.connection,
1183
+ handlers: p.connection.handlers?.(ctx) ?? {},
1184
+ ctx
1185
+ });
1186
+ const dispose = p.setup?.(ctx);
1187
+ if (dispose) pluginDisposers.push(dispose);
1188
+ }
1077
1189
  return api;
1078
1190
  }
1079
1191
  export {