@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.
- package/package.json +1 -1
- package/src/bot/botPanel.test.ts +30 -0
- package/src/bot/botPanel.ts +9 -1
- package/src/cli/validate.test.ts +73 -1
- package/src/cli/validate.ts +35 -1
- package/src/context/bridgeContext.ts +6 -1
- package/src/core/bridgeAction.ts +2 -0
- package/src/core/bridgeActivity.ts +9 -0
- package/src/core/bridgeAvailability.ts +10 -0
- package/src/core/bridgeUserError.test.ts +10 -0
- package/src/core/bridgeUserError.ts +6 -2
- package/src/core/describeDownloadFailure.test.ts +58 -0
- package/src/core/describeDownloadFailure.ts +69 -0
- package/src/core/index.ts +3 -0
- package/src/modules/bridge.ts +8 -1
- package/src/modules/bridgeModule.ts +13 -1
- package/src/panel/bridgeDetail.ts +2 -1
- package/src/protocol/bridgeActivity.ts +54 -0
- package/src/protocol/bridgeEvents.ts +30 -0
- package/src/protocol/bridgeFrame.ts +10 -1
- package/src/protocol/index.ts +1 -0
- package/src/runtime/bridgeActions.ts +19 -2
- package/src/runtime/bridgeActivity.test.ts +318 -0
- package/src/runtime/bridgeActivity.ts +268 -0
- package/src/runtime/bridgeContext.test.ts +6 -0
- package/src/runtime/bridgeContext.ts +8 -1
- package/src/runtime/bridgeDriver.test.ts +21 -0
- package/src/runtime/bridgeDriver.ts +10 -7
- package/src/runtime/bridgeLog.ts +10 -0
- package/src/runtime/bridgeRefusalError.ts +13 -0
- package/src/runtime/bridgeRegistry.test.ts +1053 -10
- package/src/runtime/bridgeRegistry.ts +464 -65
- package/src/runtime/bridgeScopedContext.ts +55 -0
- package/src/runtime/bridgeServer.test.ts +404 -4
- package/src/runtime/bridgeServer.ts +125 -35
- package/src/runtime/bridgeSetupHandlers.test.ts +69 -6
- package/src/runtime/bridgeSetupHandlers.ts +45 -10
- package/src/runtime/serve.ts +45 -3
- package/src/steam/steamcmd.test.ts +32 -1
- package/src/steam/steamcmd.ts +27 -4
- package/src/utils/changedSettingKeys.test.ts +65 -0
- package/src/utils/changedSettingKeys.ts +6 -0
- package/src/utils/describeExecFailure.test.ts +34 -0
- package/src/utils/describeExecFailure.ts +9 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/sameSettingValue.test.ts +35 -0
- package/src/utils/sameSettingValue.ts +40 -0
- package/src/wire/activity.test.ts +95 -0
- package/src/wire/activity.ts +41 -0
- package/src/wire/index.ts +1 -0
- package/src/wire/panel.test.ts +67 -1
- 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
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
+
};
|