spectrum-ts 0.0.1 → 0.1.2
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/chunk-3TBRO2J7.js +58 -0
- package/dist/chunk-UZ2CXPOD.js +175 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +348 -0
- package/dist/providers/imessage/index.d.ts +82 -0
- package/dist/providers/imessage/index.js +418 -0
- package/dist/providers/terminal/index.d.ts +35 -0
- package/dist/providers/terminal/index.js +61 -0
- package/dist/stream-DGy4geUK.d.ts +8 -0
- package/dist/types-eXHZpal1.d.ts +261 -0
- package/package.json +48 -3
- package/src/index.ts +26 -0
- package/src/platform/define.ts +309 -0
- package/src/platform/types.ts +438 -0
- package/src/providers/imessage/auth.ts +161 -0
- package/src/providers/imessage/index.ts +153 -0
- package/src/providers/imessage/local.ts +55 -0
- package/src/providers/imessage/remote.ts +157 -0
- package/src/providers/imessage/types.ts +31 -0
- package/src/providers/terminal/index.ts +66 -0
- package/src/spectrum.ts +390 -0
- package/src/types/content.ts +85 -0
- package/src/types/message.ts +18 -0
- package/src/types/space.ts +10 -0
- package/src/types/user.ts +4 -0
- package/src/utils/cloud.ts +1 -0
- package/src/utils/stream.ts +71 -0
- package/index.js +0 -1
package/src/spectrum.ts
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import z from "zod";
|
|
2
|
+
import type {
|
|
3
|
+
AnyPlatformDef,
|
|
4
|
+
CustomEventStreams,
|
|
5
|
+
PlatformProviderConfig,
|
|
6
|
+
SpectrumLike,
|
|
7
|
+
} from "./platform/types";
|
|
8
|
+
import type { Content, ContentBuilder } from "./types/content";
|
|
9
|
+
import type { Message } from "./types/message";
|
|
10
|
+
import type { Space } from "./types/space";
|
|
11
|
+
import { type ManagedStream, mergeStreams, stream } from "./utils/stream";
|
|
12
|
+
|
|
13
|
+
type ProviderMessageRecord = {
|
|
14
|
+
id: string;
|
|
15
|
+
content: Content[];
|
|
16
|
+
sender: { id: string } & Record<string, unknown>;
|
|
17
|
+
space: { id: string } & Record<string, unknown>;
|
|
18
|
+
timestamp?: Date;
|
|
19
|
+
} & Record<string, unknown>;
|
|
20
|
+
|
|
21
|
+
const providerMessageCoreKeys = new Set([
|
|
22
|
+
"content",
|
|
23
|
+
"id",
|
|
24
|
+
"sender",
|
|
25
|
+
"space",
|
|
26
|
+
"timestamp",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// SpectrumInstance — the typed return of Spectrum()
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
export type SpectrumInstance<
|
|
34
|
+
Providers extends PlatformProviderConfig[] = PlatformProviderConfig[],
|
|
35
|
+
> = SpectrumLike<Providers> &
|
|
36
|
+
CustomEventStreams<Providers> & {
|
|
37
|
+
readonly messages: AsyncIterable<[Space, Message]>;
|
|
38
|
+
stop(): Promise<void>;
|
|
39
|
+
send(
|
|
40
|
+
space: Space,
|
|
41
|
+
...content: [ContentBuilder, ...ContentBuilder[]]
|
|
42
|
+
): Promise<void>;
|
|
43
|
+
responding<T>(space: Space, fn: () => T | Promise<T>): Promise<T>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Config validation
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
const spectrumConfigSchema = z.union([
|
|
51
|
+
z.object({
|
|
52
|
+
projectId: z.string().min(1),
|
|
53
|
+
projectSecret: z.string().min(1),
|
|
54
|
+
providers: z.array(z.custom<PlatformProviderConfig>()),
|
|
55
|
+
}),
|
|
56
|
+
z.object({
|
|
57
|
+
projectId: z.undefined().optional(),
|
|
58
|
+
projectSecret: z.undefined().optional(),
|
|
59
|
+
providers: z.array(z.custom<PlatformProviderConfig>()),
|
|
60
|
+
}),
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Spectrum() factory
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
export async function Spectrum<
|
|
68
|
+
const Providers extends PlatformProviderConfig[],
|
|
69
|
+
>(
|
|
70
|
+
options:
|
|
71
|
+
| {
|
|
72
|
+
projectId: string;
|
|
73
|
+
projectSecret: string;
|
|
74
|
+
providers: [...Providers];
|
|
75
|
+
}
|
|
76
|
+
| {
|
|
77
|
+
projectId?: never;
|
|
78
|
+
projectSecret?: never;
|
|
79
|
+
providers: [...Providers];
|
|
80
|
+
}
|
|
81
|
+
): Promise<SpectrumInstance<Providers>> {
|
|
82
|
+
spectrumConfigSchema.parse(options);
|
|
83
|
+
|
|
84
|
+
const { projectId, projectSecret, providers } = options;
|
|
85
|
+
|
|
86
|
+
const platformStates = new Map<
|
|
87
|
+
string,
|
|
88
|
+
{ client: unknown; config: unknown; definition: AnyPlatformDef }
|
|
89
|
+
>();
|
|
90
|
+
|
|
91
|
+
// Custom event streams keyed by event name
|
|
92
|
+
const customEventStreams = new Map<string, ManagedStream<unknown>>();
|
|
93
|
+
|
|
94
|
+
let stopped = false;
|
|
95
|
+
|
|
96
|
+
// Initialize all provider clients eagerly
|
|
97
|
+
for (const provider of providers) {
|
|
98
|
+
const providerConfig = provider as PlatformProviderConfig;
|
|
99
|
+
const def = providerConfig.__definition;
|
|
100
|
+
const userConfig = def.config.parse(providerConfig.config);
|
|
101
|
+
|
|
102
|
+
const client = await def.lifecycle.createClient({
|
|
103
|
+
config: userConfig,
|
|
104
|
+
projectId,
|
|
105
|
+
projectSecret,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
platformStates.set(def.name, {
|
|
109
|
+
client,
|
|
110
|
+
config: userConfig,
|
|
111
|
+
definition: def,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const adaptIterable = <T>(iterable: AsyncIterable<T>): ManagedStream<T> => {
|
|
116
|
+
return stream<T>((emit, end) => {
|
|
117
|
+
const iterator = iterable[Symbol.asyncIterator]();
|
|
118
|
+
|
|
119
|
+
(async () => {
|
|
120
|
+
try {
|
|
121
|
+
let result = await iterator.next();
|
|
122
|
+
while (!result.done) {
|
|
123
|
+
emit(result.value);
|
|
124
|
+
result = await iterator.next();
|
|
125
|
+
}
|
|
126
|
+
end();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
end(error);
|
|
129
|
+
}
|
|
130
|
+
})();
|
|
131
|
+
|
|
132
|
+
return async () => {
|
|
133
|
+
await iterator.return?.();
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const createProviderMessagesStream = (state: {
|
|
139
|
+
client: unknown;
|
|
140
|
+
config: unknown;
|
|
141
|
+
definition: AnyPlatformDef;
|
|
142
|
+
}): ManagedStream<[Space, Message]> => {
|
|
143
|
+
const { client, config, definition } = state;
|
|
144
|
+
const raw = definition.events.messages({
|
|
145
|
+
client,
|
|
146
|
+
config,
|
|
147
|
+
}) as AsyncIterable<ProviderMessageRecord>;
|
|
148
|
+
|
|
149
|
+
const bindSend = async function* (): AsyncIterable<[Space, Message]> {
|
|
150
|
+
for await (const msg of raw) {
|
|
151
|
+
const extraEntries = Object.entries(msg).filter(
|
|
152
|
+
([key]) => !providerMessageCoreKeys.has(key)
|
|
153
|
+
);
|
|
154
|
+
const extra = Object.fromEntries(extraEntries);
|
|
155
|
+
const parsedExtra = definition.message?.schema
|
|
156
|
+
? definition.message.schema.parse(extra)
|
|
157
|
+
: {};
|
|
158
|
+
const spaceRef = {
|
|
159
|
+
...msg.space,
|
|
160
|
+
__platform: definition.name,
|
|
161
|
+
};
|
|
162
|
+
const typingCtx = { space: spaceRef, client, config };
|
|
163
|
+
const space = {
|
|
164
|
+
...spaceRef,
|
|
165
|
+
send: async (...content: [ContentBuilder, ...ContentBuilder[]]) => {
|
|
166
|
+
const resolved = await Promise.all(content.map((c) => c.build()));
|
|
167
|
+
await definition.actions.send({
|
|
168
|
+
...typingCtx,
|
|
169
|
+
content: resolved,
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
startTyping: async () => {
|
|
173
|
+
await definition.actions.startTyping?.(typingCtx);
|
|
174
|
+
},
|
|
175
|
+
stopTyping: async () => {
|
|
176
|
+
await definition.actions.stopTyping?.(typingCtx);
|
|
177
|
+
},
|
|
178
|
+
responding: async <T>(fn: () => T | Promise<T>): Promise<T> => {
|
|
179
|
+
await definition.actions.startTyping?.(typingCtx);
|
|
180
|
+
try {
|
|
181
|
+
return await fn();
|
|
182
|
+
} finally {
|
|
183
|
+
await definition.actions.stopTyping?.(typingCtx).catch(() => {});
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
const normalizedMessage = {
|
|
188
|
+
...parsedExtra,
|
|
189
|
+
id: msg.id,
|
|
190
|
+
content: msg.content,
|
|
191
|
+
platform: definition.name,
|
|
192
|
+
react: async (reaction: string): Promise<void> => {
|
|
193
|
+
if (!definition.actions.reactToMessage) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
await definition.actions.reactToMessage({
|
|
197
|
+
space: spaceRef,
|
|
198
|
+
messageId: msg.id,
|
|
199
|
+
reaction,
|
|
200
|
+
client,
|
|
201
|
+
config,
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
reply: async (
|
|
205
|
+
...content: [ContentBuilder, ...ContentBuilder[]]
|
|
206
|
+
): Promise<void> => {
|
|
207
|
+
if (!definition.actions.replyToMessage) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const resolved = await Promise.all(content.map((c) => c.build()));
|
|
211
|
+
await definition.actions.replyToMessage({
|
|
212
|
+
space: spaceRef,
|
|
213
|
+
messageId: msg.id,
|
|
214
|
+
content: resolved,
|
|
215
|
+
client,
|
|
216
|
+
config,
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
sender: {
|
|
220
|
+
...msg.sender,
|
|
221
|
+
__platform: definition.name,
|
|
222
|
+
},
|
|
223
|
+
space,
|
|
224
|
+
timestamp: msg.timestamp ?? new Date(),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
yield [space, normalizedMessage];
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return adaptIterable(bindSend());
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const createMessagesStream = (): ManagedStream<[Space, Message]> => {
|
|
235
|
+
return stream<[Space, Message]>(async (emit, end) => {
|
|
236
|
+
const merged = mergeStreams(
|
|
237
|
+
Array.from(platformStates.values(), createProviderMessagesStream)
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
(async () => {
|
|
241
|
+
try {
|
|
242
|
+
for await (const value of merged) {
|
|
243
|
+
emit(value);
|
|
244
|
+
}
|
|
245
|
+
end();
|
|
246
|
+
} catch (error) {
|
|
247
|
+
end(error);
|
|
248
|
+
}
|
|
249
|
+
})();
|
|
250
|
+
|
|
251
|
+
return async () => {
|
|
252
|
+
await merged.close();
|
|
253
|
+
};
|
|
254
|
+
});
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const createCustomEventStream = (
|
|
258
|
+
eventName: string
|
|
259
|
+
): ManagedStream<unknown> => {
|
|
260
|
+
return stream<unknown>(async (emit, end) => {
|
|
261
|
+
const providerStreams = Array.from(platformStates.values(), (state) => {
|
|
262
|
+
const { client, config, definition } = state;
|
|
263
|
+
const producer = definition.events[eventName] as
|
|
264
|
+
| ((ctx: {
|
|
265
|
+
client: unknown;
|
|
266
|
+
config: unknown;
|
|
267
|
+
}) => AsyncIterable<unknown>)
|
|
268
|
+
| undefined;
|
|
269
|
+
if (!producer) {
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const providerEvents = producer({ client, config });
|
|
274
|
+
const annotatePlatform = async function* (): AsyncIterable<unknown> {
|
|
275
|
+
for await (const value of providerEvents) {
|
|
276
|
+
yield { ...(value as object), platform: definition.name };
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
return adaptIterable(annotatePlatform());
|
|
281
|
+
}).filter(
|
|
282
|
+
(value): value is ManagedStream<unknown> => value !== undefined
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
const merged = mergeStreams(providerStreams);
|
|
286
|
+
|
|
287
|
+
(async () => {
|
|
288
|
+
try {
|
|
289
|
+
for await (const value of merged) {
|
|
290
|
+
emit(value);
|
|
291
|
+
}
|
|
292
|
+
end();
|
|
293
|
+
} catch (error) {
|
|
294
|
+
end(error);
|
|
295
|
+
}
|
|
296
|
+
})();
|
|
297
|
+
|
|
298
|
+
return async () => {
|
|
299
|
+
await merged.close();
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const messagesStream = createMessagesStream();
|
|
305
|
+
|
|
306
|
+
const stopOnce = async () => {
|
|
307
|
+
if (stopped) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
stopped = true;
|
|
311
|
+
|
|
312
|
+
const streamShutdowns = [
|
|
313
|
+
messagesStream.close(),
|
|
314
|
+
...Array.from(customEventStreams.values(), (eventStream) =>
|
|
315
|
+
eventStream.close()
|
|
316
|
+
),
|
|
317
|
+
];
|
|
318
|
+
|
|
319
|
+
process.off("SIGINT", handleSignal);
|
|
320
|
+
process.off("SIGTERM", handleSignal);
|
|
321
|
+
|
|
322
|
+
await Promise.allSettled(streamShutdowns);
|
|
323
|
+
const clientShutdowns = Array.from(platformStates.values(), (state) =>
|
|
324
|
+
state.definition.lifecycle.destroyClient({
|
|
325
|
+
client: state.client,
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
await Promise.allSettled(clientShutdowns);
|
|
329
|
+
customEventStreams.clear();
|
|
330
|
+
platformStates.clear();
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
const handleSignal = () => {
|
|
334
|
+
setTimeout(() => process.exit(1), 3000).unref();
|
|
335
|
+
stopOnce()
|
|
336
|
+
.then(() => process.exit(0))
|
|
337
|
+
.catch(() => process.exit(1));
|
|
338
|
+
};
|
|
339
|
+
process.on("SIGINT", handleSignal);
|
|
340
|
+
process.on("SIGTERM", handleSignal);
|
|
341
|
+
|
|
342
|
+
const messages = messagesStream as AsyncIterable<[Space, Message]>;
|
|
343
|
+
|
|
344
|
+
// Proxy for flat custom event access (app.typing, app.readReceipt, etc.)
|
|
345
|
+
const customEventProxy = new Proxy(
|
|
346
|
+
{} as Record<string, AsyncIterable<unknown>>,
|
|
347
|
+
{
|
|
348
|
+
get(_target, prop: string) {
|
|
349
|
+
let eventStream = customEventStreams.get(prop);
|
|
350
|
+
if (!eventStream) {
|
|
351
|
+
eventStream = createCustomEventStream(prop);
|
|
352
|
+
customEventStreams.set(prop, eventStream);
|
|
353
|
+
}
|
|
354
|
+
return eventStream;
|
|
355
|
+
},
|
|
356
|
+
}
|
|
357
|
+
);
|
|
358
|
+
|
|
359
|
+
const base = {
|
|
360
|
+
__providers: providers,
|
|
361
|
+
__internal: { platforms: platformStates },
|
|
362
|
+
messages,
|
|
363
|
+
stop: stopOnce,
|
|
364
|
+
send: async (
|
|
365
|
+
space: Space,
|
|
366
|
+
...content: [ContentBuilder, ...ContentBuilder[]]
|
|
367
|
+
) => {
|
|
368
|
+
await space.send(...content);
|
|
369
|
+
},
|
|
370
|
+
responding: async <T>(
|
|
371
|
+
space: Space,
|
|
372
|
+
fn: () => T | Promise<T>
|
|
373
|
+
): Promise<T> => {
|
|
374
|
+
return space.responding(fn);
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// Merge base instance with custom event proxy
|
|
379
|
+
return new Proxy(base, {
|
|
380
|
+
get(target, prop, receiver) {
|
|
381
|
+
if (prop in target) {
|
|
382
|
+
return Reflect.get(target, prop, receiver);
|
|
383
|
+
}
|
|
384
|
+
if (typeof prop === "string") {
|
|
385
|
+
return customEventProxy[prop];
|
|
386
|
+
}
|
|
387
|
+
return undefined;
|
|
388
|
+
},
|
|
389
|
+
}) as SpectrumInstance<Providers>;
|
|
390
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename } from "node:path";
|
|
3
|
+
import { lookup as lookupMimeType } from "mime-types";
|
|
4
|
+
import type { NonEmptyString } from "type-fest";
|
|
5
|
+
import z from "zod";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_ATTACHMENT_NAME = "attachment";
|
|
8
|
+
|
|
9
|
+
const contentSchema = z.discriminatedUnion("type", [
|
|
10
|
+
z.object({
|
|
11
|
+
type: z.literal("plain_text"),
|
|
12
|
+
text: z.string().nonempty(),
|
|
13
|
+
}),
|
|
14
|
+
z.object({
|
|
15
|
+
type: z.literal("custom"),
|
|
16
|
+
raw: z.json(),
|
|
17
|
+
}),
|
|
18
|
+
z.object({
|
|
19
|
+
type: z.literal("attachment"),
|
|
20
|
+
data: z.instanceof(Buffer),
|
|
21
|
+
mimeType: z.string().nonempty(),
|
|
22
|
+
name: z.string().nonempty(),
|
|
23
|
+
}),
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
export type Content = z.infer<typeof contentSchema>;
|
|
27
|
+
|
|
28
|
+
export interface ContentBuilder {
|
|
29
|
+
build(): Promise<Content>;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function text<T extends string>(
|
|
33
|
+
text: NonEmptyString<T>
|
|
34
|
+
): ContentBuilder {
|
|
35
|
+
return {
|
|
36
|
+
build: (): Promise<Content> =>
|
|
37
|
+
Promise.resolve({ type: "plain_text", text }),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function custom(
|
|
42
|
+
raw: z.infer<ReturnType<typeof z.json>>
|
|
43
|
+
): ContentBuilder {
|
|
44
|
+
return {
|
|
45
|
+
build: (): Promise<Content> => Promise.resolve({ type: "custom", raw }),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const resolveAttachmentName = (input: string | Buffer, name?: string): string =>
|
|
50
|
+
name ||
|
|
51
|
+
(typeof input === "string" ? basename(input) : DEFAULT_ATTACHMENT_NAME);
|
|
52
|
+
|
|
53
|
+
const resolveAttachmentMimeType = (name: string, mimeType?: string): string => {
|
|
54
|
+
if (mimeType) {
|
|
55
|
+
return mimeType;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const resolvedMimeType = lookupMimeType(name);
|
|
59
|
+
if (!resolvedMimeType) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Unable to resolve MIME type for attachment "${name}". Pass options.mimeType explicitly.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return resolvedMimeType;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export function attachment(
|
|
69
|
+
input: string | Buffer,
|
|
70
|
+
options?: { mimeType?: string; name?: string }
|
|
71
|
+
): ContentBuilder {
|
|
72
|
+
return {
|
|
73
|
+
build: async (): Promise<Content> => {
|
|
74
|
+
const data = typeof input === "string" ? await readFile(input) : input;
|
|
75
|
+
const name = resolveAttachmentName(input, options?.name);
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
data,
|
|
79
|
+
mimeType: resolveAttachmentMimeType(name, options?.mimeType),
|
|
80
|
+
name,
|
|
81
|
+
type: "attachment",
|
|
82
|
+
};
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Content, ContentBuilder } from "./content";
|
|
2
|
+
import type { Space } from "./space";
|
|
3
|
+
import type { User } from "./user";
|
|
4
|
+
|
|
5
|
+
export interface Message<
|
|
6
|
+
TPlatform extends string = string,
|
|
7
|
+
TSender extends User = User,
|
|
8
|
+
TSpace extends Space = Space,
|
|
9
|
+
> {
|
|
10
|
+
content: Content[];
|
|
11
|
+
readonly id: string;
|
|
12
|
+
platform: TPlatform;
|
|
13
|
+
react(reaction: string): Promise<void>;
|
|
14
|
+
reply(...content: [ContentBuilder, ...ContentBuilder[]]): Promise<void>;
|
|
15
|
+
sender: TSender;
|
|
16
|
+
space: TSpace;
|
|
17
|
+
timestamp: Date;
|
|
18
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ContentBuilder } from "./content";
|
|
2
|
+
|
|
3
|
+
export interface Space<_Def = unknown> {
|
|
4
|
+
readonly __platform: string;
|
|
5
|
+
readonly id: string;
|
|
6
|
+
responding<T>(fn: () => T | Promise<T>): Promise<T>;
|
|
7
|
+
send(...content: [ContentBuilder, ...ContentBuilder[]]): Promise<void>;
|
|
8
|
+
startTyping(): Promise<void>;
|
|
9
|
+
stopTyping(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SPECTRUM_CLOUD_URL = `https://${process.env.SPECTRUM_CLOUD_URL ?? "spectrum-cloud.photon.codes"}`;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Repeater } from "@repeaterjs/repeater";
|
|
2
|
+
|
|
3
|
+
export interface ManagedStream<T> extends AsyncIterable<T> {
|
|
4
|
+
close(): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
type StreamCleanup = void | (() => void | Promise<void>);
|
|
8
|
+
|
|
9
|
+
export function stream<T>(
|
|
10
|
+
setup: (
|
|
11
|
+
emit: (value: T) => void,
|
|
12
|
+
end: (error?: unknown) => void
|
|
13
|
+
) => StreamCleanup | Promise<StreamCleanup>
|
|
14
|
+
): ManagedStream<T> {
|
|
15
|
+
const repeater = new Repeater<T>(async (push, stop) => {
|
|
16
|
+
const emit = (value: T) => {
|
|
17
|
+
Promise.resolve(push(value)).catch((error) => {
|
|
18
|
+
stop(error);
|
|
19
|
+
return undefined;
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
const end = (error?: unknown) => {
|
|
23
|
+
stop(error);
|
|
24
|
+
};
|
|
25
|
+
const cleanup = await setup(emit, end);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
await stop;
|
|
29
|
+
} finally {
|
|
30
|
+
await cleanup?.();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return Object.assign(repeater, {
|
|
35
|
+
close: async () => {
|
|
36
|
+
await repeater.return(undefined);
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function mergeStreams<T>(
|
|
42
|
+
streams: readonly ManagedStream<T>[]
|
|
43
|
+
): ManagedStream<T> {
|
|
44
|
+
return stream<T>((emit, end) => {
|
|
45
|
+
if (streams.length === 0) {
|
|
46
|
+
end();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let openStreams = streams.length;
|
|
51
|
+
const workers = streams.map(async (source) => {
|
|
52
|
+
try {
|
|
53
|
+
for await (const value of source) {
|
|
54
|
+
emit(value);
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
end(error);
|
|
58
|
+
} finally {
|
|
59
|
+
openStreams -= 1;
|
|
60
|
+
if (openStreams === 0) {
|
|
61
|
+
end();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return async () => {
|
|
67
|
+
await Promise.allSettled(streams.map((source) => source.close()));
|
|
68
|
+
await Promise.allSettled(workers);
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
}
|
package/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
// spectrum-ts
|