@dsh-electron/dsh-plugin-multi-root-workspace 0.1.11 → 0.1.13
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.en.md +25 -1
- package/CHANGELOG.md +25 -1
- package/README.en.md +10 -8
- package/README.md +10 -8
- package/cordis.patch.yml +7 -2
- package/lib/auto-review.js +1 -1
- package/lib/client.js +197 -74
- package/lib/client.js.map +1 -1
- package/lib/{command-BGdz7VBH.js → command-B9855H7y.js} +3 -2
- package/lib/command.js +1 -1
- package/lib/{compat-BiW-m_q0.js → compat-Bg2HFVMX.js} +3 -0
- package/lib/compat.js +1 -1
- package/lib/{sandbox-CXk5h-ct.js → dialects-Dr4rDR-H.js} +6 -264
- package/lib/{fs-BTXySwSG.js → fs-CfzJq2wd.js} +23 -8
- package/lib/fs.js +1 -1
- package/lib/index.js +7 -6
- package/lib/{instructions-MePoRigy.js → instructions-KsUwUFE0.js} +53 -37
- package/lib/instructions.js +1 -1
- package/lib/lsp.js +19 -16
- package/lib/native-win32-2waCRkPI.js +312 -0
- package/lib/process-fence.js +62 -12
- package/lib/protocol-CJOvAvOS.js +652 -0
- package/lib/{registry-KSsmU393.js → registry-Bv431TfO.js} +717 -74
- package/lib/registry-operation-C2ZV3Jhw.js +116 -0
- package/lib/registry.js +3 -2
- package/lib/{review-context-AH-SXHmj.js → review-context-NDpKeMCc.js} +1 -1
- package/lib/review-context.js +1 -1
- package/lib/sandbox-C-vCJCp3.js +278 -0
- package/lib/sandbox.js +1 -1
- package/lib/scope.js +101 -11
- package/lib/skills.js +48 -29
- package/lib/types/authority/native-win32.d.ts +7 -0
- package/lib/types/authority/native.d.ts +23 -0
- package/lib/types/authority/protocol.d.ts +70 -0
- package/lib/types/authority/recovery.d.ts +58 -0
- package/lib/types/client/RootSwitcher.d.ts +6 -3
- package/lib/types/client/files-tree/adapter.d.ts +63 -1
- package/lib/types/compat/authority-storage.d.ts +6 -0
- package/lib/types/compat/dsh-version.d.ts +4 -4
- package/lib/types/compat/open-in-app-device.d.ts +1 -1
- package/lib/types/compat/process-entrypoints.d.ts +2 -0
- package/lib/types/compat/process-profile.d.ts +3 -0
- package/lib/types/compat/sandbox-confine.d.ts +2 -0
- package/lib/types/fs.d.ts +4 -1
- package/lib/types/instructions.d.ts +0 -47
- package/lib/types/registry-operation.d.ts +31 -0
- package/lib/types/registry.d.ts +145 -6
- package/lib/types/scope.d.ts +28 -0
- package/lib/types/skills/catalog.d.ts +3 -0
- package/lib/types/skills/pool.d.ts +1 -1
- package/lib/workspace-files.js +63 -7
- package/package.json +38 -31
|
@@ -1,13 +1,53 @@
|
|
|
1
|
+
import { a as runPrimaryOperation, n as freezeOperationScope, o as runRegistryOperation } from "./registry-operation-C2ZV3Jhw.js";
|
|
1
2
|
import { i as isWindowsNetworkOrDevicePath } from "./windows-runner-contract-B5R2K3mM.js";
|
|
2
3
|
import { a as classifyStoredRoots, d as reservedRootBoundary, f as resolveRootIndex, i as canonicalRoot, m as validateRootCandidate, n as additionalRootId, r as availableRoots, s as expandRootInput, t as RootValidationError, u as removeStatusAt } from "./roots-Ca_W1S9n.js";
|
|
3
4
|
import { r as validateAliasId, t as migrateRootSkills } from "./metadata-DgqSdVX_.js";
|
|
5
|
+
import { a as snapshotSchema, i as parseArguments, r as AuthorityServer, s as storeIdentity, t as AuthorityClient } from "./protocol-CJOvAvOS.js";
|
|
4
6
|
import { createRequire } from "node:module";
|
|
5
7
|
import { closeSync, fstatSync, mkdirSync, openSync, statSync } from "node:fs";
|
|
6
8
|
import { randomUUID } from "node:crypto";
|
|
7
|
-
import { dirname, isAbsolute } from "node:path";
|
|
9
|
+
import { basename, dirname, isAbsolute } from "node:path";
|
|
8
10
|
import { Service } from "@deepseek-ai/cordis";
|
|
9
|
-
import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
11
|
+
import { DomainFacility, defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
|
|
10
12
|
import { tryLockExclusive } from "@deepseek-ai/node-addon-system/flock";
|
|
13
|
+
//#region src/compat/authority-storage.ts
|
|
14
|
+
/** Open an explicitly bound medium through public storage APIs only. */
|
|
15
|
+
async function openAuthorityStorage(ctx, root, spec) {
|
|
16
|
+
const { JsonStorageBackend } = await import("@deepseek-ai/dsh-storage-json");
|
|
17
|
+
const backend = new JsonStorageBackend(root);
|
|
18
|
+
const name = `multi-root-${randomUUID()}`;
|
|
19
|
+
const unmount = ctx.get("storage").backend.register(name, backend);
|
|
20
|
+
let facility;
|
|
21
|
+
const fiber = ctx.inject(["storage"], (scope) => {
|
|
22
|
+
facility = new DomainFacility(scope, {
|
|
23
|
+
backend: name,
|
|
24
|
+
routes: {}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
try {
|
|
28
|
+
await fiber;
|
|
29
|
+
if (!facility) throw new Error("shared registry requires the storage hub");
|
|
30
|
+
const domain = await facility.open(spec);
|
|
31
|
+
return {
|
|
32
|
+
table: domain.table.bind(domain),
|
|
33
|
+
async close() {
|
|
34
|
+
try {
|
|
35
|
+
await domain.close();
|
|
36
|
+
} finally {
|
|
37
|
+
unmount();
|
|
38
|
+
await backend.close();
|
|
39
|
+
await fiber.dispose();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
} catch (error) {
|
|
44
|
+
unmount();
|
|
45
|
+
await backend.close();
|
|
46
|
+
await fiber.dispose();
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
11
51
|
//#region src/compat/storage-zod.ts
|
|
12
52
|
/**
|
|
13
53
|
* Build the domain record schemas with the zod module the installed
|
|
@@ -54,6 +94,107 @@ function domainZod() {
|
|
|
54
94
|
return createRequire(require.resolve("@deepseek-ai/dsh-storage-domain/package.json"))("zod");
|
|
55
95
|
}
|
|
56
96
|
//#endregion
|
|
97
|
+
//#region src/authority/recovery.ts
|
|
98
|
+
var RecoveryScheduler = class {
|
|
99
|
+
run;
|
|
100
|
+
eligible;
|
|
101
|
+
enabled;
|
|
102
|
+
timers;
|
|
103
|
+
timer;
|
|
104
|
+
inFlight = false;
|
|
105
|
+
disposed = false;
|
|
106
|
+
attempts = 0;
|
|
107
|
+
retryAt;
|
|
108
|
+
constructor(options) {
|
|
109
|
+
this.run = options.run;
|
|
110
|
+
this.eligible = options.eligible;
|
|
111
|
+
this.enabled = options.enabled;
|
|
112
|
+
this.timers = options.timers ?? {
|
|
113
|
+
setTimeout,
|
|
114
|
+
clearTimeout,
|
|
115
|
+
random: Math.random,
|
|
116
|
+
now: Date.now
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Ask for a recovery attempt. Called after every `ensureAuthority()` and on
|
|
121
|
+
* every downgrade event. A healthy state (`!eligible()`) resets the backoff;
|
|
122
|
+
* a pending timer or in-flight attempt is never duplicated or reset.
|
|
123
|
+
*/
|
|
124
|
+
arm() {
|
|
125
|
+
if (this.disposed || !this.enabled) return;
|
|
126
|
+
if (!this.eligible()) {
|
|
127
|
+
this.attempts = 0;
|
|
128
|
+
this.clearTimer();
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (this.timer !== void 0 || this.inFlight) return;
|
|
132
|
+
const delay = this.delay(this.attempts);
|
|
133
|
+
this.retryAt = this.timers.now() + delay;
|
|
134
|
+
this.timer = this.timers.setTimeout(() => {
|
|
135
|
+
this.tick();
|
|
136
|
+
}, delay);
|
|
137
|
+
this.timer.unref();
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Progress event (a follower connection was just adopted): the backoff is
|
|
141
|
+
* earned, so reset it and run the next attempt immediately — still through
|
|
142
|
+
* the single pending timer.
|
|
143
|
+
*/
|
|
144
|
+
kick() {
|
|
145
|
+
if (this.disposed || !this.enabled) return;
|
|
146
|
+
this.attempts = 0;
|
|
147
|
+
if (this.inFlight) return;
|
|
148
|
+
this.clearTimer();
|
|
149
|
+
this.retryAt = this.timers.now();
|
|
150
|
+
this.timer = this.timers.setTimeout(() => {
|
|
151
|
+
this.tick();
|
|
152
|
+
}, 0);
|
|
153
|
+
this.timer.unref();
|
|
154
|
+
}
|
|
155
|
+
/** Stop scheduling. An in-flight attempt is left to finish on its own. */
|
|
156
|
+
dispose() {
|
|
157
|
+
this.disposed = true;
|
|
158
|
+
this.clearTimer();
|
|
159
|
+
}
|
|
160
|
+
get state() {
|
|
161
|
+
return {
|
|
162
|
+
phase: this.disposed || !this.enabled && this.eligible() ? "disabled" : this.inFlight ? "attempting" : this.timer !== void 0 ? "waiting" : "idle",
|
|
163
|
+
attempts: this.attempts,
|
|
164
|
+
nextRetryAt: this.timer === void 0 ? void 0 : this.retryAt
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
clearTimer() {
|
|
168
|
+
if (this.timer !== void 0) {
|
|
169
|
+
this.timers.clearTimeout(this.timer);
|
|
170
|
+
this.timer = void 0;
|
|
171
|
+
}
|
|
172
|
+
this.retryAt = void 0;
|
|
173
|
+
}
|
|
174
|
+
delay(n) {
|
|
175
|
+
return Math.min(1e3 * 2 ** n, 3e4) * (.8 + .4 * this.timers.random());
|
|
176
|
+
}
|
|
177
|
+
async tick() {
|
|
178
|
+
this.timer = void 0;
|
|
179
|
+
this.retryAt = void 0;
|
|
180
|
+
if (!this.eligible()) {
|
|
181
|
+
this.attempts = 0;
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.inFlight = true;
|
|
185
|
+
try {
|
|
186
|
+
await this.run();
|
|
187
|
+
} catch {}
|
|
188
|
+
this.inFlight = false;
|
|
189
|
+
if (this.disposed || !this.eligible()) {
|
|
190
|
+
this.attempts = 0;
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
this.attempts++;
|
|
194
|
+
this.arm();
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
//#endregion
|
|
57
198
|
//#region src/registry-lease.ts
|
|
58
199
|
/**
|
|
59
200
|
* Store-wide kernel lease for the multi-root registry.
|
|
@@ -209,8 +350,8 @@ var RegistryAuthorityLease = class RegistryAuthorityLease {
|
|
|
209
350
|
* only when the operator removes it (or when the store is repaired). Storage
|
|
210
351
|
* that cannot be parsed at all is backed up and skipped by the domain layer,
|
|
211
352
|
* with a warning.
|
|
212
|
-
* - **Mutations are serialized
|
|
213
|
-
* whole read → validate → persist sequence inside
|
|
353
|
+
* - **Mutations and fs admissions are serialized store-wide.** Every mutation
|
|
354
|
+
* runs its whole read → validate → persist sequence inside the store queue. The
|
|
214
355
|
* storage domain serializes individual `put()`/`delete()` calls, which is not
|
|
215
356
|
* enough: two concurrent `add()` calls would both read the same snapshot and
|
|
216
357
|
* the later write would drop the earlier one, and a removal racing an add
|
|
@@ -251,6 +392,8 @@ const REGISTRY_CONTENDED_MESSAGE = "the root registry is owned by another DSH pr
|
|
|
251
392
|
const DEFAULT_MAX_ADDITIONAL_ROOTS = 16;
|
|
252
393
|
/** Hard ceiling for `maxAdditionalRoots`: values above it are config-invalid. */
|
|
253
394
|
const MAX_ADDITIONAL_ROOTS_CEILING = 64;
|
|
395
|
+
/** A medium/lock binding error only a changed configuration can repair. */
|
|
396
|
+
var SharedConfigError = class extends Error {};
|
|
254
397
|
/**
|
|
255
398
|
* The durable-boundary validators. The builder is the zod module the
|
|
256
399
|
* INSTALLED storage-domain resolves, not this package's own `zod` copy:
|
|
@@ -296,20 +439,46 @@ var MultiRootRegistry = class extends Service {
|
|
|
296
439
|
];
|
|
297
440
|
/** Absolute lock path; absent means the row was composed without `leasePath`. */
|
|
298
441
|
leasePath;
|
|
442
|
+
sharedStorageRoot;
|
|
443
|
+
authorityStorageContext;
|
|
444
|
+
medium;
|
|
445
|
+
server;
|
|
446
|
+
client;
|
|
447
|
+
connectionFailure;
|
|
448
|
+
connectionGeneration = 0;
|
|
449
|
+
/** Local cancel of the in-flight handshake; teardown must not wait out hello. */
|
|
450
|
+
connectionAbort;
|
|
451
|
+
remoteProjectionReady = false;
|
|
452
|
+
remoteRevisions = /* @__PURE__ */ new Map();
|
|
453
|
+
/**
|
|
454
|
+
* Primary roots whose projection must still be synced after a follower
|
|
455
|
+
* reconnect — populated from the tracked cache keys at adoption, removed as
|
|
456
|
+
* each `applySnapshot` lands. Recovery completes only when it empties.
|
|
457
|
+
*/
|
|
458
|
+
pendingSync = /* @__PURE__ */ new Set();
|
|
459
|
+
/** Bounded background retry for degraded authority states (ADR-0026 R1). */
|
|
460
|
+
recovery;
|
|
461
|
+
operationProof = Object.freeze({});
|
|
299
462
|
/** Whether this process currently owns the store. */
|
|
300
463
|
authorityState = { kind: "contended" };
|
|
301
464
|
/** Held kernel lease; present once acquire succeeded, including after a failed open. */
|
|
302
465
|
lease;
|
|
303
466
|
/** Open domain handle; present only while {@link authority} is `active`. */
|
|
304
467
|
domain;
|
|
468
|
+
/** The last failed `openDomain` reason; a repeat failure is not re-logged. */
|
|
469
|
+
lastOpenFailure;
|
|
305
470
|
/** The open table; present once this process is the authority. */
|
|
306
471
|
table;
|
|
307
472
|
/** Last classified status list per canonical primary root. */
|
|
308
473
|
cache = /* @__PURE__ */ new Map();
|
|
309
474
|
/** Signature of the last published grant per key, so a no-op refresh stays a no-op. */
|
|
310
475
|
published = /* @__PURE__ */ new Map();
|
|
311
|
-
/**
|
|
476
|
+
/** Pending store-ordered work, grouped by primary for teardown accounting. */
|
|
312
477
|
chains = /* @__PURE__ */ new Map();
|
|
478
|
+
/** Admissions and mutations share one order, including across primary roots. */
|
|
479
|
+
operationChain = Promise.resolve();
|
|
480
|
+
operationEpoch;
|
|
481
|
+
operationRevision = 0;
|
|
313
482
|
/** Store-wide queue for lease acquire / open / release. */
|
|
314
483
|
authorityChain = Promise.resolve();
|
|
315
484
|
/** Change listeners, keyed by their own disposer identity. */
|
|
@@ -328,7 +497,23 @@ var MultiRootRegistry = class extends Service {
|
|
|
328
497
|
*/
|
|
329
498
|
constructor(ctx, config = {}) {
|
|
330
499
|
super(ctx, "multiRootRegistry");
|
|
500
|
+
this.authorityStorageContext = ctx;
|
|
331
501
|
this.leasePath = config.leasePath;
|
|
502
|
+
this.sharedStorageRoot = config.sharedStorageRoot;
|
|
503
|
+
this.recovery = new RecoveryScheduler({
|
|
504
|
+
enabled: config.backgroundRecovery !== false,
|
|
505
|
+
eligible: () => !this.disposed && (this.authorityState.kind === "contended" || this.authorityState.kind === "storage-failed" && this.authorityState.recovery === "retryable" || this.authorityState.kind === "follower" && this.pendingSync.size > 0),
|
|
506
|
+
run: async () => {
|
|
507
|
+
if (this.authorityState.kind === "follower" && this.pendingSync.size > 0) {
|
|
508
|
+
await this.syncProjection();
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
await this.ensureAuthority({ background: true });
|
|
512
|
+
if (this.client) await this.syncProjection();
|
|
513
|
+
this.noteRecoveryTransition();
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
if (config.sharedStorageRoot !== void 0) this.ctx.multiRootScope.requireAdmission(this.operationProof);
|
|
332
517
|
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
333
518
|
const configured = config.maxAdditionalRoots === void 0 ? 16 : config.maxAdditionalRoots;
|
|
334
519
|
if (Number.isInteger(configured) && configured >= 1 && configured <= 64) this.maxRoots = configured;
|
|
@@ -354,6 +539,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
354
539
|
this.ctx.effect(() => () => this.releaseAuthority(), "multi-root-registry: authority release");
|
|
355
540
|
if (this.authorityState.kind === "config-invalid") {
|
|
356
541
|
this.ctx.logger.error(`multi-root-workspace: ${this.authorityState.reason}; no additional root is granted`);
|
|
542
|
+
this.noteRecoveryTransition();
|
|
357
543
|
return;
|
|
358
544
|
}
|
|
359
545
|
await this.ensureAuthority();
|
|
@@ -367,8 +553,9 @@ var MultiRootRegistry = class extends Service {
|
|
|
367
553
|
*/
|
|
368
554
|
get unavailable() {
|
|
369
555
|
switch (this.authorityState.kind) {
|
|
370
|
-
case "active":
|
|
371
|
-
case "
|
|
556
|
+
case "active":
|
|
557
|
+
case "follower": return;
|
|
558
|
+
case "contended": return this.shared && this.connectionFailure ? `shared registry is unavailable: ${this.connectionFailure}` : REGISTRY_CONTENDED_MESSAGE;
|
|
372
559
|
case "storage-failed":
|
|
373
560
|
case "config-invalid": return this.authorityState.reason;
|
|
374
561
|
}
|
|
@@ -377,9 +564,64 @@ var MultiRootRegistry = class extends Service {
|
|
|
377
564
|
* Whether this process is the Registry Authority for the store-wide medium.
|
|
378
565
|
* Tests and diagnostics read this; surfaces use {@link unavailable}.
|
|
379
566
|
*/
|
|
567
|
+
get epoch() {
|
|
568
|
+
return this.authorityState.kind === "active" ? this.operationEpoch : this.client?.epoch;
|
|
569
|
+
}
|
|
570
|
+
get shared() {
|
|
571
|
+
return this.sharedStorageRoot !== void 0;
|
|
572
|
+
}
|
|
573
|
+
get available() {
|
|
574
|
+
return this.authorityState.kind === "active" || this.authorityState.kind === "follower";
|
|
575
|
+
}
|
|
380
576
|
get authority() {
|
|
381
577
|
return this.authorityState;
|
|
382
578
|
}
|
|
579
|
+
/** The class a degraded authority belongs to; `undefined` when healthy. */
|
|
580
|
+
get recoveryClass() {
|
|
581
|
+
switch (this.authorityState.kind) {
|
|
582
|
+
case "contended": return "retryable";
|
|
583
|
+
case "storage-failed": return this.authorityState.recovery;
|
|
584
|
+
case "config-invalid": return "config";
|
|
585
|
+
case "follower": return this.pendingSync.size > 0 ? "retryable" : void 0;
|
|
586
|
+
default: return;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Read-only diagnostics for operators: the authority state, the epoch, the
|
|
591
|
+
* last connection failure, and what the recovery scheduler is doing.
|
|
592
|
+
* `requires-restart` marks classes no timer will ever fix — a `config`
|
|
593
|
+
* row or a `quarantine`d store must NOT read as "retrying". Purely a read;
|
|
594
|
+
* it never arms or runs anything.
|
|
595
|
+
*/
|
|
596
|
+
get diagnostics() {
|
|
597
|
+
const recoveryClass = this.recoveryClass;
|
|
598
|
+
const scheduled = this.recovery.state;
|
|
599
|
+
return {
|
|
600
|
+
authorityState: this.authorityState,
|
|
601
|
+
epoch: this.epoch,
|
|
602
|
+
connectionFailure: this.connectionFailure,
|
|
603
|
+
recoveryClass,
|
|
604
|
+
recovery: {
|
|
605
|
+
phase: recoveryClass === "config" || recoveryClass === "quarantine" ? "requires-restart" : scheduled.phase,
|
|
606
|
+
attempts: scheduled.attempts,
|
|
607
|
+
nextRetryAt: scheduled.nextRetryAt,
|
|
608
|
+
pendingSync: this.pendingSync.size
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
/** The last logged `(kind, recoveryClass)` pair; dedupes transition logs. */
|
|
613
|
+
loggedRecovery;
|
|
614
|
+
/**
|
|
615
|
+
* Log one line per authority/recovery-class TRANSITION — never per tick:
|
|
616
|
+
* waiting↔attempting moves say nothing an operator did not already know.
|
|
617
|
+
*/
|
|
618
|
+
noteRecoveryTransition() {
|
|
619
|
+
const key = `${this.authorityState.kind}:${this.recoveryClass ?? ""}`;
|
|
620
|
+
if (this.loggedRecovery === key) return;
|
|
621
|
+
this.loggedRecovery = key;
|
|
622
|
+
const detail = this.authorityState.kind === "storage-failed" || this.authorityState.kind === "config-invalid" ? this.authorityState.reason : this.connectionFailure ?? "";
|
|
623
|
+
this.ctx.logger.info(this.recoveryClass === void 0 ? `multi-root-workspace: registry authority recovered (${this.authorityState.kind})` : `multi-root-workspace: registry authority is ${this.authorityState.kind} (${this.recoveryClass})${detail === "" ? "" : `: ${detail.slice(0, 200)}`}`);
|
|
624
|
+
}
|
|
383
625
|
/**
|
|
384
626
|
* The configured cap on stored additional-root records per primary root.
|
|
385
627
|
* Surfaces show it next to the count so "N of LIMIT" is always explainable.
|
|
@@ -430,11 +672,47 @@ var MultiRootRegistry = class extends Service {
|
|
|
430
672
|
*/
|
|
431
673
|
async refresh(primaryRoot) {
|
|
432
674
|
await this.ensureAuthority();
|
|
675
|
+
if (this.client) try {
|
|
676
|
+
return await this.remote("snapshot", [primaryRoot]);
|
|
677
|
+
} catch (error) {
|
|
678
|
+
if (this.client) throw error;
|
|
679
|
+
await this.ensureAuthority();
|
|
680
|
+
if (this.client) return await this.remote("snapshot", [primaryRoot]);
|
|
681
|
+
}
|
|
433
682
|
if (this.authorityState.kind !== "active") return [];
|
|
434
683
|
const key = canonicalRoot(primaryRoot);
|
|
435
684
|
return await this.serialize(key, () => this.reclassify(key));
|
|
436
685
|
}
|
|
437
686
|
/**
|
|
687
|
+
* Admit one fs invocation in the same order as mutations, then run its I/O
|
|
688
|
+
* OUTSIDE the queue. Revocation does not wait for already-admitted work.
|
|
689
|
+
* Shared followers obtain the same ordered snapshot through attested IPC.
|
|
690
|
+
*/
|
|
691
|
+
async withOperation(policy, kind, body) {
|
|
692
|
+
if (this.disposed) throw new Error("multi-root registry: cannot admit an operation after disposal");
|
|
693
|
+
const capturedPolicy = structuredClone(policy);
|
|
694
|
+
await this.ensureAuthority();
|
|
695
|
+
const key = canonicalRoot(capturedPolicy.workspaceRoot);
|
|
696
|
+
if (this.shared && this.available) {
|
|
697
|
+
const snapshot = this.client ? await this.remoteSnapshot("admit", [key]) : await this.captureSnapshot(key);
|
|
698
|
+
const scope = freezeOperationScope(this.ctx.multiRootScope.resolveSnapshot(capturedPolicy, this.projectRoots(snapshot.roots), snapshot.available));
|
|
699
|
+
return await runRegistryOperation(scope, kind, snapshot.epoch, snapshot.revision, body, this.operationProof, capturedPolicy.mode);
|
|
700
|
+
}
|
|
701
|
+
const admission = await this.serialize(key, () => {
|
|
702
|
+
if (this.disposed) throw new Error("multi-root registry: cannot admit an operation after disposal");
|
|
703
|
+
return {
|
|
704
|
+
scope: freezeOperationScope(this.shared ? this.ctx.multiRootScope.resolveSnapshot(capturedPolicy, this.projectRoots(this.cache.get(key) ?? []), false) : this.ctx.multiRootScope.resolve(capturedPolicy)),
|
|
705
|
+
epoch: this.operationEpoch,
|
|
706
|
+
revision: this.operationRevision
|
|
707
|
+
};
|
|
708
|
+
});
|
|
709
|
+
return await runRegistryOperation(admission.scope, kind, admission.epoch, admission.revision, body, this.operationProof, capturedPolicy.mode);
|
|
710
|
+
}
|
|
711
|
+
withPrimaryOperation(policy, body) {
|
|
712
|
+
const scope = this.ctx.multiRootScope.resolveSnapshot(policy, this.projectRoots(this.cache.get(canonicalRoot(policy.workspaceRoot)) ?? []), false);
|
|
713
|
+
return runPrimaryOperation(freezeOperationScope(scope), policy.mode, this.operationProof, body);
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
438
716
|
* Registers one additional root.
|
|
439
717
|
* @param primaryRoot - the session workspace root the root belongs to.
|
|
440
718
|
* @param input - the directory and optional alias.
|
|
@@ -443,6 +721,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
443
721
|
*/
|
|
444
722
|
async add(primaryRoot, input) {
|
|
445
723
|
await this.ensureAuthority();
|
|
724
|
+
if (this.client) return await this.remote("add", [primaryRoot, input]);
|
|
446
725
|
this.requireActive();
|
|
447
726
|
const key = canonicalRoot(primaryRoot);
|
|
448
727
|
return await this.serialize(key, async () => {
|
|
@@ -513,6 +792,11 @@ var MultiRootRegistry = class extends Service {
|
|
|
513
792
|
*/
|
|
514
793
|
async setAccess(primaryRoot, ref, access) {
|
|
515
794
|
await this.ensureAuthority();
|
|
795
|
+
if (this.client) return await this.remote("setAccess", [
|
|
796
|
+
primaryRoot,
|
|
797
|
+
ref,
|
|
798
|
+
access
|
|
799
|
+
]);
|
|
516
800
|
this.requireActive();
|
|
517
801
|
const key = canonicalRoot(primaryRoot);
|
|
518
802
|
return await this.serialize(key, async () => {
|
|
@@ -554,6 +838,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
554
838
|
*/
|
|
555
839
|
async removeAt(primaryRoot, ref) {
|
|
556
840
|
await this.ensureAuthority();
|
|
841
|
+
if (this.client) return await this.remote("removeAt", [primaryRoot, ref]);
|
|
557
842
|
this.requireActive();
|
|
558
843
|
const key = canonicalRoot(primaryRoot);
|
|
559
844
|
return await this.serialize(key, async () => await this.persist(key, removeStatusAt(this.reclassify(key), ref)));
|
|
@@ -568,6 +853,11 @@ var MultiRootRegistry = class extends Service {
|
|
|
568
853
|
*/
|
|
569
854
|
async setAlias(primaryRoot, ref, alias) {
|
|
570
855
|
await this.ensureAuthority();
|
|
856
|
+
if (this.client) return await this.remote("setAlias", [
|
|
857
|
+
primaryRoot,
|
|
858
|
+
ref,
|
|
859
|
+
alias ?? null
|
|
860
|
+
]);
|
|
571
861
|
this.requireActive();
|
|
572
862
|
const key = canonicalRoot(primaryRoot);
|
|
573
863
|
return await this.serialize(key, async () => {
|
|
@@ -587,10 +877,22 @@ var MultiRootRegistry = class extends Service {
|
|
|
587
877
|
}
|
|
588
878
|
/** Change a Skill switch without changing any grant field. */
|
|
589
879
|
async setSkillsEnabled(primaryRoot, ref, skillsEnabled) {
|
|
880
|
+
await this.ensureAuthority();
|
|
881
|
+
if (this.client) return await this.remote("setSkillsEnabled", [
|
|
882
|
+
primaryRoot,
|
|
883
|
+
ref,
|
|
884
|
+
skillsEnabled
|
|
885
|
+
]);
|
|
590
886
|
if (typeof skillsEnabled !== "boolean") throw new RootValidationError("invalid-ref", "skillsEnabled must be a boolean");
|
|
591
887
|
return await this.setSkillsMetadata(primaryRoot, ref, { skillsEnabled });
|
|
592
888
|
}
|
|
593
889
|
async setAliasId(primaryRoot, ref, aliasId) {
|
|
890
|
+
await this.ensureAuthority();
|
|
891
|
+
if (this.client) return await this.remote("setAliasId", [
|
|
892
|
+
primaryRoot,
|
|
893
|
+
ref,
|
|
894
|
+
aliasId
|
|
895
|
+
]);
|
|
594
896
|
return await this.setSkillsMetadata(primaryRoot, ref, { aliasId });
|
|
595
897
|
}
|
|
596
898
|
async setSkillsMetadata(primaryRoot, ref, patch) {
|
|
@@ -617,6 +919,11 @@ var MultiRootRegistry = class extends Service {
|
|
|
617
919
|
*/
|
|
618
920
|
async move(primaryRoot, ref, beforeRef) {
|
|
619
921
|
await this.ensureAuthority();
|
|
922
|
+
if (this.client) return await this.remote("move", [
|
|
923
|
+
primaryRoot,
|
|
924
|
+
ref,
|
|
925
|
+
beforeRef ?? null
|
|
926
|
+
]);
|
|
620
927
|
this.requireActive();
|
|
621
928
|
const key = canonicalRoot(primaryRoot);
|
|
622
929
|
return await this.serialize(key, async () => {
|
|
@@ -645,6 +952,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
645
952
|
*/
|
|
646
953
|
async recheck(primaryRoot) {
|
|
647
954
|
await this.ensureAuthority();
|
|
955
|
+
if (this.client) return await this.remote("recheck", [primaryRoot]);
|
|
648
956
|
this.requireActive();
|
|
649
957
|
const key = canonicalRoot(primaryRoot);
|
|
650
958
|
return await this.serialize(key, async () => await this.persist(key, this.reclassify(key)));
|
|
@@ -666,7 +974,10 @@ var MultiRootRegistry = class extends Service {
|
|
|
666
974
|
* @returns the canonical paths of every `available` root.
|
|
667
975
|
*/
|
|
668
976
|
granted(primaryRoot) {
|
|
669
|
-
return
|
|
977
|
+
return this.shared ? this.ctx.multiRootScope.snapshot({
|
|
978
|
+
workspaceRoot: primaryRoot,
|
|
979
|
+
mode: "workspace-write"
|
|
980
|
+
}).additionalRoots : availableRoots(this.statusesOf(canonicalRoot(primaryRoot)));
|
|
670
981
|
}
|
|
671
982
|
/**
|
|
672
983
|
* The scope the registry publishes: only usable roots, in registry order.
|
|
@@ -679,13 +990,22 @@ var MultiRootRegistry = class extends Service {
|
|
|
679
990
|
/** Classify one stored record set and cache it. */
|
|
680
991
|
accept(primaryRoot, record) {
|
|
681
992
|
const key = canonicalRoot(primaryRoot);
|
|
682
|
-
const statuses =
|
|
993
|
+
const statuses = this.classify(key, record);
|
|
683
994
|
this.cache.set(key, statuses);
|
|
995
|
+
this.publish(key, statuses);
|
|
996
|
+
return statuses;
|
|
997
|
+
}
|
|
998
|
+
/**
|
|
999
|
+
* The pure half of {@link accept}: classification plus the over-limit
|
|
1000
|
+
* warning, without writing the cache or the scope. {@link openDomain}
|
|
1001
|
+
* staging uses it so a half-opened domain cannot materialize rows.
|
|
1002
|
+
*/
|
|
1003
|
+
classify(key, record) {
|
|
1004
|
+
const statuses = classifyStoredRoots(key, record.roots.map(toRegisteredRoot));
|
|
684
1005
|
if (statuses.length > this.maxRoots && !this.warnedOverLimit.has(key)) {
|
|
685
1006
|
this.warnedOverLimit.add(key);
|
|
686
1007
|
this.ctx.logger.warn(`multi-root-workspace: ${statuses.length} roots are stored for ${key}, above the configured limit of ${this.maxRoots}; all of them stay granted, only new adds are refused`);
|
|
687
1008
|
}
|
|
688
|
-
this.publish(key, statuses);
|
|
689
1009
|
return statuses;
|
|
690
1010
|
}
|
|
691
1011
|
/**
|
|
@@ -709,7 +1029,12 @@ var MultiRootRegistry = class extends Service {
|
|
|
709
1029
|
*/
|
|
710
1030
|
lookupRecords(key) {
|
|
711
1031
|
if (this.authorityState.kind !== "active") return void 0;
|
|
712
|
-
|
|
1032
|
+
try {
|
|
1033
|
+
return this.table?.get(key);
|
|
1034
|
+
} catch (error) {
|
|
1035
|
+
if (unknownStorageState(error)) this.enterStorageFailed("registry storage state cannot be confirmed; restart and reconcile", "quarantine");
|
|
1036
|
+
throw error;
|
|
1037
|
+
}
|
|
713
1038
|
}
|
|
714
1039
|
/**
|
|
715
1040
|
* Re-classify one key from its stored records: every directory is `stat`ed
|
|
@@ -734,22 +1059,28 @@ var MultiRootRegistry = class extends Service {
|
|
|
734
1059
|
async persist(key, statuses) {
|
|
735
1060
|
const table = this.requireTable();
|
|
736
1061
|
const durable = persistedPrimaryRoot.parse({ roots: migrateRootSkills(statuses.map(toPersistedRoot)) });
|
|
737
|
-
|
|
738
|
-
|
|
1062
|
+
try {
|
|
1063
|
+
if (statuses.length === 0) await table.delete(key);
|
|
1064
|
+
else await table.put(key, durable);
|
|
1065
|
+
} catch (error) {
|
|
1066
|
+
if (unknownStorageState(error)) this.enterStorageFailed("registry write result is unknown; restart and reconcile durable state", "quarantine");
|
|
1067
|
+
throw error;
|
|
1068
|
+
}
|
|
739
1069
|
const classified = statuses.length === 0 ? [] : classifyStoredRoots(key, durable.roots.map(toRegisteredRoot));
|
|
740
1070
|
this.cache.set(key, classified);
|
|
741
1071
|
this.publish(key, classified);
|
|
742
1072
|
return classified;
|
|
743
1073
|
}
|
|
744
1074
|
/**
|
|
745
|
-
* Run one job
|
|
746
|
-
*
|
|
1075
|
+
* Run one job after every store job already queued. The whole
|
|
1076
|
+
* read → validate → write sequence belongs inside the job:
|
|
747
1077
|
* the storage domain serializes individual writes, so without this the loser
|
|
748
1078
|
* of a race reads a snapshot that is already stale and writes it back.
|
|
749
1079
|
*/
|
|
750
1080
|
async serialize(key, job) {
|
|
751
|
-
const run =
|
|
1081
|
+
const run = this.operationChain.then(job);
|
|
752
1082
|
const tail = run.then(() => void 0, () => void 0);
|
|
1083
|
+
this.operationChain = tail;
|
|
753
1084
|
this.chains.set(key, tail);
|
|
754
1085
|
try {
|
|
755
1086
|
return await run;
|
|
@@ -781,7 +1112,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
781
1112
|
const withheld = statuses.filter((status) => status.state !== "available");
|
|
782
1113
|
const signature = [...statuses.filter((status) => status.state === "available").map((status) => `available:${status.access}:${recordedGrantOf(status)}`), ...withheld.map((status) => `${status.state}:${status.invalidReason ?? ""}:${status.access}:${status.path}:${recordedGrantOf(status) ?? ""}`)].join("\n");
|
|
783
1114
|
const previousMetadata = this.metadataPublished.get(key);
|
|
784
|
-
const metadata = JSON.stringify(statuses.map((row) => [
|
|
1115
|
+
const metadata = JSON.stringify(this.shared ? statuses : statuses.map((row) => [
|
|
785
1116
|
row.id,
|
|
786
1117
|
row.aliasId,
|
|
787
1118
|
row.skillsEnabled,
|
|
@@ -789,11 +1120,22 @@ var MultiRootRegistry = class extends Service {
|
|
|
789
1120
|
]));
|
|
790
1121
|
this.metadataPublished.set(key, metadata);
|
|
791
1122
|
if ((this.published.get(key) ?? "") === signature) {
|
|
792
|
-
if (previousMetadata !== metadata && (previousMetadata !== void 0 || statuses.length > 0))
|
|
1123
|
+
if (previousMetadata !== metadata && (previousMetadata !== void 0 || statuses.length > 0)) {
|
|
1124
|
+
this.operationRevision++;
|
|
1125
|
+
this.deliverNotifications(key);
|
|
1126
|
+
}
|
|
793
1127
|
return;
|
|
794
1128
|
}
|
|
795
|
-
this.published.
|
|
796
|
-
|
|
1129
|
+
if (signature === "") this.published.delete(key);
|
|
1130
|
+
else this.published.set(key, signature);
|
|
1131
|
+
this.operationRevision++;
|
|
1132
|
+
const effective = this.projectRoots(statuses);
|
|
1133
|
+
this.ctx.multiRootScope.setAdditionalRoots(key, effective);
|
|
1134
|
+
if (withheld.length > 0) this.ctx.logger.warn(`multi-root-workspace: ${withheld.length} registered root(s) of ${key} are not writable right now (${withheld.map((status) => `${status.path}: ${status.state}`).join(", ")}); they stay registered and are reported by /workspace-folders`);
|
|
1135
|
+
this.deliverNotifications(key);
|
|
1136
|
+
}
|
|
1137
|
+
projectRoots(statuses) {
|
|
1138
|
+
return statuses.flatMap((status) => {
|
|
797
1139
|
const recorded = recordedGrantOf(status);
|
|
798
1140
|
if (status.state === "invalid" && status.invalidReason !== "membership-conflict" || recorded === void 0) return [];
|
|
799
1141
|
return [{
|
|
@@ -805,10 +1147,188 @@ var MultiRootRegistry = class extends Service {
|
|
|
805
1147
|
...status.alias === void 0 ? {} : { alias: status.alias }
|
|
806
1148
|
}];
|
|
807
1149
|
});
|
|
808
|
-
|
|
809
|
-
|
|
1150
|
+
}
|
|
1151
|
+
snapshotOf(primary) {
|
|
1152
|
+
return snapshotSchema.parse({
|
|
1153
|
+
protocol: 1,
|
|
1154
|
+
store: this.medium.id,
|
|
1155
|
+
epoch: this.operationEpoch,
|
|
1156
|
+
revision: this.operationRevision,
|
|
1157
|
+
primary,
|
|
1158
|
+
roots: this.cache.get(primary) ?? [],
|
|
1159
|
+
available: this.authorityState.kind === "active"
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
async captureSnapshot(primary) {
|
|
1163
|
+
const key = canonicalRoot(primary);
|
|
1164
|
+
return await this.serialize(key, () => {
|
|
1165
|
+
this.requireActive();
|
|
1166
|
+
this.reclassify(key);
|
|
1167
|
+
return this.snapshotOf(key);
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
notifyShared(key) {
|
|
1171
|
+
if (this.server && this.authorityState.kind === "active") this.server.changed(this.snapshotOf(key));
|
|
1172
|
+
}
|
|
1173
|
+
/** Depth of {@link commit}; nested calls share the outer round's buffers. */
|
|
1174
|
+
commitDepth = 0;
|
|
1175
|
+
/** Keys whose publish inside a commit produced deferred notifications, first-seen order. */
|
|
1176
|
+
commitNotifications = /* @__PURE__ */ new Set();
|
|
1177
|
+
/**
|
|
1178
|
+
* Run `body` as one Registry commit: scope writes inside it go through
|
|
1179
|
+
* `multiRootScope.batch`, and the `notifyShared` broadcasts plus Registry
|
|
1180
|
+
* listener calls `publish()` produces are collected per key instead of
|
|
1181
|
+
* delivered. When the OUTERMOST commit ends — also when `body` throws — the
|
|
1182
|
+
* scope batch flushes first, then the broadcasts go out in first-seen key
|
|
1183
|
+
* order, then the listeners: no synchronous callback can observe a
|
|
1184
|
+
* half-committed state.
|
|
1185
|
+
*/
|
|
1186
|
+
commit(body) {
|
|
1187
|
+
this.commitDepth++;
|
|
1188
|
+
try {
|
|
1189
|
+
return this.ctx.multiRootScope.batch(body);
|
|
1190
|
+
} finally {
|
|
1191
|
+
if (--this.commitDepth === 0) {
|
|
1192
|
+
const keys = [...this.commitNotifications];
|
|
1193
|
+
this.commitNotifications.clear();
|
|
1194
|
+
for (const key of keys) this.notifyShared(key);
|
|
1195
|
+
for (const key of keys) for (const listener of this.listeners) listener(key);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
/** Deliver one key's notifications now, or collect them inside a commit. */
|
|
1200
|
+
deliverNotifications(key) {
|
|
1201
|
+
if (this.commitDepth > 0) {
|
|
1202
|
+
this.commitNotifications.add(key);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
this.notifyShared(key);
|
|
810
1206
|
for (const listener of this.listeners) listener(key);
|
|
811
1207
|
}
|
|
1208
|
+
applySnapshot(snapshot) {
|
|
1209
|
+
if (!this.client || snapshot.store !== this.medium?.id || snapshot.epoch !== this.client.epoch) return;
|
|
1210
|
+
const previous = this.remoteRevisions.get(snapshot.primary) ?? -1;
|
|
1211
|
+
if (snapshot.revision < previous) return;
|
|
1212
|
+
this.remoteRevisions.set(snapshot.primary, snapshot.revision);
|
|
1213
|
+
const rows = snapshot.roots.map((row) => Object.fromEntries(Object.entries({
|
|
1214
|
+
...row,
|
|
1215
|
+
id: additionalRootId(row.id)
|
|
1216
|
+
}).filter(([, value]) => value !== void 0)));
|
|
1217
|
+
this.commit(() => {
|
|
1218
|
+
this.cache.set(snapshot.primary, rows);
|
|
1219
|
+
this.publish(snapshot.primary, rows);
|
|
1220
|
+
this.ctx.multiRootScope.setRegistryAuthority(snapshot.available);
|
|
1221
|
+
});
|
|
1222
|
+
this.remoteProjectionReady = snapshot.available;
|
|
1223
|
+
this.pendingSync.delete(snapshot.primary);
|
|
1224
|
+
}
|
|
1225
|
+
remoteUnavailable() {
|
|
1226
|
+
try {
|
|
1227
|
+
this.connectionGeneration++;
|
|
1228
|
+
this.client = void 0;
|
|
1229
|
+
this.remoteRevisions.clear();
|
|
1230
|
+
this.pendingSync.clear();
|
|
1231
|
+
this.authorityState = { kind: "contended" };
|
|
1232
|
+
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
1233
|
+
if (!this.remoteProjectionReady) return;
|
|
1234
|
+
this.remoteProjectionReady = false;
|
|
1235
|
+
for (const [primary, rows] of this.cache) this.withholdScope(primary, rows);
|
|
1236
|
+
} finally {
|
|
1237
|
+
this.recovery.arm();
|
|
1238
|
+
this.noteRecoveryTransition();
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
/**
|
|
1242
|
+
* Resynchronize each still-pending tracked primary outside the authority
|
|
1243
|
+
* queue: per-key snapshots through the live client. A failed key stays in
|
|
1244
|
+
* `pendingSync` for the next tick; a lost connection abandons the round —
|
|
1245
|
+
* `remoteUnavailable` already emptied the set.
|
|
1246
|
+
*/
|
|
1247
|
+
async syncProjection() {
|
|
1248
|
+
for (const key of [...this.pendingSync]) try {
|
|
1249
|
+
await this.remoteSnapshot("snapshot", [key]);
|
|
1250
|
+
} catch {
|
|
1251
|
+
if (!this.client) break;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
/**
|
|
1255
|
+
* Write one key's withheld-only projection into the scope, keeping
|
|
1256
|
+
* `published` truthful: the signature records what the scope HOLDS, never
|
|
1257
|
+
* what `publish()` last computed — a withheld signature can never equal a
|
|
1258
|
+
* publish signature, so an empty snapshot afterwards really writes the
|
|
1259
|
+
* retraction. A repeated write of the same withheld projection is a no-op
|
|
1260
|
+
* (the same-signature skip is also the republish gate of a repeated
|
|
1261
|
+
* downgrade). `metadataPublished` is untouched: a withheld write announces
|
|
1262
|
+
* nothing to the Registry view.
|
|
1263
|
+
*/
|
|
1264
|
+
withholdScope(key, rows) {
|
|
1265
|
+
const roots = this.projectRoots(rows).map((root) => ({
|
|
1266
|
+
...root,
|
|
1267
|
+
withheld: true
|
|
1268
|
+
}));
|
|
1269
|
+
const signature = roots.length === 0 ? "" : `\u0000withheld\n${JSON.stringify(roots)}`;
|
|
1270
|
+
if ((this.published.get(key) ?? "") === signature) return;
|
|
1271
|
+
if (signature === "") this.published.delete(key);
|
|
1272
|
+
else this.published.set(key, signature);
|
|
1273
|
+
this.ctx.multiRootScope.setAdditionalRoots(key, roots);
|
|
1274
|
+
}
|
|
1275
|
+
async remoteSnapshot(method, args) {
|
|
1276
|
+
const client = this.client, generation = this.connectionGeneration;
|
|
1277
|
+
if (!client) throw new Error("registry authority is unavailable");
|
|
1278
|
+
const value = await client.request(method, args);
|
|
1279
|
+
let snapshot;
|
|
1280
|
+
try {
|
|
1281
|
+
snapshot = snapshotSchema.parse(value);
|
|
1282
|
+
} catch (error) {
|
|
1283
|
+
client.close();
|
|
1284
|
+
throw error;
|
|
1285
|
+
}
|
|
1286
|
+
if (generation !== this.connectionGeneration || client !== this.client || snapshot.store !== this.medium?.id || snapshot.epoch !== client.epoch || snapshot.primary !== canonicalRoot(String(args[0]))) {
|
|
1287
|
+
client.close();
|
|
1288
|
+
throw new Error("stale registry connection or authority epoch");
|
|
1289
|
+
}
|
|
1290
|
+
this.applySnapshot(snapshot);
|
|
1291
|
+
return snapshot;
|
|
1292
|
+
}
|
|
1293
|
+
async remote(method, args) {
|
|
1294
|
+
await this.remoteSnapshot(method, args);
|
|
1295
|
+
return this.list(String(args[0]));
|
|
1296
|
+
}
|
|
1297
|
+
async dispatchShared(method, values) {
|
|
1298
|
+
const args = parseArguments(method, values);
|
|
1299
|
+
const primary = String(args[0]);
|
|
1300
|
+
if (!isAbsolute(primary)) throw new Error("registry primary must be absolute");
|
|
1301
|
+
if (method === "snapshot" || method === "admit") return this.captureSnapshot(primary);
|
|
1302
|
+
const ref = args[1];
|
|
1303
|
+
switch (method) {
|
|
1304
|
+
case "add":
|
|
1305
|
+
await this.add(primary, args[1]);
|
|
1306
|
+
break;
|
|
1307
|
+
case "removeAt":
|
|
1308
|
+
await this.removeAt(primary, ref);
|
|
1309
|
+
break;
|
|
1310
|
+
case "setAccess":
|
|
1311
|
+
await this.setAccess(primary, ref, args[2]);
|
|
1312
|
+
break;
|
|
1313
|
+
case "setAlias":
|
|
1314
|
+
await this.setAlias(primary, ref, args[2] === null ? void 0 : args[2]);
|
|
1315
|
+
break;
|
|
1316
|
+
case "setSkillsEnabled":
|
|
1317
|
+
await this.setSkillsEnabled(primary, ref, args[2]);
|
|
1318
|
+
break;
|
|
1319
|
+
case "setAliasId":
|
|
1320
|
+
await this.setAliasId(primary, ref, args[2]);
|
|
1321
|
+
break;
|
|
1322
|
+
case "move":
|
|
1323
|
+
await this.move(primary, ref, args[2] === null ? void 0 : args[2]);
|
|
1324
|
+
break;
|
|
1325
|
+
case "recheck":
|
|
1326
|
+
await this.recheck(primary);
|
|
1327
|
+
break;
|
|
1328
|
+
default: throw new Error("unsupported registry method");
|
|
1329
|
+
}
|
|
1330
|
+
return this.captureSnapshot(primary);
|
|
1331
|
+
}
|
|
812
1332
|
/**
|
|
813
1333
|
* The open table.
|
|
814
1334
|
* @returns the live table.
|
|
@@ -817,6 +1337,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
817
1337
|
* or an error when the service has not started.
|
|
818
1338
|
*/
|
|
819
1339
|
requireTable() {
|
|
1340
|
+
if (this.authorityState.kind === "follower") throw new Error("a follower never opens registry storage");
|
|
820
1341
|
if (this.authorityState.kind === "contended") throw new RootValidationError("registry-contended", REGISTRY_CONTENDED_MESSAGE);
|
|
821
1342
|
if (this.authorityState.kind === "config-invalid") throw new RootValidationError("config-invalid", `the root registry configuration is invalid (${this.authorityState.reason}); no additional root is granted`);
|
|
822
1343
|
if (this.authorityState.kind === "storage-failed") throw new RootValidationError("storage-unavailable", `the root registry store is unavailable (${this.authorityState.reason}); remove or repair \$DSH_HOME/storages/multi_root_workspace.json and restart dsh`);
|
|
@@ -837,36 +1358,93 @@ var MultiRootRegistry = class extends Service {
|
|
|
837
1358
|
* retries here (the command `list` and the panel Retry button both call
|
|
838
1359
|
* {@link MultiRootRegistry.refresh}).
|
|
839
1360
|
*/
|
|
840
|
-
async ensureAuthority() {
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
if (leasePath === void 0 || leasePath === "") {
|
|
851
|
-
this.enterStorageFailed(`${DOMAIN_NAME}: leasePath is not configured; set it next to storage-json.root (default $DSH_HOME/storages/${DOMAIN_NAME}.lock)`);
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
if (!isAbsolute(leasePath)) {
|
|
855
|
-
this.enterStorageFailed(`${DOMAIN_NAME}: leasePath must be an absolute path`);
|
|
856
|
-
return;
|
|
857
|
-
}
|
|
858
|
-
try {
|
|
859
|
-
this.lease = await RegistryAuthorityLease.acquire(leasePath);
|
|
860
|
-
} catch (error) {
|
|
861
|
-
if (error instanceof RegistryLeaseContendedError) {
|
|
862
|
-
this.enterContended();
|
|
1361
|
+
async ensureAuthority(options = {}) {
|
|
1362
|
+
try {
|
|
1363
|
+
await this.serializeAuthority(async () => {
|
|
1364
|
+
if (this.disposed) return;
|
|
1365
|
+
if (this.authorityState.kind === "active" || this.client !== void 0) return;
|
|
1366
|
+
if (this.authorityState.kind === "config-invalid") return;
|
|
1367
|
+
if (this.authorityState.kind === "storage-failed" && this.authorityState.recovery !== "retryable") return;
|
|
1368
|
+
if (this.lease !== void 0) {
|
|
1369
|
+
if (this.domain !== void 0) return;
|
|
1370
|
+
await this.openDomain();
|
|
863
1371
|
return;
|
|
864
1372
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
1373
|
+
const leasePath = this.leasePath;
|
|
1374
|
+
if (leasePath === void 0 || leasePath === "") {
|
|
1375
|
+
this.enterStorageFailed(`${DOMAIN_NAME}: leasePath is not configured; set it next to storage-json.root (default $DSH_HOME/storages/${DOMAIN_NAME}.lock)`, "config");
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
if (!isAbsolute(leasePath)) {
|
|
1379
|
+
this.enterStorageFailed(`${DOMAIN_NAME}: leasePath must be an absolute path`, "config");
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (this.sharedStorageRoot !== void 0 && this.medium === void 0) try {
|
|
1383
|
+
if (!isAbsolute(this.sharedStorageRoot)) throw new SharedConfigError("sharedStorageRoot must be absolute");
|
|
1384
|
+
const medium = storeIdentity(this.sharedStorageRoot);
|
|
1385
|
+
if (storeIdentity(dirname(leasePath)).id !== medium.id || basename(leasePath) !== `multi_root_workspace.lock`) throw new SharedConfigError("shared registry requires its canonical medium lock, without alternate lock names");
|
|
1386
|
+
this.medium = medium;
|
|
1387
|
+
} catch (error) {
|
|
1388
|
+
this.enterStorageFailed(String(error), error instanceof SharedConfigError ? "config" : "retryable");
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
try {
|
|
1392
|
+
this.lease = await RegistryAuthorityLease.acquire(leasePath);
|
|
1393
|
+
} catch (error) {
|
|
1394
|
+
if (error instanceof RegistryLeaseContendedError) {
|
|
1395
|
+
this.enterContended();
|
|
1396
|
+
if (this.medium !== void 0) for (const delay of options.background ? [0] : [
|
|
1397
|
+
0,
|
|
1398
|
+
50,
|
|
1399
|
+
150
|
|
1400
|
+
]) {
|
|
1401
|
+
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1402
|
+
if (this.disposed) return;
|
|
1403
|
+
const generation = ++this.connectionGeneration;
|
|
1404
|
+
const controller = new AbortController();
|
|
1405
|
+
this.connectionAbort = controller;
|
|
1406
|
+
let adopted = false;
|
|
1407
|
+
try {
|
|
1408
|
+
const client = await AuthorityClient.connect(this.medium.endpoint, this.medium.id, this.maxRoots, (snapshot) => {
|
|
1409
|
+
if (adopted && generation === this.connectionGeneration) this.applySnapshot(snapshot);
|
|
1410
|
+
}, () => {
|
|
1411
|
+
if (adopted && generation === this.connectionGeneration) this.remoteUnavailable();
|
|
1412
|
+
}, controller.signal);
|
|
1413
|
+
if (this.disposed || generation !== this.connectionGeneration) {
|
|
1414
|
+
client.close();
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
this.client = client;
|
|
1418
|
+
adopted = true;
|
|
1419
|
+
this.connectionFailure = void 0;
|
|
1420
|
+
this.authorityState = {
|
|
1421
|
+
kind: "follower",
|
|
1422
|
+
epoch: client.epoch
|
|
1423
|
+
};
|
|
1424
|
+
this.pendingSync = new Set(this.cache.keys());
|
|
1425
|
+
if (this.pendingSync.size === 0) {
|
|
1426
|
+
this.ctx.multiRootScope.setRegistryAuthority(true);
|
|
1427
|
+
this.remoteProjectionReady = true;
|
|
1428
|
+
}
|
|
1429
|
+
this.recovery.kick();
|
|
1430
|
+
break;
|
|
1431
|
+
} catch (error) {
|
|
1432
|
+
this.connectionFailure = String(error);
|
|
1433
|
+
} finally {
|
|
1434
|
+
if (this.connectionAbort === controller) this.connectionAbort = void 0;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
this.enterStorageFailed(`${DOMAIN_NAME}: cannot acquire the registry lease: ${error instanceof Error ? error.message : String(error)}`, "retryable");
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
await this.openDomain();
|
|
1443
|
+
});
|
|
1444
|
+
} finally {
|
|
1445
|
+
this.recovery.arm();
|
|
1446
|
+
this.noteRecoveryTransition();
|
|
1447
|
+
}
|
|
870
1448
|
}
|
|
871
1449
|
/**
|
|
872
1450
|
* Open the domain under an already-held lease and publish every stored root.
|
|
@@ -876,35 +1454,78 @@ var MultiRootRegistry = class extends Service {
|
|
|
876
1454
|
async openDomain() {
|
|
877
1455
|
let domain;
|
|
878
1456
|
try {
|
|
879
|
-
domain = await this.ctx.storageDomain.open(multiRootDomainSpec);
|
|
1457
|
+
domain = this.medium === void 0 ? await this.ctx.storageDomain.open(multiRootDomainSpec) : await openAuthorityStorage(this.authorityStorageContext, this.medium.root, multiRootDomainSpec);
|
|
880
1458
|
} catch (error) {
|
|
881
1459
|
const reason = `${DOMAIN_NAME}: ${error instanceof Error ? error.message : String(error)}`;
|
|
882
|
-
this.
|
|
883
|
-
|
|
1460
|
+
if (this.lastOpenFailure !== reason) {
|
|
1461
|
+
this.lastOpenFailure = reason;
|
|
1462
|
+
this.ctx.logger.error(`multi-root-workspace: cannot open the root registry store (${DOMAIN_NAME}); no additional root is granted until it is repaired or removed: ${reason}`);
|
|
1463
|
+
}
|
|
1464
|
+
this.enterStorageFailed(reason, "retryable");
|
|
884
1465
|
return;
|
|
885
1466
|
}
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1467
|
+
const epoch = randomUUID();
|
|
1468
|
+
const staged = /* @__PURE__ */ new Map();
|
|
1469
|
+
let table;
|
|
1470
|
+
try {
|
|
1471
|
+
table = domain.table(TABLE_NAME);
|
|
1472
|
+
for (const [primaryRoot, record] of table.entries()) {
|
|
1473
|
+
const migrated = { roots: migrateRootSkills(record.roots) };
|
|
1474
|
+
let accepted = record;
|
|
1475
|
+
if (JSON.stringify(migrated) !== JSON.stringify(record)) try {
|
|
1476
|
+
await table.put(primaryRoot, migrated);
|
|
1477
|
+
accepted = migrated;
|
|
1478
|
+
} catch (error) {
|
|
1479
|
+
if (unknownStorageState(error)) {
|
|
1480
|
+
this.domain = domain;
|
|
1481
|
+
this.enterStorageFailed("Skills migration result is unknown; restart and reconcile", "quarantine");
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
this.ctx.logger.warn(`multi-root skills: metadata migration failed; Skills remain withheld: ${String(error)}`);
|
|
1485
|
+
}
|
|
1486
|
+
const key = canonicalRoot(primaryRoot);
|
|
1487
|
+
staged.set(key, this.classify(key, accepted));
|
|
900
1488
|
}
|
|
901
|
-
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
if (unknownStorageState(error)) {
|
|
1491
|
+
this.domain = domain;
|
|
1492
|
+
this.enterStorageFailed("registry storage state cannot be confirmed; restart and reconcile", "quarantine");
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
try {
|
|
1496
|
+
await domain.close();
|
|
1497
|
+
} catch {}
|
|
1498
|
+
throw error;
|
|
1499
|
+
}
|
|
1500
|
+
let server;
|
|
1501
|
+
if (this.medium !== void 0) try {
|
|
1502
|
+
server = await AuthorityServer.listen(this.medium.endpoint, this.medium.id, epoch, this.maxRoots, (method, args) => this.dispatchShared(method, args));
|
|
1503
|
+
} catch (error) {
|
|
1504
|
+
await domain.close();
|
|
1505
|
+
this.enterStorageFailed(`cannot start shared registry: ${String(error)}`, "retryable");
|
|
1506
|
+
return;
|
|
1507
|
+
}
|
|
1508
|
+
this.commit(() => {
|
|
1509
|
+
this.domain = domain;
|
|
1510
|
+
this.table = table;
|
|
1511
|
+
this.operationEpoch = epoch;
|
|
1512
|
+
this.operationRevision = 0;
|
|
1513
|
+
this.authorityState = { kind: "active" };
|
|
1514
|
+
this.loggedContended = false;
|
|
1515
|
+
this.server = server;
|
|
1516
|
+
const earlier = new Set(this.cache.keys());
|
|
1517
|
+
this.cache.clear();
|
|
1518
|
+
for (const [key, statuses] of staged) this.cache.set(key, statuses);
|
|
1519
|
+
for (const key of /* @__PURE__ */ new Set([...earlier, ...staged.keys()])) this.publish(key, staged.get(key) ?? []);
|
|
1520
|
+
this.ctx.multiRootScope.setRegistryAuthority(true);
|
|
1521
|
+
this.lastOpenFailure = void 0;
|
|
902
1522
|
});
|
|
903
|
-
this.ctx.multiRootScope.setRegistryAuthority(true);
|
|
904
1523
|
}
|
|
905
1524
|
/** Fail closed: empty scope, no table, contended state. */
|
|
906
1525
|
enterContended() {
|
|
907
|
-
this.
|
|
1526
|
+
this.operationEpoch = void 0;
|
|
1527
|
+
if (this.shared) this.remoteUnavailable();
|
|
1528
|
+
else this.withdrawPublished();
|
|
908
1529
|
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
909
1530
|
this.table = void 0;
|
|
910
1531
|
this.authorityState = { kind: "contended" };
|
|
@@ -914,14 +1535,21 @@ var MultiRootRegistry = class extends Service {
|
|
|
914
1535
|
}
|
|
915
1536
|
}
|
|
916
1537
|
/** Fail closed: empty scope, no table, storage-failed state. The lease, if held, stays. */
|
|
917
|
-
enterStorageFailed(reason) {
|
|
918
|
-
this.
|
|
1538
|
+
enterStorageFailed(reason, recovery) {
|
|
1539
|
+
this.operationEpoch = void 0;
|
|
919
1540
|
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
1541
|
+
if (this.shared) {
|
|
1542
|
+
for (const [key, rows] of this.cache) this.withholdScope(key, rows);
|
|
1543
|
+
this.server?.close();
|
|
1544
|
+
this.server = void 0;
|
|
1545
|
+
} else this.withdrawPublished();
|
|
920
1546
|
this.table = void 0;
|
|
921
1547
|
this.authorityState = {
|
|
922
1548
|
kind: "storage-failed",
|
|
923
|
-
reason
|
|
1549
|
+
reason,
|
|
1550
|
+
recovery
|
|
924
1551
|
};
|
|
1552
|
+
this.noteRecoveryTransition();
|
|
925
1553
|
}
|
|
926
1554
|
/** Drop every grant this process has published. Idempotent. */
|
|
927
1555
|
withdrawPublished() {
|
|
@@ -945,8 +1573,16 @@ var MultiRootRegistry = class extends Service {
|
|
|
945
1573
|
*/
|
|
946
1574
|
async releaseAuthority() {
|
|
947
1575
|
this.disposed = true;
|
|
1576
|
+
this.connectionGeneration++;
|
|
1577
|
+
this.recovery.dispose();
|
|
1578
|
+
this.connectionAbort?.abort();
|
|
1579
|
+
this.client?.close();
|
|
1580
|
+
this.client = void 0;
|
|
948
1581
|
await this.serializeAuthority(async () => {
|
|
1582
|
+
this.server?.close();
|
|
1583
|
+
this.server = void 0;
|
|
949
1584
|
this.table = void 0;
|
|
1585
|
+
this.operationEpoch = void 0;
|
|
950
1586
|
this.authorityState = { kind: "contended" };
|
|
951
1587
|
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
952
1588
|
await this.drainMutations();
|
|
@@ -964,7 +1600,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
964
1600
|
});
|
|
965
1601
|
}
|
|
966
1602
|
/**
|
|
967
|
-
* Wait until no
|
|
1603
|
+
* Wait until no store work is queued or running (operation I/O is outside).
|
|
968
1604
|
*
|
|
969
1605
|
* One pass is enough, and only because the authority state has already been
|
|
970
1606
|
* flipped: no further mutation can be admitted (it would fail in
|
|
@@ -1063,5 +1699,12 @@ function normalizeAlias(alias) {
|
|
|
1063
1699
|
if (trimmed.length > 120) throw new RootValidationError("invalid-alias", `an alias must be at most 120 characters`);
|
|
1064
1700
|
return trimmed;
|
|
1065
1701
|
}
|
|
1702
|
+
/** Structural across storage releases; older versions do not expose this code. */
|
|
1703
|
+
function unknownStorageState(error, seen = /* @__PURE__ */ new Set()) {
|
|
1704
|
+
if (error === null || typeof error !== "object" || seen.has(error)) return false;
|
|
1705
|
+
seen.add(error);
|
|
1706
|
+
const value = error;
|
|
1707
|
+
return value.code === "unknown-state" || unknownStorageState(value.cause, seen) || Array.isArray(value.errors) && value.errors.some((item) => unknownStorageState(item, seen));
|
|
1708
|
+
}
|
|
1066
1709
|
//#endregion
|
|
1067
1710
|
export { MultiRootRegistry as a, multiRootDomainSpec as c, MAX_ALIAS_LENGTH as i, RegistryAuthorityLease as l, DOMAIN_NAME as n, REGISTRY_CONTENDED_MESSAGE as o, MAX_ADDITIONAL_ROOTS_CEILING as r, TABLE_NAME as s, DEFAULT_MAX_ADDITIONAL_ROOTS as t, RegistryLeaseContendedError as u };
|