@serverkgg/bridge 0.1.13 → 0.1.15
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/rules/package.md +3 -0
- package/src/catalogs/curseforge.test.ts +46 -0
- package/src/catalogs/curseforge.ts +20 -0
- package/src/context/bridgeContext.ts +1 -0
- package/src/manifest/extension.ts +1 -0
- package/src/manifest/manifest.test.ts +18 -0
- package/src/modules/bridge.ts +13 -2
- package/src/modules/bridgeModule.ts +20 -4
- package/src/panel/bridgePanel.ts +2 -0
- package/src/protocol/bridgeCatalog.ts +7 -0
- package/src/protocol/bridgeCatalogSource.ts +9 -0
- package/src/protocol/bridgeConfig.ts +1 -0
- package/src/protocol/bridgeFrame.ts +1 -0
- package/src/protocol/bridgeLimits.ts +6 -0
- package/src/protocol/bridgeManifest.ts +2 -0
- package/src/protocol/index.ts +1 -0
- package/src/runtime/bridgeActions.ts +9 -2
- package/src/runtime/bridgeConfigReader.test.ts +31 -0
- package/src/runtime/bridgeConfigReader.ts +1 -0
- package/src/runtime/bridgeContext.test.ts +53 -0
- package/src/runtime/bridgeContext.ts +3 -0
- package/src/runtime/bridgeDriver.test.ts +347 -1
- package/src/runtime/bridgeDriver.ts +130 -18
- package/src/runtime/bridgeLiveConfig.test.ts +137 -1
- package/src/runtime/bridgeLiveConfig.ts +46 -2
- package/src/runtime/bridgeRegistry.test.ts +220 -0
- package/src/runtime/bridgeRegistry.ts +27 -1
- package/src/runtime/bridgeSample.test.ts +63 -0
- package/src/runtime/bridgeSample.ts +22 -0
- package/src/runtime/serve.ts +5 -12
- package/src/steam/workshop.test.ts +19 -1
- package/src/steam/workshop.ts +12 -0
- package/src/wire/panel.test.ts +177 -0
- package/src/wire/panel.ts +16 -0
|
@@ -2,8 +2,11 @@ import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
|
2
2
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { BridgeKind } from "../core";
|
|
6
|
+
import type { BridgeDriver } from "../modules";
|
|
7
|
+
import { BridgeControl, BridgeFormTarget, BridgeLayout } from "../panel";
|
|
5
8
|
import type { BridgeConfig } from "../protocol";
|
|
6
|
-
import { createBridgeLiveConfig } from "./bridgeLiveConfig";
|
|
9
|
+
import { createBridgeLiveConfig, variableDefaultsOf } from "./bridgeLiveConfig";
|
|
7
10
|
|
|
8
11
|
const configWith = (secrets: Record<string, string>, variables: Record<string, string> = {}): BridgeConfig => ({
|
|
9
12
|
code: "wsvv8fakxm",
|
|
@@ -146,4 +149,137 @@ describe("the bridge live config reader", () => {
|
|
|
146
149
|
expect(live.memoryMb()).toBe(4096);
|
|
147
150
|
expect(live.cpuPercent()).toBe(200);
|
|
148
151
|
});
|
|
152
|
+
|
|
153
|
+
it("answers a variable the config does not carry with the default the driver declares", async () => {
|
|
154
|
+
const config = configWith({});
|
|
155
|
+
|
|
156
|
+
await write(config);
|
|
157
|
+
|
|
158
|
+
const live = createBridgeLiveConfig(path, config, 0, {
|
|
159
|
+
WINDOWS_STEAM: "true",
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
expect(live.variable("WINDOWS_STEAM")).toBe("true");
|
|
163
|
+
expect(live.variables()).toEqual({
|
|
164
|
+
WINDOWS_STEAM: "true",
|
|
165
|
+
VERSION: "1.21",
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("answers the value the owner set over the default, and the default again once the value is gone", async () => {
|
|
170
|
+
const config = configWith(
|
|
171
|
+
{},
|
|
172
|
+
{
|
|
173
|
+
WINDOWS_STEAM: "false",
|
|
174
|
+
},
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
await write(config);
|
|
178
|
+
|
|
179
|
+
const live = createBridgeLiveConfig(path, config, 0, {
|
|
180
|
+
WINDOWS_STEAM: "true",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(live.variable("WINDOWS_STEAM")).toBe("false");
|
|
184
|
+
|
|
185
|
+
await write(configWith({}));
|
|
186
|
+
|
|
187
|
+
expect(live.variable("WINDOWS_STEAM")).toBe("true");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("answers the default for a variable the owner cleared, as the form shows it", async () => {
|
|
191
|
+
const config = configWith(
|
|
192
|
+
{},
|
|
193
|
+
{
|
|
194
|
+
SERVER_NAME: "",
|
|
195
|
+
},
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
await write(config);
|
|
199
|
+
|
|
200
|
+
const live = createBridgeLiveConfig(path, config, 0, {
|
|
201
|
+
SERVER_NAME: "Serverk",
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(live.variable("SERVER_NAME")).toBe("Serverk");
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
describe("variableDefaultsOf", () => {
|
|
209
|
+
it("collects the default of every form field that declares one", () => {
|
|
210
|
+
const driver: BridgeDriver = {
|
|
211
|
+
install: {
|
|
212
|
+
kind: BridgeKind.Install,
|
|
213
|
+
run: async () => {
|
|
214
|
+
return;
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
lifecycle: {
|
|
218
|
+
kind: BridgeKind.Lifecycle,
|
|
219
|
+
command: async () => [
|
|
220
|
+
"run",
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
panel: {
|
|
224
|
+
tabs: [
|
|
225
|
+
{
|
|
226
|
+
id: "server",
|
|
227
|
+
title: {
|
|
228
|
+
ar: "السيرفر",
|
|
229
|
+
en: "Server",
|
|
230
|
+
},
|
|
231
|
+
sections: [
|
|
232
|
+
{
|
|
233
|
+
layout: BridgeLayout.Form,
|
|
234
|
+
id: "steam",
|
|
235
|
+
target: BridgeFormTarget.Variables,
|
|
236
|
+
fields: [
|
|
237
|
+
{
|
|
238
|
+
key: "WINDOWS_STEAM",
|
|
239
|
+
control: BridgeControl.Boolean,
|
|
240
|
+
label: {
|
|
241
|
+
ar: "تفعيل Steam",
|
|
242
|
+
en: "Use Steam",
|
|
243
|
+
},
|
|
244
|
+
default: "true",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
key: "SERVER_NAME",
|
|
248
|
+
control: BridgeControl.Text,
|
|
249
|
+
label: {
|
|
250
|
+
ar: "الاسم",
|
|
251
|
+
en: "Name",
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
expect(variableDefaultsOf(driver)).toEqual({
|
|
263
|
+
WINDOWS_STEAM: "true",
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("answers no defaults for a driver without a panel", () => {
|
|
268
|
+
expect(
|
|
269
|
+
variableDefaultsOf({
|
|
270
|
+
install: {
|
|
271
|
+
kind: BridgeKind.Install,
|
|
272
|
+
run: async () => {
|
|
273
|
+
return;
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
lifecycle: {
|
|
277
|
+
kind: BridgeKind.Lifecycle,
|
|
278
|
+
command: async () => [
|
|
279
|
+
"run",
|
|
280
|
+
],
|
|
281
|
+
},
|
|
282
|
+
}),
|
|
283
|
+
).toEqual({});
|
|
284
|
+
});
|
|
149
285
|
});
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import type { BridgeDriver } from "../modules";
|
|
3
|
+
import { BridgeLayout } from "../panel";
|
|
2
4
|
import type { BridgeConfig } from "../protocol";
|
|
3
5
|
import { parseConfig } from "./bridgeConfigReader";
|
|
4
6
|
|
|
@@ -20,15 +22,54 @@ export interface BridgeLiveConfig {
|
|
|
20
22
|
memoryMb: () => number;
|
|
21
23
|
cpuPercent: () => number;
|
|
22
24
|
publicAddress: () => string | null;
|
|
25
|
+
playerAddress: () => string | null;
|
|
23
26
|
reload: () => void;
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
export const variableDefaultsOf = (driver: BridgeDriver): Record<string, string> => {
|
|
30
|
+
const defaults: Record<string, string> = {};
|
|
31
|
+
|
|
32
|
+
for (const tab of driver.panel?.tabs ?? []) {
|
|
33
|
+
for (const section of tab.sections) {
|
|
34
|
+
if (section.layout !== BridgeLayout.Form) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const field of section.fields) {
|
|
39
|
+
if (field.default !== undefined) {
|
|
40
|
+
defaults[field.key] = field.default;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return defaults;
|
|
47
|
+
};
|
|
48
|
+
|
|
26
49
|
export const createBridgeLiveConfig = (
|
|
27
50
|
path: string,
|
|
28
51
|
config: BridgeConfig,
|
|
29
52
|
refreshMs: number = BRIDGE_CONFIG_REFRESH_MS,
|
|
53
|
+
defaults: Readonly<Record<string, string>> = {},
|
|
30
54
|
): BridgeLiveConfig => {
|
|
31
|
-
|
|
55
|
+
const withDefaults = (read: BridgeConfig): BridgeConfig => {
|
|
56
|
+
const variables = {
|
|
57
|
+
...read.variables,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
for (const [key, value] of Object.entries(defaults)) {
|
|
61
|
+
if (!variables[key]) {
|
|
62
|
+
variables[key] = value;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
...read,
|
|
68
|
+
variables,
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let current = withDefaults(config);
|
|
32
73
|
|
|
33
74
|
let checkedAt: number | null = null;
|
|
34
75
|
|
|
@@ -42,7 +83,7 @@ export const createBridgeLiveConfig = (
|
|
|
42
83
|
checkedAt = stamp;
|
|
43
84
|
|
|
44
85
|
try {
|
|
45
|
-
current = parseConfig(readFileSync(path, "utf8"));
|
|
86
|
+
current = withDefaults(parseConfig(readFileSync(path, "utf8")));
|
|
46
87
|
} catch {
|
|
47
88
|
return current;
|
|
48
89
|
}
|
|
@@ -72,6 +113,9 @@ export const createBridgeLiveConfig = (
|
|
|
72
113
|
publicAddress() {
|
|
73
114
|
return refresh().publicAddress ?? null;
|
|
74
115
|
},
|
|
116
|
+
playerAddress() {
|
|
117
|
+
return refresh().playerAddress ?? null;
|
|
118
|
+
},
|
|
75
119
|
reload() {
|
|
76
120
|
checkedAt = null;
|
|
77
121
|
|
|
@@ -14,6 +14,9 @@ import { BridgeControl, BridgeFormTarget, BridgeLayout } from "../panel";
|
|
|
14
14
|
import {
|
|
15
15
|
BRIDGE_ACTIVITY_KEYS,
|
|
16
16
|
BRIDGE_TEXT_LENGTH_LIMIT,
|
|
17
|
+
type BridgeCatalogEntry,
|
|
18
|
+
type BridgeCatalogPage,
|
|
19
|
+
BridgeCatalogSourceKind,
|
|
17
20
|
type BridgeConfig,
|
|
18
21
|
BridgeConfirm,
|
|
19
22
|
BridgeEventName,
|
|
@@ -742,6 +745,223 @@ describe("detail modules", () => {
|
|
|
742
745
|
});
|
|
743
746
|
});
|
|
744
747
|
|
|
748
|
+
describe("catalog modules", () => {
|
|
749
|
+
const entry = (id: string): BridgeCatalogEntry => ({
|
|
750
|
+
id,
|
|
751
|
+
provider: null,
|
|
752
|
+
path: `Mods/${id}`,
|
|
753
|
+
title: id,
|
|
754
|
+
version: null,
|
|
755
|
+
sizeBytes: 0,
|
|
756
|
+
enabled: true,
|
|
757
|
+
gameVersion: null,
|
|
758
|
+
stale: false,
|
|
759
|
+
pageUrl: null,
|
|
760
|
+
icon: null,
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
const emptyPage = async (): Promise<BridgeCatalogPage> => ({
|
|
764
|
+
hits: [],
|
|
765
|
+
total: 0,
|
|
766
|
+
providers: [],
|
|
767
|
+
categories: [],
|
|
768
|
+
sorts: [],
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
const WORKSHOP = {
|
|
772
|
+
kind: BridgeCatalogSourceKind.Workshop,
|
|
773
|
+
appId: 1_623_730,
|
|
774
|
+
provided: [
|
|
775
|
+
"UE4SS",
|
|
776
|
+
"PalSchema",
|
|
777
|
+
],
|
|
778
|
+
};
|
|
779
|
+
|
|
780
|
+
const driver = (added: string[] = []): BridgeDriver => ({
|
|
781
|
+
...baseDriver(),
|
|
782
|
+
modules: {
|
|
783
|
+
mods: {
|
|
784
|
+
kind: BridgeKind.Catalog,
|
|
785
|
+
source: WORKSHOP,
|
|
786
|
+
installed: async () => [],
|
|
787
|
+
install: async (_context, id) => entry(id),
|
|
788
|
+
add: async (_context, input) => {
|
|
789
|
+
added.push(input);
|
|
790
|
+
|
|
791
|
+
return entry("upload:BetterMap");
|
|
792
|
+
},
|
|
793
|
+
remove: async () => {
|
|
794
|
+
return;
|
|
795
|
+
},
|
|
796
|
+
toggle: async () => {
|
|
797
|
+
return;
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
addons: {
|
|
801
|
+
kind: BridgeKind.Catalog,
|
|
802
|
+
search: emptyPage,
|
|
803
|
+
releases: async () => [],
|
|
804
|
+
preview: async () => ({
|
|
805
|
+
confirm: BridgeConfirm.Normal,
|
|
806
|
+
lines: [],
|
|
807
|
+
}),
|
|
808
|
+
installed: async () => [],
|
|
809
|
+
install: async (_context, id) => entry(id),
|
|
810
|
+
remove: async () => {
|
|
811
|
+
return;
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
it("advertises search and preview for a sourced catalog, never releases, and carries its source", () => {
|
|
818
|
+
expect(registryFor(driver()).manifest().modules).toEqual([
|
|
819
|
+
{
|
|
820
|
+
id: "mods",
|
|
821
|
+
kind: BridgeKind.Catalog,
|
|
822
|
+
requiresRunning: false,
|
|
823
|
+
refreshSeconds: null,
|
|
824
|
+
pageSize: null,
|
|
825
|
+
dependsOn: [],
|
|
826
|
+
actions: [
|
|
827
|
+
BridgeAction.Preview,
|
|
828
|
+
BridgeAction.Install,
|
|
829
|
+
BridgeAction.Installed,
|
|
830
|
+
BridgeAction.Remove,
|
|
831
|
+
BridgeAction.Search,
|
|
832
|
+
BridgeAction.Add,
|
|
833
|
+
BridgeAction.Toggle,
|
|
834
|
+
],
|
|
835
|
+
source: WORKSHOP,
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
id: "addons",
|
|
839
|
+
kind: BridgeKind.Catalog,
|
|
840
|
+
requiresRunning: false,
|
|
841
|
+
refreshSeconds: null,
|
|
842
|
+
pageSize: null,
|
|
843
|
+
dependsOn: [],
|
|
844
|
+
actions: [
|
|
845
|
+
BridgeAction.Releases,
|
|
846
|
+
BridgeAction.Preview,
|
|
847
|
+
BridgeAction.Install,
|
|
848
|
+
BridgeAction.Installed,
|
|
849
|
+
BridgeAction.Remove,
|
|
850
|
+
BridgeAction.Search,
|
|
851
|
+
],
|
|
852
|
+
},
|
|
853
|
+
]);
|
|
854
|
+
});
|
|
855
|
+
|
|
856
|
+
it("hands add the staged input and answers the entry it installed", async () => {
|
|
857
|
+
const added: string[] = [];
|
|
858
|
+
const { result } = await registryFor(driver(added)).invoke("mods", BridgeAction.Add, {
|
|
859
|
+
input: ".serverk-staging/mods-upload/BetterMap",
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
expect(added).toEqual([
|
|
863
|
+
".serverk-staging/mods-upload/BetterMap",
|
|
864
|
+
]);
|
|
865
|
+
expect(result).toEqual(entry("upload:BetterMap"));
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
it("refuses an add with no staged input before the driver is entered", async () => {
|
|
869
|
+
const added: string[] = [];
|
|
870
|
+
|
|
871
|
+
await expect(registryFor(driver(added)).invoke("mods", BridgeAction.Add, {})).rejects.toThrow(
|
|
872
|
+
'"input" must be a non-empty string',
|
|
873
|
+
);
|
|
874
|
+
expect(added).toEqual([]);
|
|
875
|
+
});
|
|
876
|
+
|
|
877
|
+
it("refuses an add on a catalog that defines none", async () => {
|
|
878
|
+
await expect(
|
|
879
|
+
registryFor(driver()).invoke("addons", BridgeAction.Add, {
|
|
880
|
+
input: ".serverk-staging/mods-upload/BetterMap",
|
|
881
|
+
}),
|
|
882
|
+
).rejects.toThrow('module "addons" does not support "add"');
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
it("refuses search, preview and releases on a sourced catalog, which the platform answers", async () => {
|
|
886
|
+
const registry = registryFor(driver());
|
|
887
|
+
|
|
888
|
+
for (const action of [
|
|
889
|
+
BridgeAction.Search,
|
|
890
|
+
BridgeAction.Preview,
|
|
891
|
+
BridgeAction.Releases,
|
|
892
|
+
]) {
|
|
893
|
+
const refused = registry.invoke("mods", action, {
|
|
894
|
+
id: "3762839811",
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
await expect(refused).rejects.toBeInstanceOf(BridgeRefusalError);
|
|
898
|
+
await expect(refused).rejects.toThrow(
|
|
899
|
+
`module "mods" takes its items from the workshop source, so the platform answers "${action}"`,
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
});
|
|
903
|
+
|
|
904
|
+
it("hands back the note an entry carries, unchanged through the wire's JSON", async () => {
|
|
905
|
+
const noted: BridgeCatalogEntry = {
|
|
906
|
+
...entry("3762839811"),
|
|
907
|
+
note: {
|
|
908
|
+
ar: "يشتغل بعد إعادة التشغيل",
|
|
909
|
+
en: "Runs after a restart",
|
|
910
|
+
},
|
|
911
|
+
};
|
|
912
|
+
|
|
913
|
+
const registry = registryFor({
|
|
914
|
+
...baseDriver(),
|
|
915
|
+
modules: {
|
|
916
|
+
mods: {
|
|
917
|
+
kind: BridgeKind.Catalog,
|
|
918
|
+
source: WORKSHOP,
|
|
919
|
+
installed: async () => [
|
|
920
|
+
noted,
|
|
921
|
+
],
|
|
922
|
+
install: async () => noted,
|
|
923
|
+
remove: async () => {
|
|
924
|
+
return;
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
},
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
const { result: installed } = await registry.invoke("mods", BridgeAction.Installed);
|
|
931
|
+
const { result: install } = await registry.invoke("mods", BridgeAction.Install, {
|
|
932
|
+
id: "3762839811",
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
expect(JSON.parse(JSON.stringify(installed))).toEqual([
|
|
936
|
+
noted,
|
|
937
|
+
]);
|
|
938
|
+
expect(JSON.parse(JSON.stringify(install))).toEqual(noted);
|
|
939
|
+
});
|
|
940
|
+
|
|
941
|
+
it("still answers search, releases and preview on a catalog that answers its own search", async () => {
|
|
942
|
+
const registry = registryFor(driver());
|
|
943
|
+
|
|
944
|
+
expect((await registry.invoke("addons", BridgeAction.Search)).result).toEqual(await emptyPage());
|
|
945
|
+
expect(
|
|
946
|
+
(
|
|
947
|
+
await registry.invoke("addons", BridgeAction.Releases, {
|
|
948
|
+
id: "sodium",
|
|
949
|
+
})
|
|
950
|
+
).result,
|
|
951
|
+
).toEqual([]);
|
|
952
|
+
expect(
|
|
953
|
+
(
|
|
954
|
+
await registry.invoke("addons", BridgeAction.Preview, {
|
|
955
|
+
id: "sodium",
|
|
956
|
+
})
|
|
957
|
+
).result,
|
|
958
|
+
).toEqual({
|
|
959
|
+
confirm: BridgeConfirm.Normal,
|
|
960
|
+
lines: [],
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
});
|
|
964
|
+
|
|
745
965
|
describe("the setup singleton", () => {
|
|
746
966
|
it("rides the manifest as its own block and never joins the panel modules", () => {
|
|
747
967
|
const steps = [
|
|
@@ -97,6 +97,16 @@ const pageSizeOf = (module: BridgePanelModule) => {
|
|
|
97
97
|
return module.kind === BridgeKind.Catalog ? (module.pageSize ?? null) : null;
|
|
98
98
|
};
|
|
99
99
|
|
|
100
|
+
const sourceOf = (module: BridgePanelModule) => {
|
|
101
|
+
return module.kind === BridgeKind.Catalog ? (module.source ?? null) : null;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const SOURCED_CATALOG_ACTIONS: readonly string[] = [
|
|
105
|
+
BridgeAction.Preview,
|
|
106
|
+
BridgeAction.Releases,
|
|
107
|
+
BridgeAction.Search,
|
|
108
|
+
];
|
|
109
|
+
|
|
100
110
|
const terminalManifestOf = (module: BridgeTerminalModule | undefined): BridgeTerminalManifest | null => {
|
|
101
111
|
if (!module) {
|
|
102
112
|
return null;
|
|
@@ -750,13 +760,19 @@ export const createRegistry = (
|
|
|
750
760
|
}
|
|
751
761
|
|
|
752
762
|
case BridgeKind.Catalog: {
|
|
763
|
+
if (module.source && SOURCED_CATALOG_ACTIONS.includes(action)) {
|
|
764
|
+
throw new BridgeRefusalError(
|
|
765
|
+
`module "${name}" takes its items from the ${module.source.kind} source, so the platform answers "${action}"`,
|
|
766
|
+
);
|
|
767
|
+
}
|
|
768
|
+
|
|
753
769
|
if (action === BridgeAction.Releases && module.releases) {
|
|
754
770
|
return await module.releases(context, asString(args, "id"));
|
|
755
771
|
}
|
|
756
772
|
if (action === BridgeAction.Preview && module.preview) {
|
|
757
773
|
return await module.preview(context, asString(args, "id"), asOptional(args, "releaseId") ?? undefined);
|
|
758
774
|
}
|
|
759
|
-
if (action === BridgeAction.Search) {
|
|
775
|
+
if (action === BridgeAction.Search && module.search) {
|
|
760
776
|
return await module.search(context, {
|
|
761
777
|
query: asText(args, "query"),
|
|
762
778
|
page: asPage(args),
|
|
@@ -774,6 +790,10 @@ export const createRegistry = (
|
|
|
774
790
|
return await module.install(context, asString(args, "id"), asOptional(args, "releaseId") ?? undefined);
|
|
775
791
|
}
|
|
776
792
|
|
|
793
|
+
if (action === BridgeAction.Add && module.add) {
|
|
794
|
+
return await module.add(context, asString(args, "input"));
|
|
795
|
+
}
|
|
796
|
+
|
|
777
797
|
if (action === BridgeAction.Remove) {
|
|
778
798
|
const id = asString(args, "id");
|
|
779
799
|
|
|
@@ -803,6 +823,7 @@ export const createRegistry = (
|
|
|
803
823
|
manifest(): BridgeManifest {
|
|
804
824
|
const entries: BridgeModuleManifest[] = Object.entries(modules).map(([id, module]) => {
|
|
805
825
|
const protectedActions = protectedActionsOf(module);
|
|
826
|
+
const source = sourceOf(module);
|
|
806
827
|
return {
|
|
807
828
|
id,
|
|
808
829
|
kind: module.kind,
|
|
@@ -816,6 +837,11 @@ export const createRegistry = (
|
|
|
816
837
|
protectedActions,
|
|
817
838
|
}
|
|
818
839
|
: {}),
|
|
840
|
+
...(source
|
|
841
|
+
? {
|
|
842
|
+
source,
|
|
843
|
+
}
|
|
844
|
+
: {}),
|
|
819
845
|
};
|
|
820
846
|
});
|
|
821
847
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { idleSample, sampleResultOf } from "./bridgeSample";
|
|
3
|
+
|
|
4
|
+
describe("sampleResultOf", () => {
|
|
5
|
+
it("serves no tick for an older driver that reports only its players", () => {
|
|
6
|
+
expect(
|
|
7
|
+
sampleResultOf({
|
|
8
|
+
online: 3,
|
|
9
|
+
max: 16,
|
|
10
|
+
}),
|
|
11
|
+
).toEqual({
|
|
12
|
+
online: 3,
|
|
13
|
+
max: 16,
|
|
14
|
+
tick: null,
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("carries the milliseconds per tick a driver measured", () => {
|
|
19
|
+
expect(
|
|
20
|
+
sampleResultOf({
|
|
21
|
+
online: 3,
|
|
22
|
+
max: 16,
|
|
23
|
+
tick: 21.4,
|
|
24
|
+
}).tick,
|
|
25
|
+
).toBe(21.4);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("keeps a tick a driver could not measure as null", () => {
|
|
29
|
+
expect(
|
|
30
|
+
sampleResultOf({
|
|
31
|
+
online: null,
|
|
32
|
+
max: null,
|
|
33
|
+
tick: null,
|
|
34
|
+
}).tick,
|
|
35
|
+
).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
for (const tick of [
|
|
39
|
+
-1,
|
|
40
|
+
Number.NaN,
|
|
41
|
+
Number.POSITIVE_INFINITY,
|
|
42
|
+
]) {
|
|
43
|
+
it(`drops a tick of ${tick}, which no game measures`, () => {
|
|
44
|
+
expect(
|
|
45
|
+
sampleResultOf({
|
|
46
|
+
online: 3,
|
|
47
|
+
max: 16,
|
|
48
|
+
tick,
|
|
49
|
+
}).tick,
|
|
50
|
+
).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("idleSample", () => {
|
|
56
|
+
it("reports no players and no tick while the game is not running", () => {
|
|
57
|
+
expect(idleSample()).toEqual({
|
|
58
|
+
online: null,
|
|
59
|
+
max: null,
|
|
60
|
+
tick: null,
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { BridgeSample } from "../modules";
|
|
2
|
+
import type { BridgeSampleResult } from "../protocol";
|
|
3
|
+
|
|
4
|
+
export const idleSample = (): BridgeSampleResult => {
|
|
5
|
+
return {
|
|
6
|
+
online: null,
|
|
7
|
+
max: null,
|
|
8
|
+
tick: null,
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const tickOf = (tick: number | null | undefined): number | null => {
|
|
13
|
+
return typeof tick === "number" && Number.isFinite(tick) && tick >= 0 ? tick : null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const sampleResultOf = (sample: BridgeSample): BridgeSampleResult => {
|
|
17
|
+
return {
|
|
18
|
+
online: sample.online,
|
|
19
|
+
max: sample.max,
|
|
20
|
+
tick: tickOf(sample.tick),
|
|
21
|
+
};
|
|
22
|
+
};
|
package/src/runtime/serve.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { createContext } from "./bridgeContext";
|
|
|
18
18
|
import { validateDriver } from "./bridgeDriver";
|
|
19
19
|
import { createEventRing } from "./bridgeEvents";
|
|
20
20
|
import { decideInstall, recountHold } from "./bridgeInstall";
|
|
21
|
-
import { createBridgeLiveConfig } from "./bridgeLiveConfig";
|
|
21
|
+
import { BRIDGE_CONFIG_REFRESH_MS, createBridgeLiveConfig, variableDefaultsOf } from "./bridgeLiveConfig";
|
|
22
22
|
import { errorDetail, errorText, print, log as stdoutLog } from "./bridgeLog";
|
|
23
23
|
import { pendingResult, rejectedPending, unsupportedPending } from "./bridgePending";
|
|
24
24
|
import {
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
secretFieldKeys,
|
|
30
30
|
} from "./bridgeRedaction";
|
|
31
31
|
import { createRegistry } from "./bridgeRegistry";
|
|
32
|
+
import { idleSample, sampleResultOf } from "./bridgeSample";
|
|
32
33
|
import { createServer } from "./bridgeServer";
|
|
33
34
|
import { setupSummaryOf } from "./bridgeSetup";
|
|
34
35
|
import { createSetupHandlers } from "./bridgeSetupHandlers";
|
|
@@ -90,7 +91,7 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
90
91
|
|
|
91
92
|
const desired = await readDesired(statePath(config));
|
|
92
93
|
|
|
93
|
-
const live = createBridgeLiveConfig(configPath(), config);
|
|
94
|
+
const live = createBridgeLiveConfig(configPath(), config, BRIDGE_CONFIG_REFRESH_MS, variableDefaultsOf(driver));
|
|
94
95
|
|
|
95
96
|
const redact = createBridgeRedaction(secretFieldKeys(driver), live);
|
|
96
97
|
|
|
@@ -266,10 +267,7 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
if (!supervisor.running) {
|
|
269
|
-
return
|
|
270
|
-
online: null,
|
|
271
|
-
max: null,
|
|
272
|
-
};
|
|
270
|
+
return idleSample();
|
|
273
271
|
}
|
|
274
272
|
|
|
275
273
|
const stamp = performance.now();
|
|
@@ -278,12 +276,7 @@ export const serve = async (driver: BridgeDriver) => {
|
|
|
278
276
|
return sampled.value;
|
|
279
277
|
}
|
|
280
278
|
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
const value: BridgeSampleResult = {
|
|
284
|
-
online: result.online,
|
|
285
|
-
max: result.max,
|
|
286
|
-
};
|
|
279
|
+
const value = sampleResultOf(await query.sample(context));
|
|
287
280
|
|
|
288
281
|
sampled = {
|
|
289
282
|
value,
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
workshopArchivePath,
|
|
4
|
+
workshopContentPath,
|
|
5
|
+
workshopDownloadArgs,
|
|
6
|
+
workshopFormBody,
|
|
7
|
+
workshopItemPageUrl,
|
|
8
|
+
} from "./workshop";
|
|
3
9
|
|
|
4
10
|
describe("workshopFormBody", () => {
|
|
5
11
|
it("writes the count and the indexed ids", () => {
|
|
@@ -43,3 +49,15 @@ describe("workshopContentPath", () => {
|
|
|
43
49
|
expect(workshopContentPath("440900", "12345")).toBe("steamapps/workshop/content/440900/12345");
|
|
44
50
|
});
|
|
45
51
|
});
|
|
52
|
+
|
|
53
|
+
describe("workshopArchivePath", () => {
|
|
54
|
+
it("names the archive the platform pushes for one item", () => {
|
|
55
|
+
expect(workshopArchivePath(1_623_730, "3762839811")).toBe(".serverk/snapshots/workshop-1623730-3762839811.zip");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("workshopItemPageUrl", () => {
|
|
60
|
+
it("links the item's steam community page", () => {
|
|
61
|
+
expect(workshopItemPageUrl("3762839811")).toBe("https://steamcommunity.com/sharedfiles/filedetails/?id=3762839811");
|
|
62
|
+
});
|
|
63
|
+
});
|