@vkma.dev/bridge 0.0.0-stage → 0.2.1

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,61 @@
1
+ /**
2
+ * Pre-`Any*` type names, kept as aliases so older code keeps compiling.
3
+ */
4
+
5
+ import type { CommunityWidgetType } from "@vkma.dev/bridge-schema";
6
+ import type {
7
+ AnyIOMethodName,
8
+ AnyReceiveMethodName,
9
+ AnyReceiveOnlyMethodName,
10
+ AnyRequestMethodName,
11
+ AnyRequestOnlyMethodName,
12
+ } from "./types.ts";
13
+
14
+ /**
15
+ * Name of a method that can be sent.
16
+ * @alias AnyRequestMethodName
17
+ * @deprecated
18
+ */
19
+ export type RequestMethodName = AnyRequestMethodName;
20
+
21
+ /**
22
+ * Name of a method that can be received.
23
+ * @alias AnyReceiveMethodName
24
+ * @deprecated
25
+ */
26
+ export type ReceiveMethodName = AnyReceiveMethodName;
27
+
28
+ /**
29
+ * Name of a method that can be only sent.
30
+ * @alias AnyRequestOnlyMethodName
31
+ * @deprecated
32
+ */
33
+ export type RequestOnlyMethodName = AnyRequestOnlyMethodName;
34
+
35
+ /**
36
+ * Name of a method that can be only received.
37
+ * @alias AnyReceiveOnlyMethodName
38
+ * @deprecated
39
+ */
40
+ export type ReceiveOnlyMethodName = AnyReceiveOnlyMethodName;
41
+
42
+ /**
43
+ * Type of any method name.
44
+ * @alias AnyMethodName
45
+ * @deprecated
46
+ */
47
+ export type MethodName = AnyRequestMethodName | AnyReceiveMethodName;
48
+
49
+ /**
50
+ * The name of the method that can be both sent and received.
51
+ * @alias AnyIOMethodName
52
+ * @deprecated
53
+ */
54
+ export type IOMethodName = AnyIOMethodName;
55
+
56
+ /**
57
+ * Community widget type
58
+ * @alias CommunityWidgetType
59
+ * @deprecated
60
+ */
61
+ export type WidgetType = CommunityWidgetType;
package/src/index.ts ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @vkma.dev/bridge — an independent VK Bridge for VK Mini Apps.
3
+ *
4
+ * The default export is a ready singleton; `createVKBridge` builds additional
5
+ * instances. Every instance carries an always-on tap (see `onSend`/`onEvent`
6
+ * and `tap`), which is what the VKMA devtools observe — no opt-in required.
7
+ *
8
+ * Merely importing this module publishes the page-wide devtools handle on
9
+ * `globalThis.__vkmaDevtoolsBridge` (`{ tap, send }`), so the devtools
10
+ * collector — loaded as a separate script, never sharing a module graph with
11
+ * the app — can observe and drive the app's own bridge instance.
12
+ */
13
+
14
+ import packageJson from "../package.json" with { type: "json" };
15
+ import { createVKBridge, type VkmaBridgeOptions } from "./bridge.ts";
16
+ import { createTap, type BridgeTap, type BridgeTapEvent } from "./tap.ts";
17
+
18
+ export { createVKBridge, type RequestIdPolicy, type VkmaBridgeOptions } from "./bridge.ts";
19
+ import type { VkmaBridge } from "./types.ts";
20
+ export { applyMiddleware, type Middleware, type MiddlewareAPI } from "./middleware.ts";
21
+ export { parseURLSearchParamsForGetLaunchParams, type LaunchParams } from "./launch-params.ts";
22
+ export { createTap } from "./tap.ts";
23
+ export type {
24
+ SendOptions,
25
+ VKBridge,
26
+ VKBridgeErrorEvent,
27
+ VKBridgeEvent,
28
+ VKBridgeEventBase,
29
+ VKBridgeIOEvent,
30
+ VKBridgeReceiveOnlyEvent,
31
+ VKBridgeResultEvent,
32
+ VKBridgeSend,
33
+ VKBridgeSubscribeHandler,
34
+ VkmaBridge,
35
+ AnyIOMethodName,
36
+ AnyMethodName,
37
+ AnyReceiveMethodName,
38
+ AnyReceiveOnlyMethodName,
39
+ AnyRequestMethodName,
40
+ AnyRequestOnlyMethodName,
41
+ ErrorData,
42
+ ErrorDataAPIError,
43
+ ErrorDataAuthError,
44
+ ErrorDataClientError,
45
+ FailedResponseEventName,
46
+ RequestIdProp,
47
+ RequestProps,
48
+ ReceiveData,
49
+ ResultResponseEventName,
50
+ } from "./types.ts";
51
+ export type {
52
+ RequestMethodName,
53
+ ReceiveMethodName,
54
+ RequestOnlyMethodName,
55
+ ReceiveOnlyMethodName,
56
+ MethodName,
57
+ IOMethodName,
58
+ WidgetType,
59
+ } from "./deprecated.ts";
60
+ export type { ReceiveDataMap, ReceiveEventMap, RequestPropsMap } from "@vkma.dev/bridge-schema";
61
+ export * from "@vkma.dev/bridge-schema";
62
+
63
+ /**
64
+ * The devtools bridge handle shared by every copy of this module on the page,
65
+ * exposed on `globalThis` under a stable key so the devtools collector —
66
+ * loaded as a separate script, never sharing a module graph with the app —
67
+ * can observe and drive the app's own bridge instance:
68
+ *
69
+ * - `tap` — replay-buffered stream of sends, paired responses and events
70
+ * - `send` — execute a bridge send inside the app (devtools "execute")
71
+ */
72
+ const GLOBAL_KEY = "__vkmaDevtoolsBridge";
73
+
74
+ export interface DevtoolsBridgeHandle {
75
+ tap: BridgeTap;
76
+ send: (name: string, params?: Record<string, unknown>) => Promise<unknown>;
77
+ }
78
+
79
+ const globalScope = globalThis as typeof globalThis & {
80
+ [GLOBAL_KEY]?: DevtoolsBridgeHandle;
81
+ };
82
+
83
+ function sharedTap(): BridgeTap {
84
+ const existing = globalScope[GLOBAL_KEY];
85
+ if (existing) {
86
+ return existing.tap;
87
+ }
88
+ const tap = createTap();
89
+ globalScope[GLOBAL_KEY] = {
90
+ tap,
91
+ // The collector executes arbitrary method names — the handle stays
92
+ // stringly typed by design.
93
+ send: (name, params) => bridge.send(name as never, params as never),
94
+ };
95
+ return tap;
96
+ }
97
+
98
+ /** Options of the page-wide default instance: it shares the global tap. */
99
+ const defaultOptions: VkmaBridgeOptions = {
100
+ tap: sharedTap(),
101
+ };
102
+
103
+ const bridge: VkmaBridge = createVKBridge(packageJson.version, defaultOptions);
104
+
105
+ /**
106
+ * Subscribe to outgoing `send` calls of the default instance.
107
+ */
108
+ export function onSend(
109
+ listener: (event: BridgeTapEvent & { direction: "out" }) => void,
110
+ ): () => void {
111
+ return bridge.onSend(listener);
112
+ }
113
+
114
+ /**
115
+ * Subscribe to everything incoming on the default instance: paired responses
116
+ * and unsolicited events.
117
+ */
118
+ export function onEvent(
119
+ listener: (event: BridgeTapEvent & { direction: "in" }) => void,
120
+ ): () => void {
121
+ return bridge.onEvent(listener);
122
+ }
123
+
124
+ /** The shared tap of the default instance (buffered until first subscribe). */
125
+ export const bridgeTap: BridgeTap = bridge.tap;
126
+
127
+ /** The `globalThis` key the devtools collector looks the handle up by. */
128
+ export const devtoolsBridgeGlobalKey = GLOBAL_KEY;
129
+
130
+ export { bridge as default };
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Launch params: parsing the `vk_*` query string every mini app is opened
3
+ * with, into the typed shape of the catalogue's `GetLaunchParamsResponse`
4
+ * plus the extra keys only present in the URL form.
5
+ */
6
+
7
+ import type { GetLaunchParamsResponse } from "@vkma.dev/bridge-schema";
8
+
9
+ const LANGUAGES = new Set(["ru", "uk", "ua", "en", "be", "kz", "pt", "es"]);
10
+
11
+ const GROUP_ROLES = new Set(["admin", "editor", "member", "moder", "none"]);
12
+
13
+ const PLATFORMS = new Set([
14
+ "desktop_web",
15
+ "desktop_web_messenger",
16
+ "desktop_app_messenger",
17
+ "desktop_web_ok",
18
+ "mobile_web",
19
+ "mobile_web_ok",
20
+ "mobile_android",
21
+ "mobile_android_ok",
22
+ "mobile_android_messenger",
23
+ "mobile_iphone",
24
+ "mobile_iphone_ok",
25
+ "mobile_iphone_messenger",
26
+ "mobile_ipad",
27
+ ]);
28
+
29
+ /**
30
+ * Launch params as they appear in the URL — a superset of
31
+ * `GetLaunchParamsResponse` with the URL-only keys. The OK (Odnoklassniki)
32
+ * keys (`vk_client`, `vk_ok_*`, `vk_original_vk_id`, `vk_are_pushes_enabled`)
33
+ * come from the catalogue as optional — vk.com hosts never send them.
34
+ */
35
+ export interface LaunchParams extends GetLaunchParamsResponse {
36
+ vk_chat_id: string;
37
+ vk_is_recommended: number;
38
+ vk_profile_id: number;
39
+ vk_has_profile_button: number;
40
+ vk_testing_group_id: number;
41
+ odr_enabled: undefined | 1;
42
+ }
43
+
44
+ /**
45
+ * Parses launch params out of a query string (à la
46
+ * `location.search.slice(1)`), field by field, keeping only the known keys
47
+ * with validated values.
48
+ *
49
+ * @see https://dev.vk.ru/mini-apps/development/launch-params
50
+ */
51
+ export const parseURLSearchParamsForGetLaunchParams = (
52
+ searchParams: string,
53
+ ): Partial<LaunchParams> => {
54
+ const launchParams: Partial<LaunchParams> = {};
55
+
56
+ try {
57
+ const parsedSearchParams = new URLSearchParams(searchParams);
58
+
59
+ const convertToggleStateFromStringToNumber = (value: string): 0 | 1 | undefined => {
60
+ // OK hosts serialize toggles both ways ("true"/"false" and "0"/"1")
61
+ switch (value) {
62
+ case "0":
63
+ case "false":
64
+ return 0;
65
+ case "1":
66
+ case "true":
67
+ return 1;
68
+ default:
69
+ return undefined;
70
+ }
71
+ };
72
+
73
+ parsedSearchParams.forEach((value, query) => {
74
+ switch (query) {
75
+ case "vk_ts":
76
+ case "vk_is_recommended":
77
+ case "vk_profile_id":
78
+ case "vk_has_profile_button":
79
+ case "vk_testing_group_id":
80
+ case "vk_user_id":
81
+ case "vk_app_id":
82
+ case "vk_group_id":
83
+ case "vk_ok_app_id":
84
+ case "vk_ok_user_id":
85
+ case "vk_original_vk_id":
86
+ launchParams[query] = Number(value);
87
+ break;
88
+ case "sign":
89
+ case "vk_chat_id":
90
+ case "vk_ref":
91
+ case "vk_ok_ref":
92
+ case "vk_client":
93
+ case "vk_access_token_settings":
94
+ launchParams[query] = value;
95
+ break;
96
+ case "odr_enabled":
97
+ launchParams.odr_enabled = value === "1" ? 1 : undefined;
98
+ break;
99
+ case "vk_is_app_user":
100
+ case "vk_are_notifications_enabled":
101
+ case "vk_are_pushes_enabled":
102
+ case "vk_is_favorite": {
103
+ launchParams[query] = convertToggleStateFromStringToNumber(value);
104
+ break;
105
+ }
106
+ case "vk_language": {
107
+ launchParams.vk_language = LANGUAGES.has(value)
108
+ ? (value as Partial<LaunchParams>["vk_language"])
109
+ : undefined;
110
+ break;
111
+ }
112
+ case "vk_viewer_group_role": {
113
+ launchParams.vk_viewer_group_role = GROUP_ROLES.has(value)
114
+ ? (value as Partial<LaunchParams>["vk_viewer_group_role"])
115
+ : undefined;
116
+ break;
117
+ }
118
+ case "vk_platform": {
119
+ launchParams.vk_platform = PLATFORMS.has(value)
120
+ ? (value as Partial<LaunchParams>["vk_platform"])
121
+ : undefined;
122
+ break;
123
+ }
124
+ }
125
+ });
126
+ } catch (error) {
127
+ console.warn(error);
128
+ }
129
+
130
+ return launchParams;
131
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Redux-style middleware over `send`: compose logging, metrics or caching
3
+ * around the dispatch without touching the transport.
4
+ */
5
+
6
+ import type { VKBridge, VKBridgeSend, VKBridgeSubscribeHandler } from "./types.ts";
7
+
8
+ /**
9
+ * API that can use middleware.
10
+ */
11
+ export interface MiddlewareAPI<
12
+ S extends VKBridgeSend = VKBridgeSend,
13
+ L extends VKBridgeSubscribeHandler = VKBridgeSubscribeHandler,
14
+ > {
15
+ send: S;
16
+ subscribe(listener: L): void;
17
+ }
18
+
19
+ /**
20
+ * A middleware is a higher-order function that composes a dispatch function
21
+ * to return a new `send` function.
22
+ */
23
+ export type Middleware<S extends VKBridgeSend = VKBridgeSend> = (
24
+ api: MiddlewareAPI<S>,
25
+ ) => (next: S) => S;
26
+
27
+ /**
28
+ * Creates the VK Bridge enhancer that applies middleware to the `send`
29
+ * method. This is handy for a variety of tasks such as logging every sent
30
+ * event.
31
+ *
32
+ * @param middlewares The middleware chain to be applied.
33
+ * @returns The VK Bridge enhancer applying the middleware.
34
+ */
35
+ export function applyMiddleware(
36
+ ...middlewares: Array<Middleware | undefined | null>
37
+ ): (bridge: VKBridge) => VKBridge {
38
+ if (middlewares.includes(undefined) || middlewares.includes(null)) {
39
+ return applyMiddleware(
40
+ ...middlewares.filter((item): item is Middleware => typeof item === "function"),
41
+ );
42
+ }
43
+
44
+ return bridge => {
45
+ if (middlewares.length === 0) {
46
+ return bridge;
47
+ }
48
+
49
+ let send: VKBridgeSend = () => {
50
+ throw new Error(
51
+ "Sending events while constructing your middleware is not allowed. " +
52
+ "Other middleware would not be applied to this send.",
53
+ );
54
+ };
55
+
56
+ const middlewareAPI: MiddlewareAPI = {
57
+ subscribe: bridge.subscribe,
58
+ send: ((...args: Parameters<VKBridgeSend>) => bridge.send(...args)) as VKBridgeSend,
59
+ };
60
+
61
+ const chain = middlewares
62
+ .filter((item): item is Middleware => typeof item === "function")
63
+ .map(middleware => middleware(middlewareAPI))
64
+ .reduce((a, b) => next => a(b(next)));
65
+
66
+ send = chain(bridge.send);
67
+
68
+ return { ...bridge, send };
69
+ };
70
+ }
package/src/tap.ts ADDED
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Pure tap machinery: a tiny fan-out hub with a bounded replay buffer,
3
+ * detached from vk-bridge so it is unit-testable and tree-shakeable.
4
+ *
5
+ * The bridge wrapper always emits into the tap. When no devtools collector
6
+ * is attached (production), the buffer fills up once and every later emit
7
+ * costs a dropped-head push — effectively free.
8
+ */
9
+
10
+ export interface BridgeTapEvent {
11
+ /** Monotonic within a page load; pairs a `response` with its `send`. */
12
+ id: number;
13
+ ts: number;
14
+ /** `out` — the app called send; `in` — VK sent something to the app. */
15
+ direction: "out" | "in";
16
+ /** `send` (out), `response` (in, paired), `event` (in, unsolicited). */
17
+ kind: "send" | "response" | "event";
18
+ name: string;
19
+ payload?: unknown;
20
+ /** Response pairing: the id of the originating `send` event. */
21
+ reqId?: number;
22
+ /** Only for `response`: whether the promise resolved or rejected. */
23
+ ok?: boolean;
24
+ }
25
+
26
+ export interface BridgeTap {
27
+ emit(event: Omit<BridgeTapEvent, "id" | "ts"> & { ts?: number }): number;
28
+ subscribe(listener: (event: BridgeTapEvent) => void): () => void;
29
+ /** Events buffered before the first subscriber attached, oldest first. */
30
+ drain(): BridgeTapEvent[];
31
+ }
32
+
33
+ const DEFAULT_BUFFER_LIMIT = 500;
34
+
35
+ export function createTap(limit: number = DEFAULT_BUFFER_LIMIT): BridgeTap {
36
+ let nextId = 0;
37
+ let buffer: BridgeTapEvent[] | undefined = [];
38
+ const listeners = new Set<(event: BridgeTapEvent) => void>();
39
+
40
+ return {
41
+ emit(partial) {
42
+ const event: BridgeTapEvent = {
43
+ ...partial,
44
+ id: ++nextId,
45
+ ts: partial.ts ?? Date.now(),
46
+ };
47
+ if (listeners.size === 0) {
48
+ // No collector yet (production or devtools not loaded): keep a
49
+ // bounded replay so a late-attaching collector still sees recent
50
+ // history, then stop growing work at the cap.
51
+ if (buffer && buffer.length >= limit) {
52
+ buffer.shift();
53
+ }
54
+ buffer?.push(event);
55
+ return event.id;
56
+ }
57
+ for (const listener of listeners) {
58
+ listener(event);
59
+ }
60
+ return event.id;
61
+ },
62
+ subscribe(listener) {
63
+ listeners.add(listener);
64
+ const replay = buffer ?? [];
65
+ buffer = undefined;
66
+ for (const event of replay) {
67
+ listener(event);
68
+ }
69
+ return () => {
70
+ listeners.delete(listener);
71
+ };
72
+ },
73
+ drain() {
74
+ return buffer ? [...buffer] : [];
75
+ },
76
+ };
77
+ }