@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.
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/dist/bridge.d.ts +38 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +384 -0
- package/dist/bridge.js.map +1 -0
- package/dist/deprecated.d.ts +47 -0
- package/dist/deprecated.d.ts.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +64 -0
- package/dist/index.js.map +1 -0
- package/dist/launch-params.d.ts +26 -0
- package/dist/launch-params.d.ts.map +1 -0
- package/dist/launch-params.js +103 -0
- package/dist/launch-params.js.map +1 -0
- package/dist/middleware.d.ts +25 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +31 -0
- package/dist/middleware.js.map +1 -0
- package/dist/package.js +6 -0
- package/dist/package.js.map +1 -0
- package/dist/tap.d.ts +35 -0
- package/dist/tap.d.ts.map +1 -0
- package/dist/tap.js +39 -0
- package/dist/tap.js.map +1 -0
- package/dist/types.d.ts +214 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +61 -3
- package/src/bridge.ts +599 -0
- package/src/deprecated.ts +61 -0
- package/src/index.ts +130 -0
- package/src/launch-params.ts +131 -0
- package/src/middleware.ts +70 -0
- package/src/tap.ts +77 -0
- package/src/types.ts +271 -0
|
@@ -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
|
+
}
|