@dsbot-channel/core 0.7.0 → 0.7.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.
@@ -46,10 +46,16 @@ export function makeAdapterPlugin(spec) {
46
46
  source: `${spec.name} plugin`,
47
47
  };
48
48
  const mount = ctx.get(MOUNT_SERVICE);
49
- if (mount === undefined)
49
+ if (mount === undefined) {
50
50
  registry.addInstance(instance);
51
- else
52
- mount.add(instance);
51
+ return;
52
+ }
53
+ mount.add(instance);
54
+ const remove = mount.remove;
55
+ if (remove === undefined)
56
+ return;
57
+ // A reload disposes this plugin first, so the instance leaves the gateway before it is applied again.
58
+ ctx.effect(() => () => Promise.resolve(remove.call(mount, instance.channel, instance.source)).catch(() => undefined), `${spec.name}: adapter instance`);
53
59
  };
54
60
  const plugin = { name: spec.name, inject: [REGISTRY_SERVICE], Config: AdapterPluginConfig, apply };
55
61
  return { Config: AdapterPluginConfig, apply, plugin };
package/dist/plugin.js CHANGED
@@ -685,6 +685,14 @@ async function applyWith(ctx, raw, options = {}, skillFile) {
685
685
  const bots = botScopes(effective, written, rawProfile.policy);
686
686
  const pipelines = new SessionPipelines({ bots });
687
687
  const channels = mountAdapters(effective, settings, warn, registry, (index) => writtenRaw[index], rawProfile.policy, roster);
688
+ // One live view: the push router and the capability gate read these, so a late mount or an unmount is
689
+ // visible to them at once without rebuilding anything per send.
690
+ const byChannel = new Map(channels.map((channel) => [channel.config.channel, channel]));
691
+ const pushChannels = channels.map((channel) => ({
692
+ channel: channel.config.channel,
693
+ botName: channel.config.botName,
694
+ }));
695
+ let refreshSendText = () => undefined;
688
696
  const scoreService = ctx.get('dsbot-channel/score');
689
697
  const judgeService = ctx.get('dsbot-channel/judge');
690
698
  const declaredPipelines = pipelineScopesOf(effective).map((scopes) => resolvePipeline(scopes).nodes);
@@ -1100,6 +1108,8 @@ async function applyWith(ctx, raw, options = {}, skillFile) {
1100
1108
  const chanceCounts = new Map();
1101
1109
  const selections = new Map();
1102
1110
  const started = [];
1111
+ const handles = new Map();
1112
+ const windowsByChannel = new Map();
1103
1113
  const runtimes = [];
1104
1114
  const configLayers = [];
1105
1115
  ctx.effect(() => async () => {
@@ -1143,6 +1153,7 @@ async function applyWith(ctx, raw, options = {}, skillFile) {
1143
1153
  const context = new ContextBuffer(turnSettings.context);
1144
1154
  const windows = new TurnWindows({ now: clock, setTimer, clearTimer }, { limits: { maxBatch: turnSettings.maxBatch, maxChars: turnSettings.maxChars }, windowMs: turnSettings.coalesceMs });
1145
1155
  windowBuffers.push(windows);
1156
+ windowsByChannel.set(adapter.channel, windows);
1146
1157
  const mergedPolicy = effectivePolicy(rawProfile.policy, writtenBlock?.policy);
1147
1158
  const compiled = compilePolicy(mergedPolicy);
1148
1159
  const policyBlock = mergedPolicy;
@@ -1401,7 +1412,9 @@ async function applyWith(ctx, raw, options = {}, skillFile) {
1401
1412
  windows.offer(`${adapter.botName}|${message.sessionKey}`, { eventKey: message.eventKey, chars: message.body.text.length, value: message }, (batch) => runBatch(batch));
1402
1413
  return { ok: true };
1403
1414
  };
1404
- started.push(await transport.start({ onMessage, resolveSecret }));
1415
+ const handle = await transport.start({ onMessage, resolveSecret });
1416
+ started.push(handle);
1417
+ handles.set(adapter.channel, handle);
1405
1418
  }
1406
1419
  };
1407
1420
  try {
@@ -1451,15 +1464,23 @@ async function applyWith(ctx, raw, options = {}, skillFile) {
1451
1464
  writtenRaw.push(raw);
1452
1465
  channels.push(...fresh);
1453
1466
  mountedBy.set(config.channel, source);
1454
- for (const mounted of fresh)
1467
+ for (const mounted of fresh) {
1455
1468
  reporting.add(mounted.adapter);
1469
+ byChannel.set(mounted.config.channel, mounted);
1470
+ pushChannels.push({ channel: mounted.config.channel, botName: mounted.config.botName });
1471
+ }
1456
1472
  try {
1457
1473
  checkReplyModes(fresh);
1458
1474
  refreshBots();
1459
1475
  }
1460
1476
  catch (error) {
1461
- for (const mounted of fresh)
1477
+ for (const mounted of fresh) {
1462
1478
  reporting.delete(mounted.adapter);
1479
+ byChannel.delete(mounted.config.channel);
1480
+ const pushed = pushChannels.findIndex((one) => one.channel === mounted.config.channel);
1481
+ if (pushed >= 0)
1482
+ pushChannels.splice(pushed, 1);
1483
+ }
1463
1484
  mountedBy.delete(config.channel);
1464
1485
  channels.splice(channels.length - fresh.length, fresh.length);
1465
1486
  writtenRaw.pop();
@@ -1471,10 +1492,89 @@ async function applyWith(ctx, raw, options = {}, skillFile) {
1471
1492
  identity = readIdentity(true);
1472
1493
  readImportTarget();
1473
1494
  }
1495
+ refreshSendText();
1474
1496
  startQueued(fresh);
1475
1497
  };
1498
+ /** Drops one mounted channel: every live view forgets it, and its listener and sessions are released. */
1499
+ const removeChannel = (channel, source) => {
1500
+ const run = async () => {
1501
+ const mounted = byChannel.get(channel);
1502
+ if (mounted === undefined)
1503
+ return;
1504
+ const declared = mountedBy.get(channel);
1505
+ if (source !== undefined && declared !== undefined && declared !== source)
1506
+ return;
1507
+ const windows = windowsByChannel.get(channel);
1508
+ if (windows !== undefined) {
1509
+ const at = windowBuffers.indexOf(windows);
1510
+ if (at >= 0)
1511
+ windowBuffers.splice(at, 1);
1512
+ windows.clear();
1513
+ windowsByChannel.delete(channel);
1514
+ }
1515
+ const sessions = selections.get(channel);
1516
+ if (sessions !== undefined) {
1517
+ const at = runtimes.indexOf(sessions);
1518
+ if (at >= 0)
1519
+ runtimes.splice(at, 1);
1520
+ selections.delete(channel);
1521
+ sessions.dispose();
1522
+ }
1523
+ const handle = handles.get(channel);
1524
+ handles.delete(channel);
1525
+ if (handle !== undefined) {
1526
+ const at = started.indexOf(handle);
1527
+ if (at >= 0)
1528
+ started.splice(at, 1);
1529
+ try {
1530
+ await handle.stop();
1531
+ }
1532
+ catch (error) {
1533
+ warn(`dsbot-channel: stopping "${channel}" failed: ${String(error)}`);
1534
+ }
1535
+ }
1536
+ const configAt = mounts.indexOf(mounted.config);
1537
+ if (configAt >= 0) {
1538
+ mounts.splice(configAt, 1);
1539
+ writtenRaw.splice(configAt, 1);
1540
+ }
1541
+ const at = channels.indexOf(mounted);
1542
+ if (at >= 0)
1543
+ channels.splice(at, 1);
1544
+ const pushed = pushChannels.findIndex((one) => one.channel === channel);
1545
+ if (pushed >= 0)
1546
+ pushChannels.splice(pushed, 1);
1547
+ byChannel.delete(channel);
1548
+ mountedBy.delete(channel);
1549
+ reporting.delete(mounted.adapter);
1550
+ chanceCounts.delete(channel);
1551
+ const layerAt = configLayers.findIndex((entry) => entry.channel === channel);
1552
+ if (layerAt >= 0)
1553
+ configLayers.splice(layerAt, 1);
1554
+ if (!channels.some((one) => one.config.botName === mounted.config.botName)) {
1555
+ const ledgerAt = ledgers.findIndex((one) => one.bot === mounted.config.botName);
1556
+ if (ledgerAt >= 0)
1557
+ ledgers.splice(ledgerAt, 1);
1558
+ }
1559
+ try {
1560
+ refreshBots();
1561
+ identity = readIdentity(true);
1562
+ readImportTarget();
1563
+ const dangling = [...identityPending.values(), ...importPending.values()].filter((error) => error.message.includes(channel));
1564
+ if (dangling.length > 0)
1565
+ warn(`dsbot-channel: after unmounting "${channel}": ${dangling[0].message}`);
1566
+ }
1567
+ catch (error) {
1568
+ warn(`dsbot-channel: re-checking the profile after unmounting "${channel}" failed: ${String(error)}`);
1569
+ }
1570
+ refreshSendText();
1571
+ };
1572
+ const queued = starting.then(run);
1573
+ starting = queued.then(() => undefined, () => undefined);
1574
+ return queued;
1575
+ };
1476
1576
  ctx.provide(MOUNT_SERVICE);
1477
- ctx.set(MOUNT_SERVICE, { add: addInstance });
1577
+ ctx.set(MOUNT_SERVICE, { add: addInstance, remove: removeChannel });
1478
1578
  // An instance queued while the gateway was still applying is mounted here instead of being lost.
1479
1579
  for (const one of registry.takeInstances())
1480
1580
  addInstance(one);
@@ -1507,12 +1607,8 @@ async function applyWith(ctx, raw, options = {}, skillFile) {
1507
1607
  ctx.provide('dsbot-channel.peers');
1508
1608
  ctx.set('dsbot-channel.peers', started);
1509
1609
  if (tools !== undefined && typeof tools.register === 'function') {
1510
- const byChannel = new Map(channels.map((channel) => [channel.config.channel, channel]));
1511
1610
  const router = new PushRouter({
1512
- channels: channels.map((channel) => ({
1513
- channel: channel.config.channel,
1514
- botName: channel.config.botName,
1515
- })),
1611
+ channels: pushChannels,
1516
1612
  source: (sessionId) => (sessionId === undefined ? undefined : sources.get(sessionId)),
1517
1613
  checkTarget: (channel, target) => byChannel.get(channel)?.adapter.checkTarget(target),
1518
1614
  describeTargets: (channel) => byChannel.get(channel)?.adapter.describeTargets() ?? '',
@@ -1556,7 +1652,19 @@ async function applyWith(ctx, raw, options = {}, skillFile) {
1556
1652
  return sent;
1557
1653
  },
1558
1654
  });
1559
- ctx.effect(() => tools.register(createSendText(router)), 'dsbot-channel: send_text tool');
1655
+ let releaseSendText;
1656
+ // The description lists the live channels, so a mount or an unmount republished it.
1657
+ refreshSendText = () => {
1658
+ releaseSendText?.();
1659
+ releaseSendText = tools.register(createSendText(router));
1660
+ };
1661
+ ctx.effect(() => {
1662
+ refreshSendText();
1663
+ return () => {
1664
+ releaseSendText?.();
1665
+ releaseSendText = undefined;
1666
+ };
1667
+ }, 'dsbot-channel: send_text tool');
1560
1668
  const gate = new CapabilityGate({
1561
1669
  router,
1562
1670
  capabilities: (channel) => byChannel.get(channel)?.adapter.capabilities,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dsbot-channel/core",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "description": "Core of dsbot-channel: gateway, sessions, pipeline, tools and the adapter registry.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",