dsh-update-status 0.1.1 → 0.1.3
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/CHANGELOG.md +25 -0
- package/README.md +31 -7
- package/README.zh.md +31 -7
- package/docs/RELEASING.md +40 -2
- package/lib/client.js +432 -82
- package/lib/client.js.map +1 -1
- package/lib/index.js +8 -1
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -42,13 +42,6 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
const PACKAGE_NAME = "@deepseek-ai/dsh";
|
|
43
43
|
/** Shared Connection RPC channel. Custom prefixes 405 on the SPA fallback. */
|
|
44
44
|
const UPDATE_STATUS_CHANNEL = "/api";
|
|
45
|
-
/** Exact DSH release this bundle declares compatible in package.json. */
|
|
46
|
-
const STATIC_COMPATIBLE_VERSION = "0.1.5-rc.1";
|
|
47
|
-
const RELEASE_CHANNELS = [
|
|
48
|
-
"latest",
|
|
49
|
-
"next",
|
|
50
|
-
"alpha"
|
|
51
|
-
];
|
|
52
45
|
const MAX_CACHE_TTL_MINUTES = 1440;
|
|
53
46
|
function isCacheTtlMinutes(value) {
|
|
54
47
|
return typeof value === "number" && Number.isInteger(value) && value >= 30 && value <= 1440;
|
|
@@ -439,7 +432,7 @@ window.__ModuleLoader__.load({
|
|
|
439
432
|
event.stopPropagation();
|
|
440
433
|
const target = event.currentTarget;
|
|
441
434
|
if (target instanceof HTMLElement) target.blur();
|
|
442
|
-
|
|
435
|
+
ui.panel.toggle("brand");
|
|
443
436
|
};
|
|
444
437
|
const onKeyDown = (event) => {
|
|
445
438
|
if (event.key !== "Enter" && event.key !== " ") return;
|
|
@@ -454,13 +447,12 @@ window.__ModuleLoader__.load({
|
|
|
454
447
|
className: "dus-badge",
|
|
455
448
|
"data-update": snapshot.status?.hasUpdate === true || void 0,
|
|
456
449
|
"data-error": state === "problem" || void 0,
|
|
457
|
-
role:
|
|
458
|
-
tabIndex:
|
|
450
|
+
role: "button",
|
|
451
|
+
tabIndex: 0,
|
|
459
452
|
"aria-label": badgeLabel(snapshot.status, snapshot.loading, snapshot.error),
|
|
460
|
-
"aria-disabled": ui.canManage ? void 0 : true,
|
|
461
453
|
title: badgeLabel(snapshot.status, snapshot.loading, snapshot.error),
|
|
462
|
-
onClick:
|
|
463
|
-
onKeyDown
|
|
454
|
+
onClick: activate,
|
|
455
|
+
onKeyDown,
|
|
464
456
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
465
457
|
className: "dus-badge-version",
|
|
466
458
|
children: shortVersion(version)
|
|
@@ -485,7 +477,6 @@ window.__ModuleLoader__.load({
|
|
|
485
477
|
type: "button",
|
|
486
478
|
"aria-label": t("footer.status"),
|
|
487
479
|
title: label,
|
|
488
|
-
disabled: !ui.canManage,
|
|
489
480
|
onClick: () => {
|
|
490
481
|
ui.panel.toggle("rail");
|
|
491
482
|
},
|
|
@@ -534,7 +525,7 @@ window.__ModuleLoader__.load({
|
|
|
534
525
|
element.focus();
|
|
535
526
|
} catch {}
|
|
536
527
|
}
|
|
537
|
-
/** Full detail
|
|
528
|
+
/** Full detail from whichever page the operator is on (the transport is authenticated). */
|
|
538
529
|
function UpdatePanel({ ui }) {
|
|
539
530
|
const panel = useObservable(ui.panel);
|
|
540
531
|
const preferences = useObservable(ui.preferences);
|
|
@@ -542,7 +533,7 @@ window.__ModuleLoader__.load({
|
|
|
542
533
|
const commandRef = React.useRef(null);
|
|
543
534
|
const dialogRef = React.useRef(null);
|
|
544
535
|
const [copyMessage, setCopyMessage] = React.useState(null);
|
|
545
|
-
const visible = panel.open && preferences.sidebarEnabled
|
|
536
|
+
const visible = panel.open && preferences.sidebarEnabled;
|
|
546
537
|
React.useLayoutEffect(() => {
|
|
547
538
|
if (!visible) return void 0;
|
|
548
539
|
const dialog = dialogRef.current;
|
|
@@ -861,7 +852,7 @@ window.__ModuleLoader__.load({
|
|
|
861
852
|
className: "dus-settings-hint",
|
|
862
853
|
children: t("preview.follow")
|
|
863
854
|
}),
|
|
864
|
-
|
|
855
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
865
856
|
className: "dus-action",
|
|
866
857
|
type: "button",
|
|
867
858
|
"aria-expanded": showGuidance,
|
|
@@ -870,7 +861,7 @@ window.__ModuleLoader__.load({
|
|
|
870
861
|
},
|
|
871
862
|
children: t(showGuidance ? "preview.close" : "preview.open")
|
|
872
863
|
}),
|
|
873
|
-
|
|
864
|
+
showGuidance && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
874
865
|
"aria-label": t("preview.open"),
|
|
875
866
|
children: [
|
|
876
867
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -986,40 +977,12 @@ window.__ModuleLoader__.load({
|
|
|
986
977
|
};
|
|
987
978
|
var StatusStore = class {
|
|
988
979
|
connection;
|
|
989
|
-
snapshot;
|
|
990
|
-
allowRequests;
|
|
980
|
+
snapshot = INITIAL_STATUS;
|
|
991
981
|
listeners = /* @__PURE__ */ new Set();
|
|
992
982
|
inFlight;
|
|
993
983
|
stopped = false;
|
|
994
|
-
constructor(connection
|
|
984
|
+
constructor(connection) {
|
|
995
985
|
this.connection = connection;
|
|
996
|
-
this.allowRequests = staticVersion === null;
|
|
997
|
-
this.snapshot = staticVersion === null ? INITIAL_STATUS : {
|
|
998
|
-
status: {
|
|
999
|
-
currentVersion: staticVersion,
|
|
1000
|
-
latestVersion: null,
|
|
1001
|
-
hasUpdate: false,
|
|
1002
|
-
cached: true,
|
|
1003
|
-
checkedAt: null,
|
|
1004
|
-
warning: null,
|
|
1005
|
-
installKind: "unknown",
|
|
1006
|
-
upgradeCommand: "",
|
|
1007
|
-
releaseUrl: "",
|
|
1008
|
-
changelogUrl: "",
|
|
1009
|
-
publishedAt: null,
|
|
1010
|
-
packageName: "@deepseek-ai/dsh",
|
|
1011
|
-
channel: "latest",
|
|
1012
|
-
channels: RELEASE_CHANNELS.map((channel) => ({
|
|
1013
|
-
channel,
|
|
1014
|
-
version: null,
|
|
1015
|
-
publishedAt: null,
|
|
1016
|
-
compatibility: "unverified"
|
|
1017
|
-
})),
|
|
1018
|
-
canApplyInPlace: false
|
|
1019
|
-
},
|
|
1020
|
-
loading: false,
|
|
1021
|
-
error: null
|
|
1022
|
-
};
|
|
1023
986
|
}
|
|
1024
987
|
getSnapshot = () => this.snapshot;
|
|
1025
988
|
subscribe = (listener) => {
|
|
@@ -1030,15 +993,14 @@ window.__ModuleLoader__.load({
|
|
|
1030
993
|
};
|
|
1031
994
|
/** First mount reads the Host's cached status; it never starts a browser timer. */
|
|
1032
995
|
async load(cacheTtlMinutes = 360) {
|
|
1033
|
-
|
|
996
|
+
await this.request(false, this.snapshot.status?.channel ?? "latest", cacheTtlMinutes);
|
|
1034
997
|
}
|
|
1035
998
|
/** User gesture only: force the Host to bypass TTL (while retaining single-flight). */
|
|
1036
999
|
async refresh(channel = this.snapshot.status?.channel ?? "latest", cacheTtlMinutes = 360) {
|
|
1037
|
-
|
|
1000
|
+
await this.request(true, channel, cacheTtlMinutes);
|
|
1038
1001
|
}
|
|
1039
1002
|
/** Channel selection re-projects the Host cache; it is not a forced refresh. */
|
|
1040
1003
|
async selectChannel(channel, cacheTtlMinutes = 360) {
|
|
1041
|
-
if (!this.allowRequests) return;
|
|
1042
1004
|
for (let attempts = 0; attempts < 3 && !this.stopped && this.snapshot.status?.channel !== channel; attempts += 1) {
|
|
1043
1005
|
const pending = this.inFlight;
|
|
1044
1006
|
if (pending !== void 0 && !await pending) return;
|
|
@@ -1237,6 +1199,355 @@ window.__ModuleLoader__.load({
|
|
|
1237
1199
|
/** The one namespace name used by Host registration and browser binding. */
|
|
1238
1200
|
const SETTINGS_NAMESPACE = PLUGIN_ID$1;
|
|
1239
1201
|
//#endregion
|
|
1202
|
+
//#region src/client/settings/scopeFaces.ts
|
|
1203
|
+
/** Guard: does an object look like a binder with bind()? */
|
|
1204
|
+
function binderOf(raw) {
|
|
1205
|
+
if (raw === null || typeof raw !== "object") return void 0;
|
|
1206
|
+
const binder = raw.settingsScope;
|
|
1207
|
+
if (binder === null || typeof binder !== "object") return void 0;
|
|
1208
|
+
if (typeof binder.bind !== "function") return void 0;
|
|
1209
|
+
return binder;
|
|
1210
|
+
}
|
|
1211
|
+
/** Guard: does an object look like a bound scope? */
|
|
1212
|
+
function scopeOf(value) {
|
|
1213
|
+
if (value === null || typeof value !== "object") return void 0;
|
|
1214
|
+
const scope = value;
|
|
1215
|
+
if (typeof scope.getSnapshot !== "function") return void 0;
|
|
1216
|
+
if (typeof scope.subscribe !== "function") return void 0;
|
|
1217
|
+
if (typeof scope.set !== "function") return void 0;
|
|
1218
|
+
return scope;
|
|
1219
|
+
}
|
|
1220
|
+
//#endregion
|
|
1221
|
+
//#region src/client/settings/hostDirectScope.ts
|
|
1222
|
+
/** A refused namespace write; `code` keeps the Host's own diagnosis. */
|
|
1223
|
+
var SettingsWriteFailure = class extends Error {
|
|
1224
|
+
code;
|
|
1225
|
+
constructor(code, message) {
|
|
1226
|
+
super(message === void 0 || message === "" ? code : message);
|
|
1227
|
+
this.name = "SettingsWriteFailure";
|
|
1228
|
+
this.code = code;
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
function isRecord(value) {
|
|
1232
|
+
return typeof value === "object" && value !== null;
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Guard: does this value look like the `remote.settings` namespace face?
|
|
1236
|
+
* Applied to a bare service object (`ctx.get('remote.settings')` or a payload
|
|
1237
|
+
* member), and it never calls anything.
|
|
1238
|
+
*/
|
|
1239
|
+
function settingsRemoteFace(value) {
|
|
1240
|
+
if (!isRecord(value)) return void 0;
|
|
1241
|
+
if (typeof value.describe !== "function" || typeof value.mutate !== "function") return void 0;
|
|
1242
|
+
return value;
|
|
1243
|
+
}
|
|
1244
|
+
/**
|
|
1245
|
+
* Guard: read the `remote.settings` face off an injection payload.
|
|
1246
|
+
*
|
|
1247
|
+
* The payload a `ctx.inject(['remote.settings'], …)` callback receives exposes
|
|
1248
|
+
* the LITERAL service key. Reading the dotted PARENT off that injected context
|
|
1249
|
+
* (`payload.remote`) throws `cannot get property "remote" without inject` — a
|
|
1250
|
+
* throw that used to abort a whole wiring callback on a LAN page. So the literal
|
|
1251
|
+
* key is read FIRST and every read is contained; the nested
|
|
1252
|
+
* (`payload.remote.settings`) shape is only a fallback for a plain object
|
|
1253
|
+
* payload. Both absence and refusal answer `undefined`.
|
|
1254
|
+
*/
|
|
1255
|
+
function settingsRemoteOf(raw) {
|
|
1256
|
+
if (!isRecord(raw)) return void 0;
|
|
1257
|
+
try {
|
|
1258
|
+
const direct = settingsRemoteFace(raw["remote.settings"]);
|
|
1259
|
+
if (direct !== void 0) return direct;
|
|
1260
|
+
} catch {}
|
|
1261
|
+
try {
|
|
1262
|
+
const remote = raw.remote;
|
|
1263
|
+
if (isRecord(remote)) return settingsRemoteFace(remote.settings);
|
|
1264
|
+
} catch {}
|
|
1265
|
+
}
|
|
1266
|
+
/**
|
|
1267
|
+
* Guard: subscribe to the Host's settings-document invalidation on the Remote
|
|
1268
|
+
* service object (`remote.$on`). Returns undefined when the service exposes no
|
|
1269
|
+
* event seam — the preferences then refresh on reconnect only.
|
|
1270
|
+
*/
|
|
1271
|
+
function settingsInvalidationsOf(service) {
|
|
1272
|
+
if (!isRecord(service)) return void 0;
|
|
1273
|
+
const on = service.$on;
|
|
1274
|
+
if (typeof on !== "function") return void 0;
|
|
1275
|
+
return (listener) => {
|
|
1276
|
+
const dispose = on.call(service, "settings/document-updated", listener);
|
|
1277
|
+
return typeof dispose === "function" ? dispose : () => {};
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
function cloneOps(ops) {
|
|
1281
|
+
try {
|
|
1282
|
+
return structuredClone(ops);
|
|
1283
|
+
} catch {
|
|
1284
|
+
return ops.map((op) => op.op === "set" ? {
|
|
1285
|
+
op: "set",
|
|
1286
|
+
path: [...op.path],
|
|
1287
|
+
value: op.value
|
|
1288
|
+
} : {
|
|
1289
|
+
op: "unset",
|
|
1290
|
+
path: [...op.path]
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
function messageOf(error) {
|
|
1295
|
+
return error instanceof Error ? error.message : String(error);
|
|
1296
|
+
}
|
|
1297
|
+
function sameSnapshot$1(a, b) {
|
|
1298
|
+
return a.status === b.status && a.revision === b.revision && a.writable === b.writable && a.mode === b.mode && a.value === b.value && a.base === b.base && a.user === b.user;
|
|
1299
|
+
}
|
|
1300
|
+
function readySnapshot(row, writable) {
|
|
1301
|
+
return {
|
|
1302
|
+
status: "ready",
|
|
1303
|
+
value: row.value,
|
|
1304
|
+
base: row.base,
|
|
1305
|
+
user: row.user,
|
|
1306
|
+
revision: row.revision,
|
|
1307
|
+
writable,
|
|
1308
|
+
mode: "host"
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
function unavailableSnapshot(writable) {
|
|
1312
|
+
return {
|
|
1313
|
+
status: "unavailable",
|
|
1314
|
+
value: void 0,
|
|
1315
|
+
base: void 0,
|
|
1316
|
+
user: void 0,
|
|
1317
|
+
revision: void 0,
|
|
1318
|
+
writable,
|
|
1319
|
+
mode: "host"
|
|
1320
|
+
};
|
|
1321
|
+
}
|
|
1322
|
+
/**
|
|
1323
|
+
* Build the direct Host settings scope for one namespace.
|
|
1324
|
+
*
|
|
1325
|
+
* Snapshot semantics mirror the official per-namespace derivation: a namespace
|
|
1326
|
+
* row answers `ready` with the resolved section, its `base`/`user` layers and its
|
|
1327
|
+
* revision fence, and the document's `writable` decides editability. A row the
|
|
1328
|
+
* Host does not serve answers `unavailable`; a refused read keeps the last
|
|
1329
|
+
* confirmed document and reports `unavailable` only while it has never held one.
|
|
1330
|
+
*
|
|
1331
|
+
* Writes are serialized in issue order, fenced by the latest known revision and
|
|
1332
|
+
* REJECT with {@link SettingsWriteFailure} on a Host refusal — that rejection is
|
|
1333
|
+
* what makes the settings page surface a conflict instead of pretending the edit
|
|
1334
|
+
* landed (the official scope resolves on `!ok` after its recovery read).
|
|
1335
|
+
*
|
|
1336
|
+
* @param remote - the `remote.settings` face of the page's Remote service.
|
|
1337
|
+
* @param namespace - the settings namespace this scope derives from.
|
|
1338
|
+
* @returns the scope plus its load/dispose seams.
|
|
1339
|
+
*/
|
|
1340
|
+
function createHostDirectScope(remote, namespace) {
|
|
1341
|
+
let snapshot = {
|
|
1342
|
+
status: "loading",
|
|
1343
|
+
value: void 0,
|
|
1344
|
+
base: void 0,
|
|
1345
|
+
user: void 0,
|
|
1346
|
+
revision: void 0,
|
|
1347
|
+
writable: false,
|
|
1348
|
+
mode: "host"
|
|
1349
|
+
};
|
|
1350
|
+
let held;
|
|
1351
|
+
let error;
|
|
1352
|
+
let disposed = false;
|
|
1353
|
+
let tail = Promise.resolve();
|
|
1354
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1355
|
+
const publish = (next) => {
|
|
1356
|
+
if (disposed || sameSnapshot$1(next, snapshot)) return;
|
|
1357
|
+
snapshot = next;
|
|
1358
|
+
for (const listener of [...listeners]) try {
|
|
1359
|
+
listener();
|
|
1360
|
+
} catch {}
|
|
1361
|
+
};
|
|
1362
|
+
const adopt = (next) => {
|
|
1363
|
+
if (next.status === "ready") held = next;
|
|
1364
|
+
publish(next);
|
|
1365
|
+
};
|
|
1366
|
+
async function load() {
|
|
1367
|
+
if (disposed) return;
|
|
1368
|
+
let outcome;
|
|
1369
|
+
try {
|
|
1370
|
+
outcome = await remote.describe();
|
|
1371
|
+
} catch (caught) {
|
|
1372
|
+
outcome = {
|
|
1373
|
+
ok: false,
|
|
1374
|
+
error: { message: messageOf(caught) }
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1377
|
+
if (disposed) return;
|
|
1378
|
+
if (!outcome.ok) {
|
|
1379
|
+
error = outcome.error?.message ?? outcome.error?.code ?? "settings/describe-failed";
|
|
1380
|
+
publish(held ?? unavailableSnapshot(snapshot.writable));
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
error = void 0;
|
|
1384
|
+
const view = outcome.value;
|
|
1385
|
+
const writable = view.writable === true;
|
|
1386
|
+
const row = Array.isArray(view.namespaces) ? view.namespaces.find((candidate) => candidate.ns === namespace) : void 0;
|
|
1387
|
+
if (row === void 0 || typeof row.revision !== "number") {
|
|
1388
|
+
publish(unavailableSnapshot(writable));
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
adopt(readySnapshot(row, writable));
|
|
1392
|
+
}
|
|
1393
|
+
/** Fold one write answer's namespace row in without a second wire read. */
|
|
1394
|
+
function fold(row, writable) {
|
|
1395
|
+
if (typeof row.revision !== "number") return;
|
|
1396
|
+
adopt(readySnapshot(row, writable));
|
|
1397
|
+
}
|
|
1398
|
+
function mutate(ops, expectedRevision) {
|
|
1399
|
+
const owned = cloneOps(ops);
|
|
1400
|
+
const run = tail.then(async () => {
|
|
1401
|
+
if (disposed) throw new SettingsWriteFailure("settings/unavailable");
|
|
1402
|
+
const revision = expectedRevision ?? snapshot.revision;
|
|
1403
|
+
let outcome;
|
|
1404
|
+
try {
|
|
1405
|
+
outcome = revision === void 0 ? await remote.mutate(namespace, owned) : await remote.mutate(namespace, owned, revision);
|
|
1406
|
+
} catch (caught) {
|
|
1407
|
+
await load();
|
|
1408
|
+
throw new SettingsWriteFailure("settings/unreachable", messageOf(caught));
|
|
1409
|
+
}
|
|
1410
|
+
if (outcome.ok) {
|
|
1411
|
+
fold(outcome.value, true);
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
await load();
|
|
1415
|
+
throw new SettingsWriteFailure(outcome.error?.code ?? "settings/unknown", outcome.error?.message);
|
|
1416
|
+
});
|
|
1417
|
+
tail = run.then(() => void 0, () => void 0);
|
|
1418
|
+
return run;
|
|
1419
|
+
}
|
|
1420
|
+
return {
|
|
1421
|
+
getSnapshot: () => snapshot,
|
|
1422
|
+
subscribe(listener) {
|
|
1423
|
+
listeners.add(listener);
|
|
1424
|
+
return () => {
|
|
1425
|
+
listeners.delete(listener);
|
|
1426
|
+
};
|
|
1427
|
+
},
|
|
1428
|
+
set: (field, value) => mutate([{
|
|
1429
|
+
op: "set",
|
|
1430
|
+
path: [field],
|
|
1431
|
+
value
|
|
1432
|
+
}]),
|
|
1433
|
+
load,
|
|
1434
|
+
dispose() {
|
|
1435
|
+
disposed = true;
|
|
1436
|
+
listeners.clear();
|
|
1437
|
+
},
|
|
1438
|
+
lastError: () => error
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
//#endregion
|
|
1442
|
+
//#region src/client/settings/settingsChannel.ts
|
|
1443
|
+
/** Snapshot used while no channel has answered yet. */
|
|
1444
|
+
const LOADING_SNAPSHOT = Object.freeze({
|
|
1445
|
+
status: "loading",
|
|
1446
|
+
value: void 0,
|
|
1447
|
+
base: void 0,
|
|
1448
|
+
user: void 0,
|
|
1449
|
+
revision: void 0,
|
|
1450
|
+
writable: false,
|
|
1451
|
+
mode: "host"
|
|
1452
|
+
});
|
|
1453
|
+
function sameSnapshot(a, b) {
|
|
1454
|
+
return a.status === b.status && a.revision === b.revision && a.writable === b.writable && a.mode === b.mode && a.value === b.value && a.base === b.base && a.user === b.user;
|
|
1455
|
+
}
|
|
1456
|
+
/** Ask a channel for its first/next read; a refusal surfaces as a snapshot, never a throw. */
|
|
1457
|
+
function loadSource(source) {
|
|
1458
|
+
const loadable = source;
|
|
1459
|
+
if (loadable === void 0 || typeof loadable.load !== "function") return Promise.resolve();
|
|
1460
|
+
return Promise.resolve(loadable.load()).then(() => void 0, () => void 0);
|
|
1461
|
+
}
|
|
1462
|
+
function createSettingsChannel(options) {
|
|
1463
|
+
let official;
|
|
1464
|
+
let direct;
|
|
1465
|
+
let active;
|
|
1466
|
+
let snapshot = LOADING_SNAPSHOT;
|
|
1467
|
+
let disposed = false;
|
|
1468
|
+
const disposers = [];
|
|
1469
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1470
|
+
const notify = () => {
|
|
1471
|
+
for (const listener of [...listeners]) try {
|
|
1472
|
+
listener();
|
|
1473
|
+
} catch {}
|
|
1474
|
+
};
|
|
1475
|
+
const publish = (next) => {
|
|
1476
|
+
if (disposed || sameSnapshot(next, snapshot)) return;
|
|
1477
|
+
snapshot = next;
|
|
1478
|
+
notify();
|
|
1479
|
+
};
|
|
1480
|
+
const select = () => {
|
|
1481
|
+
if (official !== void 0 && official.getSnapshot().status !== "unavailable") return official;
|
|
1482
|
+
if (direct === void 0) {
|
|
1483
|
+
try {
|
|
1484
|
+
direct = options.openDirect();
|
|
1485
|
+
} catch {
|
|
1486
|
+
direct = void 0;
|
|
1487
|
+
}
|
|
1488
|
+
if (direct !== void 0) {
|
|
1489
|
+
try {
|
|
1490
|
+
disposers.push(direct.subscribe(() => {
|
|
1491
|
+
if (active === direct) publish(direct.getSnapshot());
|
|
1492
|
+
}));
|
|
1493
|
+
} catch {}
|
|
1494
|
+
loadSource(direct);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return direct ?? official;
|
|
1498
|
+
};
|
|
1499
|
+
const reselect = () => {
|
|
1500
|
+
if (disposed) return;
|
|
1501
|
+
const next = select();
|
|
1502
|
+
if (next !== active) {
|
|
1503
|
+
active = next;
|
|
1504
|
+
publish(next?.getSnapshot() ?? LOADING_SNAPSHOT);
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
publish(next?.getSnapshot() ?? LOADING_SNAPSHOT);
|
|
1508
|
+
};
|
|
1509
|
+
return {
|
|
1510
|
+
getSnapshot: () => snapshot,
|
|
1511
|
+
subscribe(listener) {
|
|
1512
|
+
listeners.add(listener);
|
|
1513
|
+
return () => {
|
|
1514
|
+
listeners.delete(listener);
|
|
1515
|
+
};
|
|
1516
|
+
},
|
|
1517
|
+
set(field, value) {
|
|
1518
|
+
const source = active;
|
|
1519
|
+
if (source === void 0) return Promise.reject(/* @__PURE__ */ new Error("dsh-update-status: settings channel is unavailable"));
|
|
1520
|
+
return source.set(field, value);
|
|
1521
|
+
},
|
|
1522
|
+
setOfficial(scope) {
|
|
1523
|
+
if (disposed) return;
|
|
1524
|
+
official = scope;
|
|
1525
|
+
if (scope !== void 0) try {
|
|
1526
|
+
disposers.push(scope.subscribe(() => {
|
|
1527
|
+
reselect();
|
|
1528
|
+
}));
|
|
1529
|
+
} catch {}
|
|
1530
|
+
reselect();
|
|
1531
|
+
},
|
|
1532
|
+
refresh: reselect,
|
|
1533
|
+
reload() {
|
|
1534
|
+
if (active === void 0 || active === official) return;
|
|
1535
|
+
loadSource(active);
|
|
1536
|
+
},
|
|
1537
|
+
dispose() {
|
|
1538
|
+
disposed = true;
|
|
1539
|
+
listeners.clear();
|
|
1540
|
+
for (const dispose of disposers.splice(0)) try {
|
|
1541
|
+
dispose();
|
|
1542
|
+
} catch {}
|
|
1543
|
+
const disposable = active;
|
|
1544
|
+
if (disposable !== void 0 && typeof disposable.dispose === "function") try {
|
|
1545
|
+
disposable.dispose();
|
|
1546
|
+
} catch {}
|
|
1547
|
+
}
|
|
1548
|
+
};
|
|
1549
|
+
}
|
|
1550
|
+
//#endregion
|
|
1240
1551
|
//#region src/client/styles.ts
|
|
1241
1552
|
/** Plugin-owned CSS only; no shell DOM selection or official SVG manipulation. */
|
|
1242
1553
|
const UPDATE_STATUS_CSS = `
|
|
@@ -1338,48 +1649,87 @@ window.__ModuleLoader__.load({
|
|
|
1338
1649
|
}
|
|
1339
1650
|
}
|
|
1340
1651
|
function apply(ctx) {
|
|
1341
|
-
const
|
|
1342
|
-
const canManage = connection.isLoopback === true;
|
|
1343
|
-
const status = new StatusStore(connection, canManage ? null : STATIC_COMPATIBLE_VERSION);
|
|
1652
|
+
const status = new StatusStore(connectionOf(ctx));
|
|
1344
1653
|
const preferences = new PreferencesStore();
|
|
1345
1654
|
const ui = {
|
|
1346
1655
|
status,
|
|
1347
1656
|
preferences,
|
|
1348
|
-
panel: new PanelStore()
|
|
1349
|
-
canManage
|
|
1657
|
+
panel: new PanelStore()
|
|
1350
1658
|
};
|
|
1659
|
+
const disposers = [];
|
|
1660
|
+
let stopped = false;
|
|
1661
|
+
/** Own a subscription raised from an injection callback; release after teardown at once. */
|
|
1662
|
+
const own = (dispose) => {
|
|
1663
|
+
if (typeof dispose !== "function") return;
|
|
1664
|
+
if (stopped) {
|
|
1665
|
+
try {
|
|
1666
|
+
dispose();
|
|
1667
|
+
} catch {}
|
|
1668
|
+
return;
|
|
1669
|
+
}
|
|
1670
|
+
disposers.push(dispose);
|
|
1671
|
+
};
|
|
1672
|
+
let remoteSettings;
|
|
1673
|
+
/** Resolve the direct channel's Remote face; `ctx.get` covers a payload we could not read. */
|
|
1674
|
+
const directRemote = () => {
|
|
1675
|
+
if (remoteSettings !== void 0) return remoteSettings;
|
|
1676
|
+
try {
|
|
1677
|
+
remoteSettings = settingsRemoteFace(ctx.get("remote.settings"));
|
|
1678
|
+
} catch {}
|
|
1679
|
+
return remoteSettings;
|
|
1680
|
+
};
|
|
1681
|
+
const channel = createSettingsChannel({ openDirect: () => {
|
|
1682
|
+
const remote = directRemote();
|
|
1683
|
+
return remote === void 0 ? void 0 : createHostDirectScope(remote, SETTINGS_NAMESPACE);
|
|
1684
|
+
} });
|
|
1351
1685
|
ctx.effect(() => {
|
|
1352
1686
|
const disposeStyles = installStyles();
|
|
1353
|
-
|
|
1687
|
+
const detach = preferences.attach(channel);
|
|
1688
|
+
let selected = status.getSnapshot().status?.channel ?? "latest";
|
|
1689
|
+
const syncPreferences = () => {
|
|
1690
|
+
const next = preferences.getSnapshot();
|
|
1691
|
+
if (next.channel !== selected) {
|
|
1692
|
+
selected = next.channel;
|
|
1693
|
+
status.selectChannel(selected, next.cacheTtlMinutes);
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
syncPreferences();
|
|
1697
|
+
const unsubscribe = preferences.subscribe(syncPreferences);
|
|
1698
|
+
status.load(preferences.getSnapshot().cacheTtlMinutes);
|
|
1354
1699
|
return () => {
|
|
1700
|
+
stopped = true;
|
|
1701
|
+
unsubscribe();
|
|
1702
|
+
detach();
|
|
1703
|
+
for (const dispose of disposers.splice(0)) try {
|
|
1704
|
+
dispose();
|
|
1705
|
+
} catch {}
|
|
1706
|
+
channel.dispose();
|
|
1355
1707
|
status.stop();
|
|
1356
1708
|
disposeStyles();
|
|
1357
1709
|
};
|
|
1358
|
-
}, "dsh-update-status:
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
} catch {}
|
|
1382
|
-
});
|
|
1710
|
+
}, "dsh-update-status: styles, settings channel and first status read");
|
|
1711
|
+
try {
|
|
1712
|
+
ctx.inject(["settingsScope"], (raw) => {
|
|
1713
|
+
try {
|
|
1714
|
+
const binder = binderOf(raw);
|
|
1715
|
+
if (binder === void 0) return;
|
|
1716
|
+
channel.setOfficial(scopeOf(binder.bind({ namespace: SETTINGS_NAMESPACE })));
|
|
1717
|
+
} catch {}
|
|
1718
|
+
});
|
|
1719
|
+
} catch {}
|
|
1720
|
+
try {
|
|
1721
|
+
ctx.inject(["remote.settings"], (raw) => {
|
|
1722
|
+
try {
|
|
1723
|
+
remoteSettings = settingsRemoteOf(raw) ?? remoteSettings;
|
|
1724
|
+
channel.refresh();
|
|
1725
|
+
const invalidations = settingsInvalidationsOf(ctx.get("remote"));
|
|
1726
|
+
if (invalidations !== void 0) own(invalidations(() => channel.reload()));
|
|
1727
|
+
} catch {}
|
|
1728
|
+
});
|
|
1729
|
+
} catch {}
|
|
1730
|
+
if (typeof ctx.on === "function") try {
|
|
1731
|
+
own(ctx.on("connection/reset", () => channel.reload()));
|
|
1732
|
+
} catch {}
|
|
1383
1733
|
ctx.slots.inject("sidebar.brand.name", () => ctx.slots.register({
|
|
1384
1734
|
name: "sidebar.brand.name",
|
|
1385
1735
|
priority: -10
|