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