@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,17 +1,50 @@
1
1
  import type { BridgeContext } from "../context";
2
- import { BridgeAction, BridgeKind, type BridgeRow, type BridgeValues, isReadAction } from "../core";
3
- import type { BridgeDriver, BridgePanelModule, BridgeSetupModule, BridgeTerminalModule } from "../modules";
4
- import { BridgeLayout } from "../panel";
5
2
  import {
3
+ BRIDGE_AVAILABILITY_KEY_PATTERN,
4
+ BridgeAction,
5
+ type BridgeActivity,
6
+ type BridgeAvailability,
7
+ BridgeKind,
8
+ type BridgeRow,
9
+ type BridgeUnavailable,
10
+ type BridgeValues,
11
+ isLocalizedText,
12
+ isReadAction,
13
+ type LocalizedText,
14
+ } from "../core";
15
+ import type {
16
+ BridgeCatalogModule,
17
+ BridgeCollectionModule,
18
+ BridgeDriver,
19
+ BridgePanelModule,
20
+ BridgeSettingsModule,
21
+ BridgeSetupModule,
22
+ BridgeTerminalModule,
23
+ } from "../modules";
24
+ import { type BridgeDetail, BridgeLayout } from "../panel";
25
+ import {
26
+ BRIDGE_KEY_LENGTH_LIMIT,
27
+ BRIDGE_TEXT_LENGTH_LIMIT,
6
28
  BRIDGE_VERSION,
29
+ BridgeEventName,
7
30
  type BridgeManifest,
8
31
  type BridgeModuleManifest,
9
32
  type BridgeSetupManifest,
10
33
  type BridgeTerminalManifest,
11
34
  } from "../protocol";
35
+ import { changedSettingKeys } from "../utils";
12
36
  import { actionsOf } from "./bridgeActions";
37
+ import {
38
+ type ActivityNormalizeOptions,
39
+ type BridgeActivityAnswer,
40
+ type BridgeActivityScope,
41
+ createActivityScope,
42
+ } from "./bridgeActivity";
13
43
  import { BridgeDriverError } from "./bridgeDriver";
14
44
  import type { BridgeEventRing } from "./bridgeEvents";
45
+ import { errorText } from "./bridgeLog";
46
+ import { BridgeRefusalError } from "./bridgeRefusalError";
47
+ import { scopedContext } from "./bridgeScopedContext";
15
48
  import type { BridgeSupervisor } from "./bridgeSupervisor";
16
49
 
17
50
  interface CacheEntry {
@@ -19,6 +52,11 @@ interface CacheEntry {
19
52
  expiresAt: number;
20
53
  }
21
54
 
55
+ const UNAVAILABLE_TEXT: LocalizedText = {
56
+ ar: "هذا الإجراء مو متاح الحين",
57
+ en: "This action is not available right now",
58
+ };
59
+
22
60
  const requiresRunning = (module: BridgePanelModule) => {
23
61
  if (
24
62
  module.kind === BridgeKind.Actions
@@ -87,53 +125,9 @@ const setupManifestOf = (module: BridgeSetupModule | undefined): BridgeSetupMani
87
125
  };
88
126
  };
89
127
 
90
- const withVariables = (context: BridgeContext, overlay: Record<string, string>): BridgeContext => {
91
- return {
92
- get server() {
93
- return context.server;
94
- },
95
- variable(key) {
96
- return Object.hasOwn(overlay, key) ? (overlay[key] ?? null) : context.variable(key);
97
- },
98
- variables() {
99
- return {
100
- ...context.variables(),
101
- ...overlay,
102
- };
103
- },
104
- secret(key) {
105
- return context.secret(key);
106
- },
107
- secrets() {
108
- return context.secrets();
109
- },
110
- port(key) {
111
- return context.port(key);
112
- },
113
- portBlock(key) {
114
- return context.portBlock(key);
115
- },
116
- files: context.files,
117
- codec: context.codec,
118
- net: context.net,
119
- probe: context.probe,
120
- rcon: context.rcon,
121
- command(input, options) {
122
- return context.command(input, options);
123
- },
124
- exec(command, options) {
125
- return context.exec(command, options);
126
- },
127
- logs: context.logs,
128
- setup: context.setup,
129
- emit(event, payload) {
130
- context.emit(event, payload);
131
- },
132
- restart() {
133
- return context.restart();
134
- },
135
- log: context.log,
136
- };
128
+ const SETTINGS_CHANGED_TEXT: LocalizedText = {
129
+ ar: "تم تغيير الإعدادات",
130
+ en: "Changed the settings",
137
131
  };
138
132
 
139
133
  const asString = (args: BridgeValues, key: string) => {
@@ -176,6 +170,65 @@ const asPage = (args: BridgeValues) => {
176
170
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
177
171
  };
178
172
 
173
+ const guardedActionsOf = (module: BridgePanelModule): string[] => {
174
+ switch (module.kind) {
175
+ case BridgeKind.Actions: {
176
+ return Object.keys(module.actions);
177
+ }
178
+
179
+ case BridgeKind.Collection:
180
+ case BridgeKind.Detail: {
181
+ return Object.keys(module.actions ?? {});
182
+ }
183
+
184
+ case BridgeKind.Settings: {
185
+ return module.write
186
+ ? [
187
+ BridgeAction.Write,
188
+ ]
189
+ : [];
190
+ }
191
+
192
+ case BridgeKind.Catalog: {
193
+ return [
194
+ BridgeAction.Remove,
195
+ ...(module.toggle
196
+ ? [
197
+ BridgeAction.Toggle,
198
+ ]
199
+ : []),
200
+ ];
201
+ }
202
+
203
+ default: {
204
+ return [];
205
+ }
206
+ }
207
+ };
208
+
209
+ const isAvailabilityKey = (key: string) => {
210
+ return key.length <= BRIDGE_KEY_LENGTH_LIMIT && BRIDGE_AVAILABILITY_KEY_PATTERN.test(key);
211
+ };
212
+
213
+ const refuse = (name: string, action: string, reasons: BridgeUnavailable) => {
214
+ const text = reasons[action];
215
+
216
+ if (text) {
217
+ throw new BridgeRefusalError(`module "${name}" cannot run "${action}" right now`, text);
218
+ }
219
+ };
220
+
221
+ const settingsChanged = (keys: string[]): BridgeActivity => {
222
+ return {
223
+ name: BridgeEventName.SettingsChanged,
224
+ text: SETTINGS_CHANGED_TEXT,
225
+ details: {
226
+ keys: keys.join(","),
227
+ count: keys.length,
228
+ },
229
+ };
230
+ };
231
+
179
232
  const offlineActionsOf = (driver: BridgeDriver): Map<string, Set<string>> => {
180
233
  const offline = new Map<string, Set<string>>();
181
234
 
@@ -201,9 +254,10 @@ const offlineActionsOf = (driver: BridgeDriver): Map<string, Set<string>> => {
201
254
 
202
255
  export const createRegistry = (
203
256
  driver: BridgeDriver,
204
- context: BridgeContext,
257
+ base: BridgeContext,
205
258
  supervisor: BridgeSupervisor,
206
259
  ring: BridgeEventRing,
260
+ activity: ActivityNormalizeOptions,
207
261
  ) => {
208
262
  const cache = new Map<string, CacheEntry>();
209
263
  const inflight = new Map<string, Promise<unknown>>();
@@ -211,7 +265,11 @@ export const createRegistry = (
211
265
  const offlineActions = offlineActionsOf(driver);
212
266
 
213
267
  for (const [id, module] of Object.entries(modules)) {
214
- if (module.kind !== BridgeKind.Actions && module.kind !== BridgeKind.Detail) {
268
+ if (
269
+ module.kind !== BridgeKind.Actions
270
+ && module.kind !== BridgeKind.Collection
271
+ && module.kind !== BridgeKind.Detail
272
+ ) {
215
273
  continue;
216
274
  }
217
275
 
@@ -262,12 +320,286 @@ export const createRegistry = (
262
320
  }
263
321
  };
264
322
 
265
- const dispatch = async (module: BridgePanelModule, name: string, action: string, args: BridgeValues) => {
323
+ const listings = new Map<string, Promise<BridgeRow[]>>();
324
+
325
+ const listFresh = (module: BridgeCollectionModule, name: string, context: BridgeContext) => {
326
+ const rows = Promise.resolve().then(() => module.list(context));
327
+
328
+ listings.set(name, rows);
329
+
330
+ const settle = () => {
331
+ if (listings.get(name) === rows) {
332
+ listings.delete(name);
333
+ }
334
+ };
335
+
336
+ rows.then(settle, settle);
337
+
338
+ return rows;
339
+ };
340
+
341
+ const listForAvailability = (module: BridgeCollectionModule, name: string, context: BridgeContext) => {
342
+ return listings.get(name) ?? listFresh(module, name, context);
343
+ };
344
+
345
+ const reasonOf = (text: LocalizedText): LocalizedText => {
346
+ const ar = activity.redact(text.ar.trim()).slice(0, BRIDGE_TEXT_LENGTH_LIMIT);
347
+ const en = activity.redact(text.en.trim()).slice(0, BRIDGE_TEXT_LENGTH_LIMIT);
348
+
349
+ return ar.length > 0 && en.length > 0
350
+ ? {
351
+ ar,
352
+ en,
353
+ }
354
+ : UNAVAILABLE_TEXT;
355
+ };
356
+
357
+ const unavailableOf = (name: string, raw: unknown, guarded: readonly string[]): BridgeUnavailable => {
358
+ const kept: BridgeUnavailable = {};
359
+
360
+ if (raw === undefined || raw === null) {
361
+ return kept;
362
+ }
363
+
364
+ if (typeof raw !== "object" || Array.isArray(raw)) {
365
+ activity.warn("availability answer is not an object, it is ignored", {
366
+ module: name,
367
+ });
368
+
369
+ return kept;
370
+ }
371
+
372
+ const dropped: string[] = [];
373
+
374
+ for (const [action, text] of Object.entries(raw)) {
375
+ if (!guarded.includes(action) || !isAvailabilityKey(action) || !isLocalizedText(text)) {
376
+ dropped.push(action);
377
+
378
+ continue;
379
+ }
380
+
381
+ kept[action] = reasonOf(text);
382
+ }
383
+
384
+ if (dropped.length > 0) {
385
+ activity.warn("availability names actions the module cannot refuse, they are ignored", {
386
+ module: name,
387
+ actions: dropped.join(","),
388
+ });
389
+ }
390
+
391
+ return kept;
392
+ };
393
+
394
+ const isSubjectId = (name: string, id: unknown): id is string => {
395
+ if (typeof id === "string" && id.length > 0) {
396
+ return true;
397
+ }
398
+
399
+ activity.warn("availability skipped a subject without an id", {
400
+ module: name,
401
+ });
402
+
403
+ return false;
404
+ };
405
+
406
+ const askUnavailable = async (
407
+ name: string,
408
+ guarded: readonly string[],
409
+ ask: () => Promise<unknown> | undefined,
410
+ ): Promise<BridgeUnavailable> => {
411
+ try {
412
+ return unavailableOf(name, await ask(), guarded);
413
+ } catch (error) {
414
+ activity.warn("availability could not be read, the actions count as available", {
415
+ module: name,
416
+ error: errorText(error),
417
+ });
418
+
419
+ return {};
420
+ }
421
+ };
422
+
423
+ const detailUnavailable = (name: string, detail: BridgeDetail, guarded: readonly string[]): BridgeUnavailable => {
424
+ const reasons = unavailableOf(name, detail.unavailable, guarded);
425
+ const pressable = Array.isArray(detail.actions) ? detail.actions : [];
426
+ const closed: BridgeUnavailable = {};
427
+
428
+ for (const action of guarded) {
429
+ if (!pressable.includes(action) && isAvailabilityKey(action)) {
430
+ closed[action] = reasons[action] ?? UNAVAILABLE_TEXT;
431
+ }
432
+ }
433
+
434
+ return closed;
435
+ };
436
+
437
+ const availabilityOf = async (
438
+ module: BridgePanelModule,
439
+ name: string,
440
+ args: BridgeValues,
441
+ context: BridgeContext,
442
+ ): Promise<BridgeAvailability> => {
443
+ const guarded = guardedActionsOf(module);
444
+ const id = asOptional(args, "id");
445
+ const availability: BridgeAvailability = {
446
+ module: {},
447
+ subjects: {},
448
+ };
449
+
450
+ switch (module.kind) {
451
+ case BridgeKind.Collection: {
452
+ if (!module.unavailable) {
453
+ break;
454
+ }
455
+
456
+ for (const row of await listForAvailability(module, name, context)) {
457
+ if (!isSubjectId(name, row.id)) {
458
+ continue;
459
+ }
460
+
461
+ if (id !== null && row.id !== id) {
462
+ continue;
463
+ }
464
+
465
+ const reasons = await askUnavailable(name, guarded, () => module.unavailable?.(context, row));
466
+
467
+ if (Object.keys(reasons).length > 0) {
468
+ availability.subjects[row.id] = reasons;
469
+ }
470
+ }
471
+
472
+ break;
473
+ }
474
+
475
+ case BridgeKind.Catalog: {
476
+ if (!module.unavailable) {
477
+ break;
478
+ }
479
+
480
+ for (const entry of await module.installed(context)) {
481
+ if (!isSubjectId(name, entry.id)) {
482
+ continue;
483
+ }
484
+
485
+ if (id !== null && entry.id !== id) {
486
+ continue;
487
+ }
488
+
489
+ const reasons = await askUnavailable(name, guarded, () => module.unavailable?.(context, entry));
490
+
491
+ if (Object.keys(reasons).length > 0) {
492
+ availability.subjects[entry.id] = reasons;
493
+ }
494
+ }
495
+
496
+ break;
497
+ }
498
+
499
+ case BridgeKind.Actions:
500
+ case BridgeKind.Settings: {
501
+ availability.module = await askUnavailable(name, guarded, () => module.unavailable?.(context));
502
+
503
+ break;
504
+ }
505
+
506
+ case BridgeKind.Detail: {
507
+ const detail = await module.read(context);
508
+
509
+ if (detail) {
510
+ availability.module = detailUnavailable(name, detail, guarded);
511
+ }
512
+
513
+ break;
514
+ }
515
+ }
516
+
517
+ return availability;
518
+ };
519
+
520
+ const refuseEntry = async (
521
+ module: BridgeCatalogModule,
522
+ name: string,
523
+ action: string,
524
+ id: string,
525
+ context: BridgeContext,
526
+ ) => {
527
+ if (!module.unavailable) {
528
+ return;
529
+ }
530
+
531
+ refuse(
532
+ name,
533
+ action,
534
+ await askUnavailable(name, guardedActionsOf(module), async () => {
535
+ const entry = (await module.installed(context)).find((installed) => installed.id === id);
536
+
537
+ return entry ? await module.unavailable?.(context, entry) : undefined;
538
+ }),
539
+ );
540
+ };
541
+
542
+ const readBefore = async (module: BridgeSettingsModule, scoped: BridgeContext, name: string) => {
543
+ try {
544
+ return {
545
+ ...(await module.read(scoped)),
546
+ };
547
+ } catch (error) {
548
+ activity.warn("settings could not be read before the write", {
549
+ module: name,
550
+ error: errorText(error),
551
+ });
552
+
553
+ return null;
554
+ }
555
+ };
556
+
557
+ const reportSettingsDefault = (
558
+ scope: BridgeActivityScope,
559
+ before: BridgeValues | null,
560
+ written: BridgeValues,
561
+ name: string,
562
+ ) => {
563
+ try {
564
+ const keys = changedSettingKeys(before, written);
565
+
566
+ if (scope.reported === 0 && keys.length > 0) {
567
+ scope.reporter.report(settingsChanged(keys));
568
+ }
569
+ } catch (error) {
570
+ activity.warn("settings change could not be reported", {
571
+ module: name,
572
+ error: errorText(error),
573
+ });
574
+ }
575
+ };
576
+
577
+ const dispatch = async (
578
+ module: BridgePanelModule,
579
+ name: string,
580
+ action: string,
581
+ args: BridgeValues,
582
+ scope: BridgeActivityScope,
583
+ ) => {
584
+ const context = scopedContext(base, scope.reporter);
585
+
586
+ if (action === BridgeAction.Availability && actionsOf(module).includes(BridgeAction.Availability)) {
587
+ return await availabilityOf(module, name, args, context);
588
+ }
589
+
266
590
  switch (module.kind) {
267
591
  case BridgeKind.Actions: {
268
592
  const serverAction = module.actions[action];
269
593
 
270
594
  if (serverAction) {
595
+ if (module.unavailable) {
596
+ refuse(
597
+ name,
598
+ action,
599
+ await askUnavailable(name, guardedActionsOf(module), () => module.unavailable?.(context)),
600
+ );
601
+ }
602
+
271
603
  await serverAction(context, args);
272
604
 
273
605
  return null;
@@ -288,7 +620,7 @@ export const createRegistry = (
288
620
  }
289
621
  }
290
622
 
291
- const scoped = withVariables(context, overlay);
623
+ const scoped = scopedContext(base, scope.reporter, overlay);
292
624
 
293
625
  const key = [
294
626
  name,
@@ -311,7 +643,7 @@ export const createRegistry = (
311
643
  }
312
644
  }
313
645
 
314
- const scoped = withVariables(context, overlay);
646
+ const scoped = scopedContext(base, scope.reporter, overlay);
315
647
 
316
648
  return await module.preview(scoped, args);
317
649
  }
@@ -325,8 +657,20 @@ export const createRegistry = (
325
657
  }
326
658
 
327
659
  if (action === BridgeAction.Write && module.write) {
660
+ if (module.unavailable) {
661
+ refuse(
662
+ name,
663
+ action,
664
+ await askUnavailable(name, guardedActionsOf(module), () => module.unavailable?.(context)),
665
+ );
666
+ }
667
+
668
+ const before = await readBefore(module, context, name);
669
+
328
670
  await module.write(context, args);
329
671
 
672
+ reportSettingsDefault(scope, before, args, name);
673
+
330
674
  return null;
331
675
  }
332
676
 
@@ -335,7 +679,7 @@ export const createRegistry = (
335
679
 
336
680
  case BridgeKind.Collection: {
337
681
  if (action === BridgeAction.List) {
338
- return await module.list(context);
682
+ return await listFresh(module, name, context);
339
683
  }
340
684
 
341
685
  if (action === BridgeAction.Add && module.add) {
@@ -357,7 +701,17 @@ export const createRegistry = (
357
701
  : undefined);
358
702
 
359
703
  if (!row) {
360
- throw new BridgeDriverError(`module "${name}" has no row "${id}"`);
704
+ throw new BridgeRefusalError(`module "${name}" has no row "${id}"`);
705
+ }
706
+
707
+ if (module.unavailable) {
708
+ refuse(
709
+ name,
710
+ action,
711
+ await askUnavailable(name, guardedActionsOf(module), () =>
712
+ module.unavailable?.(context, row as BridgeRow),
713
+ ),
714
+ );
361
715
  }
362
716
 
363
717
  await rowAction(context, row as BridgeRow, withoutId(args));
@@ -376,6 +730,18 @@ export const createRegistry = (
376
730
  const detailAction = module.actions?.[action];
377
731
 
378
732
  if (detailAction) {
733
+ const guarded = guardedActionsOf(module);
734
+
735
+ refuse(
736
+ name,
737
+ action,
738
+ await askUnavailable(name, guarded, async () => {
739
+ const detail = await module.read(context);
740
+
741
+ return detail ? detailUnavailable(name, detail, guarded) : undefined;
742
+ }),
743
+ );
744
+
379
745
  return await detailAction(context, args);
380
746
  }
381
747
 
@@ -408,13 +774,19 @@ export const createRegistry = (
408
774
  }
409
775
 
410
776
  if (action === BridgeAction.Remove) {
411
- await module.remove(context, asString(args, "id"));
777
+ const id = asString(args, "id");
778
+
779
+ await refuseEntry(module, name, action, id, context);
780
+ await module.remove(context, id);
412
781
 
413
782
  return null;
414
783
  }
415
784
 
416
785
  if (action === BridgeAction.Toggle && module.toggle) {
417
- await module.toggle(context, asString(args, "id"), args.enabled !== false);
786
+ const id = asString(args, "id");
787
+
788
+ await refuseEntry(module, name, action, id, context);
789
+ await module.toggle(context, id, args.enabled !== false);
418
790
 
419
791
  return null;
420
792
  }
@@ -423,7 +795,7 @@ export const createRegistry = (
423
795
  }
424
796
  }
425
797
 
426
- throw new BridgeDriverError(`module "${name}" does not support "${action}"`);
798
+ throw new BridgeRefusalError(`module "${name}" does not support "${action}"`);
427
799
  };
428
800
 
429
801
  return {
@@ -456,22 +828,49 @@ export const createRegistry = (
456
828
  };
457
829
  },
458
830
 
459
- async invoke(name: string, action: string, args: BridgeValues = {}, recoveryOperationId?: string) {
831
+ async invoke(
832
+ name: string,
833
+ action: string,
834
+ args: BridgeValues = {},
835
+ recoveryOperationId?: string,
836
+ ): Promise<BridgeActivityAnswer<unknown>> {
460
837
  const module = modules[name];
461
838
 
462
839
  if (!module) {
463
- throw new BridgeDriverError(`no module named "${name}"`);
840
+ throw new BridgeRefusalError(`no module named "${name}"`);
464
841
  }
465
842
 
466
843
  if (protectedActionsOf(module).includes(action) && (!recoveryOperationId || supervisor.running)) {
467
- throw new BridgeDriverError("this action needs a recovery operation with the game stopped");
844
+ throw new BridgeRefusalError("this action needs a recovery operation with the game stopped");
468
845
  }
469
846
 
470
847
  if (requiresRunning(module) && !supervisor.running) {
471
- throw new BridgeDriverError(`module "${name}" needs the game to be running`);
848
+ throw new BridgeRefusalError(`module "${name}" needs the game to be running`);
849
+ }
850
+
851
+ const scope = createActivityScope(base.activity, activity);
852
+ const mutation = !isReadAction(action);
853
+
854
+ if (mutation) {
855
+ listings.clear();
472
856
  }
473
857
 
474
- return await dispatch(module, name, action, args);
858
+ try {
859
+ const result = await dispatch(module, name, action, args, scope);
860
+
861
+ return {
862
+ result,
863
+ activity: scope.close(),
864
+ };
865
+ } catch (error) {
866
+ scope.drain();
867
+
868
+ throw error;
869
+ } finally {
870
+ if (mutation) {
871
+ listings.clear();
872
+ }
873
+ }
475
874
  },
476
875
  };
477
876
  };
@@ -0,0 +1,55 @@
1
+ import type { BridgeActivityReporter, BridgeContext } from "../context";
2
+
3
+ export const scopedContext = (
4
+ context: BridgeContext,
5
+ activity: BridgeActivityReporter,
6
+ overlay: Record<string, string> = {},
7
+ ): BridgeContext => {
8
+ return {
9
+ get server() {
10
+ return context.server;
11
+ },
12
+ variable(key) {
13
+ return Object.hasOwn(overlay, key) ? (overlay[key] ?? null) : context.variable(key);
14
+ },
15
+ variables() {
16
+ return {
17
+ ...context.variables(),
18
+ ...overlay,
19
+ };
20
+ },
21
+ secret(key) {
22
+ return context.secret(key);
23
+ },
24
+ secrets() {
25
+ return context.secrets();
26
+ },
27
+ port(key) {
28
+ return context.port(key);
29
+ },
30
+ portBlock(key) {
31
+ return context.portBlock(key);
32
+ },
33
+ files: context.files,
34
+ codec: context.codec,
35
+ net: context.net,
36
+ probe: context.probe,
37
+ rcon: context.rcon,
38
+ command(input, options) {
39
+ return context.command(input, options);
40
+ },
41
+ exec(command, options) {
42
+ return context.exec(command, options);
43
+ },
44
+ logs: context.logs,
45
+ setup: context.setup,
46
+ activity,
47
+ emit(event, payload) {
48
+ context.emit(event, payload);
49
+ },
50
+ restart() {
51
+ return context.restart();
52
+ },
53
+ log: context.log,
54
+ };
55
+ };