spectrum-ts 1.17.1 → 2.0.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.
Files changed (36) hide show
  1. package/README.md +11 -1
  2. package/dist/{attachment-DfWSZS5L.d.ts → attachment-B4nSrKVd.d.ts} +1 -1
  3. package/dist/{authoring-C9uDdZ2F.d.ts → authoring-BjE5BvlO.d.ts} +2 -2
  4. package/dist/authoring.d.ts +3 -3
  5. package/dist/authoring.js +6 -3
  6. package/dist/chunk-34FQGGD7.js +34 -0
  7. package/dist/chunk-3B4QH4JG.js +35 -0
  8. package/dist/chunk-3GEJYGZK.js +84 -0
  9. package/dist/chunk-5LT5J3NR.js +695 -0
  10. package/dist/{chunk-MC6ZKFSG.js → chunk-5XEFJBN2.js} +25 -103
  11. package/dist/{chunk-JQN6CRSC.js → chunk-6BI4PFTP.js} +10 -39
  12. package/dist/{chunk-QGJFZMD5.js → chunk-6UZFVXQF.js} +17 -101
  13. package/dist/{chunk-YJMPSD3S.js → chunk-ATNAE7OR.js} +196 -47
  14. package/dist/{chunk-IPOFBAIM.js → chunk-NGC4DJIX.js} +23 -19
  15. package/dist/{chunk-5TIF3FIE.js → chunk-Q537JPTG.js} +8 -6
  16. package/dist/{chunk-5BKZJMZV.js → chunk-U3LXXT3W.js} +61 -32
  17. package/dist/chunk-U7AWXDH6.js +91 -0
  18. package/dist/{chunk-3OTECDNH.js → chunk-WXY5QP3M.js} +5 -3
  19. package/dist/index.d.ts +71 -126
  20. package/dist/index.js +350 -90
  21. package/dist/manifest.json +6 -0
  22. package/dist/providers/imessage/index.d.ts +75 -3
  23. package/dist/providers/imessage/index.js +10 -5
  24. package/dist/providers/index.d.ts +5 -2
  25. package/dist/providers/index.js +16 -8
  26. package/dist/providers/slack/index.d.ts +1 -1
  27. package/dist/providers/slack/index.js +4 -3
  28. package/dist/providers/telegram/index.d.ts +47 -0
  29. package/dist/providers/telegram/index.js +13 -0
  30. package/dist/providers/terminal/index.d.ts +17 -419
  31. package/dist/providers/terminal/index.js +5 -3
  32. package/dist/providers/whatsapp-business/index.d.ts +1 -1
  33. package/dist/providers/whatsapp-business/index.js +6 -4
  34. package/dist/types-BD0-kKyv.d.ts +82 -0
  35. package/dist/{types-DcQ5a7PK.d.ts → types-Bje8aq1k.d.ts} +34 -4
  36. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -1,8 +1,17 @@
1
1
  import { createRequire as __spectrumCreateRequire } from "node:module"; const require = __spectrumCreateRequire(import.meta.url);
2
2
  import {
3
- group,
4
3
  richlink
5
- } from "./chunk-JQN6CRSC.js";
4
+ } from "./chunk-6BI4PFTP.js";
5
+ import {
6
+ FUSOR_MESSAGES_CHANNEL,
7
+ fusor,
8
+ fusorEvent,
9
+ isFusorClient,
10
+ isFusorEvent
11
+ } from "./chunk-34FQGGD7.js";
12
+ import {
13
+ group
14
+ } from "./chunk-3B4QH4JG.js";
6
15
  import {
7
16
  voice
8
17
  } from "./chunk-NNY6LMSC.js";
@@ -12,23 +21,26 @@ import {
12
21
  } from "./chunk-2D27WW5B.js";
13
22
  import {
14
23
  SpectrumCloudError,
24
+ cloud
25
+ } from "./chunk-3GEJYGZK.js";
26
+ import {
27
+ contact
28
+ } from "./chunk-U7AWXDH6.js";
29
+ import {
15
30
  broadcast,
16
- cloud,
17
31
  createAsyncQueue,
18
32
  mergeStreams,
19
33
  stream
20
- } from "./chunk-MC6ZKFSG.js";
34
+ } from "./chunk-5XEFJBN2.js";
21
35
  import {
22
- contact,
23
36
  fromVCard,
24
37
  toVCard
25
- } from "./chunk-QGJFZMD5.js";
38
+ } from "./chunk-6UZFVXQF.js";
26
39
  import {
27
40
  UnsupportedError,
28
41
  avatar,
29
42
  buildSpace,
30
43
  contentAttrs,
31
- defineFusorPlatform,
32
44
  definePlatform,
33
45
  edit,
34
46
  rename,
@@ -36,7 +48,7 @@ import {
36
48
  senderAttrs,
37
49
  typing,
38
50
  wrapProviderMessage
39
- } from "./chunk-IPOFBAIM.js";
51
+ } from "./chunk-NGC4DJIX.js";
40
52
  import {
41
53
  __commonJS,
42
54
  __esm,
@@ -6444,13 +6456,13 @@ var require_path = __commonJS({
6444
6456
  return /^(?:\/|\w+:)/.test(path2);
6445
6457
  }
6446
6458
  );
6447
- var normalize = (
6459
+ var normalize2 = (
6448
6460
  /**
6449
6461
  * Normalizes the specified path.
6450
6462
  * @param {string} path Path to normalize
6451
6463
  * @returns {string} Normalized path
6452
6464
  */
6453
- path.normalize = function normalize2(path2) {
6465
+ path.normalize = function normalize3(path2) {
6454
6466
  path2 = path2.replace(/\\/g, "/").replace(/\/{2,}/g, "/");
6455
6467
  var parts = path2.split("/"), absolute = isAbsolute(path2), prefix = "";
6456
6468
  if (absolute)
@@ -6473,12 +6485,12 @@ var require_path = __commonJS({
6473
6485
  );
6474
6486
  path.resolve = function resolve(originPath, includePath, alreadyNormalized) {
6475
6487
  if (!alreadyNormalized)
6476
- includePath = normalize(includePath);
6488
+ includePath = normalize2(includePath);
6477
6489
  if (isAbsolute(includePath))
6478
6490
  return includePath;
6479
6491
  if (!alreadyNormalized)
6480
- originPath = normalize(originPath);
6481
- return (originPath = originPath.replace(/(?:\/|^)[^/]+$/, "")).length ? normalize(originPath + "/" + includePath) : includePath;
6492
+ originPath = normalize2(originPath);
6493
+ return (originPath = originPath.replace(/(?:\/|^)[^/]+$/, "")).length ? normalize2(originPath + "/" + includePath) : includePath;
6482
6494
  };
6483
6495
  }
6484
6496
  });
@@ -24195,6 +24207,162 @@ var require_src3 = __commonJS({
24195
24207
  }
24196
24208
  });
24197
24209
 
24210
+ // src/content/stream-text.ts
24211
+ import z from "zod";
24212
+ var streamTextSchema = z.object({
24213
+ type: z.literal("streamText"),
24214
+ // A single-consumption producer of normalized text deltas. The builder
24215
+ // closes over the normalized source; the platform driver calls it once.
24216
+ // Kept opaque to Zod via `z.custom` (same approach as `attachment.read`).
24217
+ stream: z.custom(
24218
+ (v) => typeof v === "function",
24219
+ {
24220
+ message: "streamText.stream must be a function returning AsyncIterable<string>"
24221
+ }
24222
+ )
24223
+ });
24224
+ var asRecord = (value) => typeof value === "object" && value !== null ? value : void 0;
24225
+ var SKIP_EVENT_TYPES = /* @__PURE__ */ new Set([
24226
+ "message_start",
24227
+ "message_delta",
24228
+ "message_stop",
24229
+ "content_block_start",
24230
+ "content_block_stop",
24231
+ "ping"
24232
+ ]);
24233
+ var fromOpenAIResponses = (obj) => {
24234
+ const type = obj.type;
24235
+ if (typeof type !== "string" || !type.startsWith("response.")) {
24236
+ return;
24237
+ }
24238
+ if (type === "response.output_text.delta" && typeof obj.delta === "string") {
24239
+ return obj.delta;
24240
+ }
24241
+ return null;
24242
+ };
24243
+ var fromAnthropicDelta = (obj) => {
24244
+ if (obj.type !== "content_block_delta") {
24245
+ return;
24246
+ }
24247
+ const delta = asRecord(obj.delta);
24248
+ if (delta?.type === "text_delta" && typeof delta.text === "string") {
24249
+ return delta.text;
24250
+ }
24251
+ return null;
24252
+ };
24253
+ var fromAiSdkPart = (obj) => {
24254
+ if (obj.type !== "text-delta") {
24255
+ return;
24256
+ }
24257
+ if (typeof obj.textDelta === "string") {
24258
+ return obj.textDelta;
24259
+ }
24260
+ return typeof obj.text === "string" ? obj.text : null;
24261
+ };
24262
+ var fromOpenAIChat = (obj) => {
24263
+ if (!Array.isArray(obj.choices)) {
24264
+ return;
24265
+ }
24266
+ const delta = asRecord(asRecord(obj.choices[0])?.delta);
24267
+ const content = delta?.content;
24268
+ return typeof content === "string" ? content : null;
24269
+ };
24270
+ var fromControlEvent = (obj) => typeof obj.type === "string" && SKIP_EVENT_TYPES.has(obj.type) ? null : void 0;
24271
+ var OBJECT_EXTRACTORS = [
24272
+ fromOpenAIResponses,
24273
+ fromAnthropicDelta,
24274
+ fromAiSdkPart,
24275
+ fromOpenAIChat,
24276
+ fromControlEvent
24277
+ ];
24278
+ var defaultExtract = (chunk) => {
24279
+ if (typeof chunk === "string") {
24280
+ return chunk;
24281
+ }
24282
+ const record = asRecord(chunk);
24283
+ if (!record) {
24284
+ throw new Error(
24285
+ `streamText: cannot extract a text delta from a ${typeof chunk} chunk. Pass { extract } to map your stream's chunks to text.`
24286
+ );
24287
+ }
24288
+ for (const extractor of OBJECT_EXTRACTORS) {
24289
+ const result = extractor(record);
24290
+ if (result !== void 0) {
24291
+ return result;
24292
+ }
24293
+ }
24294
+ throw new Error(
24295
+ `streamText: unrecognized chunk shape (type=${String(record.type)}). Pass an { extract } function to map your provider's chunk to a text delta.`
24296
+ );
24297
+ };
24298
+ var isReadableStream = (value) => typeof value?.getReader === "function";
24299
+ var isAsyncIterable = (value) => typeof value?.[Symbol.asyncIterator] === "function";
24300
+ async function* readableToAsync(source) {
24301
+ if (isAsyncIterable(source)) {
24302
+ yield* source;
24303
+ return;
24304
+ }
24305
+ const reader = source.getReader();
24306
+ try {
24307
+ while (true) {
24308
+ const { done, value } = await reader.read();
24309
+ if (done) {
24310
+ return;
24311
+ }
24312
+ yield value;
24313
+ }
24314
+ } finally {
24315
+ reader.releaseLock();
24316
+ }
24317
+ }
24318
+ var resolveChunkIterable = (source) => {
24319
+ const textStream = source.textStream;
24320
+ if (textStream != null) {
24321
+ if (isReadableStream(textStream)) {
24322
+ return readableToAsync(textStream);
24323
+ }
24324
+ if (isAsyncIterable(textStream)) {
24325
+ return textStream;
24326
+ }
24327
+ throw new Error(
24328
+ "streamText: `.textStream` must be an AsyncIterable or a ReadableStream."
24329
+ );
24330
+ }
24331
+ if (isReadableStream(source)) {
24332
+ return readableToAsync(source);
24333
+ }
24334
+ if (isAsyncIterable(source)) {
24335
+ return source;
24336
+ }
24337
+ throw new Error(
24338
+ "streamText: source must be an AsyncIterable, a ReadableStream, or an object with a `.textStream` (e.g. the AI SDK streamText() result)."
24339
+ );
24340
+ };
24341
+ var normalize = (source, options) => {
24342
+ const extract = options?.extract ? options.extract : defaultExtract;
24343
+ let consumed = false;
24344
+ return async function* normalized() {
24345
+ if (consumed) {
24346
+ throw new Error(
24347
+ "streamText: this source has already been consumed \u2014 a stream can only be sent once."
24348
+ );
24349
+ }
24350
+ consumed = true;
24351
+ for await (const chunk of resolveChunkIterable(source)) {
24352
+ const delta = extract(chunk);
24353
+ if (delta) {
24354
+ yield delta;
24355
+ }
24356
+ }
24357
+ };
24358
+ };
24359
+ var asStreamText = (input) => streamTextSchema.parse({ type: "streamText", stream: input.stream });
24360
+ function streamText(source, options) {
24361
+ return {
24362
+ build: async () => asStreamText({ stream: normalize(source, options) })
24363
+ };
24364
+ }
24365
+
24198
24366
  // src/emoji/generated.ts
24199
24367
  var GeneratedEmoji = {
24200
24368
  _1stPlaceMedal: "\u{1F947}",
@@ -26124,21 +26292,6 @@ var aliases = {
26124
26292
  };
26125
26293
  var Emoji = { ...GeneratedEmoji, ...aliases };
26126
26294
 
26127
- // src/fusor/types.ts
26128
- var FUSOR_BRAND = /* @__PURE__ */ Symbol.for("spectrum.fusor.client");
26129
-
26130
- // src/fusor/index.ts
26131
- function fusor(platform, verify) {
26132
- return {
26133
- [FUSOR_BRAND]: true,
26134
- platform,
26135
- verify
26136
- };
26137
- }
26138
- function isFusorClient(value) {
26139
- return typeof value === "object" && value !== null && value[FUSOR_BRAND] === true;
26140
- }
26141
-
26142
26295
  // src/spectrum.ts
26143
26296
  import {
26144
26297
  createLogger as createLogger2,
@@ -26146,7 +26299,7 @@ import {
26146
26299
  withSpan
26147
26300
  } from "@photon-ai/otel";
26148
26301
  import { RawInboundEvent as RawInboundEvent2 } from "@photon-ai/proto/photon/fusor/v1/inbound";
26149
- import z from "zod";
26302
+ import z2 from "zod";
26150
26303
 
26151
26304
  // src/build-env.ts
26152
26305
  var SPECTRUM_SDK_VERSION = "local";
@@ -27282,6 +27435,23 @@ function combineReplies(outcomes) {
27282
27435
  body
27283
27436
  };
27284
27437
  }
27438
+ function routeHandlerResult(result, handler, deliver) {
27439
+ if (result === void 0) {
27440
+ return;
27441
+ }
27442
+ const items = Array.isArray(result) ? result : [result];
27443
+ for (const item of items) {
27444
+ if (!isFusorEvent(item)) {
27445
+ deliver(item);
27446
+ continue;
27447
+ }
27448
+ if (item.name === FUSOR_MESSAGES_CHANNEL) {
27449
+ deliver(item.data);
27450
+ } else {
27451
+ handler.pushEvent(item.name, item.data);
27452
+ }
27453
+ }
27454
+ }
27285
27455
  function runHandlerOnce(handler, parsedRequest, deliver = handler.pushMessage) {
27286
27456
  return (async () => {
27287
27457
  try {
@@ -27302,12 +27472,7 @@ function runHandlerOnce(handler, parsedRequest, deliver = handler.pushMessage) {
27302
27472
  };
27303
27473
  const result = await handler.messages({ payload, respond });
27304
27474
  returned = true;
27305
- if (result !== void 0) {
27306
- const records = Array.isArray(result) ? result : [result];
27307
- for (const record of records) {
27308
- deliver(record);
27309
- }
27310
- }
27475
+ routeHandlerResult(result, handler, deliver);
27311
27476
  return { ok: true, reply: reply2 };
27312
27477
  } catch (error) {
27313
27478
  const errorReason = error instanceof Error ? error.message : String(error);
@@ -27403,7 +27568,7 @@ var FusorCore = class {
27403
27568
  const { iterable: requestIterable, sink } = createRequestSink();
27404
27569
  this.requestSink = sink;
27405
27570
  sink.push({ init: { startSeq: 0 }, reply: void 0 });
27406
- const metadata = Metadata().set("authorization", `Bearer ${token}`);
27571
+ const metadata = Metadata().set("access_token", token);
27407
27572
  const stream2 = client.subscribe(requestIterable, { metadata });
27408
27573
  try {
27409
27574
  for await (const response of stream2) {
@@ -27558,25 +27723,26 @@ function createStore() {
27558
27723
 
27559
27724
  // src/spectrum.ts
27560
27725
  var PHOTON_OTEL_ENDPOINT = "https://otlp.photon.codes";
27726
+ var STREAM_CLOSE_TIMEOUT_MS = 5e3;
27561
27727
  var lifecycleLog = createLogger2("spectrum.lifecycle");
27562
27728
  var ignoreCleanupError = () => void 0;
27563
- var spectrumOptionsSchema = z.object({
27564
- flattenGroups: z.boolean().optional()
27729
+ var spectrumOptionsSchema = z2.object({
27730
+ flattenGroups: z2.boolean().optional()
27565
27731
  }).optional();
27566
- var spectrumConfigSchema = z.union([
27567
- z.object({
27568
- projectId: z.string().min(1),
27569
- projectSecret: z.string().min(1),
27570
- providers: z.array(z.custom()),
27732
+ var spectrumConfigSchema = z2.union([
27733
+ z2.object({
27734
+ projectId: z2.string().min(1),
27735
+ projectSecret: z2.string().min(1),
27736
+ providers: z2.array(z2.custom()),
27571
27737
  options: spectrumOptionsSchema,
27572
- telemetry: z.boolean().optional()
27738
+ telemetry: z2.boolean().optional()
27573
27739
  }),
27574
- z.object({
27575
- projectId: z.undefined().optional(),
27576
- projectSecret: z.undefined().optional(),
27577
- providers: z.array(z.custom()),
27740
+ z2.object({
27741
+ projectId: z2.undefined().optional(),
27742
+ projectSecret: z2.undefined().optional(),
27743
+ providers: z2.array(z2.custom()),
27578
27744
  options: spectrumOptionsSchema,
27579
- telemetry: z.boolean().optional()
27745
+ telemetry: z2.boolean().optional()
27580
27746
  })
27581
27747
  ]);
27582
27748
  function bootstrapTelemetry(opts) {
@@ -27614,15 +27780,21 @@ async function Spectrum(options) {
27614
27780
  const platformStates = /* @__PURE__ */ new Map();
27615
27781
  const fusorMessageSources = /* @__PURE__ */ new Map();
27616
27782
  const messageBroadcasters = /* @__PURE__ */ new Map();
27783
+ const fusorEventSources = /* @__PURE__ */ new Map();
27784
+ const eventBroadcasters = /* @__PURE__ */ new Map();
27617
27785
  const customEventStreams = /* @__PURE__ */ new Map();
27618
27786
  let stopped = false;
27619
- const adaptIterable = (iterable) => stream((emit, end) => {
27787
+ const adaptIterable = (iterable, project) => stream((emit, end) => {
27620
27788
  const iterator = iterable[Symbol.asyncIterator]();
27621
27789
  const pump = (async () => {
27622
27790
  try {
27623
27791
  let result = await iterator.next();
27624
27792
  while (!result.done) {
27625
- await emit(result.value);
27793
+ if (project) {
27794
+ await project(result.value, emit);
27795
+ } else {
27796
+ await emit(result.value);
27797
+ }
27626
27798
  result = await iterator.next();
27627
27799
  }
27628
27800
  end();
@@ -27694,20 +27866,20 @@ async function Spectrum(options) {
27694
27866
  projectConfig,
27695
27867
  store
27696
27868
  });
27697
- const bindSend = async function* () {
27698
- for await (const msg of raw) {
27699
- const tuples = await resolveRecordToMessages(msg, {
27869
+ return adaptIterable(
27870
+ raw,
27871
+ async (record, emit) => {
27872
+ const tuples = await resolveRecordToMessages(record, {
27700
27873
  client,
27701
27874
  config,
27702
27875
  definition,
27703
27876
  store
27704
27877
  });
27705
27878
  for (const tuple of tuples) {
27706
- yield tuple;
27879
+ await emit(tuple);
27707
27880
  }
27708
27881
  }
27709
- };
27710
- return adaptIterable(bindSend());
27882
+ );
27711
27883
  };
27712
27884
  const getOrCreateMessageBroadcast = (state) => {
27713
27885
  if (stopped) {
@@ -27723,6 +27895,24 @@ async function Spectrum(options) {
27723
27895
  }
27724
27896
  return broadcaster;
27725
27897
  };
27898
+ const getOrCreateEventBroadcast = (platform, channel) => {
27899
+ const queue = fusorEventSources.get(platform)?.get(channel);
27900
+ if (!queue) {
27901
+ return;
27902
+ }
27903
+ if (stopped) {
27904
+ throw new Error(
27905
+ `Spectrum instance has been stopped; cannot subscribe to "${platform}" event "${channel}"`
27906
+ );
27907
+ }
27908
+ const key = `${platform}\0${channel}`;
27909
+ let broadcaster = eventBroadcasters.get(key);
27910
+ if (!broadcaster) {
27911
+ broadcaster = broadcast(adaptIterable(queue.iterable));
27912
+ eventBroadcasters.set(key, broadcaster);
27913
+ }
27914
+ return broadcaster;
27915
+ };
27726
27916
  await withSpan(
27727
27917
  "spectrum.init",
27728
27918
  {
@@ -27744,6 +27934,7 @@ async function Spectrum(options) {
27744
27934
  config: userConfig,
27745
27935
  projectId,
27746
27936
  projectSecret,
27937
+ projectConfig,
27747
27938
  store
27748
27939
  })
27749
27940
  );
@@ -27755,7 +27946,13 @@ async function Spectrum(options) {
27755
27946
  };
27756
27947
  platformStates.set(def.name, {
27757
27948
  ...state,
27758
- subscribeMessages: () => getOrCreateMessageBroadcast(state).subscribe()
27949
+ projectConfig,
27950
+ subscribeMessages: () => getOrCreateMessageBroadcast(state).subscribe(),
27951
+ // Fanout subscription to a fusor event channel. Returns undefined for
27952
+ // regular platforms (no per-channel queue) — callers fall back to the
27953
+ // producer path. Resolved lazily, after the fusor bootstrap below has
27954
+ // created the per-(platform, channel) queues.
27955
+ subscribeEvent: (channel) => getOrCreateEventBroadcast(def.name, channel)?.subscribe()
27759
27956
  });
27760
27957
  }
27761
27958
  }
@@ -27778,10 +27975,36 @@ async function Spectrum(options) {
27778
27975
  continue;
27779
27976
  }
27780
27977
  const userMessages = runtime.definition.messages;
27978
+ const declaredEvents = runtime.definition.events ?? {};
27979
+ const eventQueues = /* @__PURE__ */ new Map();
27980
+ for (const channel of Object.keys(declaredEvents)) {
27981
+ eventQueues.set(channel, createAsyncQueue());
27982
+ }
27983
+ fusorEventSources.set(name, eventQueues);
27781
27984
  const handler = {
27782
27985
  verify: client.verify,
27783
- messages: async (ctx) => userMessages(ctx),
27784
- pushMessage: (record) => queue.push(record)
27986
+ // Enrich the transport-level `{ payload, respond }` ctx with the same
27987
+ // runtime context every other platform callback receives, so fusor
27988
+ // handlers can read config/store/projectConfig directly instead of
27989
+ // smuggling state through the payload.
27990
+ messages: async (ctx) => userMessages({
27991
+ ...ctx,
27992
+ config: runtime.config,
27993
+ store: runtime.store,
27994
+ projectConfig: runtime.projectConfig
27995
+ }),
27996
+ pushMessage: (record) => queue.push(record),
27997
+ pushEvent: (channel, data) => {
27998
+ const eventQueue = eventQueues.get(channel);
27999
+ if (!eventQueue) {
28000
+ lifecycleLog.warn(
28001
+ `spectrum: fusorEvent("${channel}", \u2026) names a channel not declared in "${name}".events; dropping`,
28002
+ { platform: name, channel }
28003
+ );
28004
+ return;
28005
+ }
28006
+ eventQueue.push(data);
28007
+ }
27785
28008
  };
27786
28009
  fusorCore.register(client.platform, handler);
27787
28010
  }
@@ -27828,30 +28051,35 @@ async function Spectrum(options) {
27828
28051
  const providerStreams = [];
27829
28052
  for (const state of platformStates.values()) {
27830
28053
  const { client, config, definition, store } = state;
27831
- const producer = definition.events?.[eventName];
27832
- if (!producer) {
27833
- continue;
27834
- }
27835
- const providerEvents = producer({
27836
- client,
27837
- config,
27838
- projectConfig,
27839
- store
27840
- });
27841
- const annotatePlatform = async function* () {
27842
- for await (const value of providerEvents) {
27843
- const annotated = await withSpan(
27844
- "spectrum.event",
27845
- {
27846
- "spectrum.provider": definition.name,
27847
- "spectrum.event.name": eventName
27848
- },
27849
- () => ({ ...value, platform: definition.name })
27850
- );
27851
- yield annotated;
28054
+ let source = state.subscribeEvent?.(eventName);
28055
+ if (!source) {
28056
+ const producer = definition.events?.[eventName];
28057
+ if (typeof producer !== "function") {
28058
+ continue;
27852
28059
  }
27853
- };
27854
- providerStreams.push(adaptIterable(annotatePlatform()));
28060
+ source = producer({ client, config, projectConfig, store });
28061
+ }
28062
+ const providerEvents = source;
28063
+ providerStreams.push(
28064
+ adaptIterable(
28065
+ providerEvents,
28066
+ async (value, emit2) => {
28067
+ const annotated = await withSpan(
28068
+ "spectrum.event",
28069
+ {
28070
+ "spectrum.provider": definition.name,
28071
+ "spectrum.event.name": eventName
28072
+ },
28073
+ // Object payloads are flattened and tagged with `platform`. A
28074
+ // primitive/null payload can't be spread (a string would mangle
28075
+ // into indexed chars, a number/bool would vanish), so wrap it
28076
+ // under `payload` instead.
28077
+ () => typeof value === "object" && value !== null ? { ...value, platform: definition.name } : { platform: definition.name, payload: value }
28078
+ );
28079
+ await emit2(annotated);
28080
+ }
28081
+ )
28082
+ );
27855
28083
  }
27856
28084
  const merged = mergeStreams(providerStreams);
27857
28085
  const pump = (async () => {
@@ -27870,6 +28098,18 @@ async function Spectrum(options) {
27870
28098
  };
27871
28099
  });
27872
28100
  const messagesStream = createMessagesStream();
28101
+ const closeFusorSources = () => {
28102
+ for (const queue of fusorMessageSources.values()) {
28103
+ queue.close();
28104
+ }
28105
+ fusorMessageSources.clear();
28106
+ for (const queues of fusorEventSources.values()) {
28107
+ for (const queue of queues.values()) {
28108
+ queue.close();
28109
+ }
28110
+ }
28111
+ fusorEventSources.clear();
28112
+ };
27873
28113
  const stopOnce = async () => {
27874
28114
  if (stopped) {
27875
28115
  return;
@@ -27884,13 +28124,31 @@ async function Spectrum(options) {
27884
28124
  ...Array.from(
27885
28125
  messageBroadcasters.values(),
27886
28126
  (broadcaster) => broadcaster.close()
28127
+ ),
28128
+ ...Array.from(
28129
+ eventBroadcasters.values(),
28130
+ (broadcaster) => broadcaster.close()
27887
28131
  )
27888
28132
  ];
27889
28133
  process.off("SIGINT", handleSignal);
27890
28134
  process.off("SIGTERM", handleSignal);
27891
28135
  const streamCloseStart = performance.now();
27892
- await Promise.allSettled(streamShutdowns);
27893
- const streamCloseMs = Math.round(performance.now() - streamCloseStart);
28136
+ const streamSettled = Promise.allSettled(streamShutdowns);
28137
+ let streamTimedOut = false;
28138
+ await Promise.race([
28139
+ streamSettled,
28140
+ new Promise((resolve) => {
28141
+ setTimeout(() => {
28142
+ streamTimedOut = true;
28143
+ resolve();
28144
+ }, STREAM_CLOSE_TIMEOUT_MS).unref();
28145
+ })
28146
+ ]);
28147
+ if (streamTimedOut) {
28148
+ lifecycleLog.warn("stream close timed out; proceeding to teardown", {
28149
+ timeoutMs: STREAM_CLOSE_TIMEOUT_MS
28150
+ });
28151
+ }
27894
28152
  let fusorCloseMs = 0;
27895
28153
  if (fusorCore) {
27896
28154
  const fusorCloseStart = performance.now();
@@ -27901,10 +28159,7 @@ async function Spectrum(options) {
27901
28159
  lifecycleLog.warn("fusor core close failed", { error });
27902
28160
  });
27903
28161
  fusorCloseMs = Math.round(performance.now() - fusorCloseStart);
27904
- for (const queue of fusorMessageSources.values()) {
27905
- queue.close();
27906
- }
27907
- fusorMessageSources.clear();
28162
+ closeFusorSources();
27908
28163
  }
27909
28164
  const clientShutdowns = [];
27910
28165
  for (const state of platformStates.values()) {
@@ -27928,8 +28183,11 @@ async function Spectrum(options) {
27928
28183
  const clientCloseStart = performance.now();
27929
28184
  await Promise.allSettled(clientShutdowns);
27930
28185
  const clientCloseMs = Math.round(performance.now() - clientCloseStart);
28186
+ await streamSettled.catch(() => void 0);
28187
+ const streamCloseMs = Math.round(performance.now() - streamCloseStart);
27931
28188
  customEventStreams.clear();
27932
28189
  messageBroadcasters.clear();
28190
+ eventBroadcasters.clear();
27933
28191
  platformStates.clear();
27934
28192
  lifecycleLog.info("Spectrum stopped", {
27935
28193
  providers: providerNames,
@@ -28102,13 +28360,14 @@ export {
28102
28360
  cloud,
28103
28361
  contact,
28104
28362
  custom,
28105
- defineFusorPlatform,
28106
28363
  definePlatform,
28107
28364
  edit,
28108
28365
  fromVCard,
28109
28366
  fusor,
28367
+ fusorEvent,
28110
28368
  group,
28111
28369
  isFusorClient,
28370
+ isFusorEvent,
28112
28371
  mergeStreams,
28113
28372
  option,
28114
28373
  poll,
@@ -28118,6 +28377,7 @@ export {
28118
28377
  resolveContents,
28119
28378
  richlink,
28120
28379
  stream,
28380
+ streamText,
28121
28381
  text,
28122
28382
  toVCard,
28123
28383
  typing,
@@ -11,6 +11,12 @@
11
11
  "path": "spectrum-ts/providers/slack",
12
12
  "label": "Slack"
13
13
  },
14
+ {
15
+ "key": "telegram",
16
+ "import": "telegram",
17
+ "path": "spectrum-ts/providers/telegram",
18
+ "label": "telegram"
19
+ },
14
20
  {
15
21
  "key": "terminal",
16
22
  "import": "terminal",