@bunworks/inngest-realtime 0.1.0 → 0.1.5

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 (101) hide show
  1. package/CHANGELOG.md +6 -6
  2. package/README.md +1 -1
  3. package/_virtual/_rolldown/runtime.cjs +29 -0
  4. package/api.cjs +45 -0
  5. package/api.cjs.map +1 -0
  6. package/api.mjs +44 -0
  7. package/api.mjs.map +1 -0
  8. package/channel.cjs +67 -0
  9. package/channel.cjs.map +1 -0
  10. package/channel.d.cts +19 -0
  11. package/channel.d.cts.map +1 -0
  12. package/channel.d.mts +19 -0
  13. package/channel.d.mts.map +1 -0
  14. package/channel.mjs +66 -0
  15. package/channel.mjs.map +1 -0
  16. package/env.cjs +72 -0
  17. package/env.cjs.map +1 -0
  18. package/env.mjs +71 -0
  19. package/env.mjs.map +1 -0
  20. package/hooks.cjs +147 -0
  21. package/hooks.cjs.map +1 -0
  22. package/hooks.d.cts +70 -0
  23. package/hooks.d.cts.map +1 -0
  24. package/hooks.d.mts +70 -0
  25. package/hooks.d.mts.map +1 -0
  26. package/hooks.mjs +144 -0
  27. package/hooks.mjs.map +1 -0
  28. package/index.cjs +19 -0
  29. package/index.d.cts +5 -0
  30. package/index.d.mts +6 -0
  31. package/index.mjs +7 -0
  32. package/middleware.cjs +41 -0
  33. package/middleware.cjs.map +1 -0
  34. package/middleware.d.cts +29 -0
  35. package/middleware.d.cts.map +1 -0
  36. package/middleware.d.mts +29 -0
  37. package/middleware.d.mts.map +1 -0
  38. package/middleware.mjs +34 -0
  39. package/middleware.mjs.map +1 -0
  40. package/package.json +6 -5
  41. package/subscribe/StreamFanout.cjs +47 -0
  42. package/subscribe/StreamFanout.cjs.map +1 -0
  43. package/subscribe/StreamFanout.mjs +46 -0
  44. package/subscribe/StreamFanout.mjs.map +1 -0
  45. package/subscribe/TokenSubscription.cjs +438 -0
  46. package/subscribe/TokenSubscription.cjs.map +1 -0
  47. package/subscribe/TokenSubscription.mjs +436 -0
  48. package/subscribe/TokenSubscription.mjs.map +1 -0
  49. package/subscribe/helpers.cjs +41 -0
  50. package/subscribe/helpers.cjs.map +1 -0
  51. package/subscribe/helpers.d.cts +64 -0
  52. package/subscribe/helpers.d.cts.map +1 -0
  53. package/subscribe/helpers.d.mts +64 -0
  54. package/subscribe/helpers.d.mts.map +1 -0
  55. package/subscribe/helpers.mjs +40 -0
  56. package/subscribe/helpers.mjs.map +1 -0
  57. package/subscribe/index.cjs +1 -0
  58. package/subscribe/index.d.mts +1 -0
  59. package/subscribe/index.mjs +3 -0
  60. package/topic.cjs +30 -0
  61. package/topic.cjs.map +1 -0
  62. package/topic.d.cts +19 -0
  63. package/topic.d.cts.map +1 -0
  64. package/topic.d.mts +19 -0
  65. package/topic.d.mts.map +1 -0
  66. package/topic.mjs +28 -0
  67. package/topic.mjs.map +1 -0
  68. package/types.cjs +45 -0
  69. package/types.cjs.map +1 -0
  70. package/types.d.cts +241 -0
  71. package/types.d.cts.map +1 -0
  72. package/types.d.mts +241 -0
  73. package/types.d.mts.map +1 -0
  74. package/types.mjs +39 -0
  75. package/types.mjs.map +1 -0
  76. package/util.cjs +72 -0
  77. package/util.cjs.map +1 -0
  78. package/util.mjs +69 -0
  79. package/util.mjs.map +1 -0
  80. package/CLAUDE.md +0 -69
  81. package/bun.lock +0 -527
  82. package/eslint.config.mjs +0 -19
  83. package/src/api.ts +0 -83
  84. package/src/channel.ts +0 -122
  85. package/src/env.d.ts +0 -15
  86. package/src/env.ts +0 -152
  87. package/src/hooks.ts +0 -256
  88. package/src/index.test.ts +0 -840
  89. package/src/index.ts +0 -4
  90. package/src/middleware.ts +0 -68
  91. package/src/subscribe/StreamFanout.ts +0 -82
  92. package/src/subscribe/TokenSubscription.ts +0 -524
  93. package/src/subscribe/helpers.ts +0 -153
  94. package/src/subscribe/index.ts +0 -1
  95. package/src/topic.ts +0 -51
  96. package/src/types.ts +0 -499
  97. package/src/util.ts +0 -104
  98. package/tsconfig.build.json +0 -7
  99. package/tsconfig.json +0 -114
  100. package/tsdown.config.ts +0 -18
  101. package/vitest.config.ts +0 -22
package/src/index.ts DELETED
@@ -1,4 +0,0 @@
1
- export * from './channel';
2
- export * from './subscribe';
3
- export * from './topic';
4
- export * from './types';
package/src/middleware.ts DELETED
@@ -1,68 +0,0 @@
1
- import type { Realtime } from "./types";
2
-
3
- export interface BunworksMiddleware {
4
- name: string;
5
- init: (config: { client: any }) => {
6
- onFunctionRun: (config: { ctx: { runId: string } }) => {
7
- transformInput: (config: { ctx: { step: any } }) => {
8
- ctx: {
9
- publish: Realtime.PublishFn;
10
- };
11
- };
12
- };
13
- };
14
- }
15
-
16
- export const realtimeMiddleware = () => {
17
- return {
18
- name: "publish",
19
- init({ client }: { client: any }) {
20
- return {
21
- onFunctionRun({ ctx: { runId } }: { ctx: { runId: string } }) {
22
- return {
23
- transformInput({ ctx: { step } }: { ctx: { step: any } }) {
24
- const publish: Realtime.PublishFn = async (input) => {
25
- const { topic, channel, data } = await input;
26
-
27
- const publishOpts = {
28
- topics: [topic],
29
- channel,
30
- runId,
31
- };
32
-
33
- const action = async () => {
34
- const result = await client["api"].publish(
35
- publishOpts,
36
- data,
37
- );
38
-
39
- if (!result.ok) {
40
- throw new Error(
41
- `Не удалось опубликовать событие: ${result.error?.error}`,
42
- );
43
- }
44
- };
45
-
46
- return step.run(`publish:${publishOpts.channel}`, action).then(() => {
47
- return data;
48
- });
49
- };
50
-
51
- return {
52
- ctx: {
53
- /**
54
- * Функция для публикации сообщений в realtime канал
55
- */
56
- publish,
57
- },
58
- };
59
- },
60
- };
61
- },
62
- };
63
- },
64
- } as BunworksMiddleware;
65
- };
66
-
67
- // Re-export types from here, as this is used as a separate entrypoint now
68
- export * from "./types";
@@ -1,82 +0,0 @@
1
- /**
2
- * TODO
3
- */
4
- export class StreamFanout<TInput = unknown> {
5
- #writers = new Set<WritableStreamDefaultWriter<TInput>>();
6
-
7
- /**
8
- * TODO
9
- */
10
- createStream<TOutput = TInput>(
11
- /**
12
- * TODO
13
- */
14
- transform?: (
15
- /**
16
- * TODO
17
- */
18
- chunk: TInput,
19
- ) => TOutput,
20
- ): ReadableStream<TOutput> {
21
- const { readable, writable } = new TransformStream<TInput, TOutput>({
22
- transform: (chunk, controller) => {
23
- controller.enqueue(
24
- transform ? transform(chunk) : (chunk as unknown as TOutput),
25
- );
26
- },
27
- });
28
-
29
- const writer = writable.getWriter();
30
- this.#writers.add(writer);
31
-
32
- // Eagerly remove the writer is the stream is closed
33
- writer.closed
34
- .catch(() => {}) // Suppress unhandled promise rejection to avoid noisy logs
35
- .finally(() => {
36
- this.#writers.delete(writer);
37
- });
38
-
39
- return readable;
40
- }
41
-
42
- /**
43
- * TODO
44
- */
45
- write(
46
- /**
47
- * TODO
48
- */
49
- chunk: TInput,
50
- ) {
51
- for (const writer of this.#writers) {
52
- writer.ready
53
- .then(() => writer.write(chunk))
54
- // Dereference the writer if we fail, as this means it's closed
55
- .catch(() => this.#writers.delete(writer));
56
- }
57
- }
58
-
59
- /**
60
- * TODO
61
- */
62
- close() {
63
- for (const writer of this.#writers) {
64
- try {
65
- writer.close();
66
- } catch {
67
- // Ignore errors, as we are closing the stream and the writer may
68
- // already be closed, especially if the stream is closed before the
69
- // writer is closed or if the stream is cancelled.
70
- }
71
- }
72
-
73
- this.#writers.clear();
74
- }
75
-
76
- /**
77
- * TODO
78
- */
79
- size() {
80
- return this.#writers.size;
81
- }
82
- }
@@ -1,524 +0,0 @@
1
- import debug from "debug";
2
- import { api } from "../api";
3
- import { getEnvVar } from "../env";
4
- import { topic } from "../topic";
5
- import { Realtime } from "../types";
6
- import { createDeferredPromise, parseAsBoolean } from "../util";
7
- import { StreamFanout } from "./StreamFanout";
8
-
9
- /**
10
- * Подписка на realtime канал через WebSocket
11
- */
12
- export class TokenSubscription {
13
- #apiBaseUrl?: string;
14
- #channelId: string;
15
- #debug = debug("bunworks:realtime");
16
- #encoder = new TextEncoder();
17
- #fanout = new StreamFanout<Realtime.Message>();
18
- #running = false;
19
- #topics: Map<string, Realtime.Topic.Definition>;
20
- #ws: WebSocket | null = null;
21
- #signingKey: string | undefined;
22
- #signingKeyFallback: string | undefined;
23
-
24
- /**
25
- * Карта stream ID к их потокам и контроллерам
26
- */
27
- #chunkStreams = new Map<
28
- string,
29
- { stream: ReadableStream; controller: ReadableStreamDefaultController }
30
- >();
31
-
32
- constructor(
33
- /**
34
- * Токен подписки
35
- */
36
- public token: Realtime.Subscribe.Token,
37
- apiBaseUrl: string | undefined,
38
- signingKey: string | undefined,
39
- signingKeyFallback: string | undefined,
40
- ) {
41
- this.#apiBaseUrl = apiBaseUrl;
42
- this.#signingKey = signingKey;
43
- this.#signingKeyFallback = signingKeyFallback;
44
-
45
- if (typeof token.channel === "string") {
46
- this.#channelId = token.channel;
47
-
48
- this.#topics = this.token.topics.reduce<
49
- Map<string, Realtime.Topic.Definition>
50
- >((acc, name) => {
51
- acc.set(name, topic(name));
52
-
53
- return acc;
54
- }, new Map<string, Realtime.Topic.Definition>());
55
- } else {
56
- this.#channelId = token.channel.name;
57
-
58
- this.#topics = this.token.topics.reduce<
59
- Map<string, Realtime.Topic.Definition>
60
- >((acc, name) => {
61
- acc.set(name, token.channel.topics[name] ?? topic(name));
62
-
63
- return acc;
64
- }, new Map<string, Realtime.Topic.Definition>());
65
- }
66
- }
67
-
68
- private async getWsUrl(token: string): Promise<URL> {
69
- let url: URL;
70
- const path = "/v1/realtime/connect";
71
- const devEnvVar = getEnvVar("BUNWORKS_DEV");
72
-
73
- if (this.#apiBaseUrl) {
74
- url = new URL(path, this.#apiBaseUrl);
75
- } else if (devEnvVar) {
76
- try {
77
- const devUrl = new URL(devEnvVar);
78
- url = new URL(path, devUrl);
79
- } catch {
80
- if (parseAsBoolean(devEnvVar)) {
81
- url = new URL(path, "http://localhost:8288/");
82
- } else {
83
- url = new URL(path, "https://api.bunworks.com/");
84
- }
85
- }
86
- } else {
87
- url = new URL(
88
- path,
89
- getEnvVar("NODE_ENV") === "production"
90
- ? "https://api.bunworks.com/"
91
- : "http://localhost:8288/",
92
- );
93
- }
94
-
95
- url.protocol = url.protocol === "http:" ? "ws:" : "wss:";
96
- url.searchParams.set("token", token);
97
-
98
- return url;
99
- }
100
-
101
- /**
102
- * Установка WebSocket соединения
103
- */
104
- public async connect() {
105
- this.#debug(
106
- `Establishing connection to channel "${
107
- this.#channelId
108
- }" with topics ${JSON.stringify([...this.#topics.keys()])}...`,
109
- );
110
-
111
- if (typeof WebSocket === "undefined") {
112
- throw new Error("WebSocket не поддерживается в текущем окружении");
113
- }
114
-
115
- let key = this.token.key;
116
- if (!key) {
117
- this.#debug(
118
- "No subscription token key passed; attempting to retrieve one automatically...",
119
- );
120
-
121
- key = (
122
- await this.lazilyGetSubscriptionToken({
123
- ...this.token,
124
- signingKey: this.#signingKey,
125
- signingKeyFallback: this.#signingKeyFallback,
126
- })
127
- ).key;
128
-
129
- if (!key) {
130
- throw new Error(
131
- "Не передан ключ токена подписки и не удалось получить его автоматически",
132
- );
133
- }
134
- }
135
-
136
- const ret = createDeferredPromise<void>();
137
-
138
- try {
139
- this.#ws = new WebSocket(await this.getWsUrl(key));
140
-
141
- this.#ws.onopen = () => {
142
- this.#debug("WebSocket connection established");
143
- ret.resolve();
144
- };
145
-
146
- this.#ws.onmessage = async (event) => {
147
- const parseRes = await Realtime.messageSchema.safeParseAsync(
148
- JSON.parse(event.data as string),
149
- );
150
-
151
- if (!parseRes.success) {
152
- this.#debug("Received invalid message:", parseRes.error);
153
- return;
154
- }
155
-
156
- const msg = parseRes.data;
157
-
158
- if (!this.#running) {
159
- this.#debug(
160
- `Received message on channel "${msg.channel}" for topic "${msg.topic}" but stream is closed`,
161
- );
162
- }
163
-
164
- switch (msg.kind) {
165
- case "data": {
166
- if (!msg.channel) {
167
- this.#debug(
168
- `Received message on channel "${msg.channel}" with no channel`,
169
- );
170
- return;
171
- }
172
-
173
- if (!msg.topic) {
174
- this.#debug(
175
- `Received message on channel "${msg.channel}" with no topic`,
176
- );
177
- return;
178
- }
179
-
180
- const topic = this.#topics.get(msg.topic);
181
- if (!topic) {
182
- this.#debug(
183
- `Received message on channel "${msg.channel}" for unknown topic "${msg.topic}"`,
184
- );
185
- return;
186
- }
187
-
188
- const schema = topic.getSchema();
189
- if (schema) {
190
- const validateRes = await schema["~standard"].validate(msg.data);
191
- if (validateRes.issues) {
192
- console.error(
193
- `Received message on channel "${msg.channel}" for topic "${msg.topic}" that failed schema validation:`,
194
- validateRes.issues,
195
- );
196
- return;
197
- }
198
-
199
- msg.data = validateRes.value;
200
- }
201
-
202
- this.#debug(
203
- `Received message on channel "${msg.channel}" for topic "${msg.topic}":`,
204
- msg.data,
205
- );
206
- return this.#fanout.write({
207
- channel: msg.channel,
208
- topic: msg.topic,
209
- data: msg.data,
210
- fnId: msg.fn_id,
211
- createdAt: msg.created_at || new Date(),
212
- runId: msg.run_id,
213
- kind: "data",
214
- envId: msg.env_id,
215
- });
216
- }
217
-
218
- case "datastream-start": {
219
- if (!msg.channel) {
220
- this.#debug(
221
- `Received message on channel "${msg.channel}" with no channel`,
222
- );
223
- return;
224
- }
225
-
226
- if (!msg.topic) {
227
- this.#debug(
228
- `Received message on channel "${msg.channel}" with no topic`,
229
- );
230
- return;
231
- }
232
-
233
- const streamId: unknown = msg.data;
234
- if (typeof streamId !== "string" || !streamId) {
235
- this.#debug(
236
- `Received message on channel "${msg.channel}" with no stream ID`,
237
- );
238
- return;
239
- }
240
-
241
- // `data` is a stream ID that we'll start receiving chunks with
242
- if (this.#chunkStreams.has(streamId)) {
243
- this.#debug(
244
- `Received message on channel "${msg.channel}" to create stream ID "${streamId}" that already exists`,
245
- );
246
- return;
247
- }
248
-
249
- const stream = new ReadableStream({
250
- start: (controller) => {
251
- this.#chunkStreams.set(streamId, { stream, controller });
252
- },
253
-
254
- cancel: () => {
255
- this.#chunkStreams.delete(streamId);
256
- },
257
- });
258
-
259
- this.#debug(
260
- `Created stream ID "${streamId}" on channel "${msg.channel}"`,
261
- );
262
- return this.#fanout.write({
263
- channel: msg.channel,
264
- topic: msg.topic,
265
- kind: "datastream-start",
266
- data: streamId,
267
- streamId,
268
- fnId: msg.fn_id,
269
- runId: msg.run_id,
270
- stream,
271
- });
272
- }
273
-
274
- case "datastream-end": {
275
- if (!msg.channel) {
276
- this.#debug(
277
- `Received message on channel "${msg.channel}" with no channel`,
278
- );
279
- return;
280
- }
281
-
282
- if (!msg.topic) {
283
- this.#debug(
284
- `Received message on channel "${msg.channel}" with no topic`,
285
- );
286
- return;
287
- }
288
-
289
- const streamId: unknown = msg.data;
290
- if (typeof streamId !== "string" || !streamId) {
291
- this.#debug(
292
- `Received message on channel "${msg.channel}" with no stream ID`,
293
- );
294
- return;
295
- }
296
-
297
- // `data` is a stream ID that we'll stop receiving chunks with
298
- const stream = this.#chunkStreams.get(streamId);
299
- if (!stream) {
300
- this.#debug(
301
- `Received message on channel "${msg.channel}" to close stream ID "${streamId}" that doesn't exist`,
302
- );
303
- return;
304
- }
305
-
306
- stream.controller.close();
307
- this.#chunkStreams.delete(streamId);
308
-
309
- this.#debug(
310
- `Closed stream ID "${streamId}" on channel "${msg.channel}"`,
311
- );
312
- return this.#fanout.write({
313
- channel: msg.channel,
314
- topic: msg.topic,
315
- kind: "datastream-end",
316
- data: streamId,
317
- streamId,
318
- fnId: msg.fn_id,
319
- runId: msg.run_id,
320
- stream: stream.stream,
321
- });
322
- }
323
-
324
- case "chunk": {
325
- if (!msg.channel) {
326
- this.#debug(
327
- `Received message on channel "${msg.channel}" with no channel`,
328
- );
329
- return;
330
- }
331
-
332
- if (!msg.topic) {
333
- this.#debug(
334
- `Received message on channel "${msg.channel}" with no topic`,
335
- );
336
- return;
337
- }
338
-
339
- // `stream_id` is the ID of the stream we're receiving chunks for
340
- if (!msg.stream_id) {
341
- this.#debug(
342
- `Received message on channel "${msg.channel}" with no stream ID`,
343
- );
344
- return;
345
- }
346
-
347
- const stream = this.#chunkStreams.get(msg.stream_id);
348
- if (!stream) {
349
- this.#debug(
350
- `Received message on channel "${msg.channel}" for unknown stream ID "${msg.stream_id}"`,
351
- );
352
- return;
353
- }
354
-
355
- this.#debug(
356
- `Received chunk on channel "${msg.channel}" for stream ID "${msg.stream_id}":`,
357
- msg.data,
358
- );
359
-
360
- stream.controller.enqueue(msg.data);
361
-
362
- return this.#fanout.write({
363
- channel: msg.channel,
364
- topic: msg.topic,
365
- kind: "chunk",
366
- data: msg.data,
367
- streamId: msg.stream_id,
368
- fnId: msg.fn_id,
369
- runId: msg.run_id,
370
- stream: stream.stream,
371
- });
372
- }
373
-
374
- default: {
375
- this.#debug(
376
- `Received message on channel "${msg.channel}" with unhandled kind "${msg.kind}"`,
377
- );
378
- return;
379
- }
380
- }
381
- };
382
-
383
- this.#ws.onerror = (event) => {
384
- console.error("WebSocket error observed:", event);
385
- ret.reject(event);
386
- };
387
-
388
- this.#ws.onclose = (event) => {
389
- this.#debug("WebSocket closed:", event.reason);
390
- this.close();
391
- };
392
-
393
- this.#running = true;
394
- } catch (err) {
395
- ret.reject(err);
396
- }
397
-
398
- return ret.promise;
399
- }
400
-
401
- /**
402
- * TODO
403
- */
404
- private async lazilyGetSubscriptionToken<
405
- const InputChannel extends Realtime.Channel | string,
406
- const InputTopics extends (keyof Realtime.Channel.InferTopics<
407
- Realtime.Channel.AsChannel<InputChannel>
408
- > &
409
- string)[],
410
- const TToken extends Realtime.Subscribe.Token<
411
- Realtime.Channel.AsChannel<InputChannel>,
412
- InputTopics
413
- >,
414
- >(
415
- /**
416
- * TODO
417
- */
418
- args: {
419
- /**
420
- * TODO
421
- */
422
- channel: Realtime.Subscribe.InferChannelInput<InputChannel>;
423
-
424
- /**
425
- * TODO
426
- */
427
- topics: InputTopics;
428
-
429
- /**
430
- * TODO
431
- */
432
- signingKey: string | undefined;
433
-
434
- /**
435
- * TODO
436
- */
437
- signingKeyFallback: string | undefined;
438
- },
439
- ): Promise<TToken> {
440
- const channelId =
441
- typeof args.channel === "string" ? args.channel : args.channel.name;
442
-
443
- if (!channelId) {
444
- throw new Error("Channel ID is required to create a subscription token");
445
- }
446
-
447
- const key = await api.getSubscriptionToken({
448
- channel: channelId,
449
- topics: args.topics,
450
- signingKey: args.signingKey,
451
- signingKeyFallback: args.signingKeyFallback,
452
- apiBaseUrl: this.#apiBaseUrl,
453
- });
454
-
455
- const token = {
456
- channel: channelId,
457
- topics: args.topics,
458
- key,
459
- } as TToken;
460
-
461
- return token;
462
- }
463
-
464
- /**
465
- * TODO
466
- */
467
- public close(
468
- /**
469
- * TODO
470
- */
471
- reason = "Userland closed connection",
472
- ) {
473
- if (!this.#running) {
474
- return;
475
- }
476
-
477
- this.#debug("close() called; closing connection...");
478
- this.#running = false;
479
- this.#ws?.close(1000, reason);
480
-
481
- this.#debug(`Closing ${this.#fanout.size()} streams...`);
482
- this.#fanout.close();
483
- }
484
-
485
- /**
486
- * TODO
487
- */
488
- public getJsonStream() {
489
- return this.#fanout.createStream();
490
- }
491
-
492
- /**
493
- * TODO
494
- */
495
- public getEncodedStream() {
496
- return this.#fanout.createStream((chunk) => {
497
- return this.#encoder.encode(`${JSON.stringify(chunk)}\n`);
498
- });
499
- }
500
-
501
- /**
502
- * TODO
503
- */
504
- public useCallback(
505
- callback: Realtime.Subscribe.Callback,
506
- stream: ReadableStream<Realtime.Message> = this.getJsonStream(),
507
- ) {
508
- void (async () => {
509
- // Explicitly get and manage the reader so that we can manually release
510
- // the lock if anything goes wrong or we're done with it.
511
- const reader = stream.getReader();
512
- try {
513
- while (this.#running) {
514
- const { done, value } = await reader.read();
515
- if (done || !this.#running) break;
516
-
517
- callback(value);
518
- }
519
- } finally {
520
- reader.releaseLock();
521
- }
522
- })();
523
- }
524
- }