@dsh-electron/dsh-plugin-multi-root-workspace 0.1.11 → 0.1.12
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 +13 -1
- package/CHANGELOG.md +13 -1
- package/README.en.md +9 -8
- package/README.md +9 -8
- package/cordis.patch.yml +3 -2
- package/lib/auto-review.js +1 -1
- package/lib/client.js +96 -49
- package/lib/client.js.map +1 -1
- package/lib/{command-BGdz7VBH.js → command-CmLekG1M.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-4zdwV7u4.js +641 -0
- package/lib/{registry-KSsmU393.js → registry-YTUOjmVu.js} +368 -25
- 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 +65 -9
- 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/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 +47 -6
- package/lib/types/scope.d.ts +10 -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-4zdwV7u4.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
|
|
@@ -209,8 +249,8 @@ var RegistryAuthorityLease = class RegistryAuthorityLease {
|
|
|
209
249
|
* only when the operator removes it (or when the store is repaired). Storage
|
|
210
250
|
* that cannot be parsed at all is backed up and skipped by the domain layer,
|
|
211
251
|
* with a warning.
|
|
212
|
-
* - **Mutations are serialized
|
|
213
|
-
* whole read → validate → persist sequence inside
|
|
252
|
+
* - **Mutations and fs admissions are serialized store-wide.** Every mutation
|
|
253
|
+
* runs its whole read → validate → persist sequence inside the store queue. The
|
|
214
254
|
* storage domain serializes individual `put()`/`delete()` calls, which is not
|
|
215
255
|
* enough: two concurrent `add()` calls would both read the same snapshot and
|
|
216
256
|
* the later write would drop the earlier one, and a removal racing an add
|
|
@@ -296,6 +336,16 @@ var MultiRootRegistry = class extends Service {
|
|
|
296
336
|
];
|
|
297
337
|
/** Absolute lock path; absent means the row was composed without `leasePath`. */
|
|
298
338
|
leasePath;
|
|
339
|
+
sharedStorageRoot;
|
|
340
|
+
authorityStorageContext;
|
|
341
|
+
medium;
|
|
342
|
+
server;
|
|
343
|
+
client;
|
|
344
|
+
connectionFailure;
|
|
345
|
+
connectionGeneration = 0;
|
|
346
|
+
remoteProjectionReady = false;
|
|
347
|
+
remoteRevisions = /* @__PURE__ */ new Map();
|
|
348
|
+
operationProof = Object.freeze({});
|
|
299
349
|
/** Whether this process currently owns the store. */
|
|
300
350
|
authorityState = { kind: "contended" };
|
|
301
351
|
/** Held kernel lease; present once acquire succeeded, including after a failed open. */
|
|
@@ -308,8 +358,12 @@ var MultiRootRegistry = class extends Service {
|
|
|
308
358
|
cache = /* @__PURE__ */ new Map();
|
|
309
359
|
/** Signature of the last published grant per key, so a no-op refresh stays a no-op. */
|
|
310
360
|
published = /* @__PURE__ */ new Map();
|
|
311
|
-
/**
|
|
361
|
+
/** Pending store-ordered work, grouped by primary for teardown accounting. */
|
|
312
362
|
chains = /* @__PURE__ */ new Map();
|
|
363
|
+
/** Admissions and mutations share one order, including across primary roots. */
|
|
364
|
+
operationChain = Promise.resolve();
|
|
365
|
+
operationEpoch;
|
|
366
|
+
operationRevision = 0;
|
|
313
367
|
/** Store-wide queue for lease acquire / open / release. */
|
|
314
368
|
authorityChain = Promise.resolve();
|
|
315
369
|
/** Change listeners, keyed by their own disposer identity. */
|
|
@@ -328,7 +382,10 @@ var MultiRootRegistry = class extends Service {
|
|
|
328
382
|
*/
|
|
329
383
|
constructor(ctx, config = {}) {
|
|
330
384
|
super(ctx, "multiRootRegistry");
|
|
385
|
+
this.authorityStorageContext = ctx;
|
|
331
386
|
this.leasePath = config.leasePath;
|
|
387
|
+
this.sharedStorageRoot = config.sharedStorageRoot;
|
|
388
|
+
if (config.sharedStorageRoot !== void 0) this.ctx.multiRootScope.requireAdmission(this.operationProof);
|
|
332
389
|
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
333
390
|
const configured = config.maxAdditionalRoots === void 0 ? 16 : config.maxAdditionalRoots;
|
|
334
391
|
if (Number.isInteger(configured) && configured >= 1 && configured <= 64) this.maxRoots = configured;
|
|
@@ -367,8 +424,9 @@ var MultiRootRegistry = class extends Service {
|
|
|
367
424
|
*/
|
|
368
425
|
get unavailable() {
|
|
369
426
|
switch (this.authorityState.kind) {
|
|
370
|
-
case "active":
|
|
371
|
-
case "
|
|
427
|
+
case "active":
|
|
428
|
+
case "follower": return;
|
|
429
|
+
case "contended": return this.shared && this.connectionFailure ? `shared registry is unavailable: ${this.connectionFailure}` : REGISTRY_CONTENDED_MESSAGE;
|
|
372
430
|
case "storage-failed":
|
|
373
431
|
case "config-invalid": return this.authorityState.reason;
|
|
374
432
|
}
|
|
@@ -377,6 +435,15 @@ var MultiRootRegistry = class extends Service {
|
|
|
377
435
|
* Whether this process is the Registry Authority for the store-wide medium.
|
|
378
436
|
* Tests and diagnostics read this; surfaces use {@link unavailable}.
|
|
379
437
|
*/
|
|
438
|
+
get epoch() {
|
|
439
|
+
return this.authorityState.kind === "active" ? this.operationEpoch : this.client?.epoch;
|
|
440
|
+
}
|
|
441
|
+
get shared() {
|
|
442
|
+
return this.sharedStorageRoot !== void 0;
|
|
443
|
+
}
|
|
444
|
+
get available() {
|
|
445
|
+
return this.authorityState.kind === "active" || this.authorityState.kind === "follower";
|
|
446
|
+
}
|
|
380
447
|
get authority() {
|
|
381
448
|
return this.authorityState;
|
|
382
449
|
}
|
|
@@ -430,11 +497,47 @@ var MultiRootRegistry = class extends Service {
|
|
|
430
497
|
*/
|
|
431
498
|
async refresh(primaryRoot) {
|
|
432
499
|
await this.ensureAuthority();
|
|
500
|
+
if (this.client) try {
|
|
501
|
+
return await this.remote("snapshot", [primaryRoot]);
|
|
502
|
+
} catch (error) {
|
|
503
|
+
if (this.client) throw error;
|
|
504
|
+
await this.ensureAuthority();
|
|
505
|
+
if (this.client) return await this.remote("snapshot", [primaryRoot]);
|
|
506
|
+
}
|
|
433
507
|
if (this.authorityState.kind !== "active") return [];
|
|
434
508
|
const key = canonicalRoot(primaryRoot);
|
|
435
509
|
return await this.serialize(key, () => this.reclassify(key));
|
|
436
510
|
}
|
|
437
511
|
/**
|
|
512
|
+
* Admit one fs invocation in the same order as mutations, then run its I/O
|
|
513
|
+
* OUTSIDE the queue. Revocation does not wait for already-admitted work.
|
|
514
|
+
* Shared followers obtain the same ordered snapshot through attested IPC.
|
|
515
|
+
*/
|
|
516
|
+
async withOperation(policy, kind, body) {
|
|
517
|
+
if (this.disposed) throw new Error("multi-root registry: cannot admit an operation after disposal");
|
|
518
|
+
const capturedPolicy = structuredClone(policy);
|
|
519
|
+
await this.ensureAuthority();
|
|
520
|
+
const key = canonicalRoot(capturedPolicy.workspaceRoot);
|
|
521
|
+
if (this.shared && this.available) {
|
|
522
|
+
const snapshot = this.client ? await this.remoteSnapshot("admit", [key]) : await this.captureSnapshot(key);
|
|
523
|
+
const scope = freezeOperationScope(this.ctx.multiRootScope.resolveSnapshot(capturedPolicy, this.projectRoots(snapshot.roots), snapshot.available));
|
|
524
|
+
return await runRegistryOperation(scope, kind, snapshot.epoch, snapshot.revision, body, this.operationProof, capturedPolicy.mode);
|
|
525
|
+
}
|
|
526
|
+
const admission = await this.serialize(key, () => {
|
|
527
|
+
if (this.disposed) throw new Error("multi-root registry: cannot admit an operation after disposal");
|
|
528
|
+
return {
|
|
529
|
+
scope: freezeOperationScope(this.shared ? this.ctx.multiRootScope.resolveSnapshot(capturedPolicy, this.projectRoots(this.cache.get(key) ?? []), false) : this.ctx.multiRootScope.resolve(capturedPolicy)),
|
|
530
|
+
epoch: this.operationEpoch,
|
|
531
|
+
revision: this.operationRevision
|
|
532
|
+
};
|
|
533
|
+
});
|
|
534
|
+
return await runRegistryOperation(admission.scope, kind, admission.epoch, admission.revision, body, this.operationProof, capturedPolicy.mode);
|
|
535
|
+
}
|
|
536
|
+
withPrimaryOperation(policy, body) {
|
|
537
|
+
const scope = this.ctx.multiRootScope.resolveSnapshot(policy, this.projectRoots(this.cache.get(canonicalRoot(policy.workspaceRoot)) ?? []), false);
|
|
538
|
+
return runPrimaryOperation(freezeOperationScope(scope), policy.mode, this.operationProof, body);
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
438
541
|
* Registers one additional root.
|
|
439
542
|
* @param primaryRoot - the session workspace root the root belongs to.
|
|
440
543
|
* @param input - the directory and optional alias.
|
|
@@ -443,6 +546,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
443
546
|
*/
|
|
444
547
|
async add(primaryRoot, input) {
|
|
445
548
|
await this.ensureAuthority();
|
|
549
|
+
if (this.client) return await this.remote("add", [primaryRoot, input]);
|
|
446
550
|
this.requireActive();
|
|
447
551
|
const key = canonicalRoot(primaryRoot);
|
|
448
552
|
return await this.serialize(key, async () => {
|
|
@@ -513,6 +617,11 @@ var MultiRootRegistry = class extends Service {
|
|
|
513
617
|
*/
|
|
514
618
|
async setAccess(primaryRoot, ref, access) {
|
|
515
619
|
await this.ensureAuthority();
|
|
620
|
+
if (this.client) return await this.remote("setAccess", [
|
|
621
|
+
primaryRoot,
|
|
622
|
+
ref,
|
|
623
|
+
access
|
|
624
|
+
]);
|
|
516
625
|
this.requireActive();
|
|
517
626
|
const key = canonicalRoot(primaryRoot);
|
|
518
627
|
return await this.serialize(key, async () => {
|
|
@@ -554,6 +663,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
554
663
|
*/
|
|
555
664
|
async removeAt(primaryRoot, ref) {
|
|
556
665
|
await this.ensureAuthority();
|
|
666
|
+
if (this.client) return await this.remote("removeAt", [primaryRoot, ref]);
|
|
557
667
|
this.requireActive();
|
|
558
668
|
const key = canonicalRoot(primaryRoot);
|
|
559
669
|
return await this.serialize(key, async () => await this.persist(key, removeStatusAt(this.reclassify(key), ref)));
|
|
@@ -568,6 +678,11 @@ var MultiRootRegistry = class extends Service {
|
|
|
568
678
|
*/
|
|
569
679
|
async setAlias(primaryRoot, ref, alias) {
|
|
570
680
|
await this.ensureAuthority();
|
|
681
|
+
if (this.client) return await this.remote("setAlias", [
|
|
682
|
+
primaryRoot,
|
|
683
|
+
ref,
|
|
684
|
+
alias ?? null
|
|
685
|
+
]);
|
|
571
686
|
this.requireActive();
|
|
572
687
|
const key = canonicalRoot(primaryRoot);
|
|
573
688
|
return await this.serialize(key, async () => {
|
|
@@ -587,10 +702,22 @@ var MultiRootRegistry = class extends Service {
|
|
|
587
702
|
}
|
|
588
703
|
/** Change a Skill switch without changing any grant field. */
|
|
589
704
|
async setSkillsEnabled(primaryRoot, ref, skillsEnabled) {
|
|
705
|
+
await this.ensureAuthority();
|
|
706
|
+
if (this.client) return await this.remote("setSkillsEnabled", [
|
|
707
|
+
primaryRoot,
|
|
708
|
+
ref,
|
|
709
|
+
skillsEnabled
|
|
710
|
+
]);
|
|
590
711
|
if (typeof skillsEnabled !== "boolean") throw new RootValidationError("invalid-ref", "skillsEnabled must be a boolean");
|
|
591
712
|
return await this.setSkillsMetadata(primaryRoot, ref, { skillsEnabled });
|
|
592
713
|
}
|
|
593
714
|
async setAliasId(primaryRoot, ref, aliasId) {
|
|
715
|
+
await this.ensureAuthority();
|
|
716
|
+
if (this.client) return await this.remote("setAliasId", [
|
|
717
|
+
primaryRoot,
|
|
718
|
+
ref,
|
|
719
|
+
aliasId
|
|
720
|
+
]);
|
|
594
721
|
return await this.setSkillsMetadata(primaryRoot, ref, { aliasId });
|
|
595
722
|
}
|
|
596
723
|
async setSkillsMetadata(primaryRoot, ref, patch) {
|
|
@@ -617,6 +744,11 @@ var MultiRootRegistry = class extends Service {
|
|
|
617
744
|
*/
|
|
618
745
|
async move(primaryRoot, ref, beforeRef) {
|
|
619
746
|
await this.ensureAuthority();
|
|
747
|
+
if (this.client) return await this.remote("move", [
|
|
748
|
+
primaryRoot,
|
|
749
|
+
ref,
|
|
750
|
+
beforeRef ?? null
|
|
751
|
+
]);
|
|
620
752
|
this.requireActive();
|
|
621
753
|
const key = canonicalRoot(primaryRoot);
|
|
622
754
|
return await this.serialize(key, async () => {
|
|
@@ -645,6 +777,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
645
777
|
*/
|
|
646
778
|
async recheck(primaryRoot) {
|
|
647
779
|
await this.ensureAuthority();
|
|
780
|
+
if (this.client) return await this.remote("recheck", [primaryRoot]);
|
|
648
781
|
this.requireActive();
|
|
649
782
|
const key = canonicalRoot(primaryRoot);
|
|
650
783
|
return await this.serialize(key, async () => await this.persist(key, this.reclassify(key)));
|
|
@@ -666,7 +799,10 @@ var MultiRootRegistry = class extends Service {
|
|
|
666
799
|
* @returns the canonical paths of every `available` root.
|
|
667
800
|
*/
|
|
668
801
|
granted(primaryRoot) {
|
|
669
|
-
return
|
|
802
|
+
return this.shared ? this.ctx.multiRootScope.snapshot({
|
|
803
|
+
workspaceRoot: primaryRoot,
|
|
804
|
+
mode: "workspace-write"
|
|
805
|
+
}).additionalRoots : availableRoots(this.statusesOf(canonicalRoot(primaryRoot)));
|
|
670
806
|
}
|
|
671
807
|
/**
|
|
672
808
|
* The scope the registry publishes: only usable roots, in registry order.
|
|
@@ -709,7 +845,12 @@ var MultiRootRegistry = class extends Service {
|
|
|
709
845
|
*/
|
|
710
846
|
lookupRecords(key) {
|
|
711
847
|
if (this.authorityState.kind !== "active") return void 0;
|
|
712
|
-
|
|
848
|
+
try {
|
|
849
|
+
return this.table?.get(key);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
if (unknownStorageState(error)) this.enterStorageFailed("registry storage state cannot be confirmed; restart and reconcile");
|
|
852
|
+
throw error;
|
|
853
|
+
}
|
|
713
854
|
}
|
|
714
855
|
/**
|
|
715
856
|
* Re-classify one key from its stored records: every directory is `stat`ed
|
|
@@ -734,22 +875,28 @@ var MultiRootRegistry = class extends Service {
|
|
|
734
875
|
async persist(key, statuses) {
|
|
735
876
|
const table = this.requireTable();
|
|
736
877
|
const durable = persistedPrimaryRoot.parse({ roots: migrateRootSkills(statuses.map(toPersistedRoot)) });
|
|
737
|
-
|
|
738
|
-
|
|
878
|
+
try {
|
|
879
|
+
if (statuses.length === 0) await table.delete(key);
|
|
880
|
+
else await table.put(key, durable);
|
|
881
|
+
} catch (error) {
|
|
882
|
+
if (unknownStorageState(error)) this.enterStorageFailed("registry write result is unknown; restart and reconcile durable state");
|
|
883
|
+
throw error;
|
|
884
|
+
}
|
|
739
885
|
const classified = statuses.length === 0 ? [] : classifyStoredRoots(key, durable.roots.map(toRegisteredRoot));
|
|
740
886
|
this.cache.set(key, classified);
|
|
741
887
|
this.publish(key, classified);
|
|
742
888
|
return classified;
|
|
743
889
|
}
|
|
744
890
|
/**
|
|
745
|
-
* Run one job
|
|
746
|
-
*
|
|
891
|
+
* Run one job after every store job already queued. The whole
|
|
892
|
+
* read → validate → write sequence belongs inside the job:
|
|
747
893
|
* the storage domain serializes individual writes, so without this the loser
|
|
748
894
|
* of a race reads a snapshot that is already stale and writes it back.
|
|
749
895
|
*/
|
|
750
896
|
async serialize(key, job) {
|
|
751
|
-
const run =
|
|
897
|
+
const run = this.operationChain.then(job);
|
|
752
898
|
const tail = run.then(() => void 0, () => void 0);
|
|
899
|
+
this.operationChain = tail;
|
|
753
900
|
this.chains.set(key, tail);
|
|
754
901
|
try {
|
|
755
902
|
return await run;
|
|
@@ -781,7 +928,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
781
928
|
const withheld = statuses.filter((status) => status.state !== "available");
|
|
782
929
|
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
930
|
const previousMetadata = this.metadataPublished.get(key);
|
|
784
|
-
const metadata = JSON.stringify(statuses.map((row) => [
|
|
931
|
+
const metadata = JSON.stringify(this.shared ? statuses : statuses.map((row) => [
|
|
785
932
|
row.id,
|
|
786
933
|
row.aliasId,
|
|
787
934
|
row.skillsEnabled,
|
|
@@ -789,11 +936,23 @@ var MultiRootRegistry = class extends Service {
|
|
|
789
936
|
]));
|
|
790
937
|
this.metadataPublished.set(key, metadata);
|
|
791
938
|
if ((this.published.get(key) ?? "") === signature) {
|
|
792
|
-
if (previousMetadata !== metadata && (previousMetadata !== void 0 || statuses.length > 0))
|
|
939
|
+
if (previousMetadata !== metadata && (previousMetadata !== void 0 || statuses.length > 0)) {
|
|
940
|
+
this.operationRevision++;
|
|
941
|
+
this.notifyShared(key);
|
|
942
|
+
for (const listener of this.listeners) listener(key);
|
|
943
|
+
}
|
|
793
944
|
return;
|
|
794
945
|
}
|
|
795
946
|
this.published.set(key, signature);
|
|
796
|
-
|
|
947
|
+
this.operationRevision++;
|
|
948
|
+
const effective = this.projectRoots(statuses);
|
|
949
|
+
this.ctx.multiRootScope.setAdditionalRoots(key, effective);
|
|
950
|
+
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`);
|
|
951
|
+
this.notifyShared(key);
|
|
952
|
+
for (const listener of this.listeners) listener(key);
|
|
953
|
+
}
|
|
954
|
+
projectRoots(statuses) {
|
|
955
|
+
return statuses.flatMap((status) => {
|
|
797
956
|
const recorded = recordedGrantOf(status);
|
|
798
957
|
if (status.state === "invalid" && status.invalidReason !== "membership-conflict" || recorded === void 0) return [];
|
|
799
958
|
return [{
|
|
@@ -805,9 +964,113 @@ var MultiRootRegistry = class extends Service {
|
|
|
805
964
|
...status.alias === void 0 ? {} : { alias: status.alias }
|
|
806
965
|
}];
|
|
807
966
|
});
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
967
|
+
}
|
|
968
|
+
snapshotOf(primary) {
|
|
969
|
+
return snapshotSchema.parse({
|
|
970
|
+
protocol: 1,
|
|
971
|
+
store: this.medium.id,
|
|
972
|
+
epoch: this.operationEpoch,
|
|
973
|
+
revision: this.operationRevision,
|
|
974
|
+
primary,
|
|
975
|
+
roots: this.cache.get(primary) ?? [],
|
|
976
|
+
available: this.authorityState.kind === "active"
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
async captureSnapshot(primary) {
|
|
980
|
+
const key = canonicalRoot(primary);
|
|
981
|
+
return await this.serialize(key, () => {
|
|
982
|
+
this.requireActive();
|
|
983
|
+
this.reclassify(key);
|
|
984
|
+
return this.snapshotOf(key);
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
notifyShared(key) {
|
|
988
|
+
if (this.server && this.authorityState.kind === "active") this.server.changed(this.snapshotOf(key));
|
|
989
|
+
}
|
|
990
|
+
applySnapshot(snapshot) {
|
|
991
|
+
if (!this.client || snapshot.store !== this.medium?.id || snapshot.epoch !== this.client.epoch) return;
|
|
992
|
+
const previous = this.remoteRevisions.get(snapshot.primary) ?? -1;
|
|
993
|
+
if (snapshot.revision < previous) return;
|
|
994
|
+
this.remoteRevisions.set(snapshot.primary, snapshot.revision);
|
|
995
|
+
const rows = snapshot.roots.map((row) => Object.fromEntries(Object.entries({
|
|
996
|
+
...row,
|
|
997
|
+
id: additionalRootId(row.id)
|
|
998
|
+
}).filter(([, value]) => value !== void 0)));
|
|
999
|
+
this.cache.set(snapshot.primary, rows);
|
|
1000
|
+
this.publish(snapshot.primary, rows);
|
|
1001
|
+
this.ctx.multiRootScope.setRegistryAuthority(snapshot.available);
|
|
1002
|
+
this.remoteProjectionReady = snapshot.available;
|
|
1003
|
+
}
|
|
1004
|
+
remoteUnavailable() {
|
|
1005
|
+
this.connectionGeneration++;
|
|
1006
|
+
this.client = void 0;
|
|
1007
|
+
this.remoteRevisions.clear();
|
|
1008
|
+
this.authorityState = { kind: "contended" };
|
|
1009
|
+
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
1010
|
+
if (!this.remoteProjectionReady) return;
|
|
1011
|
+
this.remoteProjectionReady = false;
|
|
1012
|
+
for (const [primary, rows] of this.cache) this.ctx.multiRootScope.setAdditionalRoots(primary, this.projectRoots(rows).map((root) => ({
|
|
1013
|
+
...root,
|
|
1014
|
+
withheld: true
|
|
1015
|
+
})));
|
|
1016
|
+
this.published.clear();
|
|
1017
|
+
}
|
|
1018
|
+
async remoteSnapshot(method, args) {
|
|
1019
|
+
const client = this.client, generation = this.connectionGeneration;
|
|
1020
|
+
if (!client) throw new Error("registry authority is unavailable");
|
|
1021
|
+
const value = await client.request(method, args);
|
|
1022
|
+
let snapshot;
|
|
1023
|
+
try {
|
|
1024
|
+
snapshot = snapshotSchema.parse(value);
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
client.close();
|
|
1027
|
+
throw error;
|
|
1028
|
+
}
|
|
1029
|
+
if (generation !== this.connectionGeneration || client !== this.client || snapshot.store !== this.medium?.id || snapshot.epoch !== client.epoch || snapshot.primary !== canonicalRoot(String(args[0]))) {
|
|
1030
|
+
client.close();
|
|
1031
|
+
throw new Error("stale registry connection or authority epoch");
|
|
1032
|
+
}
|
|
1033
|
+
this.applySnapshot(snapshot);
|
|
1034
|
+
return snapshot;
|
|
1035
|
+
}
|
|
1036
|
+
async remote(method, args) {
|
|
1037
|
+
await this.remoteSnapshot(method, args);
|
|
1038
|
+
return this.list(String(args[0]));
|
|
1039
|
+
}
|
|
1040
|
+
async dispatchShared(method, values) {
|
|
1041
|
+
const args = parseArguments(method, values);
|
|
1042
|
+
const primary = String(args[0]);
|
|
1043
|
+
if (!isAbsolute(primary)) throw new Error("registry primary must be absolute");
|
|
1044
|
+
if (method === "snapshot" || method === "admit") return this.captureSnapshot(primary);
|
|
1045
|
+
const ref = args[1];
|
|
1046
|
+
switch (method) {
|
|
1047
|
+
case "add":
|
|
1048
|
+
await this.add(primary, args[1]);
|
|
1049
|
+
break;
|
|
1050
|
+
case "removeAt":
|
|
1051
|
+
await this.removeAt(primary, ref);
|
|
1052
|
+
break;
|
|
1053
|
+
case "setAccess":
|
|
1054
|
+
await this.setAccess(primary, ref, args[2]);
|
|
1055
|
+
break;
|
|
1056
|
+
case "setAlias":
|
|
1057
|
+
await this.setAlias(primary, ref, args[2] === null ? void 0 : args[2]);
|
|
1058
|
+
break;
|
|
1059
|
+
case "setSkillsEnabled":
|
|
1060
|
+
await this.setSkillsEnabled(primary, ref, args[2]);
|
|
1061
|
+
break;
|
|
1062
|
+
case "setAliasId":
|
|
1063
|
+
await this.setAliasId(primary, ref, args[2]);
|
|
1064
|
+
break;
|
|
1065
|
+
case "move":
|
|
1066
|
+
await this.move(primary, ref, args[2] === null ? void 0 : args[2]);
|
|
1067
|
+
break;
|
|
1068
|
+
case "recheck":
|
|
1069
|
+
await this.recheck(primary);
|
|
1070
|
+
break;
|
|
1071
|
+
default: throw new Error("unsupported registry method");
|
|
1072
|
+
}
|
|
1073
|
+
return this.captureSnapshot(primary);
|
|
811
1074
|
}
|
|
812
1075
|
/**
|
|
813
1076
|
* The open table.
|
|
@@ -817,6 +1080,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
817
1080
|
* or an error when the service has not started.
|
|
818
1081
|
*/
|
|
819
1082
|
requireTable() {
|
|
1083
|
+
if (this.authorityState.kind === "follower") throw new Error("a follower never opens registry storage");
|
|
820
1084
|
if (this.authorityState.kind === "contended") throw new RootValidationError("registry-contended", REGISTRY_CONTENDED_MESSAGE);
|
|
821
1085
|
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
1086
|
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`);
|
|
@@ -840,9 +1104,10 @@ var MultiRootRegistry = class extends Service {
|
|
|
840
1104
|
async ensureAuthority() {
|
|
841
1105
|
await this.serializeAuthority(async () => {
|
|
842
1106
|
if (this.disposed) return;
|
|
843
|
-
if (this.authorityState.kind === "active") return;
|
|
1107
|
+
if (this.authorityState.kind === "active" || this.client !== void 0) return;
|
|
844
1108
|
if (this.authorityState.kind === "config-invalid") return;
|
|
845
1109
|
if (this.lease !== void 0) {
|
|
1110
|
+
if (this.domain !== void 0) return;
|
|
846
1111
|
await this.openDomain();
|
|
847
1112
|
return;
|
|
848
1113
|
}
|
|
@@ -855,11 +1120,50 @@ var MultiRootRegistry = class extends Service {
|
|
|
855
1120
|
this.enterStorageFailed(`${DOMAIN_NAME}: leasePath must be an absolute path`);
|
|
856
1121
|
return;
|
|
857
1122
|
}
|
|
1123
|
+
if (this.sharedStorageRoot !== void 0 && this.medium === void 0) try {
|
|
1124
|
+
if (!isAbsolute(this.sharedStorageRoot)) throw new Error("sharedStorageRoot must be absolute");
|
|
1125
|
+
this.medium = storeIdentity(this.sharedStorageRoot);
|
|
1126
|
+
if (storeIdentity(dirname(leasePath)).id !== this.medium.id || basename(leasePath) !== `multi_root_workspace.lock`) throw new Error("shared registry requires its canonical medium lock, without alternate lock names");
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
this.enterStorageFailed(String(error));
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
858
1131
|
try {
|
|
859
1132
|
this.lease = await RegistryAuthorityLease.acquire(leasePath);
|
|
860
1133
|
} catch (error) {
|
|
861
1134
|
if (error instanceof RegistryLeaseContendedError) {
|
|
862
1135
|
this.enterContended();
|
|
1136
|
+
if (this.medium !== void 0) for (const delay of [
|
|
1137
|
+
0,
|
|
1138
|
+
50,
|
|
1139
|
+
150
|
|
1140
|
+
]) {
|
|
1141
|
+
if (delay) await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1142
|
+
if (this.disposed) return;
|
|
1143
|
+
const generation = ++this.connectionGeneration;
|
|
1144
|
+
let adopted = false;
|
|
1145
|
+
try {
|
|
1146
|
+
const client = await AuthorityClient.connect(this.medium.endpoint, this.medium.id, this.maxRoots, (snapshot) => {
|
|
1147
|
+
if (adopted && generation === this.connectionGeneration) this.applySnapshot(snapshot);
|
|
1148
|
+
}, () => {
|
|
1149
|
+
if (adopted && generation === this.connectionGeneration) this.remoteUnavailable();
|
|
1150
|
+
});
|
|
1151
|
+
if (this.disposed || generation !== this.connectionGeneration) {
|
|
1152
|
+
client.close();
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
this.client = client;
|
|
1156
|
+
adopted = true;
|
|
1157
|
+
this.connectionFailure = void 0;
|
|
1158
|
+
this.authorityState = {
|
|
1159
|
+
kind: "follower",
|
|
1160
|
+
epoch: client.epoch
|
|
1161
|
+
};
|
|
1162
|
+
break;
|
|
1163
|
+
} catch (error) {
|
|
1164
|
+
this.connectionFailure = String(error);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
863
1167
|
return;
|
|
864
1168
|
}
|
|
865
1169
|
this.enterStorageFailed(`${DOMAIN_NAME}: cannot acquire the registry lease: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -876,7 +1180,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
876
1180
|
async openDomain() {
|
|
877
1181
|
let domain;
|
|
878
1182
|
try {
|
|
879
|
-
domain = await this.ctx.storageDomain.open(multiRootDomainSpec);
|
|
1183
|
+
domain = this.medium === void 0 ? await this.ctx.storageDomain.open(multiRootDomainSpec) : await openAuthorityStorage(this.authorityStorageContext, this.medium.root, multiRootDomainSpec);
|
|
880
1184
|
} catch (error) {
|
|
881
1185
|
const reason = `${DOMAIN_NAME}: ${error instanceof Error ? error.message : String(error)}`;
|
|
882
1186
|
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}`);
|
|
@@ -885,6 +1189,8 @@ var MultiRootRegistry = class extends Service {
|
|
|
885
1189
|
}
|
|
886
1190
|
this.domain = domain;
|
|
887
1191
|
this.table = domain.table(TABLE_NAME);
|
|
1192
|
+
this.operationEpoch = randomUUID();
|
|
1193
|
+
this.operationRevision = 0;
|
|
888
1194
|
this.authorityState = { kind: "active" };
|
|
889
1195
|
this.loggedContended = false;
|
|
890
1196
|
this.cache.clear();
|
|
@@ -896,15 +1202,30 @@ var MultiRootRegistry = class extends Service {
|
|
|
896
1202
|
await this.table.put(primaryRoot, migrated);
|
|
897
1203
|
accepted = migrated;
|
|
898
1204
|
} catch (error) {
|
|
1205
|
+
if (unknownStorageState(error)) {
|
|
1206
|
+
this.enterStorageFailed("Skills migration result is unknown; restart and reconcile");
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
899
1209
|
this.ctx.logger.warn(`multi-root skills: metadata migration failed; Skills remain withheld: ${String(error)}`);
|
|
900
1210
|
}
|
|
901
1211
|
this.accept(primaryRoot, accepted);
|
|
902
1212
|
});
|
|
1213
|
+
if (this.authorityState.kind !== "active") return;
|
|
1214
|
+
if (this.medium !== void 0) try {
|
|
1215
|
+
this.server = await AuthorityServer.listen(this.medium.endpoint, this.medium.id, this.operationEpoch, this.maxRoots, (method, args) => this.dispatchShared(method, args));
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
await domain.close();
|
|
1218
|
+
this.domain = void 0;
|
|
1219
|
+
this.enterStorageFailed(`cannot start shared registry: ${String(error)}`);
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
903
1222
|
this.ctx.multiRootScope.setRegistryAuthority(true);
|
|
904
1223
|
}
|
|
905
1224
|
/** Fail closed: empty scope, no table, contended state. */
|
|
906
1225
|
enterContended() {
|
|
907
|
-
this.
|
|
1226
|
+
this.operationEpoch = void 0;
|
|
1227
|
+
if (this.shared) this.remoteUnavailable();
|
|
1228
|
+
else this.withdrawPublished();
|
|
908
1229
|
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
909
1230
|
this.table = void 0;
|
|
910
1231
|
this.authorityState = { kind: "contended" };
|
|
@@ -915,8 +1236,17 @@ var MultiRootRegistry = class extends Service {
|
|
|
915
1236
|
}
|
|
916
1237
|
/** Fail closed: empty scope, no table, storage-failed state. The lease, if held, stays. */
|
|
917
1238
|
enterStorageFailed(reason) {
|
|
918
|
-
this.
|
|
1239
|
+
this.operationEpoch = void 0;
|
|
919
1240
|
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
1241
|
+
if (this.shared) {
|
|
1242
|
+
for (const [key, rows] of this.cache) this.ctx.multiRootScope.setAdditionalRoots(key, this.projectRoots(rows).map((root) => ({
|
|
1243
|
+
...root,
|
|
1244
|
+
withheld: true
|
|
1245
|
+
})));
|
|
1246
|
+
this.published.clear();
|
|
1247
|
+
this.server?.close();
|
|
1248
|
+
this.server = void 0;
|
|
1249
|
+
} else this.withdrawPublished();
|
|
920
1250
|
this.table = void 0;
|
|
921
1251
|
this.authorityState = {
|
|
922
1252
|
kind: "storage-failed",
|
|
@@ -945,8 +1275,14 @@ var MultiRootRegistry = class extends Service {
|
|
|
945
1275
|
*/
|
|
946
1276
|
async releaseAuthority() {
|
|
947
1277
|
this.disposed = true;
|
|
1278
|
+
this.connectionGeneration++;
|
|
1279
|
+
this.client?.close();
|
|
1280
|
+
this.client = void 0;
|
|
948
1281
|
await this.serializeAuthority(async () => {
|
|
1282
|
+
this.server?.close();
|
|
1283
|
+
this.server = void 0;
|
|
949
1284
|
this.table = void 0;
|
|
1285
|
+
this.operationEpoch = void 0;
|
|
950
1286
|
this.authorityState = { kind: "contended" };
|
|
951
1287
|
this.ctx.multiRootScope.setRegistryAuthority(false);
|
|
952
1288
|
await this.drainMutations();
|
|
@@ -964,7 +1300,7 @@ var MultiRootRegistry = class extends Service {
|
|
|
964
1300
|
});
|
|
965
1301
|
}
|
|
966
1302
|
/**
|
|
967
|
-
* Wait until no
|
|
1303
|
+
* Wait until no store work is queued or running (operation I/O is outside).
|
|
968
1304
|
*
|
|
969
1305
|
* One pass is enough, and only because the authority state has already been
|
|
970
1306
|
* flipped: no further mutation can be admitted (it would fail in
|
|
@@ -1063,5 +1399,12 @@ function normalizeAlias(alias) {
|
|
|
1063
1399
|
if (trimmed.length > 120) throw new RootValidationError("invalid-alias", `an alias must be at most 120 characters`);
|
|
1064
1400
|
return trimmed;
|
|
1065
1401
|
}
|
|
1402
|
+
/** Structural across storage releases; older versions do not expose this code. */
|
|
1403
|
+
function unknownStorageState(error, seen = /* @__PURE__ */ new Set()) {
|
|
1404
|
+
if (error === null || typeof error !== "object" || seen.has(error)) return false;
|
|
1405
|
+
seen.add(error);
|
|
1406
|
+
const value = error;
|
|
1407
|
+
return value.code === "unknown-state" || unknownStorageState(value.cause, seen) || Array.isArray(value.errors) && value.errors.some((item) => unknownStorageState(item, seen));
|
|
1408
|
+
}
|
|
1066
1409
|
//#endregion
|
|
1067
1410
|
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 };
|