@serverkgg/bridge 0.1.1 → 0.1.2

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 (52) hide show
  1. package/package.json +1 -1
  2. package/src/bot/botPanel.test.ts +30 -0
  3. package/src/bot/botPanel.ts +9 -1
  4. package/src/cli/validate.test.ts +73 -1
  5. package/src/cli/validate.ts +35 -1
  6. package/src/context/bridgeContext.ts +6 -1
  7. package/src/core/bridgeAction.ts +2 -0
  8. package/src/core/bridgeActivity.ts +9 -0
  9. package/src/core/bridgeAvailability.ts +10 -0
  10. package/src/core/bridgeUserError.test.ts +10 -0
  11. package/src/core/bridgeUserError.ts +6 -2
  12. package/src/core/describeDownloadFailure.test.ts +58 -0
  13. package/src/core/describeDownloadFailure.ts +69 -0
  14. package/src/core/index.ts +3 -0
  15. package/src/modules/bridge.ts +8 -1
  16. package/src/modules/bridgeModule.ts +13 -1
  17. package/src/panel/bridgeDetail.ts +2 -1
  18. package/src/protocol/bridgeActivity.ts +54 -0
  19. package/src/protocol/bridgeEvents.ts +30 -0
  20. package/src/protocol/bridgeFrame.ts +10 -1
  21. package/src/protocol/index.ts +1 -0
  22. package/src/runtime/bridgeActions.ts +19 -2
  23. package/src/runtime/bridgeActivity.test.ts +318 -0
  24. package/src/runtime/bridgeActivity.ts +268 -0
  25. package/src/runtime/bridgeContext.test.ts +6 -0
  26. package/src/runtime/bridgeContext.ts +8 -1
  27. package/src/runtime/bridgeDriver.test.ts +21 -0
  28. package/src/runtime/bridgeDriver.ts +10 -7
  29. package/src/runtime/bridgeLog.ts +10 -0
  30. package/src/runtime/bridgeRefusalError.ts +13 -0
  31. package/src/runtime/bridgeRegistry.test.ts +1053 -10
  32. package/src/runtime/bridgeRegistry.ts +464 -65
  33. package/src/runtime/bridgeScopedContext.ts +55 -0
  34. package/src/runtime/bridgeServer.test.ts +404 -4
  35. package/src/runtime/bridgeServer.ts +125 -35
  36. package/src/runtime/bridgeSetupHandlers.test.ts +69 -6
  37. package/src/runtime/bridgeSetupHandlers.ts +45 -10
  38. package/src/runtime/serve.ts +45 -3
  39. package/src/steam/steamcmd.test.ts +32 -1
  40. package/src/steam/steamcmd.ts +27 -4
  41. package/src/utils/changedSettingKeys.test.ts +65 -0
  42. package/src/utils/changedSettingKeys.ts +6 -0
  43. package/src/utils/describeExecFailure.test.ts +34 -0
  44. package/src/utils/describeExecFailure.ts +9 -0
  45. package/src/utils/index.ts +3 -0
  46. package/src/utils/sameSettingValue.test.ts +35 -0
  47. package/src/utils/sameSettingValue.ts +40 -0
  48. package/src/wire/activity.test.ts +95 -0
  49. package/src/wire/activity.ts +41 -0
  50. package/src/wire/index.ts +1 -0
  51. package/src/wire/panel.test.ts +67 -1
  52. package/src/wire/panel.ts +11 -0
@@ -1,6 +1,7 @@
1
1
  export * from "../core";
2
2
  export * from "../panel";
3
3
  export * from "../terminal";
4
+ export * from "./bridgeActivity";
4
5
  export * from "./bridgeCatalog";
5
6
  export * from "./bridgeConfig";
6
7
  export * from "./bridgeCrash";
@@ -1,10 +1,21 @@
1
1
  import { BridgeAction, BridgeKind } from "../core";
2
2
  import type { BridgePanelModule } from "../modules";
3
3
 
4
+ const advertisedAvailability = (advertised: boolean): string[] => {
5
+ return advertised
6
+ ? [
7
+ BridgeAction.Availability,
8
+ ]
9
+ : [];
10
+ };
11
+
4
12
  export const actionsOf = (module: BridgePanelModule): string[] => {
5
13
  switch (module.kind) {
6
14
  case BridgeKind.Actions: {
7
- return Object.keys(module.actions);
15
+ return [
16
+ ...Object.keys(module.actions),
17
+ ...advertisedAvailability(module.unavailable !== undefined),
18
+ ];
8
19
  }
9
20
 
10
21
  case BridgeKind.Options: {
@@ -27,6 +38,7 @@ export const actionsOf = (module: BridgePanelModule): string[] => {
27
38
  BridgeAction.Write,
28
39
  ]
29
40
  : []),
41
+ ...advertisedAvailability(module.unavailable !== undefined),
30
42
  ];
31
43
  }
32
44
 
@@ -39,13 +51,17 @@ export const actionsOf = (module: BridgePanelModule): string[] => {
39
51
  ]
40
52
  : []),
41
53
  ...Object.keys(module.actions ?? {}),
54
+ ...advertisedAvailability(module.unavailable !== undefined),
42
55
  ];
43
56
  }
44
57
 
45
58
  case BridgeKind.Detail: {
59
+ const actions = Object.keys(module.actions ?? {});
60
+
46
61
  return [
47
62
  BridgeAction.Read,
48
- ...Object.keys(module.actions ?? {}),
63
+ ...actions,
64
+ ...advertisedAvailability(actions.length > 0),
49
65
  ];
50
66
  }
51
67
 
@@ -70,6 +86,7 @@ export const actionsOf = (module: BridgePanelModule): string[] => {
70
86
  BridgeAction.Toggle,
71
87
  ]
72
88
  : []),
89
+ ...advertisedAvailability(module.unavailable !== undefined),
73
90
  ];
74
91
  }
75
92
  }
@@ -0,0 +1,318 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { BridgeActivity, BridgeValues } from "../core";
3
+ import {
4
+ BRIDGE_ACTIVITY_DETAILS_LIMIT,
5
+ BRIDGE_ACTIVITY_KEYS,
6
+ BRIDGE_ACTIVITY_LIMIT,
7
+ BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT,
8
+ BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT,
9
+ BridgeEventName,
10
+ } from "../protocol";
11
+ import {
12
+ type ActivityNormalizeOptions,
13
+ createActivityReporter,
14
+ createActivityScope,
15
+ normalizeActivity,
16
+ } from "./bridgeActivity";
17
+
18
+ const SECRET = "tok-1234567890-secret";
19
+
20
+ const recorder = () => {
21
+ const warnings: {
22
+ message: string;
23
+ fields?: Record<string, unknown>;
24
+ }[] = [];
25
+
26
+ const emitted: {
27
+ event: string;
28
+ payload: BridgeValues;
29
+ }[] = [];
30
+
31
+ const options: ActivityNormalizeOptions = {
32
+ secretKeys: new Set([
33
+ "RCON_PASSWORD",
34
+ ]),
35
+ redact: (text) => text.split(SECRET).join("••••"),
36
+ warn: (message, fields) => {
37
+ warnings.push({
38
+ message,
39
+ fields,
40
+ });
41
+ },
42
+ };
43
+
44
+ const reporter = createActivityReporter({
45
+ ...options,
46
+ onEmit(event, payload) {
47
+ emitted.push({
48
+ event,
49
+ payload,
50
+ });
51
+ },
52
+ });
53
+
54
+ return {
55
+ options,
56
+ reporter,
57
+ warnings,
58
+ emitted,
59
+ };
60
+ };
61
+
62
+ const granted = (subject = "Steve"): BridgeActivity => ({
63
+ name: BridgeEventName.AccessGranted,
64
+ text: {
65
+ ar: `أضفت ${subject} للقائمة البيضاء`,
66
+ en: `Added ${subject} to the whitelist`,
67
+ },
68
+ subject,
69
+ });
70
+
71
+ describe("normalizeActivity", () => {
72
+ test("keeps a well-formed entry as it is", () => {
73
+ const { options, warnings } = recorder();
74
+
75
+ expect(normalizeActivity(granted(), options)).toEqual(granted());
76
+ expect(warnings).toEqual([]);
77
+ });
78
+
79
+ test("drops a name the protocol does not know, with a warning", () => {
80
+ const { options, warnings } = recorder();
81
+
82
+ expect(
83
+ normalizeActivity(
84
+ {
85
+ ...granted(),
86
+ name: "WhitelistAdded" as BridgeEventName,
87
+ },
88
+ options,
89
+ ),
90
+ ).toBeNull();
91
+ expect(warnings).toEqual([
92
+ {
93
+ message: "activity entry normalized",
94
+ fields: {
95
+ name: "WhitelistAdded",
96
+ reason: "unknown-name",
97
+ },
98
+ },
99
+ ]);
100
+ });
101
+
102
+ test("drops an entry whose text is empty in one language", () => {
103
+ const { options } = recorder();
104
+
105
+ expect(
106
+ normalizeActivity(
107
+ {
108
+ ...granted(),
109
+ text: {
110
+ ar: " ",
111
+ en: "Added Steve",
112
+ },
113
+ },
114
+ options,
115
+ ),
116
+ ).toBeNull();
117
+ });
118
+
119
+ test("cuts the text and the subject to their limits", () => {
120
+ const { options, warnings } = recorder();
121
+
122
+ const normalized = normalizeActivity(
123
+ {
124
+ ...granted(),
125
+ text: {
126
+ ar: "ا".repeat(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT + 10),
127
+ en: "a".repeat(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT + 10),
128
+ },
129
+ subject: "s".repeat(BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT + 10),
130
+ },
131
+ options,
132
+ );
133
+
134
+ expect(normalized?.text.ar).toHaveLength(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT);
135
+ expect(normalized?.text.en).toHaveLength(BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT);
136
+ expect(normalized?.subject).toHaveLength(BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT);
137
+ expect(warnings.map((warning) => warning.fields?.reason)).toEqual([
138
+ "long-text",
139
+ "long-subject",
140
+ ]);
141
+ });
142
+
143
+ test("keeps at most sixteen details", () => {
144
+ const { options } = recorder();
145
+
146
+ const details: BridgeValues = {};
147
+
148
+ for (let index = 0; index < BRIDGE_ACTIVITY_DETAILS_LIMIT + 4; index++) {
149
+ details[`key${index}`] = index;
150
+ }
151
+
152
+ const normalized = normalizeActivity(
153
+ {
154
+ ...granted(),
155
+ details,
156
+ },
157
+ options,
158
+ );
159
+
160
+ expect(Object.keys(normalized?.details ?? {})).toHaveLength(BRIDGE_ACTIVITY_DETAILS_LIMIT);
161
+ });
162
+
163
+ test("removes secret fields, reserved keys and values that are not scalars", () => {
164
+ const { options } = recorder();
165
+
166
+ const normalized = normalizeActivity(
167
+ {
168
+ ...granted(),
169
+ details: {
170
+ RCON_PASSWORD: "hunter22hunter22",
171
+ [BRIDGE_ACTIVITY_KEYS.ar]: "مزوّر",
172
+ "bad key": "x",
173
+ nested: {
174
+ deep: true,
175
+ } as unknown as string,
176
+ count: Number.NaN,
177
+ from: "1.20.4",
178
+ },
179
+ },
180
+ options,
181
+ );
182
+
183
+ expect(normalized?.details).toEqual({
184
+ from: "1.20.4",
185
+ });
186
+ });
187
+
188
+ test("blanks a secret value in the text, the subject and the details", () => {
189
+ const { options } = recorder();
190
+
191
+ const normalized = normalizeActivity(
192
+ {
193
+ name: BridgeEventName.BroadcastSent,
194
+ text: {
195
+ ar: `أرسلت ${SECRET}`,
196
+ en: `Sent ${SECRET}`,
197
+ },
198
+ subject: SECRET,
199
+ details: {
200
+ message: `hello ${SECRET}`,
201
+ },
202
+ },
203
+ options,
204
+ );
205
+
206
+ expect(JSON.stringify(normalized)).not.toContain(SECRET);
207
+ expect(normalized?.details).toEqual({
208
+ message: "hello ••••",
209
+ });
210
+ });
211
+ });
212
+
213
+ describe("createActivityReporter", () => {
214
+ test("emits the entry as an event carrying the text under the reserved keys", () => {
215
+ const { reporter, emitted } = recorder();
216
+
217
+ reporter.report({
218
+ ...granted(),
219
+ details: {
220
+ source: "panel",
221
+ },
222
+ });
223
+
224
+ expect(emitted).toEqual([
225
+ {
226
+ event: BridgeEventName.AccessGranted,
227
+ payload: {
228
+ source: "panel",
229
+ [BRIDGE_ACTIVITY_KEYS.ar]: "أضفت Steve للقائمة البيضاء",
230
+ [BRIDGE_ACTIVITY_KEYS.en]: "Added Steve to the whitelist",
231
+ [BRIDGE_ACTIVITY_KEYS.subject]: "Steve",
232
+ },
233
+ },
234
+ ]);
235
+ });
236
+
237
+ test("emits nothing for an entry normalisation drops", () => {
238
+ const { reporter, emitted } = recorder();
239
+
240
+ reporter.report({
241
+ ...granted(),
242
+ name: "Nope" as BridgeEventName,
243
+ });
244
+
245
+ expect(emitted).toEqual([]);
246
+ });
247
+ });
248
+
249
+ describe("createActivityScope", () => {
250
+ test("collects entries for the answer instead of emitting them", () => {
251
+ const { options, reporter, emitted } = recorder();
252
+ const scope = createActivityScope(reporter, options);
253
+
254
+ scope.reporter.report(granted("Steve"));
255
+ scope.reporter.report(granted("Alex"));
256
+
257
+ expect(scope.reported).toBe(2);
258
+ expect(scope.close()).toEqual([
259
+ granted("Steve"),
260
+ granted("Alex"),
261
+ ]);
262
+ expect(emitted).toEqual([]);
263
+ });
264
+
265
+ test("drains what it collected to the ring when the handler throws", () => {
266
+ const { options, reporter, emitted } = recorder();
267
+ const scope = createActivityScope(reporter, options);
268
+
269
+ scope.reporter.report(granted());
270
+ scope.drain();
271
+
272
+ expect(emitted.map((entry) => entry.event)).toEqual([
273
+ BridgeEventName.AccessGranted,
274
+ ]);
275
+ expect(scope.close()).toEqual([]);
276
+ });
277
+
278
+ test("sends a report made after the answer to the ring", () => {
279
+ const { options, reporter, emitted } = recorder();
280
+ const scope = createActivityScope(reporter, options);
281
+
282
+ expect(scope.close()).toEqual([]);
283
+
284
+ scope.reporter.report(granted());
285
+
286
+ expect(emitted.map((entry) => entry.payload[BRIDGE_ACTIVITY_KEYS.subject])).toEqual([
287
+ "Steve",
288
+ ]);
289
+ });
290
+
291
+ test("keeps at most eight entries on one answer", () => {
292
+ const { options, reporter, warnings } = recorder();
293
+ const scope = createActivityScope(reporter, options);
294
+
295
+ for (let index = 0; index < BRIDGE_ACTIVITY_LIMIT + 2; index++) {
296
+ scope.reporter.report(granted(`player${index}`));
297
+ }
298
+
299
+ expect(scope.close()).toHaveLength(BRIDGE_ACTIVITY_LIMIT);
300
+ expect(warnings.filter((warning) => warning.fields?.reason === "extra-entries")).toHaveLength(2);
301
+ });
302
+
303
+ test("never throws into the caller, whatever the entry holds", () => {
304
+ const { options, reporter, warnings } = recorder();
305
+ const scope = createActivityScope(reporter, options);
306
+
307
+ const hostile = {
308
+ ...granted(),
309
+ get details(): BridgeValues {
310
+ throw new Error("a getter that throws");
311
+ },
312
+ };
313
+
314
+ expect(() => scope.reporter.report(hostile)).not.toThrow();
315
+ expect(() => reporter.report(hostile)).not.toThrow();
316
+ expect(warnings.filter((warning) => warning.message === "activity report failed")).toHaveLength(2);
317
+ });
318
+ });
@@ -0,0 +1,268 @@
1
+ import type { BridgeActivityReporter } from "../context";
2
+ import { type BridgeActivity, type BridgeValue, type BridgeValues, isLocalizedText } from "../core";
3
+ import {
4
+ activityPayload,
5
+ BRIDGE_ACTIVITY_DETAIL_KEY_LENGTH_LIMIT,
6
+ BRIDGE_ACTIVITY_DETAIL_KEY_PATTERN,
7
+ BRIDGE_ACTIVITY_DETAILS_LIMIT,
8
+ BRIDGE_ACTIVITY_KEYS,
9
+ BRIDGE_ACTIVITY_LIMIT,
10
+ BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT,
11
+ BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT,
12
+ isBridgeEventName,
13
+ } from "../protocol";
14
+ import { type BridgeLogger, errorText } from "./bridgeLog";
15
+ import type { BridgeRedact } from "./bridgeRedaction";
16
+
17
+ export interface ActivityNormalizeOptions {
18
+ secretKeys: ReadonlySet<string>;
19
+ redact: BridgeRedact;
20
+ warn: BridgeLogger;
21
+ }
22
+
23
+ export interface ActivityReporterOptions extends ActivityNormalizeOptions {
24
+ onEmit: (event: string, payload: BridgeValues) => void;
25
+ }
26
+
27
+ export interface BridgeActivityAnswer<Result> {
28
+ result: Result;
29
+ activity: BridgeActivity[];
30
+ }
31
+
32
+ export interface BridgeActivityScope {
33
+ readonly reporter: BridgeActivityReporter;
34
+ readonly reported: number;
35
+ close(): BridgeActivity[];
36
+ drain(): void;
37
+ }
38
+
39
+ const RESERVED_KEYS: ReadonlySet<string> = new Set(Object.values(BRIDGE_ACTIVITY_KEYS));
40
+
41
+ const cut = (value: string, limit: number, reason: string, report: (reason: string) => void) => {
42
+ if (value.length <= limit) {
43
+ return value;
44
+ }
45
+
46
+ report(`long-${reason}`);
47
+
48
+ return value.slice(0, limit);
49
+ };
50
+
51
+ const isScalar = (value: unknown): value is BridgeValue => {
52
+ return (
53
+ value === null
54
+ || typeof value === "boolean"
55
+ || typeof value === "string"
56
+ || (typeof value === "number" && Number.isFinite(value))
57
+ );
58
+ };
59
+
60
+ const normalizeDetails = (
61
+ details: unknown,
62
+ options: ActivityNormalizeOptions,
63
+ report: (reason: string) => void,
64
+ ): BridgeValues | null => {
65
+ if (details === undefined || details === null) {
66
+ return null;
67
+ }
68
+
69
+ if (typeof details !== "object" || Array.isArray(details)) {
70
+ report("unusable-details");
71
+
72
+ return null;
73
+ }
74
+
75
+ const kept: BridgeValues = {};
76
+
77
+ let count = 0;
78
+
79
+ for (const [key, value] of Object.entries(details)) {
80
+ if (RESERVED_KEYS.has(key)) {
81
+ report("reserved-detail");
82
+
83
+ continue;
84
+ }
85
+
86
+ if (options.secretKeys.has(key)) {
87
+ report("secret-detail");
88
+
89
+ continue;
90
+ }
91
+
92
+ if (key.length > BRIDGE_ACTIVITY_DETAIL_KEY_LENGTH_LIMIT || !BRIDGE_ACTIVITY_DETAIL_KEY_PATTERN.test(key)) {
93
+ report("unusable-detail-key");
94
+
95
+ continue;
96
+ }
97
+
98
+ if (!isScalar(value)) {
99
+ report("unusable-detail-value");
100
+
101
+ continue;
102
+ }
103
+
104
+ if (count >= BRIDGE_ACTIVITY_DETAILS_LIMIT) {
105
+ report("extra-details");
106
+
107
+ break;
108
+ }
109
+
110
+ kept[key] =
111
+ typeof value === "string"
112
+ ? cut(options.redact(value), BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT, "detail", report)
113
+ : value;
114
+ count += 1;
115
+ }
116
+
117
+ return count > 0 ? kept : null;
118
+ };
119
+
120
+ export const normalizeActivity = (entry: BridgeActivity, options: ActivityNormalizeOptions): BridgeActivity | null => {
121
+ const name = String(entry.name);
122
+ const reported = new Set<string>();
123
+
124
+ const report = (reason: string) => {
125
+ if (reported.has(reason)) {
126
+ return;
127
+ }
128
+
129
+ reported.add(reason);
130
+
131
+ options.warn("activity entry normalized", {
132
+ name,
133
+ reason,
134
+ });
135
+ };
136
+
137
+ if (!isBridgeEventName(name)) {
138
+ report("unknown-name");
139
+
140
+ return null;
141
+ }
142
+
143
+ if (!isLocalizedText(entry.text)) {
144
+ report("unusable-text");
145
+
146
+ return null;
147
+ }
148
+
149
+ const ar = cut(options.redact(entry.text.ar).trim(), BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT, "text", report);
150
+ const en = cut(options.redact(entry.text.en).trim(), BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT, "text", report);
151
+
152
+ if (ar.length === 0 || en.length === 0) {
153
+ report("empty-text");
154
+
155
+ return null;
156
+ }
157
+
158
+ const subject =
159
+ typeof entry.subject === "string"
160
+ ? cut(options.redact(entry.subject).trim(), BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT, "subject", report)
161
+ : "";
162
+
163
+ if (entry.subject !== undefined && subject.length === 0) {
164
+ report("empty-subject");
165
+ }
166
+
167
+ const details = normalizeDetails(entry.details, options, report);
168
+
169
+ return {
170
+ name: entry.name,
171
+ text: {
172
+ ar,
173
+ en,
174
+ },
175
+ ...(subject.length > 0
176
+ ? {
177
+ subject,
178
+ }
179
+ : {}),
180
+ ...(details
181
+ ? {
182
+ details,
183
+ }
184
+ : {}),
185
+ };
186
+ };
187
+
188
+ const safely = (options: ActivityNormalizeOptions, run: () => void) => {
189
+ try {
190
+ run();
191
+ } catch (error) {
192
+ options.warn("activity report failed", {
193
+ error: errorText(error),
194
+ });
195
+ }
196
+ };
197
+
198
+ export const createActivityReporter = (options: ActivityReporterOptions): BridgeActivityReporter => {
199
+ return {
200
+ report(entry) {
201
+ safely(options, () => {
202
+ const normalized = normalizeActivity(entry, options);
203
+
204
+ if (normalized) {
205
+ options.onEmit(normalized.name, activityPayload(normalized));
206
+ }
207
+ });
208
+ },
209
+ };
210
+ };
211
+
212
+ export const createActivityScope = (
213
+ fallback: BridgeActivityReporter,
214
+ options: ActivityNormalizeOptions,
215
+ ): BridgeActivityScope => {
216
+ const entries: BridgeActivity[] = [];
217
+
218
+ let open = true;
219
+
220
+ return {
221
+ reporter: {
222
+ report(entry) {
223
+ safely(options, () => {
224
+ if (!open) {
225
+ fallback.report(entry);
226
+
227
+ return;
228
+ }
229
+
230
+ const normalized = normalizeActivity(entry, options);
231
+
232
+ if (!normalized) {
233
+ return;
234
+ }
235
+
236
+ if (entries.length >= BRIDGE_ACTIVITY_LIMIT) {
237
+ options.warn("activity entry dropped", {
238
+ name: normalized.name,
239
+ reason: "extra-entries",
240
+ });
241
+
242
+ return;
243
+ }
244
+
245
+ entries.push(normalized);
246
+ });
247
+ },
248
+ },
249
+
250
+ get reported() {
251
+ return entries.length;
252
+ },
253
+
254
+ close() {
255
+ open = false;
256
+
257
+ return entries.splice(0);
258
+ },
259
+
260
+ drain() {
261
+ open = false;
262
+
263
+ for (const entry of entries.splice(0)) {
264
+ fallback.report(entry);
265
+ }
266
+ },
267
+ };
268
+ };
@@ -72,6 +72,9 @@ describe("the context's variable maps", () => {
72
72
  onSetup() {
73
73
  return;
74
74
  },
75
+ onActivity() {
76
+ return;
77
+ },
75
78
  async onRestart() {
76
79
  return;
77
80
  },
@@ -158,6 +161,9 @@ describe("the context's ports and address", () => {
158
161
  onSetup() {
159
162
  return;
160
163
  },
164
+ onActivity() {
165
+ return;
166
+ },
161
167
  async onRestart() {
162
168
  return;
163
169
  },
@@ -1,5 +1,5 @@
1
1
  import type { BridgeContext } from "../context";
2
- import type { BridgeValues } from "../core";
2
+ import type { BridgeActivity, BridgeValues } from "../core";
3
3
  import type { BridgeConfig, BridgeSetupRuntime } from "../protocol";
4
4
  import { stripAnsi } from "../utils";
5
5
  import { createBridgeCodecs } from "./bridgeCodec";
@@ -18,6 +18,7 @@ export interface ContextHooks {
18
18
  onLog: BridgeLeveledLogger;
19
19
  onEmit: (event: string, payload?: BridgeValues) => void;
20
20
  onSetup: (runtime: BridgeSetupRuntime | null) => void;
21
+ onActivity: (entry: BridgeActivity) => void;
21
22
  onRestart: () => Promise<void>;
22
23
  }
23
24
 
@@ -141,6 +142,12 @@ export const createContext = (
141
142
  },
142
143
  },
143
144
 
145
+ activity: {
146
+ report(entry) {
147
+ hooks.onActivity(entry);
148
+ },
149
+ },
150
+
144
151
  emit(event, payload) {
145
152
  hooks.onEmit(event, payload);
146
153
  },