@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serverkgg/bridge",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Serverk game bridge and driver runtime, wire protocol, manifest schema, and maintainer CLI",
5
5
  "homepage": "https://serverk.gg",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -213,4 +213,34 @@ describe("the bot modules", () => {
213
213
 
214
214
  await expect(module.actions[BotDeployAction.Redeploy]?.(context, {})).rejects.toThrow(BridgeUserError);
215
215
  });
216
+
217
+ test("greys out both deploy buttons with the refusal's words while a running bot has not taken a deploy", async () => {
218
+ const module = createBotDeployModule();
219
+ const pending = deployContext(true, [
220
+ DEPLOY_MARKER,
221
+ ]);
222
+
223
+ const unavailable = await module.unavailable?.(pending.context);
224
+
225
+ expect(Object.keys(unavailable ?? {}).sort()).toEqual([
226
+ BotDeployAction.Redeploy,
227
+ BotDeployAction.Reinstall,
228
+ ]);
229
+ await expect(module.actions[BotDeployAction.Redeploy]?.(pending.context, {})).rejects.toThrow(
230
+ unavailable?.[BotDeployAction.Redeploy]?.en,
231
+ );
232
+ });
233
+
234
+ test("offers both deploy buttons when nothing is pending or the bot is stopped", async () => {
235
+ const module = createBotDeployModule();
236
+
237
+ expect(await module.unavailable?.(deployContext(true).context)).toEqual({});
238
+ expect(
239
+ await module.unavailable?.(
240
+ deployContext(false, [
241
+ DEPLOY_MARKER,
242
+ ]).context,
243
+ ),
244
+ ).toEqual({});
245
+ });
216
246
  });
@@ -1,5 +1,5 @@
1
1
  import type { BridgeContext } from "../context";
2
- import { BridgeKind, BridgeUserError, type BridgeValues, type LocalizedText } from "../core";
2
+ import { BridgeKind, type BridgeUnavailable, BridgeUserError, type BridgeValues, type LocalizedText } from "../core";
3
3
  import type { BridgeActionsModule, BridgeDetailModule } from "../modules";
4
4
  import { BridgeIcon, BridgeLayout, BridgePlace, type BridgeTab } from "../panel";
5
5
  import { type DeployRequest, deployPending, requestDeploy } from "./botDeploy";
@@ -117,5 +117,13 @@ export const createBotDeployModule = (): BridgeActionsModule => {
117
117
  [BotDeployAction.Redeploy]: deploy(false),
118
118
  [BotDeployAction.Reinstall]: deploy(true),
119
119
  },
120
+ async unavailable(context): Promise<BridgeUnavailable> {
121
+ return (await deployPending(context))
122
+ ? {
123
+ [BotDeployAction.Redeploy]: DEPLOY_PENDING,
124
+ [BotDeployAction.Reinstall]: DEPLOY_PENDING,
125
+ }
126
+ : {};
127
+ },
120
128
  };
121
129
  };
@@ -16,7 +16,7 @@ import {
16
16
  type BridgeTab,
17
17
  type BridgeTableSection,
18
18
  } from "../panel";
19
- import { placementReportLines, runValidate, setupReportLines } from "./validate";
19
+ import { availabilityReportLines, placementReportLines, runValidate, setupReportLines } from "./validate";
20
20
 
21
21
  const text = (value: string) => ({
22
22
  ar: value,
@@ -602,3 +602,75 @@ describe("validating a bot package", () => {
602
602
  );
603
603
  });
604
604
  });
605
+
606
+ describe("availability report", () => {
607
+ const driverWithModules = (modules: BridgeDriver["modules"]): BridgeDriver => ({
608
+ install: {
609
+ kind: BridgeKind.Install,
610
+ run: async () => {
611
+ return;
612
+ },
613
+ },
614
+ lifecycle: {
615
+ kind: BridgeKind.Lifecycle,
616
+ command: async () => [
617
+ "run",
618
+ ],
619
+ },
620
+ modules,
621
+ });
622
+
623
+ test("warns about a protected module that cannot say when its action is unavailable", () => {
624
+ expect(
625
+ availabilityReportLines(
626
+ driverWithModules({
627
+ worlds: {
628
+ kind: BridgeKind.Collection,
629
+ protectedActions: [
630
+ "delete",
631
+ ],
632
+ list: async () => [],
633
+ actions: {
634
+ delete: async () => {
635
+ return;
636
+ },
637
+ },
638
+ },
639
+ }),
640
+ ),
641
+ ).toEqual([
642
+ ' warning: module "worlds" protects delete but declares no `unavailable` — the panel offers the action everywhere and the platform stops the game before the driver can refuse it',
643
+ ]);
644
+ });
645
+
646
+ test("says nothing when the protected module declares the hook or protects nothing", () => {
647
+ expect(
648
+ availabilityReportLines(
649
+ driverWithModules({
650
+ worlds: {
651
+ kind: BridgeKind.Collection,
652
+ protectedActions: [
653
+ "delete",
654
+ ],
655
+ list: async () => [],
656
+ actions: {
657
+ delete: async () => {
658
+ return;
659
+ },
660
+ },
661
+ unavailable: async () => ({}),
662
+ },
663
+ players: {
664
+ kind: BridgeKind.Collection,
665
+ list: async () => [],
666
+ actions: {
667
+ kick: async () => {
668
+ return;
669
+ },
670
+ },
671
+ },
672
+ }),
673
+ ),
674
+ ).toEqual([]);
675
+ });
676
+ });
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
5
5
  import { z } from "zod";
6
6
  import { type ArticlePack, compileArticles } from "../blog";
7
7
  import { DETECT_TABLE_FILE, renderDetectTable } from "../bot/detect";
8
- import { PortProtocol } from "../core";
8
+ import { BridgeKind, PortProtocol } from "../core";
9
9
  import { GuideOpenTab, type GuideOpenTarget, type GuidePack, readGuidePack, readGuidePackage } from "../guides";
10
10
  import {
11
11
  declaredFiles,
@@ -279,6 +279,39 @@ const reportPlacement = (driver: BridgeDriver) => {
279
279
  }
280
280
  };
281
281
 
282
+ export const availabilityReportLines = (driver: BridgeDriver): string[] => {
283
+ const lines: string[] = [];
284
+
285
+ for (const [id, module] of Object.entries(driver.modules ?? {})) {
286
+ if (
287
+ module.kind !== BridgeKind.Actions
288
+ && module.kind !== BridgeKind.Catalog
289
+ && module.kind !== BridgeKind.Collection
290
+ && module.kind !== BridgeKind.Settings
291
+ ) {
292
+ continue;
293
+ }
294
+
295
+ const protectedActions = module.protectedActions ?? [];
296
+
297
+ if (protectedActions.length === 0 || module.unavailable) {
298
+ continue;
299
+ }
300
+
301
+ lines.push(
302
+ ` warning: module "${id}" protects ${protectedActions.join(", ")} but declares no \`unavailable\` — the panel offers the action everywhere and the platform stops the game before the driver can refuse it`,
303
+ );
304
+ }
305
+
306
+ return lines;
307
+ };
308
+
309
+ const reportAvailability = (driver: BridgeDriver) => {
310
+ for (const line of availabilityReportLines(driver)) {
311
+ console.log(line);
312
+ }
313
+ };
314
+
282
315
  const openTargetOf = (target: GuideOpenTarget) => {
283
316
  if (target.tab === GuideOpenTab.Panel) {
284
317
  return `panel tab "${target.tabId}"`;
@@ -432,6 +465,7 @@ const validateRunnablePackage = async (
432
465
  reportEvents(driver);
433
466
  reportActionTargets(driver);
434
467
  reportPlacement(driver);
468
+ reportAvailability(driver);
435
469
  };
436
470
 
437
471
  export const runValidate = async (args: string[]) => {
@@ -1,4 +1,4 @@
1
- import type { BridgeStream, BridgeValues } from "../core";
1
+ import type { BridgeActivity, BridgeStream, BridgeValues } from "../core";
2
2
  import type { BridgeHttpMethod, BridgeSetupRuntime } from "../protocol";
3
3
 
4
4
  export interface BridgeServer {
@@ -189,6 +189,10 @@ export interface BridgeSetupReporter {
189
189
  clear(): void;
190
190
  }
191
191
 
192
+ export interface BridgeActivityReporter {
193
+ report(entry: BridgeActivity): void;
194
+ }
195
+
192
196
  export interface BridgeLog {
193
197
  (message: string, fields?: BridgeValues): void;
194
198
  warn(message: string, fields?: BridgeValues): void;
@@ -212,6 +216,7 @@ export interface BridgeContext {
212
216
  exec(command: string[], options?: BridgeExecOptions): Promise<BridgeExecResult>;
213
217
  logs: BridgeLogs;
214
218
  setup: BridgeSetupReporter;
219
+ activity: BridgeActivityReporter;
215
220
  emit(event: string, payload?: BridgeValues): void;
216
221
  restart(): Promise<void>;
217
222
  log: BridgeLog;
@@ -1,5 +1,6 @@
1
1
  export enum BridgeAction {
2
2
  Add = "add",
3
+ Availability = "availability",
3
4
  Install = "install",
4
5
  Installed = "installed",
5
6
  List = "list",
@@ -16,6 +17,7 @@ export enum BridgeAction {
16
17
  }
17
18
 
18
19
  export const BRIDGE_READ_ACTIONS: readonly string[] = [
20
+ BridgeAction.Availability,
19
21
  BridgeAction.Releases,
20
22
  BridgeAction.Installed,
21
23
  BridgeAction.List,
@@ -0,0 +1,9 @@
1
+ import type { BridgeEventName } from "../protocol/bridgeEvents";
2
+ import type { BridgeValues, LocalizedText } from "./bridgeValue";
3
+
4
+ export interface BridgeActivity {
5
+ name: BridgeEventName;
6
+ text: LocalizedText;
7
+ subject?: string;
8
+ details?: BridgeValues;
9
+ }
@@ -0,0 +1,10 @@
1
+ import type { LocalizedText } from "./bridgeValue";
2
+
3
+ export const BRIDGE_AVAILABILITY_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
4
+
5
+ export type BridgeUnavailable = Record<string, LocalizedText>;
6
+
7
+ export interface BridgeAvailability {
8
+ module: BridgeUnavailable;
9
+ subjects: Record<string, BridgeUnavailable>;
10
+ }
@@ -17,6 +17,16 @@ describe("BridgeUserError", () => {
17
17
  expect(error.text).toEqual(text);
18
18
  });
19
19
 
20
+ test("keeps the cause a driver attaches for the staff log", () => {
21
+ const cause = new Error("cdn.modrinth.com answered 404");
22
+ const error = new BridgeUserError(text, {
23
+ cause,
24
+ });
25
+
26
+ expect(error.cause).toBe(cause);
27
+ expect(error.message).toBe(text.en);
28
+ });
29
+
20
30
  test("is the class a driver reaches through the published namespace", () => {
21
31
  expect(new Bridge.UserError(text)).toBeInstanceOf(BridgeUserError);
22
32
  });
@@ -1,10 +1,14 @@
1
1
  import type { LocalizedText } from "./bridgeValue";
2
2
 
3
+ export interface BridgeUserErrorOptions {
4
+ cause?: unknown;
5
+ }
6
+
3
7
  export class BridgeUserError extends Error {
4
8
  readonly text: LocalizedText;
5
9
 
6
- constructor(text: LocalizedText) {
7
- super(text.en);
10
+ constructor(text: LocalizedText, options: BridgeUserErrorOptions = {}) {
11
+ super(text.en, options);
8
12
 
9
13
  this.name = "BridgeUserError";
10
14
  this.text = text;
@@ -0,0 +1,58 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { BridgeDownloadError } from "./bridgeDownloadError";
3
+ import { BridgeNetError } from "./bridgeNetError";
4
+ import { describeDownloadFailure } from "./describeDownloadFailure";
5
+
6
+ const SUBJECT = "All the Mods 9";
7
+
8
+ describe("describeDownloadFailure", () => {
9
+ test("says the file is gone for 404 and 410", () => {
10
+ for (const status of [
11
+ 404,
12
+ 410,
13
+ ]) {
14
+ expect(describeDownloadFailure(new BridgeDownloadError(status, "gone"), SUBJECT).en).toBe(
15
+ "All the Mods 9 is no longer available from its source.",
16
+ );
17
+ }
18
+ });
19
+
20
+ test("says the source refused for 401 and 403", () => {
21
+ expect(describeDownloadFailure(new BridgeNetError(403, "forbidden"), SUBJECT).en).toBe(
22
+ "The source refused the download of All the Mods 9.",
23
+ );
24
+ expect(describeDownloadFailure(new BridgeDownloadError(401, "unauthorized"), SUBJECT).ar).toBe(
25
+ "المصدر رفض تنزيل All the Mods 9.",
26
+ );
27
+ });
28
+
29
+ test("says the source is busy for 429", () => {
30
+ expect(describeDownloadFailure(new BridgeDownloadError(429, "slow down"), SUBJECT).en).toContain("busy");
31
+ });
32
+
33
+ test("says the source is down for a 5xx", () => {
34
+ expect(describeDownloadFailure(new BridgeNetError(503, "unavailable"), SUBJECT).en).toBe(
35
+ "The source of All the Mods 9 is down right now. Try again in a bit.",
36
+ );
37
+ });
38
+
39
+ test("says the source could not be reached when there is no status", () => {
40
+ expect(describeDownloadFailure(new BridgeDownloadError(null, "ECONNRESET"), SUBJECT).en).toBe(
41
+ "We could not reach the source to download All the Mods 9. Try again.",
42
+ );
43
+ });
44
+
45
+ test("falls back to a plain download failure for anything else", () => {
46
+ const generic = "We could not download All the Mods 9. Try again.";
47
+
48
+ expect(describeDownloadFailure(new BridgeDownloadError(400, "bad request"), SUBJECT).en).toBe(generic);
49
+ expect(describeDownloadFailure(new Error("digest mismatch"), SUBJECT).en).toBe(generic);
50
+ });
51
+
52
+ test("always writes both languages", () => {
53
+ const text = describeDownloadFailure(new BridgeDownloadError(500, "boom"), SUBJECT);
54
+
55
+ expect(text.ar.length).toBeGreaterThan(0);
56
+ expect(text.en.length).toBeGreaterThan(0);
57
+ });
58
+ });
@@ -0,0 +1,69 @@
1
+ import { BridgeDownloadError } from "./bridgeDownloadError";
2
+ import { BridgeNetError } from "./bridgeNetError";
3
+ import type { LocalizedText } from "./bridgeValue";
4
+
5
+ const REMOVED_STATUSES: readonly number[] = [
6
+ 404,
7
+ 410,
8
+ ];
9
+
10
+ const REFUSED_STATUSES: readonly number[] = [
11
+ 401,
12
+ 403,
13
+ ];
14
+
15
+ const BUSY_STATUS = 429;
16
+
17
+ const SERVER_ERROR_STATUS = 500;
18
+
19
+ const statusOf = (error: unknown): number | null | undefined => {
20
+ if (error instanceof BridgeDownloadError || error instanceof BridgeNetError) {
21
+ return error.status;
22
+ }
23
+
24
+ return undefined;
25
+ };
26
+
27
+ export const describeDownloadFailure = (error: unknown, subject: string): LocalizedText => {
28
+ const status = statusOf(error);
29
+
30
+ if (status === null) {
31
+ return {
32
+ ar: `ما قدرنا نوصل للمصدر عشان ننزّل ${subject}. جرّب مرة ثانية.`,
33
+ en: `We could not reach the source to download ${subject}. Try again.`,
34
+ };
35
+ }
36
+
37
+ if (status !== undefined && REMOVED_STATUSES.includes(status)) {
38
+ return {
39
+ ar: `${subject} ما عاد موجود عند المصدر.`,
40
+ en: `${subject} is no longer available from its source.`,
41
+ };
42
+ }
43
+
44
+ if (status !== undefined && REFUSED_STATUSES.includes(status)) {
45
+ return {
46
+ ar: `المصدر رفض تنزيل ${subject}.`,
47
+ en: `The source refused the download of ${subject}.`,
48
+ };
49
+ }
50
+
51
+ if (status === BUSY_STATUS) {
52
+ return {
53
+ ar: `المصدر مشغول الحين وما قدرنا ننزّل ${subject}. جرّب بعد شوي.`,
54
+ en: `The source is busy right now, so we could not download ${subject}. Try again in a bit.`,
55
+ };
56
+ }
57
+
58
+ if (status !== undefined && status >= SERVER_ERROR_STATUS) {
59
+ return {
60
+ ar: `مصدر ${subject} فيه عطل الحين. جرّب بعد شوي.`,
61
+ en: `The source of ${subject} is down right now. Try again in a bit.`,
62
+ };
63
+ }
64
+
65
+ return {
66
+ ar: `ما قدرنا ننزّل ${subject}. جرّب مرة ثانية.`,
67
+ en: `We could not download ${subject}. Try again.`,
68
+ };
69
+ };
package/src/core/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from "./bridgeAction";
2
+ export * from "./bridgeActivity";
3
+ export * from "./bridgeAvailability";
2
4
  export * from "./bridgeDownloadError";
3
5
  export * from "./bridgeFailure";
4
6
  export * from "./bridgeHold";
@@ -14,3 +16,4 @@ export * from "./bridgeSetup";
14
16
  export * from "./bridgeStream";
15
17
  export * from "./bridgeUserError";
16
18
  export * from "./bridgeValue";
19
+ export * from "./describeDownloadFailure";
@@ -1,5 +1,7 @@
1
- import type { BridgeContext, BridgeExecResult, BridgeFileEntry, BridgeReply } from "../context";
1
+ import type { BridgeActivityReporter, BridgeContext, BridgeExecResult, BridgeFileEntry, BridgeReply } from "../context";
2
2
  import type {
3
+ BridgeActivity,
4
+ BridgeAvailability,
3
5
  BridgeCell,
4
6
  BridgeCells,
5
7
  BridgeCopy,
@@ -7,6 +9,7 @@ import type {
7
9
  BridgeInstallOutcome,
8
10
  BridgeOption,
9
11
  BridgeRow,
12
+ BridgeUnavailable,
10
13
  BridgeValue,
11
14
  BridgeValues,
12
15
  LocalizedText,
@@ -75,8 +78,11 @@ import type {
75
78
 
76
79
  export namespace Bridge {
77
80
  export type Actions = BridgeActionsModule;
81
+ export type Activity = BridgeActivity;
82
+ export type ActivityReporter = BridgeActivityReporter;
78
83
  export type AddOutcome = BridgeAddOutcome;
79
84
  export type Announce = BridgeAnnounceModule;
85
+ export type Availability = BridgeAvailability;
80
86
  export type Backup = BridgeBackupModule;
81
87
  export type Catalog = BridgeCatalogModule;
82
88
  export type CatalogEntry = BridgeCatalogEntry;
@@ -138,6 +144,7 @@ export namespace Bridge {
138
144
  export type TerminalRule = BridgeTerminalRule;
139
145
  export type Tab = BridgeTab;
140
146
  export type TabRef = BridgeTabRef;
147
+ export type Unavailable = BridgeUnavailable;
141
148
  export type UploadMode = BridgeUploadMode;
142
149
  export const UserError = BridgeUserError;
143
150
  export type UserError = BridgeUserError;
@@ -1,5 +1,13 @@
1
1
  import type { BridgeContext, BridgeReply } from "../context";
2
- import type { BridgeInstallOutcome, BridgeKind, BridgeOption, BridgeRow, BridgeStream, BridgeValues } from "../core";
2
+ import type {
3
+ BridgeInstallOutcome,
4
+ BridgeKind,
5
+ BridgeOption,
6
+ BridgeRow,
7
+ BridgeStream,
8
+ BridgeUnavailable,
9
+ BridgeValues,
10
+ } from "../core";
3
11
  import type { BridgeAddOutcome, BridgeDetail, BridgeDetailOutcome } from "../panel";
4
12
  import type {
5
13
  BridgeCatalogEntry,
@@ -115,6 +123,7 @@ export interface BridgeSettingsModule {
115
123
  protectedActions?: string[];
116
124
  read(context: BridgeContext): Promise<BridgeValues>;
117
125
  write?(context: BridgeContext, values: BridgeValues): Promise<void>;
126
+ unavailable?(context: BridgeContext): Promise<BridgeUnavailable>;
118
127
  }
119
128
 
120
129
  export type BridgeRowAction<Row extends BridgeRow = BridgeRow> = (
@@ -130,6 +139,7 @@ export interface BridgeActionsModule {
130
139
  protectedActions?: string[];
131
140
  requiresRunning?: boolean;
132
141
  actions: Record<string, BridgeServerAction>;
142
+ unavailable?(context: BridgeContext): Promise<BridgeUnavailable>;
133
143
  }
134
144
 
135
145
  export interface BridgeCollectionModule<Row extends BridgeRow = BridgeRow> {
@@ -140,6 +150,7 @@ export interface BridgeCollectionModule<Row extends BridgeRow = BridgeRow> {
140
150
  list(context: BridgeContext): Promise<Row[]>;
141
151
  add?(context: BridgeContext, input: string): Promise<BridgeAddOutcome | undefined>;
142
152
  actions?: Record<string, BridgeRowAction<Row>>;
153
+ unavailable?(context: BridgeContext, row: Row): Promise<BridgeUnavailable>;
143
154
  }
144
155
 
145
156
  export type BridgeDetailAction = (context: BridgeContext, args: BridgeValues) => Promise<BridgeDetailOutcome | null>;
@@ -164,6 +175,7 @@ export interface BridgeCatalogModule {
164
175
  install(context: BridgeContext, id: string, releaseId?: string): Promise<BridgeCatalogEntry>;
165
176
  remove(context: BridgeContext, id: string): Promise<void>;
166
177
  toggle?(context: BridgeContext, id: string, enabled: boolean): Promise<void>;
178
+ unavailable?(context: BridgeContext, entry: BridgeCatalogEntry): Promise<BridgeUnavailable>;
167
179
  }
168
180
 
169
181
  export type BridgePanelModule =
@@ -1,4 +1,4 @@
1
- import type { BridgeCopy, BridgeValue, LocalizedText } from "../core";
1
+ import type { BridgeCopy, BridgeUnavailable, BridgeValue, LocalizedText } from "../core";
2
2
  import type { BridgeIcon } from "./bridgeIcon";
3
3
 
4
4
  export enum BridgeDetailTone {
@@ -45,6 +45,7 @@ export interface BridgeDetail {
45
45
  links: BridgeDetailLink[];
46
46
  stale: boolean;
47
47
  actions: string[];
48
+ unavailable?: BridgeUnavailable;
48
49
  }
49
50
 
50
51
  export interface BridgeDetailOutcome {
@@ -0,0 +1,54 @@
1
+ import type { BridgeActivity, BridgeValues } from "../core";
2
+
3
+ export const BRIDGE_ACTIVITY_KEYS = {
4
+ ar: "activityAr",
5
+ en: "activityEn",
6
+ subject: "subject",
7
+ } as const;
8
+
9
+ export const BRIDGE_ACTIVITY_LIMIT = 8;
10
+
11
+ export const BRIDGE_ACTIVITY_TEXT_LENGTH_LIMIT = 256;
12
+
13
+ export const BRIDGE_ACTIVITY_SUBJECT_LENGTH_LIMIT = 64;
14
+
15
+ export const BRIDGE_ACTIVITY_DETAILS_LIMIT = 16;
16
+
17
+ export const BRIDGE_ACTIVITY_DETAIL_KEY_LENGTH_LIMIT = 64;
18
+
19
+ export const BRIDGE_ACTIVITY_EXCERPT_LENGTH_LIMIT = 80;
20
+
21
+ export const BRIDGE_ACTIVITY_NAME_LENGTH_LIMIT = 64;
22
+
23
+ export const BRIDGE_ACTIVITY_NAME_PATTERN = /^[A-Z][A-Za-z0-9]*$/;
24
+
25
+ export const BRIDGE_ACTIVITY_DETAIL_KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_-]*$/;
26
+
27
+ const ELLIPSIS = "…";
28
+
29
+ export const isBridgeActivity = (payload: BridgeValues | null): boolean => {
30
+ return typeof payload?.[BRIDGE_ACTIVITY_KEYS.ar] === "string" && typeof payload[BRIDGE_ACTIVITY_KEYS.en] === "string";
31
+ };
32
+
33
+ export const activityPayload = (entry: BridgeActivity): BridgeValues => {
34
+ return {
35
+ ...entry.details,
36
+ [BRIDGE_ACTIVITY_KEYS.ar]: entry.text.ar,
37
+ [BRIDGE_ACTIVITY_KEYS.en]: entry.text.en,
38
+ ...(entry.subject === undefined
39
+ ? {}
40
+ : {
41
+ [BRIDGE_ACTIVITY_KEYS.subject]: entry.subject,
42
+ }),
43
+ };
44
+ };
45
+
46
+ export const activityExcerpt = (text: string): string => {
47
+ const line = text.trim();
48
+
49
+ if (line.length <= BRIDGE_ACTIVITY_EXCERPT_LENGTH_LIMIT) {
50
+ return line;
51
+ }
52
+
53
+ return `${line.slice(0, BRIDGE_ACTIVITY_EXCERPT_LENGTH_LIMIT - ELLIPSIS.length).trimEnd()}${ELLIPSIS}`;
54
+ };
@@ -1,23 +1,53 @@
1
1
  export enum BridgeEventName {
2
+ AccessGranted = "AccessGranted",
3
+ AccessRevoked = "AccessRevoked",
4
+ BroadcastSent = "BroadcastSent",
5
+ GameInstalled = "GameInstalled",
2
6
  GameModeChanged = "GameModeChanged",
7
+ GameplayChanged = "GameplayChanged",
8
+ InstallFailed = "InstallFailed",
3
9
  MemoryUndersized = "MemoryUndersized",
4
10
  MissingDependency = "MissingDependency",
5
11
  ModCrashed = "ModCrashed",
12
+ ModDisabled = "ModDisabled",
13
+ ModEnabled = "ModEnabled",
14
+ ModInstalled = "ModInstalled",
6
15
  ModLoaded = "ModLoaded",
16
+ ModLoaderInstalled = "ModLoaderInstalled",
17
+ ModLoaderRemoved = "ModLoaderRemoved",
18
+ ModRemoved = "ModRemoved",
19
+ ModpackInstalled = "ModpackInstalled",
20
+ ModpackRemoved = "ModpackRemoved",
21
+ ModpackSelected = "ModpackSelected",
7
22
  PlayerAdvanced = "PlayerAdvanced",
8
23
  PlayerBanned = "PlayerBanned",
24
+ PlayerChanged = "PlayerChanged",
9
25
  PlayerDied = "PlayerDied",
10
26
  PlayerJoined = "PlayerJoined",
11
27
  PlayerKicked = "PlayerKicked",
12
28
  PlayerLeft = "PlayerLeft",
29
+ PlayerUnbanned = "PlayerUnbanned",
13
30
  PortBindFailed = "PortBindFailed",
31
+ SecretRotated = "SecretRotated",
14
32
  ServerCrashed = "ServerCrashed",
15
33
  ServerStarted = "ServerStarted",
16
34
  ServerStopping = "ServerStopping",
17
35
  ServerUpdated = "ServerUpdated",
36
+ SettingsApplied = "SettingsApplied",
37
+ SettingsChanged = "SettingsChanged",
38
+ SetupAction = "SetupAction",
39
+ SetupCompleted = "SetupCompleted",
40
+ SetupRejected = "SetupRejected",
41
+ SignInExpired = "SignInExpired",
18
42
  TickLagging = "TickLagging",
43
+ WorldActivated = "WorldActivated",
44
+ WorldCloned = "WorldCloned",
19
45
  WorldCorrupt = "WorldCorrupt",
46
+ WorldCreated = "WorldCreated",
47
+ WorldDeleted = "WorldDeleted",
48
+ WorldImported = "WorldImported",
20
49
  WorldSaved = "WorldSaved",
50
+ WorldWiped = "WorldWiped",
21
51
  WrongJavaVersion = "WrongJavaVersion",
22
52
  }
23
53
 
@@ -1,4 +1,11 @@
1
- import type { BridgeDesired, BridgeInstallHold, BridgeRuntime, BridgeValues, LocalizedText } from "../core";
1
+ import type {
2
+ BridgeActivity,
3
+ BridgeDesired,
4
+ BridgeInstallHold,
5
+ BridgeRuntime,
6
+ BridgeValues,
7
+ LocalizedText,
8
+ } from "../core";
2
9
  import type { BridgeSetupSummary } from "./bridgeSetup";
3
10
 
4
11
  export const BRIDGE_SOCKET_NAME = "bridge.sock";
@@ -59,6 +66,7 @@ export interface BridgeSuccess {
59
66
  id: number | null;
60
67
  ok: true;
61
68
  result: unknown;
69
+ activity?: BridgeActivity[];
62
70
  }
63
71
 
64
72
  export interface BridgeFailure {
@@ -68,6 +76,7 @@ export interface BridgeFailure {
68
76
  secret?: string;
69
77
  failure?: string;
70
78
  text?: LocalizedText;
79
+ unchanged?: boolean;
71
80
  }
72
81
 
73
82
  export type BridgeResponse = BridgeFailure | BridgeSuccess;