@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.
- package/CHANGELOG.md +6 -6
- package/README.md +1 -1
- package/_virtual/_rolldown/runtime.cjs +29 -0
- package/api.cjs +45 -0
- package/api.cjs.map +1 -0
- package/api.mjs +44 -0
- package/api.mjs.map +1 -0
- package/channel.cjs +67 -0
- package/channel.cjs.map +1 -0
- package/channel.d.cts +19 -0
- package/channel.d.cts.map +1 -0
- package/channel.d.mts +19 -0
- package/channel.d.mts.map +1 -0
- package/channel.mjs +66 -0
- package/channel.mjs.map +1 -0
- package/env.cjs +72 -0
- package/env.cjs.map +1 -0
- package/env.mjs +71 -0
- package/env.mjs.map +1 -0
- package/hooks.cjs +147 -0
- package/hooks.cjs.map +1 -0
- package/hooks.d.cts +70 -0
- package/hooks.d.cts.map +1 -0
- package/hooks.d.mts +70 -0
- package/hooks.d.mts.map +1 -0
- package/hooks.mjs +144 -0
- package/hooks.mjs.map +1 -0
- package/index.cjs +19 -0
- package/index.d.cts +5 -0
- package/index.d.mts +6 -0
- package/index.mjs +7 -0
- package/middleware.cjs +41 -0
- package/middleware.cjs.map +1 -0
- package/middleware.d.cts +29 -0
- package/middleware.d.cts.map +1 -0
- package/middleware.d.mts +29 -0
- package/middleware.d.mts.map +1 -0
- package/middleware.mjs +34 -0
- package/middleware.mjs.map +1 -0
- package/package.json +6 -5
- package/subscribe/StreamFanout.cjs +47 -0
- package/subscribe/StreamFanout.cjs.map +1 -0
- package/subscribe/StreamFanout.mjs +46 -0
- package/subscribe/StreamFanout.mjs.map +1 -0
- package/subscribe/TokenSubscription.cjs +438 -0
- package/subscribe/TokenSubscription.cjs.map +1 -0
- package/subscribe/TokenSubscription.mjs +436 -0
- package/subscribe/TokenSubscription.mjs.map +1 -0
- package/subscribe/helpers.cjs +41 -0
- package/subscribe/helpers.cjs.map +1 -0
- package/subscribe/helpers.d.cts +64 -0
- package/subscribe/helpers.d.cts.map +1 -0
- package/subscribe/helpers.d.mts +64 -0
- package/subscribe/helpers.d.mts.map +1 -0
- package/subscribe/helpers.mjs +40 -0
- package/subscribe/helpers.mjs.map +1 -0
- package/subscribe/index.cjs +1 -0
- package/subscribe/index.d.mts +1 -0
- package/subscribe/index.mjs +3 -0
- package/topic.cjs +30 -0
- package/topic.cjs.map +1 -0
- package/topic.d.cts +19 -0
- package/topic.d.cts.map +1 -0
- package/topic.d.mts +19 -0
- package/topic.d.mts.map +1 -0
- package/topic.mjs +28 -0
- package/topic.mjs.map +1 -0
- package/types.cjs +45 -0
- package/types.cjs.map +1 -0
- package/types.d.cts +241 -0
- package/types.d.cts.map +1 -0
- package/types.d.mts +241 -0
- package/types.d.mts.map +1 -0
- package/types.mjs +39 -0
- package/types.mjs.map +1 -0
- package/util.cjs +72 -0
- package/util.cjs.map +1 -0
- package/util.mjs +69 -0
- package/util.mjs.map +1 -0
- package/CLAUDE.md +0 -69
- package/bun.lock +0 -527
- package/eslint.config.mjs +0 -19
- package/src/api.ts +0 -83
- package/src/channel.ts +0 -122
- package/src/env.d.ts +0 -15
- package/src/env.ts +0 -152
- package/src/hooks.ts +0 -256
- package/src/index.test.ts +0 -840
- package/src/index.ts +0 -4
- package/src/middleware.ts +0 -68
- package/src/subscribe/StreamFanout.ts +0 -82
- package/src/subscribe/TokenSubscription.ts +0 -524
- package/src/subscribe/helpers.ts +0 -153
- package/src/subscribe/index.ts +0 -1
- package/src/topic.ts +0 -51
- package/src/types.ts +0 -499
- package/src/util.ts +0 -104
- package/tsconfig.build.json +0 -7
- package/tsconfig.json +0 -114
- package/tsdown.config.ts +0 -18
- package/vitest.config.ts +0 -22
package/src/index.ts
DELETED
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
|
-
}
|