@bunworks/inngest-realtime 0.1.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.
@@ -0,0 +1,19 @@
1
+ import pluginJs from "@eslint/js";
2
+ import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
3
+ import globals from "globals";
4
+ import tseslint from "typescript-eslint";
5
+
6
+ /** @type {import('eslint').Linter.Config[]} */
7
+ export default [
8
+ { files: ["**/*.{js,mjs,cjs,ts}"] },
9
+ { languageOptions: { globals: globals.browser } },
10
+ pluginJs.configs.recommended,
11
+ ...tseslint.configs.recommended,
12
+ eslintPluginPrettierRecommended,
13
+ {
14
+ rules: {
15
+ "@typescript-eslint/no-namespace": "off",
16
+ "@typescript-eslint/ban-types": "off",
17
+ },
18
+ },
19
+ ];
package/package.json ADDED
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "@bunworks/inngest-realtime",
3
+ "version": "0.1.0",
4
+ "description": "Realtime messaging для @bunworks",
5
+ "main": "./index.js",
6
+ "publishConfig": {
7
+ "registry": "https://registry.npmjs.org",
8
+ "access": "public"
9
+ },
10
+ "scripts": {
11
+ "test": "vitest run",
12
+ "build": "tsc -p tsconfig.build.json && tsdown --config tsdown.config.ts",
13
+ "postversion": "pnpm run build",
14
+ "release": "cross-env DIST_DIR=dist node ../../scripts/release/publish.js",
15
+ "pack": "pnpm run build && mv $(npm pack ./dist --pack-destination . --silent) inngest-realtime.tgz"
16
+ },
17
+ "exports": {
18
+ ".": {
19
+ "import": {
20
+ "types": "./index.d.mts",
21
+ "default": "./index.mjs"
22
+ },
23
+ "require": {
24
+ "types": "./index.d.ts",
25
+ "default": "./index.js"
26
+ }
27
+ },
28
+ "./hooks": {
29
+ "import": {
30
+ "types": "./hooks.d.mts",
31
+ "default": "./hooks.mjs"
32
+ },
33
+ "require": {
34
+ "types": "./hooks.d.ts",
35
+ "default": "./hooks.js"
36
+ }
37
+ },
38
+ "./middleware": {
39
+ "import": {
40
+ "types": "./middleware.d.mts",
41
+ "default": "./middleware.mjs"
42
+ },
43
+ "require": {
44
+ "types": "./middleware.d.ts",
45
+ "default": "./middleware.js"
46
+ }
47
+ }
48
+ },
49
+ "keywords": [
50
+ "bunworks",
51
+ "realtime",
52
+ "websocket",
53
+ "messaging"
54
+ ],
55
+ "homepage": "https://github.com/bunworks/bunworks#readme",
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/bunworks/bunworks.git",
59
+ "directory": "external/realtime"
60
+ },
61
+ "author": "Bunworks Team",
62
+ "license": "MIT",
63
+ "peerDependencies": {
64
+ "react": ">=18.0.0",
65
+ "zod": "^4.0.0"
66
+ },
67
+ "devDependencies": {
68
+ "@eslint/js": "^10.0.1",
69
+ "@types/debug": "^4.1.12",
70
+ "@types/node": "^25.2.2",
71
+ "@types/react": "^19.2.13",
72
+ "eslint": "^10.0.0",
73
+ "eslint-plugin-prettier": "^5.5.5",
74
+ "globals": "^17.3.0",
75
+ "react": "^19.2.4",
76
+ "tsdown": "^0.20.3",
77
+ "typescript": "^5.9.3",
78
+ "typescript-eslint": "^8.54.0",
79
+ "valibot": "1.2.0",
80
+ "vite-tsconfig-paths": "^6.1.0",
81
+ "vitest": "^4.0.18"
82
+ },
83
+ "dependencies": {
84
+ "@standard-schema/spec": "^1.1.0",
85
+ "debug": "^4.4.3",
86
+ "zod": "^4.3.6"
87
+ }
88
+ }
package/src/api.ts ADDED
@@ -0,0 +1,83 @@
1
+ import { z } from "zod";
2
+ import { getEnvVar } from "./env";
3
+ import { fetchWithAuthFallback, parseAsBoolean } from "./util";
4
+
5
+ const tokenSchema = z.object({ jwt: z.string() });
6
+
7
+ export const api = {
8
+ async getSubscriptionToken({
9
+ channel,
10
+ topics,
11
+ signingKey,
12
+ signingKeyFallback,
13
+ apiBaseUrl,
14
+ }: {
15
+ channel: string;
16
+ topics: string[];
17
+ signingKey: string | undefined;
18
+ signingKeyFallback: string | undefined;
19
+ apiBaseUrl: string | undefined;
20
+ }): Promise<string> {
21
+ let url: URL;
22
+ const path = "/v1/realtime/token";
23
+ const inputBaseUrl =
24
+ apiBaseUrl ||
25
+ getEnvVar("BUNWORKS_BASE_URL") ||
26
+ getEnvVar("BUNWORKS_API_BASE_URL");
27
+
28
+ const devEnvVar = getEnvVar("BUNWORKS_DEV");
29
+
30
+ if (inputBaseUrl) {
31
+ url = new URL(path, inputBaseUrl);
32
+ } else if (devEnvVar) {
33
+ try {
34
+ const devUrl = new URL(devEnvVar);
35
+ url = new URL(path, devUrl);
36
+ } catch {
37
+ if (parseAsBoolean(devEnvVar)) {
38
+ url = new URL(path, "http://localhost:8288/");
39
+ } else {
40
+ url = new URL(path, "https://api.inngest.com/");
41
+ }
42
+ }
43
+ } else {
44
+ url = new URL(
45
+ path,
46
+ getEnvVar("NODE_ENV") === "production"
47
+ ? "https://api.inngest.com/"
48
+ : "http://localhost:8288/",
49
+ );
50
+ }
51
+
52
+ const body = topics.map((topic) => ({
53
+ channel,
54
+ name: topic,
55
+ kind: "run",
56
+ }));
57
+
58
+ const res = await fetchWithAuthFallback({
59
+ authToken: signingKey,
60
+ authTokenFallback: signingKeyFallback,
61
+ fetch,
62
+ url,
63
+ options: {
64
+ method: "POST",
65
+ body: JSON.stringify(body),
66
+ headers: {
67
+ "Content-Type": "application/json",
68
+ },
69
+ },
70
+ });
71
+
72
+ if (!res.ok) {
73
+ throw new Error(
74
+ `Не удалось получить токен подписки: ${res.status} ${
75
+ res.statusText
76
+ } - ${await res.text()}`,
77
+ );
78
+ }
79
+
80
+ const data = await res.json();
81
+ return tokenSchema.parse(data).jwt;
82
+ },
83
+ };
package/src/channel.ts ADDED
@@ -0,0 +1,122 @@
1
+ import { topic } from "./topic";
2
+ import { type Realtime } from "./types";
3
+
4
+ /**
5
+ * TODO
6
+ */
7
+ export const channel: Realtime.Channel.Builder = (
8
+ /**
9
+ * TODO
10
+ */
11
+ id,
12
+ ) => {
13
+ // eslint-disable-next-line prefer-const, @typescript-eslint/no-explicit-any
14
+ let channelDefinition: any;
15
+ const topics: Record<string, Realtime.Topic.Definition> = {};
16
+
17
+ const builder = (...args: unknown[]) => {
18
+ const finalId: string = typeof id === "string" ? id : id(...args);
19
+
20
+ const topicsFns = Object.entries(topics).reduce<
21
+ Record<string, (data: unknown) => Promise<Realtime.Message.Input>>
22
+ >((acc, [name, topic]) => {
23
+ acc[name] = createTopicFn(finalId, topic);
24
+
25
+ return acc;
26
+ }, {});
27
+
28
+ const channel: Realtime.Channel = {
29
+ name: finalId,
30
+ topics,
31
+ ...topicsFns,
32
+ };
33
+
34
+ return channel;
35
+ };
36
+
37
+ const extras: Record<string, unknown> = {
38
+ topics,
39
+ addTopic: (topic: Realtime.Topic.Definition) => {
40
+ topics[topic.name] = topic;
41
+
42
+ return channelDefinition;
43
+ },
44
+ };
45
+
46
+ channelDefinition = Object.assign(builder, extras);
47
+
48
+ return channelDefinition;
49
+ };
50
+
51
+ /**
52
+ * TODO
53
+ */
54
+ export const typeOnlyChannel = <
55
+ TChannelDef extends Realtime.Channel.Definition,
56
+ TId extends string = Realtime.Channel.Definition.InferId<TChannelDef>,
57
+ TTopics extends Record<
58
+ string,
59
+ Realtime.Topic.Definition
60
+ > = Realtime.Channel.Definition.InferTopics<TChannelDef>,
61
+ TOutput extends Realtime.Channel = Realtime.Channel<TId, TTopics>,
62
+ >(
63
+ /**
64
+ * TODO
65
+ */
66
+ id: TId,
67
+ ) => {
68
+ const blankChannel = {
69
+ ...channel(id),
70
+ topics: new Proxy(
71
+ {},
72
+ {
73
+ get: (target, prop) => {
74
+ if (prop in target) {
75
+ return target[prop as keyof typeof target];
76
+ }
77
+
78
+ if (typeof prop === "string") {
79
+ return topic(prop);
80
+ }
81
+ },
82
+ },
83
+ ),
84
+ };
85
+
86
+ const ch = new Proxy(blankChannel, {
87
+ get: (target, prop) => {
88
+ if (prop in target) {
89
+ return target[prop as keyof typeof target];
90
+ }
91
+
92
+ if (typeof prop === "string") {
93
+ return createTopicFn(id, topic(prop));
94
+ }
95
+ },
96
+ });
97
+
98
+ return ch as unknown as TOutput;
99
+ };
100
+
101
+ const createTopicFn = (channelId: string, topic: Realtime.Topic.Definition) => {
102
+ return async (data: unknown) => {
103
+ const schema = topic.getSchema();
104
+ if (schema) {
105
+ try {
106
+ await schema["~standard"].validate(data);
107
+ } catch (err) {
108
+ console.error(
109
+ `Failed schema validation for channel "${channelId}" topic "${topic.name}":`,
110
+ err,
111
+ );
112
+ throw new Error("Failed schema validation");
113
+ }
114
+ }
115
+
116
+ return {
117
+ channel: channelId,
118
+ topic: topic.name,
119
+ data,
120
+ };
121
+ };
122
+ };
package/src/env.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ // For Vite
2
+ interface ImportMeta {
3
+ env: {
4
+ INNGEST_DEV?: string;
5
+ VITE_INNGEST_DEV?: string;
6
+ MODE: "development" | "production";
7
+ VITE_MODE: "development" | "production";
8
+ INNGEST_BASE_URL?: string;
9
+ VITE_INNGEST_BASE_URL?: string;
10
+ INNGEST_API_BASE_URL?: string;
11
+ VITE_INNGEST_API_BASE_URL?: string;
12
+ INNGEST_SIGNING_KEY?: string;
13
+ INNGEST_SIGNING_KEY_FALLBACK?: string;
14
+ };
15
+ }
package/src/env.ts ADDED
@@ -0,0 +1,152 @@
1
+ export type EnvValue = string | undefined;
2
+ export type Env = Record<string, EnvValue>;
3
+
4
+ export type ExpectedEnv = {
5
+ BUNWORKS_DEV: string | undefined;
6
+ NODE_ENV: string | undefined;
7
+ BUNWORKS_BASE_URL: string | undefined;
8
+ BUNWORKS_API_BASE_URL: string | undefined;
9
+ BUNWORKS_SIGNING_KEY: string | undefined;
10
+ BUNWORKS_SIGNING_KEY_FALLBACK: string | undefined;
11
+ };
12
+
13
+ /**
14
+ * The environment variables that we wish to access in the environment.
15
+ *
16
+ * Due to the way that some environment variables are exposed across different
17
+ * runtimes and bundling tools, we need to be careful about how we access them.
18
+ *
19
+ * The most basic annoyance is that environment variables are exposed in
20
+ * different locations (e.g. `process.env`, `Deno.env`, `Netlify.env`,
21
+ * `import.meta.env`).
22
+ *
23
+ * Bundling can be more disruptive though, where some will literally
24
+ * find/replace `process.env.MY_VAR` with the value of `MY_VAR` at build time,
25
+ * which requires us to ensure that the full env var is used in code instead of
26
+ * dynamically building it.
27
+ */
28
+ const env: ExpectedEnv | undefined = (() => {
29
+ // Pure vite
30
+ try {
31
+ // @ts-expect-error - import.meta only available in some environments
32
+ const viteEnv = import.meta.env;
33
+
34
+ if (viteEnv) {
35
+ return {
36
+ BUNWORKS_DEV: viteEnv.BUNWORKS_DEV ?? viteEnv.VITE_BUNWORKS_DEV,
37
+ NODE_ENV: viteEnv.NODE_ENV,
38
+ BUNWORKS_BASE_URL:
39
+ viteEnv.BUNWORKS_BASE_URL ?? viteEnv.VITE_BUNWORKS_BASE_URL,
40
+ BUNWORKS_API_BASE_URL:
41
+ viteEnv.BUNWORKS_API_BASE_URL ?? viteEnv.VITE_BUNWORKS_API_BASE_URL,
42
+ BUNWORKS_SIGNING_KEY: viteEnv.BUNWORKS_SIGNING_KEY,
43
+ BUNWORKS_SIGNING_KEY_FALLBACK: viteEnv.BUNWORKS_SIGNING_KEY_FALLBACK,
44
+ };
45
+ }
46
+ } catch {
47
+ // noop
48
+ }
49
+
50
+ try {
51
+ // Node-like environments (sometimes polyfilled Vite)
52
+ if (process.env) {
53
+ return {
54
+ BUNWORKS_DEV:
55
+ process.env.BUNWORKS_DEV ??
56
+ process.env.NEXT_PUBLIC_BUNWORKS_DEV ??
57
+ process.env.REACT_APP_BUNWORKS_DEV ??
58
+ process.env.NUXT_PUBLIC_BUNWORKS_DEV ??
59
+ process.env.VUE_APP_BUNWORKS_DEV ??
60
+ process.env.VITE_BUNWORKS_DEV,
61
+
62
+ NODE_ENV:
63
+ process.env.NODE_ENV ??
64
+ process.env.NEXT_PUBLIC_NODE_ENV ??
65
+ process.env.REACT_APP_NODE_ENV ??
66
+ process.env.NUXT_PUBLIC_NODE_ENV ??
67
+ process.env.VUE_APP_NODE_ENV ??
68
+ process.env.VITE_NODE_ENV ??
69
+ process.env.VITE_MODE,
70
+
71
+ BUNWORKS_BASE_URL:
72
+ process.env.BUNWORKS_BASE_URL ??
73
+ process.env.NEXT_PUBLIC_BUNWORKS_BASE_URL ??
74
+ process.env.REACT_APP_BUNWORKS_BASE_URL ??
75
+ process.env.NUXT_PUBLIC_BUNWORKS_BASE_URL ??
76
+ process.env.VUE_APP_BUNWORKS_BASE_URL ??
77
+ process.env.VITE_BUNWORKS_BASE_URL,
78
+
79
+ BUNWORKS_API_BASE_URL:
80
+ process.env.BUNWORKS_API_BASE_URL ??
81
+ process.env.NEXT_PUBLIC_BUNWORKS_API_BASE_URL ??
82
+ process.env.REACT_APP_BUNWORKS_API_BASE_URL ??
83
+ process.env.NUXT_PUBLIC_BUNWORKS_API_BASE_URL ??
84
+ process.env.VUE_APP_BUNWORKS_API_BASE_URL ??
85
+ process.env.VITE_BUNWORKS_API_BASE_URL,
86
+
87
+ BUNWORKS_SIGNING_KEY: process.env.BUNWORKS_SIGNING_KEY,
88
+
89
+ BUNWORKS_SIGNING_KEY_FALLBACK: process.env.BUNWORKS_SIGNING_KEY_FALLBACK,
90
+ };
91
+ }
92
+ } catch {
93
+ // noop
94
+ }
95
+
96
+ // Deno
97
+ try {
98
+ const denoEnv = Deno.env.toObject();
99
+
100
+ if (denoEnv) {
101
+ return {
102
+ BUNWORKS_DEV: denoEnv.BUNWORKS_DEV,
103
+ NODE_ENV: denoEnv.NODE_ENV,
104
+ BUNWORKS_BASE_URL: denoEnv.BUNWORKS_BASE_URL,
105
+ BUNWORKS_API_BASE_URL: denoEnv.BUNWORKS_API_BASE_URL,
106
+ BUNWORKS_SIGNING_KEY: denoEnv.BUNWORKS_SIGNING_KEY,
107
+ BUNWORKS_SIGNING_KEY_FALLBACK: denoEnv.BUNWORKS_SIGNING_KEY_FALLBACK,
108
+ };
109
+ }
110
+ } catch {
111
+ // noop
112
+ }
113
+
114
+ // Netlify
115
+ try {
116
+ const netlifyEnv = Netlify.env.toObject();
117
+
118
+ if (netlifyEnv) {
119
+ return {
120
+ BUNWORKS_DEV: netlifyEnv.BUNWORKS_DEV,
121
+ NODE_ENV: netlifyEnv.NODE_ENV,
122
+ BUNWORKS_BASE_URL: netlifyEnv.BUNWORKS_BASE_URL,
123
+ BUNWORKS_API_BASE_URL: netlifyEnv.BUNWORKS_API_BASE_URL,
124
+ BUNWORKS_SIGNING_KEY: netlifyEnv.BUNWORKS_SIGNING_KEY,
125
+ BUNWORKS_SIGNING_KEY_FALLBACK: netlifyEnv.BUNWORKS_SIGNING_KEY_FALLBACK,
126
+ };
127
+ }
128
+ } catch {
129
+ // noop
130
+ }
131
+ })();
132
+
133
+ /**
134
+ * The Deno environment, which is not always available.
135
+ */
136
+ declare const Deno: {
137
+ env: { toObject: () => Env };
138
+ };
139
+
140
+ /**
141
+ * The Netlify environment, which is not always available.
142
+ */
143
+ declare const Netlify: {
144
+ env: { toObject: () => Env };
145
+ };
146
+
147
+ /**
148
+ * Given a `key`, get the environment variable under that key.
149
+ */
150
+ export const getEnvVar = (key: keyof ExpectedEnv): string | undefined => {
151
+ return env?.[key];
152
+ };