vde-open 0.1.0 → 0.1.2

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.
@@ -1,22 +1,24 @@
1
1
  import { r as __toESM, t as __commonJSMin } from "./rolldown-runtime-DC62tzP2.js";
2
- import { $ as literal, A as successEnvelope, B as readParamsSchema, C as VdeError, D as ExitCode, E as isVdeError, F as htmlModeChangeParamsSchema, G as sha256Schema, H as reorderParamsSchema, I as htmlModeSchema, J as watchRuleSchema, K as sourceKindSchema, L as linkOpenParamsSchema, M as closeParamsSchema, N as documentFormatSchema, P as documentIdSchema, Q as boolean, R as listParamsSchema, S as validateAnswers, U as revisionSchema, V as renderGrantParamsSchema, W as searchParamsSchema, X as _enum, Y as LIMITS, Z as array, _ as feedbackWaitParamsSchema, a as uniqueTokens, at as canonicalJson, b as submissionSchema, c as feedbackAckParamsSchema, d as feedbackDraftParamsSchema, et as number, f as feedbackForgetParamsSchema, g as feedbackSubmitParamsSchema, h as feedbackStatusSchema, i as codePointLength, it as ZodError, j as assetRecordSchema, k as errorEnvelope, l as feedbackCancelParamsSchema, m as feedbackListParamsSchema, n as batchesOf, nt as strictObject, o as answersSchema, p as feedbackIdParamsSchema, q as sourceStateSchema, r as partsOf, rt as string, s as cancellationSchema, tt as record, u as feedbackCreateParamsSchema, v as questionnaireSchema, w as exitCodeForError, x as utf8Length, y as requestIdSchema, z as openParamsSchema } from "./search-index-C0szMSlZ.js";
3
- import { a as analyzeDocument, c as classifyReference, d as hasHiddenSegment, f as isValidLogicalPath, i as ParseLimitError, l as dirnameOfLogicalPath, n as scanReferences, o as assetTypeOf, p as roleAllowed, r as extractTitle, s as classifyLink, u as encodeLogicalPath } from "./render-yCayIbhf.js";
4
- import { t as measureHeap } from "./heap-Dev_gSQn.js";
2
+ import { $ as literal, A as successEnvelope, B as readParamsSchema, C as VdeError, D as ExitCode, E as isVdeError, F as htmlModeChangeParamsSchema, G as sha256Schema, H as reorderParamsSchema, I as htmlModeSchema, J as watchRuleSchema, K as sourceKindSchema, L as linkOpenParamsSchema, M as closeParamsSchema, N as documentFormatSchema, P as documentIdSchema, Q as boolean, R as listParamsSchema, S as validateAnswers, U as revisionSchema, V as renderGrantParamsSchema, W as searchParamsSchema, X as _enum, Y as LIMITS, Z as array, _ as feedbackWaitParamsSchema, a as uniqueTokens, at as canonicalJson, b as submissionSchema, c as feedbackAckParamsSchema, d as feedbackDraftParamsSchema, et as number, f as feedbackForgetParamsSchema, g as feedbackSubmitParamsSchema, h as feedbackStatusSchema, i as codePointLength, it as ZodError, j as assetRecordSchema, k as errorEnvelope, l as feedbackCancelParamsSchema, m as feedbackListParamsSchema, n as batchesOf, nt as strictObject, o as answersSchema, p as feedbackIdParamsSchema, q as sourceStateSchema, r as partsOf, rt as string, s as cancellationSchema, tt as record, u as feedbackCreateParamsSchema, v as questionnaireSchema, w as exitCodeForError, x as utf8Length, y as requestIdSchema, z as openParamsSchema } from "./search-index-__laOomT.js";
3
+ import { a as analyzeDocument, c as classifyReference, d as hasHiddenSegment, f as isValidLogicalPath, i as ParseLimitError, l as dirnameOfLogicalPath, n as scanReferences, o as assetTypeOf, p as roleAllowed, r as extractTitle, s as classifyLink, u as encodeLogicalPath } from "./render-BNZz0CjQ.js";
4
+ import { n as measureHeap, t as collectGarbage } from "./heap-C3qHm7SE.js";
5
5
  import { constants, existsSync, stat, unwatchFile, watch, watchFile } from "node:fs";
6
- import { appendFile, chmod, link, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, rmdir, stat as stat$1, writeFile } from "node:fs/promises";
6
+ import { appendFile, chmod, link, lstat, mkdir, open, readFile, readdir, readlink, realpath, rename, rm, rmdir, stat as stat$1, writeFile } from "node:fs/promises";
7
7
  import * as sp from "node:path";
8
- import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
8
+ import { basename, dirname, extname, isAbsolute, join, posix, relative, resolve, sep, win32 } from "node:path";
9
9
  import { EventEmitter } from "node:events";
10
10
  import { execFile } from "node:child_process";
11
11
  import { promisify } from "node:util";
12
12
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
13
13
  import * as nativeFs from "fs";
14
14
  import { readdir as readdir$1, readdirSync, realpath as realpath$1, realpathSync, stat as stat$2, statSync } from "fs";
15
- import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, normalize, posix, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "path";
15
+ import { basename as basename$1, dirname as dirname$1, isAbsolute as isAbsolute$1, normalize, posix as posix$1, relative as relative$1, resolve as resolve$1, sep as sep$1 } from "path";
16
16
  import { fileURLToPath } from "url";
17
17
  import { createRequire } from "module";
18
18
  import { fileURLToPath as fileURLToPath$1 } from "node:url";
19
19
  import { Worker } from "node:worker_threads";
20
+ import { performance } from "node:perf_hooks";
21
+ import { getHeapStatistics } from "node:v8";
20
22
  import { STATUS_CODES, ServerResponse, createServer } from "node:http";
21
23
  import { Http2ServerRequest, constants as constants$1 } from "node:http2";
22
24
  import { Readable } from "node:stream";
@@ -152,7 +154,7 @@ function parseStrictJson(text) {
152
154
  }
153
155
  //#endregion
154
156
  //#region package.json
155
- var version = "0.1.0";
157
+ var version = "0.1.2";
156
158
  //#endregion
157
159
  //#region src/documents/cursor.ts
158
160
  function invalidCursor(reason) {
@@ -184,7 +186,7 @@ function createCursorCodec(secret, now = Date.now) {
184
186
  //#endregion
185
187
  //#region ../../packages/document/src/profiles.ts
186
188
  const MARKDOWN_PARSER_PROFILE = "markdown-tanstack-v1";
187
- const HTML_STATIC_PARSER_PROFILE = "html-static-v1";
189
+ const HTML_STATIC_PARSER_PROFILE = "html-static-v2";
188
190
  //#endregion
189
191
  //#region ../../packages/document/src/source-lines.ts
190
192
  const LF = 10;
@@ -3150,8 +3152,8 @@ function buildFormat(cwd, root, absolute) {
3150
3152
  };
3151
3153
  return (p, isDir) => isDir && p !== "." ? p.slice(0, -1) : p;
3152
3154
  }
3153
- if (absolute) return (p) => posix.relative(cwd, p) || ".";
3154
- return (p) => posix.relative(cwd, `${root}/${p}`) || ".";
3155
+ if (absolute) return (p) => posix$1.relative(cwd, p) || ".";
3156
+ return (p) => posix$1.relative(cwd, `${root}/${p}`) || ".";
3155
3157
  }
3156
3158
  function buildRelative(cwd, root) {
3157
3159
  if (root.startsWith(`${cwd}/`)) {
@@ -3159,7 +3161,7 @@ function buildRelative(cwd, root) {
3159
3161
  return (p) => `${prefix}/${p}`;
3160
3162
  }
3161
3163
  return (p) => {
3162
- const result = posix.relative(cwd, `${root}/${p}`);
3164
+ const result = posix$1.relative(cwd, `${root}/${p}`);
3163
3165
  return p[p.length - 1] === "/" && result !== "" ? `${result}/` : result || ".";
3164
3166
  };
3165
3167
  }
@@ -3214,7 +3216,7 @@ function normalizePattern(pattern, opts, props, isIgnore) {
3214
3216
  if (pattern[pattern.length - 1] === "/") result = pattern.slice(0, -1);
3215
3217
  if (result[result.length - 1] !== "*" && opts.expandDirectories) result += "/**";
3216
3218
  const escapedCwd = escapePath(cwd);
3217
- result = isAbsolute$1(result.replace(ESCAPING_BACKSLASHES, "")) ? posix.relative(escapedCwd, result) : posix.normalize(result);
3219
+ result = isAbsolute$1(result.replace(ESCAPING_BACKSLASHES, "")) ? posix$1.relative(escapedCwd, result) : posix$1.normalize(result);
3218
3220
  const parentDir = (_PARENT_DIRECTORY$exe = PARENT_DIRECTORY.exec(result)) === null || _PARENT_DIRECTORY$exe === void 0 ? void 0 : _PARENT_DIRECTORY$exe[0];
3219
3221
  const parts = splitPattern(result);
3220
3222
  if (parentDir) {
@@ -3225,7 +3227,7 @@ function normalizePattern(pattern, opts, props, isIgnore) {
3225
3227
  result = result.slice(0, (n - i - 1) * 3) + result.slice((n - i) * 3 + parts[i + n].length + 1) || ".";
3226
3228
  i++;
3227
3229
  }
3228
- const potentialRoot = posix.join(cwd, parentDir.slice(i * 3));
3230
+ const potentialRoot = posix$1.join(cwd, parentDir.slice(i * 3));
3229
3231
  if (potentialRoot[0] !== "." && props.root.length > potentialRoot.length) {
3230
3232
  props.root = ensureNonDriveRelativePath(potentialRoot);
3231
3233
  props.depthOffset = -n + i;
@@ -3247,7 +3249,7 @@ function normalizePattern(pattern, opts, props, isIgnore) {
3247
3249
  }
3248
3250
  props.depthOffset = newCommonPath.length;
3249
3251
  props.commonPath = newCommonPath;
3250
- props.root = ensureNonDriveRelativePath(newCommonPath.length > 0 ? posix.join(cwd, ...newCommonPath) : cwd);
3252
+ props.root = ensureNonDriveRelativePath(newCommonPath.length > 0 ? posix$1.join(cwd, ...newCommonPath) : cwd);
3251
3253
  }
3252
3254
  return result;
3253
3255
  }
@@ -3513,6 +3515,550 @@ function isExcludedDirectoryName(name) {
3513
3515
  return isHidden(name) || EXCLUDED_DIRECTORIES.has(name);
3514
3516
  }
3515
3517
  //#endregion
3518
+ //#region src/documents/repository.ts
3519
+ const METADATA_LIMIT = 4096;
3520
+ function kindOf(stats) {
3521
+ if (stats.isSymbolicLink()) return "symlink";
3522
+ if (stats.isFile()) return "file";
3523
+ if (stats.isDirectory()) return "directory";
3524
+ return "other";
3525
+ }
3526
+ function notRegularFile(path) {
3527
+ return Object.assign(/* @__PURE__ */ new Error(`${path} is not a regular file.`), { code: "EFTYPE" });
3528
+ }
3529
+ const nodeRepositoryFs = {
3530
+ lstat: async (path) => kindOf(await lstat(path)),
3531
+ stat: async (path) => kindOf(await stat$1(path)),
3532
+ readlink: (path) => readlink(path),
3533
+ realpath: (path) => realpath(path),
3534
+ readSmall: async (path, limit) => {
3535
+ const before = await lstat(path);
3536
+ if (!before.isFile()) throw notRegularFile(path);
3537
+ const handle = await open(path, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
3538
+ try {
3539
+ const opened = await handle.stat();
3540
+ if (!opened.isFile() || opened.ino !== before.ino || opened.dev !== before.dev) throw notRegularFile(path);
3541
+ return await readBounded(handle, limit);
3542
+ } finally {
3543
+ await handle.close();
3544
+ }
3545
+ }
3546
+ };
3547
+ function createLimiter(limit) {
3548
+ let active = 0;
3549
+ const waiting = [];
3550
+ return {
3551
+ async run(work) {
3552
+ if (active < limit) active += 1;
3553
+ else await new Promise((resolveTurn) => waiting.push(resolveTurn));
3554
+ try {
3555
+ return await work();
3556
+ } finally {
3557
+ const next = waiting.shift();
3558
+ if (next) next();
3559
+ else active -= 1;
3560
+ }
3561
+ },
3562
+ get active() {
3563
+ return active;
3564
+ }
3565
+ };
3566
+ }
3567
+ function limitRepositoryFs(fs, limiter) {
3568
+ return {
3569
+ lstat: (path) => limiter.run(() => fs.lstat(path)),
3570
+ stat: (path) => limiter.run(() => fs.stat(path)),
3571
+ readlink: (path) => limiter.run(() => fs.readlink(path)),
3572
+ realpath: (path) => limiter.run(() => fs.realpath(path)),
3573
+ readSmall: (path, size) => limiter.run(() => fs.readSmall(path, size))
3574
+ };
3575
+ }
3576
+ var Unresolved = class extends Error {
3577
+ reason;
3578
+ constructor(reason) {
3579
+ super(reason);
3580
+ this.reason = reason;
3581
+ }
3582
+ };
3583
+ function failureOf(error, invalid) {
3584
+ if (error instanceof Unresolved) return error;
3585
+ const code = error?.code;
3586
+ if (code === "ENOENT" || code === "ENOTDIR" || code === "ELOOP" || code === "EFTYPE") return new Unresolved(invalid);
3587
+ return new Unresolved("unreadable");
3588
+ }
3589
+ const HEX_OBJECT_ID = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
3590
+ const REFTABLE_HEAD = "refs/heads/.invalid";
3591
+ function isValidRefName(ref) {
3592
+ if (ref === "@" || ref.endsWith("/") || ref.endsWith(".")) return false;
3593
+ if (ref.includes("..") || ref.includes("@{") || ref.includes("//")) return false;
3594
+ if (/[\u0000- \u007f~^:?*[\\]/.test(ref)) return false;
3595
+ return ref.split("/").every((part) => part !== "" && !part.startsWith(".") && !part.endsWith(".lock"));
3596
+ }
3597
+ const utf8 = new TextDecoder("utf-8", { fatal: true });
3598
+ function parseHead(bytes) {
3599
+ const content = bytes.toString("latin1").replace(/\s+$/, "");
3600
+ if (HEX_OBJECT_ID.test(content)) return null;
3601
+ if (!content.startsWith("ref:")) throw new Unresolved("invalid-git-dir");
3602
+ const ref = content.slice(4).replace(/^\s+/, "");
3603
+ if (ref === REFTABLE_HEAD) return null;
3604
+ if (!ref.startsWith("refs/") || !isValidRefName(ref)) throw new Unresolved("invalid-git-dir");
3605
+ if (!ref.startsWith("refs/heads/")) return null;
3606
+ try {
3607
+ return utf8.decode(Buffer.from(ref.slice(11), "latin1"));
3608
+ } catch {
3609
+ return null;
3610
+ }
3611
+ }
3612
+ function parseLinkFile(bytes, prefix, invalid) {
3613
+ let content;
3614
+ try {
3615
+ content = utf8.decode(bytes);
3616
+ } catch {
3617
+ throw new Unresolved(invalid);
3618
+ }
3619
+ content = content.replace(/\r?\n$/, "");
3620
+ if (!content.startsWith(prefix)) throw new Unresolved(invalid);
3621
+ const path = content.slice(prefix.length);
3622
+ if (path === "" || path.includes("\n") || path.includes("\0")) throw new Unresolved(invalid);
3623
+ return path;
3624
+ }
3625
+ function createRepositoryDetector(options = {}) {
3626
+ const fs = options.fs ?? nodeRepositoryFs;
3627
+ const path = options.platform === "win32" ? win32 : posix;
3628
+ const isWindows = options.platform === "win32";
3629
+ const probes = /* @__PURE__ */ new Map();
3630
+ const checkouts = /* @__PURE__ */ new Map();
3631
+ const segmentsOf = (absolute) => absolute.split(path.sep).filter((part) => part !== "");
3632
+ const relativeSegments = (from, to) => segmentsOf(path.relative(from, to));
3633
+ const permits = (documentRoot, target) => {
3634
+ if (!isWindows) return true;
3635
+ const root = path.parse(target).root;
3636
+ if (/^[A-Za-z]:[\\/]$/.test(root)) return true;
3637
+ return root.toLowerCase() === documentRoot.toLowerCase();
3638
+ };
3639
+ const allowed = (documentRoot, target) => {
3640
+ if (!permits(documentRoot, target)) throw new Unresolved("blocked-path");
3641
+ return target;
3642
+ };
3643
+ const probe = (directory) => {
3644
+ let pending = probes.get(directory);
3645
+ if (!pending) {
3646
+ pending = fs.lstat(path.join(directory, ".git")).then((kind) => kind, (error) => error.code === "ENOENT" || error.code === "ENOTDIR" ? "absent" : "error");
3647
+ probes.set(directory, pending);
3648
+ }
3649
+ return pending;
3650
+ };
3651
+ const readMetadata = async (file, invalid) => {
3652
+ let bytes;
3653
+ try {
3654
+ bytes = await fs.readSmall(file, METADATA_LIMIT);
3655
+ } catch (error) {
3656
+ throw failureOf(error, invalid);
3657
+ }
3658
+ if (bytes === null) throw new Unresolved("limit-exceeded");
3659
+ return bytes;
3660
+ };
3661
+ const realpathOf = async (target, invalid) => {
3662
+ try {
3663
+ return await fs.realpath(target);
3664
+ } catch (error) {
3665
+ throw failureOf(error, invalid);
3666
+ }
3667
+ };
3668
+ const expectDirectory = async (target) => {
3669
+ let kind;
3670
+ try {
3671
+ kind = await fs.stat(target);
3672
+ } catch (error) {
3673
+ throw failureOf(error, "invalid-git-dir");
3674
+ }
3675
+ if (kind !== "directory") throw new Unresolved("invalid-git-dir");
3676
+ };
3677
+ const validated = /* @__PURE__ */ new Map();
3678
+ const validateGitDir = (gitDir) => {
3679
+ let pending = validated.get(gitDir);
3680
+ if (!pending) {
3681
+ pending = (async () => {
3682
+ await expectDirectory(gitDir);
3683
+ const branch = parseHead(await readMetadata(path.join(gitDir, "HEAD"), "invalid-git-dir"));
3684
+ await expectDirectory(path.join(gitDir, "objects"));
3685
+ await expectDirectory(path.join(gitDir, "refs"));
3686
+ return branch;
3687
+ })();
3688
+ validated.set(gitDir, pending);
3689
+ }
3690
+ return pending;
3691
+ };
3692
+ const repositoryNameSegments = (commonDir) => {
3693
+ const segments = segmentsOf(commonDir);
3694
+ const last = segments.at(-1) ?? "";
3695
+ if (last === ".git") return segments.slice(0, -1);
3696
+ if (last.length > 4 && last.endsWith(".git")) return [...segments.slice(0, -1), last.slice(0, -4)];
3697
+ return segments;
3698
+ };
3699
+ const standalone = (checkoutDir) => ({
3700
+ state: "resolved",
3701
+ id: checkoutDir,
3702
+ nameSegments: segmentsOf(checkoutDir),
3703
+ checkout: {
3704
+ id: checkoutDir,
3705
+ kind: "main"
3706
+ }
3707
+ });
3708
+ const commonDirOf = async (gitDir, documentRoot) => {
3709
+ try {
3710
+ await fs.lstat(path.join(gitDir, "commondir"));
3711
+ } catch (error) {
3712
+ const code = error.code;
3713
+ if (code === "ENOENT" || code === "ENOTDIR") return null;
3714
+ throw failureOf(error, "invalid-git-file");
3715
+ }
3716
+ const written = parseLinkFile(await readMetadata(path.join(gitDir, "commondir"), "invalid-git-file"), "", "invalid-git-file");
3717
+ return realpathOf(allowed(documentRoot, path.resolve(gitDir, written)), "invalid-git-dir");
3718
+ };
3719
+ const resolveGitFile = async (checkoutDir, gitFile, documentRoot) => {
3720
+ const written = parseLinkFile(await readMetadata(gitFile, "invalid-git-file"), "gitdir: ", "invalid-git-file");
3721
+ const gitDir = await realpathOf(allowed(documentRoot, path.resolve(checkoutDir, written)), "invalid-git-file");
3722
+ const commonDir = await commonDirOf(gitDir, documentRoot);
3723
+ if (commonDir === null) {
3724
+ await validateGitDir(gitDir);
3725
+ return standalone(checkoutDir);
3726
+ }
3727
+ await validateGitDir(commonDir);
3728
+ if (path.dirname(gitDir) !== path.join(commonDir, "worktrees")) throw new Unresolved("link-mismatch");
3729
+ const branch = parseHead(await readMetadata(path.join(gitDir, "HEAD"), "invalid-git-dir"));
3730
+ const backWritten = parseLinkFile(await readMetadata(path.join(gitDir, "gitdir"), "link-mismatch"), "", "link-mismatch");
3731
+ const back = allowed(documentRoot, path.resolve(gitDir, backWritten));
3732
+ if (path.basename(back) !== ".git") throw new Unresolved("link-mismatch");
3733
+ if (await realpathOf(path.dirname(back), "link-mismatch") !== checkoutDir) throw new Unresolved("link-mismatch");
3734
+ return {
3735
+ state: "resolved",
3736
+ id: commonDir,
3737
+ nameSegments: repositoryNameSegments(commonDir),
3738
+ checkout: {
3739
+ id: checkoutDir,
3740
+ kind: "linked",
3741
+ name: path.basename(gitDir),
3742
+ branch
3743
+ }
3744
+ };
3745
+ };
3746
+ const resolveGitLink = async (checkoutDir, gitLink, documentRoot) => {
3747
+ let target;
3748
+ try {
3749
+ target = allowed(documentRoot, path.resolve(checkoutDir, await fs.readlink(gitLink)));
3750
+ } catch (error) {
3751
+ throw failureOf(error, "invalid-git-dir");
3752
+ }
3753
+ let kind;
3754
+ try {
3755
+ kind = await fs.lstat(target);
3756
+ } catch (error) {
3757
+ throw failureOf(error, "invalid-git-dir");
3758
+ }
3759
+ if (kind === "directory") {
3760
+ await validateGitDir(target);
3761
+ return standalone(checkoutDir);
3762
+ }
3763
+ if (kind === "file") {
3764
+ const written = parseLinkFile(await readMetadata(target, "invalid-git-file"), "gitdir: ", "invalid-git-file");
3765
+ const gitDir = await realpathOf(allowed(documentRoot, path.resolve(checkoutDir, written)), "invalid-git-file");
3766
+ const common = await commonDirOf(gitDir, documentRoot);
3767
+ if (common === null) await validateGitDir(gitDir);
3768
+ else {
3769
+ await validateGitDir(common);
3770
+ parseHead(await readMetadata(path.join(gitDir, "HEAD"), "invalid-git-dir"));
3771
+ }
3772
+ return standalone(checkoutDir);
3773
+ }
3774
+ throw new Unresolved("invalid-git-dir");
3775
+ };
3776
+ const resolveCheckout = (checkoutDir, kind, documentRoot) => {
3777
+ let pending = checkouts.get(checkoutDir);
3778
+ if (!pending) {
3779
+ const gitEntry = path.join(checkoutDir, ".git");
3780
+ const work = async () => {
3781
+ if (kind === "directory") {
3782
+ await validateGitDir(gitEntry);
3783
+ return {
3784
+ state: "resolved",
3785
+ id: gitEntry,
3786
+ nameSegments: repositoryNameSegments(gitEntry),
3787
+ checkout: {
3788
+ id: checkoutDir,
3789
+ kind: "main"
3790
+ }
3791
+ };
3792
+ }
3793
+ if (kind === "file") return resolveGitFile(checkoutDir, gitEntry, documentRoot);
3794
+ if (kind === "symlink") return resolveGitLink(checkoutDir, gitEntry, documentRoot);
3795
+ throw new Unresolved("invalid-git-dir");
3796
+ };
3797
+ pending = work().catch((error) => ({
3798
+ state: "unresolved",
3799
+ reason: failureOf(error, "invalid-git-dir").reason
3800
+ }));
3801
+ checkouts.set(checkoutDir, pending);
3802
+ }
3803
+ return pending;
3804
+ };
3805
+ const detect = async (canonicalPath) => {
3806
+ const documentRoot = path.parse(canonicalPath).root;
3807
+ let directory = path.dirname(canonicalPath);
3808
+ let kind;
3809
+ for (;;) {
3810
+ const found = await probe(directory);
3811
+ if (found === "error") return {
3812
+ state: "unresolved",
3813
+ id: directory,
3814
+ nameSegments: segmentsOf(directory),
3815
+ pathInCheckout: relativeSegments(directory, canonicalPath),
3816
+ reason: "unreadable"
3817
+ };
3818
+ if (found !== "absent") {
3819
+ kind = found;
3820
+ break;
3821
+ }
3822
+ const parent = path.dirname(directory);
3823
+ if (parent === directory || directory === options.stopAt) return { state: "outside" };
3824
+ directory = parent;
3825
+ }
3826
+ const gitEntry = path.join(directory, ".git");
3827
+ if (kind === "directory" && canonicalPath.startsWith(gitEntry + path.sep)) {
3828
+ try {
3829
+ await validateGitDir(gitEntry);
3830
+ } catch (error) {
3831
+ return {
3832
+ state: "unresolved",
3833
+ id: directory,
3834
+ nameSegments: segmentsOf(directory),
3835
+ pathInCheckout: relativeSegments(directory, canonicalPath),
3836
+ reason: failureOf(error, "invalid-git-dir").reason
3837
+ };
3838
+ }
3839
+ return {
3840
+ state: "resolved",
3841
+ id: gitEntry,
3842
+ nameSegments: repositoryNameSegments(gitEntry),
3843
+ checkout: null,
3844
+ pathInCheckout: relativeSegments(directory, canonicalPath)
3845
+ };
3846
+ }
3847
+ const resolution = await resolveCheckout(directory, kind, documentRoot);
3848
+ const pathInCheckout = relativeSegments(directory, canonicalPath);
3849
+ if (resolution.state === "unresolved") return {
3850
+ state: "unresolved",
3851
+ id: directory,
3852
+ nameSegments: segmentsOf(directory),
3853
+ pathInCheckout,
3854
+ reason: resolution.reason ?? "invalid-git-dir"
3855
+ };
3856
+ return {
3857
+ state: "resolved",
3858
+ id: resolution.id,
3859
+ nameSegments: resolution.nameSegments,
3860
+ checkout: resolution.checkout,
3861
+ pathInCheckout
3862
+ };
3863
+ };
3864
+ return { detect };
3865
+ }
3866
+ function supersedes(current, sequence) {
3867
+ return current === void 0 || current.sequence < sequence || current.sequence === sequence && current.value?.state === "pending";
3868
+ }
3869
+ var RepositoryTracker = class {
3870
+ #fs;
3871
+ #platform;
3872
+ #limiter = createLimiter(2);
3873
+ #stopAt;
3874
+ #createDetector;
3875
+ waitMs;
3876
+ lateApplyMs;
3877
+ lateApplyMaxMs;
3878
+ #sequence = 0;
3879
+ #documents = /* @__PURE__ */ new Map();
3880
+ #checkouts = /* @__PURE__ */ new Map();
3881
+ #members = /* @__PURE__ */ new Map();
3882
+ constructor(options = {}) {
3883
+ this.#fs = limitRepositoryFs(options.fs ?? nodeRepositoryFs, this.#limiter);
3884
+ this.#platform = options.platform ?? (process.platform === "win32" ? "win32" : "posix");
3885
+ this.waitMs = options.waitMs ?? 2e3;
3886
+ this.lateApplyMs = options.lateApplyMs ?? 250;
3887
+ this.lateApplyMaxMs = options.lateApplyMaxMs ?? 1e3;
3888
+ this.#stopAt = options.stopAt;
3889
+ this.#createDetector = options.createDetector;
3890
+ }
3891
+ begin(paths) {
3892
+ this.#sequence += 1;
3893
+ const detector = this.#createDetector?.() ?? createRepositoryDetector({
3894
+ fs: this.#fs,
3895
+ platform: this.#platform,
3896
+ ...this.#stopAt === void 0 ? {} : { stopAt: this.#stopAt }
3897
+ });
3898
+ const results = /* @__PURE__ */ new Map();
3899
+ const settled = /* @__PURE__ */ new Map();
3900
+ const work = [...new Set(paths)].map((path) => {
3901
+ const finished = detector.detect(path).then((detection) => {
3902
+ results.set(path, detection);
3903
+ }, () => {
3904
+ results.set(path, {
3905
+ state: "unresolved",
3906
+ id: path,
3907
+ nameSegments: [],
3908
+ pathInCheckout: [],
3909
+ reason: "unreadable"
3910
+ });
3911
+ });
3912
+ settled.set(path, finished);
3913
+ return finished;
3914
+ });
3915
+ return {
3916
+ sequence: this.#sequence,
3917
+ startedAt: Date.now(),
3918
+ results,
3919
+ done: Promise.all(work).then(() => void 0),
3920
+ settled: (path) => settled.get(path) ?? Promise.resolve()
3921
+ };
3922
+ }
3923
+ async wait(batch) {
3924
+ const remaining = batch.startedAt + this.waitMs - Date.now();
3925
+ if (remaining <= 0) return;
3926
+ let timer;
3927
+ await Promise.race([batch.done, new Promise((resolveTimeout) => {
3928
+ timer = setTimeout(resolveTimeout, remaining);
3929
+ })]);
3930
+ clearTimeout(timer);
3931
+ }
3932
+ #compose(stored, checkouts = this.#checkouts) {
3933
+ if (stored === void 0) return { state: "pending" };
3934
+ if (stored.state === "outside") return null;
3935
+ if (stored.state !== "resolved") return stored;
3936
+ if (stored.checkoutId === null) {
3937
+ const { checkoutId: _none, ...rest } = stored;
3938
+ return {
3939
+ ...rest,
3940
+ checkout: null
3941
+ };
3942
+ }
3943
+ const entry = checkouts.get(stored.checkoutId);
3944
+ if (entry === void 0) return { state: "pending" };
3945
+ return {
3946
+ state: "resolved",
3947
+ id: entry.repositoryId,
3948
+ nameSegments: entry.nameSegments,
3949
+ checkout: entry.info,
3950
+ pathInCheckout: stored.pathInCheckout
3951
+ };
3952
+ }
3953
+ view(documentId) {
3954
+ return this.#compose(this.#documents.get(documentId)?.value);
3955
+ }
3956
+ stage(batch, targets, options) {
3957
+ const documents = /* @__PURE__ */ new Map();
3958
+ const checkouts = /* @__PURE__ */ new Map();
3959
+ const unfinished = [];
3960
+ const appliesTo = (current) => supersedes(current, batch.sequence);
3961
+ for (const target of targets) {
3962
+ const current = this.#documents.get(target.documentId);
3963
+ const detection = batch.results.get(target.path);
3964
+ if (detection === void 0) {
3965
+ unfinished.push(target);
3966
+ if (options.pendingWhenUnfinished && current === void 0) documents.set(target.documentId, {
3967
+ sequence: batch.sequence,
3968
+ value: { state: "pending" }
3969
+ });
3970
+ continue;
3971
+ }
3972
+ if (!appliesTo(current)) continue;
3973
+ let value;
3974
+ if (detection.state === "resolved" && detection.checkout !== null) {
3975
+ const { checkout } = detection;
3976
+ value = {
3977
+ state: "resolved",
3978
+ checkoutId: checkout.id,
3979
+ pathInCheckout: detection.pathInCheckout
3980
+ };
3981
+ if (appliesTo(this.#checkouts.get(checkout.id))) checkouts.set(checkout.id, {
3982
+ sequence: batch.sequence,
3983
+ info: checkout,
3984
+ repositoryId: detection.id,
3985
+ nameSegments: detection.nameSegments
3986
+ });
3987
+ } else if (detection.state === "resolved") {
3988
+ const { checkout: _none, ...rest } = detection;
3989
+ value = {
3990
+ ...rest,
3991
+ checkoutId: null
3992
+ };
3993
+ } else value = detection;
3994
+ documents.set(target.documentId, {
3995
+ sequence: batch.sequence,
3996
+ value
3997
+ });
3998
+ }
3999
+ const merged = new Map(this.#checkouts);
4000
+ for (const [id, entry] of checkouts) merged.set(id, entry);
4001
+ const affected = new Set(documents.keys());
4002
+ for (const id of checkouts.keys()) for (const member of this.#members.get(id) ?? []) affected.add(member);
4003
+ let changed = false;
4004
+ for (const documentId of affected) {
4005
+ const before = this.view(documentId);
4006
+ const after = this.#compose(documents.get(documentId)?.value ?? this.#documents.get(documentId)?.value, merged);
4007
+ if (canonicalJson(before) !== canonicalJson(after)) {
4008
+ changed = true;
4009
+ break;
4010
+ }
4011
+ }
4012
+ return {
4013
+ changed,
4014
+ unfinished,
4015
+ documents,
4016
+ checkouts
4017
+ };
4018
+ }
4019
+ apply(staged) {
4020
+ for (const [id, entry] of staged.checkouts) if (supersedes(this.#checkouts.get(id), entry.sequence)) this.#checkouts.set(id, entry);
4021
+ for (const [documentId, entry] of staged.documents) {
4022
+ const current = this.#documents.get(documentId);
4023
+ if (entry.value.state === "pending" && current !== void 0) continue;
4024
+ if (supersedes(current, entry.sequence)) this.#setDocument(documentId, entry);
4025
+ }
4026
+ this.#dropUnused();
4027
+ }
4028
+ #setDocument(documentId, entry) {
4029
+ const before = this.#documents.get(documentId)?.value;
4030
+ if (before?.state === "resolved" && before.checkoutId !== null) this.#members.get(before.checkoutId)?.delete(documentId);
4031
+ this.#documents.set(documentId, entry);
4032
+ if (entry.value.state === "resolved" && entry.value.checkoutId !== null) {
4033
+ let members = this.#members.get(entry.value.checkoutId);
4034
+ if (!members) {
4035
+ members = /* @__PURE__ */ new Set();
4036
+ this.#members.set(entry.value.checkoutId, members);
4037
+ }
4038
+ members.add(documentId);
4039
+ }
4040
+ }
4041
+ forget(documentIds) {
4042
+ for (const documentId of documentIds) {
4043
+ const before = this.#documents.get(documentId)?.value;
4044
+ if (before?.state === "resolved" && before.checkoutId !== null) this.#members.get(before.checkoutId)?.delete(documentId);
4045
+ this.#documents.delete(documentId);
4046
+ }
4047
+ this.#dropUnused();
4048
+ }
4049
+ #dropUnused() {
4050
+ for (const [id, members] of this.#members) if (members.size === 0) this.#members.delete(id);
4051
+ for (const id of this.#checkouts.keys()) if (!this.#members.has(id)) this.#checkouts.delete(id);
4052
+ }
4053
+ retainedCounts() {
4054
+ return {
4055
+ repositoryDocuments: this.#documents.size,
4056
+ repositoryCheckouts: this.#checkouts.size,
4057
+ repositoryOperations: this.#limiter.active
4058
+ };
4059
+ }
4060
+ };
4061
+ //#endregion
3516
4062
  //#region src/documents/revision.ts
3517
4063
  function sha256Hex(bytes) {
3518
4064
  return createHash("sha256").update(bytes).digest("hex");
@@ -3542,7 +4088,7 @@ const MISSING_RETRY_INTERVAL_MS = 100;
3542
4088
  function profileOf(format) {
3543
4089
  return format === "markdown" ? MARKDOWN_PARSER_PROFILE : HTML_STATIC_PARSER_PROFILE;
3544
4090
  }
3545
- function toSummary(record, order, searchState = "excluded", pendingRequestIds = [], interactiveAllowed = false) {
4091
+ function toSummary(record, order, searchState = "excluded", pendingRequestIds = [], interactiveAllowed = false, repository = null) {
3546
4092
  return {
3547
4093
  documentId: record.documentId,
3548
4094
  key: record.key,
@@ -3551,6 +4097,8 @@ function toSummary(record, order, searchState = "excluded", pendingRequestIds =
3551
4097
  title: record.title,
3552
4098
  displayPath: record.displayPath,
3553
4099
  pathSegments: record.pathSegments,
4100
+ canonicalPath: record.sourceKind === "file" ? record.canonicalPath : null,
4101
+ repository,
3554
4102
  revision: record.currentRevision,
3555
4103
  sourceState: record.sourceState,
3556
4104
  searchState,
@@ -3625,6 +4173,7 @@ var DocumentService = class {
3625
4173
  #interactive = /* @__PURE__ */ new Map();
3626
4174
  #interactiveGenerations = 0;
3627
4175
  #linkConfirmations = /* @__PURE__ */ new Map();
4176
+ #repositories;
3628
4177
  constructor(options) {
3629
4178
  this.#store = options.store;
3630
4179
  this.#cursors = options.cursors;
@@ -3634,6 +4183,68 @@ var DocumentService = class {
3634
4183
  this.#readSource = options.readSource ?? readSourceFile;
3635
4184
  this.#scan = options.scan ?? ((kind, text) => Promise.resolve(scanReferences(kind, text)));
3636
4185
  this.#searchStateOf = options.searchStateOf ?? (() => "excluded");
4186
+ this.#repositories = new RepositoryTracker(options.repositories);
4187
+ }
4188
+ async initializeRepositories() {
4189
+ const state = this.#store.payload;
4190
+ const targets = this.#trackedFiles(state, state.openOrder);
4191
+ if (targets.length === 0) return;
4192
+ const batch = this.#repositories.begin(targets.map((target) => target.path));
4193
+ await this.#repositories.wait(batch);
4194
+ const staged = this.#repositories.stage(batch, targets, { pendingWhenUnfinished: true });
4195
+ this.#repositories.apply(staged);
4196
+ this.#applyWhenDone(batch, staged.unfinished);
4197
+ }
4198
+ #trackedFiles(state, documentIds) {
4199
+ const targets = [];
4200
+ for (const documentId of documentIds) {
4201
+ const record = state.documents[documentId];
4202
+ if (record?.isOpen && record.sourceKind === "file" && record.canonicalPath !== null) targets.push({
4203
+ documentId,
4204
+ path: record.canonicalPath,
4205
+ epoch: this.openEpoch(documentId)
4206
+ });
4207
+ }
4208
+ return targets;
4209
+ }
4210
+ async #applyBatch(batch, targets) {
4211
+ let staged = null;
4212
+ await this.#store.transaction((tx) => {
4213
+ const live = targets.filter((target) => tx.state.documents[target.documentId]?.isOpen === true && this.openEpoch(target.documentId) === target.epoch);
4214
+ const next = this.#repositories.stage(batch, live, { pendingWhenUnfinished: false });
4215
+ if (next.changed) tx.state.catalogVersion += 1;
4216
+ staged = next;
4217
+ });
4218
+ const applied = staged;
4219
+ if (applied === null) return [];
4220
+ this.#repositories.apply(applied);
4221
+ if (applied.changed) this.#emit({ type: "catalog-changed" });
4222
+ return applied.unfinished;
4223
+ }
4224
+ #applyWhenDone(batch, targets) {
4225
+ if (targets.length === 0) return;
4226
+ let ready = [];
4227
+ let timer = null;
4228
+ let delay = this.#repositories.lateApplyMs;
4229
+ const flush = () => {
4230
+ timer = null;
4231
+ const now = ready;
4232
+ ready = [];
4233
+ delay = Math.min(delay * 2, this.#repositories.lateApplyMaxMs);
4234
+ this.#applyBatch(batch, now).catch(() => void 0);
4235
+ };
4236
+ for (const target of targets) batch.settled(target.path).then(() => {
4237
+ ready.push(target);
4238
+ timer ??= setTimeout(flush, delay);
4239
+ });
4240
+ }
4241
+ async #redetect(documentIds) {
4242
+ const state = this.#store.payload;
4243
+ const targets = this.#trackedFiles(state, documentIds.filter((documentId) => state.documents[documentId]?.sourceState !== "missing"));
4244
+ if (targets.length === 0) return;
4245
+ const batch = this.#repositories.begin(targets.map((target) => target.path));
4246
+ await this.#repositories.wait(batch);
4247
+ this.#applyWhenDone(batch, await this.#applyBatch(batch, targets));
3637
4248
  }
3638
4249
  async #withPathLocks(paths, work) {
3639
4250
  const releases = [];
@@ -3805,7 +4416,8 @@ var DocumentService = class {
3805
4416
  pathQueues: this.#pathQueues.size,
3806
4417
  openEpochs: this.#openEpochs.size,
3807
4418
  interactive: this.#interactive.size,
3808
- linkConfirmations: this.#linkConfirmations.size
4419
+ linkConfirmations: this.#linkConfirmations.size,
4420
+ ...this.#repositories.retainedCounts()
3809
4421
  };
3810
4422
  }
3811
4423
  async open(rawParams) {
@@ -3854,15 +4466,18 @@ var DocumentService = class {
3854
4466
  const settings = await this.#assetSettings(params);
3855
4467
  if (settings.explicit !== void 0 && resolved.unique.size !== 1) throw new VdeError("E_INVALID_ARGUMENT", "--asset can only be specified when opening a single document.", { documents: resolved.unique.size });
3856
4468
  if (params.htmlMode !== void 0 && ![...resolved.unique.values()].some((entry) => entry.format === "html")) throw new VdeError("E_INVALID_ARGUMENT", "--html-mode applies to HTML documents.");
4469
+ const batch = this.#repositories.begin(resolved.unique.keys());
3857
4470
  return this.#withPathLocks(resolved.unique.keys(), async () => {
3858
4471
  const incoming = await this.#readCandidates(resolved, settings, warnings);
3859
4472
  if (incoming.length === 0 && watchTargets.length === 0) throw new VdeError("E_PATH_NOT_FOUND", "No documents found.", { paths: params.paths });
3860
- return this.#commitOpen(params, incoming, watchTargets, settings, warnings);
4473
+ await this.#repositories.wait(batch);
4474
+ return this.#commitOpen(params, incoming, watchTargets, settings, warnings, batch);
3861
4475
  });
3862
4476
  }
3863
- async #commitOpen(params, incoming, watchTargets, settings, warnings) {
4477
+ async #commitOpen(params, incoming, watchTargets, settings, warnings, batch) {
3864
4478
  if ((params.title !== void 0 || params.key !== void 0) && incoming.length !== 1) throw new VdeError("E_INVALID_ARGUMENT", "--title and --key can only be specified when opening a single document.", { documents: incoming.length });
3865
4479
  const events = [];
4480
+ let repositories = null;
3866
4481
  const result = await this.#store.transaction((tx) => {
3867
4482
  const state = tx.state;
3868
4483
  const timestamp = this.#now().toISOString();
@@ -3906,9 +4521,11 @@ var DocumentService = class {
3906
4521
  rules.push(rule);
3907
4522
  }
3908
4523
  assertOpenLimits(state);
3909
- if (created + updated > 0) state.catalogVersion += 1;
4524
+ if (batch !== void 0) repositories = this.#repositories.stage(batch, this.#trackedFiles(state, touched), { pendingWhenUnfinished: true });
4525
+ const repositoryChanged = repositories?.changed === true;
4526
+ if (created + updated > 0 || repositoryChanged) state.catalogVersion += 1;
3910
4527
  if (state.activeDocumentId === null) state.activeDocumentId = state.openOrder[0] ?? null;
3911
- if (openSetChanged) events.unshift({ type: "catalog-changed" });
4528
+ if (openSetChanged || repositoryChanged) events.unshift({ type: "catalog-changed" });
3912
4529
  return {
3913
4530
  data: {
3914
4531
  documents: touched.map((documentId) => this.#summary(state, documentId)),
@@ -3921,6 +4538,11 @@ var DocumentService = class {
3921
4538
  warnings
3922
4539
  };
3923
4540
  });
4541
+ const staged = repositories;
4542
+ if (staged !== null && batch !== void 0) {
4543
+ this.#repositories.apply(staged);
4544
+ this.#applyWhenDone(batch, staged.unfinished);
4545
+ }
3924
4546
  for (const item of incoming) this.#record(item);
3925
4547
  const permissionChanged = [];
3926
4548
  result.data.documents.forEach((summary, index) => {
@@ -4183,7 +4805,7 @@ var DocumentService = class {
4183
4805
  };
4184
4806
  }
4185
4807
  #summary(state, documentId) {
4186
- return toSummary(state.documents[documentId], state.openOrder.indexOf(documentId), this.#searchStateOf(documentId), pendingRequestIdsOf(state, documentId), this.interactiveAllowed(documentId));
4808
+ return toSummary(state.documents[documentId], state.openOrder.indexOf(documentId), this.#searchStateOf(documentId), pendingRequestIdsOf(state, documentId), this.interactiveAllowed(documentId), state.documents[documentId]?.sourceKind === "file" ? this.#repositories.view(documentId) : null);
4187
4809
  }
4188
4810
  #findExisting(state, item, key) {
4189
4811
  if (item.sourceKind === "file") return Object.values(state.documents).find((record) => record.sourceKind === "file" && record.canonicalPath === item.canonicalPath);
@@ -4518,6 +5140,7 @@ var DocumentService = class {
4518
5140
  };
4519
5141
  });
4520
5142
  for (const documentId of result.data.closed) this.#openEpochs.set(documentId, this.openEpoch(documentId) + 1);
5143
+ this.#repositories.forget(result.data.closed);
4521
5144
  if (changed) this.#emit({ type: "catalog-changed" });
4522
5145
  for (const event of cancelled) this.#emit(event);
4523
5146
  return result;
@@ -4595,6 +5218,7 @@ var DocumentService = class {
4595
5218
  const targets = raw === void 0 ? this.#store.payload.openOrder : [this.#requireOpen(documentIdSchema.parse(raw)).documentId];
4596
5219
  const changed = [];
4597
5220
  for (const documentId of targets) if ((await this.refreshFromDisk(documentId)).changed) changed.push(documentId);
5221
+ await this.#redetect(targets);
4598
5222
  const state = this.#store.payload;
4599
5223
  return {
4600
5224
  data: {
@@ -4730,6 +5354,7 @@ var DocumentService = class {
4730
5354
  const before = this.#store.payload;
4731
5355
  const rule = before.watchRules.find((candidate) => candidate.watchId === watchId);
4732
5356
  if (!rule) return [];
5357
+ const batch = this.#repositories.begin(candidates.keys());
4733
5358
  const isOpenPath = (state, path) => Object.values(state.documents).some((record) => record.isOpen && record.sourceKind === "file" && record.canonicalPath === path);
4734
5359
  let totalBytes = openSourceBytes(before);
4735
5360
  const discovered = [];
@@ -4769,7 +5394,9 @@ var DocumentService = class {
4769
5394
  }
4770
5395
  }
4771
5396
  if (discovered.length === 0) return [];
5397
+ await this.#repositories.wait(batch);
4772
5398
  const appliedItems = [];
5399
+ let repositories = null;
4773
5400
  const registered = await this.#store.transaction((tx) => {
4774
5401
  const state = tx.state;
4775
5402
  const current = state.watchRules.find((candidate) => candidate.watchId === watchId);
@@ -4785,10 +5412,16 @@ var DocumentService = class {
4785
5412
  }
4786
5413
  if (added.length === 0) return [];
4787
5414
  assertOpenLimits(state);
5415
+ repositories = this.#repositories.stage(batch, this.#trackedFiles(state, added), { pendingWhenUnfinished: true });
4788
5416
  state.catalogVersion += 1;
4789
5417
  if (state.activeDocumentId === null) state.activeDocumentId = state.openOrder[0] ?? null;
4790
5418
  return added;
4791
5419
  });
5420
+ const staged = repositories;
5421
+ if (staged !== null) {
5422
+ this.#repositories.apply(staged);
5423
+ this.#applyWhenDone(batch, staged.unfinished);
5424
+ }
4792
5425
  for (const item of appliedItems) this.#record(item);
4793
5426
  if (registered.length > 0) this.#emit({ type: "catalog-changed" });
4794
5427
  return registered;
@@ -5869,7 +6502,8 @@ function createRenderService(options) {
5869
6502
  role
5870
6503
  })),
5871
6504
  links: output.links,
5872
- diagnostics: output.diagnostics
6505
+ diagnostics: output.diagnostics,
6506
+ headingTargets: output.headingTargets
5873
6507
  };
5874
6508
  snapshots.set(cacheKey, snapshot);
5875
6509
  while (snapshots.size > SNAPSHOT_CACHE_SIZE) {
@@ -5965,6 +6599,7 @@ function createRenderService(options) {
5965
6599
  target: null,
5966
6600
  count: 1
5967
6601
  }, ...snapshot.diagnostics] : snapshot.diagnostics,
6602
+ headingTargets: snapshot.headingTargets,
5968
6603
  bridge: bridge === null ? null : {
5969
6604
  instanceId: bridge.instanceId,
5970
6605
  requestId: bridge.requestId
@@ -6063,6 +6698,108 @@ function createRenderService(options) {
6063
6698
  };
6064
6699
  }
6065
6700
  //#endregion
6701
+ //#region src/diagnostics/idle-collect.ts
6702
+ const COLLECT_AFTER_IDLE_MS = 2e3;
6703
+ function createIdleTrigger(run, delayMs = COLLECT_AFTER_IDLE_MS) {
6704
+ let timer = null;
6705
+ const cancel = () => {
6706
+ if (timer) clearTimeout(timer);
6707
+ timer = null;
6708
+ };
6709
+ return {
6710
+ touch() {
6711
+ cancel();
6712
+ timer = setTimeout(() => {
6713
+ timer = null;
6714
+ run();
6715
+ }, delayMs);
6716
+ timer.unref();
6717
+ },
6718
+ cancel
6719
+ };
6720
+ }
6721
+ function createWorkerCollect(options) {
6722
+ let collecting = null;
6723
+ let closed = false;
6724
+ const held = [];
6725
+ const release = () => {
6726
+ if (collecting) clearTimeout(collecting.timer);
6727
+ collecting = null;
6728
+ for (const request of held.splice(0)) request.send();
6729
+ };
6730
+ const idle = createIdleTrigger(() => {
6731
+ const worker = options.worker();
6732
+ if (closed || worker === null || collecting) return;
6733
+ if (options.busy()) {
6734
+ idle.touch();
6735
+ return;
6736
+ }
6737
+ const id = options.nextId();
6738
+ const timer = setTimeout(release, options.waitLimitMs ?? 1e4);
6739
+ timer.unref();
6740
+ collecting = {
6741
+ id,
6742
+ timer
6743
+ };
6744
+ worker.postMessage({
6745
+ id,
6746
+ op: "collect"
6747
+ });
6748
+ }, options.delayMs);
6749
+ return {
6750
+ dispatch(send, reject) {
6751
+ if (collecting) held.push({
6752
+ send,
6753
+ reject
6754
+ });
6755
+ else send();
6756
+ },
6757
+ touch: () => idle.touch(),
6758
+ settle(id) {
6759
+ if (collecting?.id !== id) return false;
6760
+ release();
6761
+ return true;
6762
+ },
6763
+ reset: () => {
6764
+ if (collecting) release();
6765
+ },
6766
+ close(error) {
6767
+ closed = true;
6768
+ idle.cancel();
6769
+ if (collecting) clearTimeout(collecting.timer);
6770
+ collecting = null;
6771
+ for (const request of held.splice(0)) request.reject(error);
6772
+ }
6773
+ };
6774
+ }
6775
+ function utilizationSincePreviousCall() {
6776
+ let previous = performance.eventLoopUtilization();
6777
+ return () => {
6778
+ const current = performance.eventLoopUtilization();
6779
+ const { utilization } = performance.eventLoopUtilization(current, previous);
6780
+ previous = current;
6781
+ return utilization;
6782
+ };
6783
+ }
6784
+ function startHeapWatch(options = {}) {
6785
+ const growthBytes = options.growthBytes ?? 8388608;
6786
+ const idleUtilization = options.idleUtilization ?? .05;
6787
+ const heapUsed = options.heapUsed ?? (() => getHeapStatistics().used_heap_size);
6788
+ const utilization = options.utilization ?? utilizationSincePreviousCall();
6789
+ const collect = options.collect ?? collectGarbage;
6790
+ let baseline = heapUsed();
6791
+ const timer = setInterval(() => {
6792
+ const busy = utilization() >= idleUtilization;
6793
+ const used = heapUsed();
6794
+ baseline = Math.min(baseline, used);
6795
+ if (busy || used - baseline < growthBytes) return;
6796
+ collect();
6797
+ baseline = heapUsed();
6798
+ }, options.intervalMs ?? 2e3);
6799
+ timer.unref();
6800
+ return { stop: () => clearInterval(timer) };
6801
+ }
6802
+ //#endregion
6066
6803
  //#region src/diagnostics/until-aborted.ts
6067
6804
  function untilAborted(promise, signal) {
6068
6805
  if (!signal) return promise;
@@ -6148,6 +6885,15 @@ function createSearchService(options) {
6148
6885
  let closed = false;
6149
6886
  let indexedAt = now().toISOString();
6150
6887
  let syncing = null;
6888
+ const collector = createWorkerCollect({
6889
+ worker: () => worker,
6890
+ busy: () => pending.size > 0,
6891
+ nextId: () => {
6892
+ const id = nextId;
6893
+ nextId += 1;
6894
+ return id;
6895
+ }
6896
+ });
6151
6897
  let syncRequested = false;
6152
6898
  const failAll = (error) => {
6153
6899
  for (const waiter of pending.values()) {
@@ -6168,6 +6914,7 @@ function createSearchService(options) {
6168
6914
  const created = new Worker(workerPath);
6169
6915
  created.unref();
6170
6916
  created.on("message", (reply) => {
6917
+ if (collector.settle(reply.id)) return;
6171
6918
  const waiter = pending.get(reply.id);
6172
6919
  if (!waiter) return;
6173
6920
  pending.delete(reply.id);
@@ -6178,31 +6925,36 @@ function createSearchService(options) {
6178
6925
  created.on("error", () => {
6179
6926
  if (worker === created) discardWorker();
6180
6927
  failAll(new VdeError("E_INDEX_NOT_READY", "The search index is not available."));
6928
+ collector.reset();
6181
6929
  });
6182
6930
  worker = created;
6183
6931
  return created;
6184
6932
  };
6185
6933
  const request = (work) => {
6186
6934
  if (closed) return Promise.reject(new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
6187
- return new Promise((resolve, reject) => {
6188
- const id = nextId;
6189
- nextId += 1;
6190
- const timer = setTimeout(() => {
6191
- pending.delete(id);
6192
- discardWorker();
6193
- failAll(new VdeError("E_INDEX_NOT_READY", "The search index is not available."));
6194
- reject(new VdeError("E_INDEX_NOT_READY", "The search index operation did not finish in time.", { reason: "timeout" }));
6195
- }, timeoutMs);
6196
- pending.set(id, {
6197
- resolve,
6198
- reject,
6199
- timer
6200
- });
6201
- ensureWorker().postMessage({
6202
- id,
6203
- ...work
6204
- });
6935
+ const result = new Promise((resolve, reject) => {
6936
+ collector.dispatch(() => {
6937
+ const id = nextId;
6938
+ nextId += 1;
6939
+ const timer = setTimeout(() => {
6940
+ pending.delete(id);
6941
+ discardWorker();
6942
+ failAll(new VdeError("E_INDEX_NOT_READY", "The search index is not available."));
6943
+ reject(new VdeError("E_INDEX_NOT_READY", "The search index operation did not finish in time.", { reason: "timeout" }));
6944
+ }, timeoutMs);
6945
+ pending.set(id, {
6946
+ resolve,
6947
+ reject,
6948
+ timer
6949
+ });
6950
+ ensureWorker().postMessage({
6951
+ id,
6952
+ ...work
6953
+ });
6954
+ }, reject);
6205
6955
  });
6956
+ if (work.op !== "diagnostics") result.then(collector.touch, collector.touch);
6957
+ return result;
6206
6958
  };
6207
6959
  const analyzeOnce = async (format, text) => {
6208
6960
  try {
@@ -6513,6 +7265,7 @@ function createSearchService(options) {
6513
7265
  },
6514
7266
  async close() {
6515
7267
  closed = true;
7268
+ collector.close(new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
6516
7269
  failAll(new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
6517
7270
  const current = worker;
6518
7271
  worker = null;
@@ -12505,6 +13258,15 @@ function createParseService(options = {}) {
12505
13258
  let nextId = 1;
12506
13259
  let closed = false;
12507
13260
  const pending = /* @__PURE__ */ new Map();
13261
+ const collector = createWorkerCollect({
13262
+ worker: () => worker,
13263
+ busy: () => pending.size > 0,
13264
+ nextId: () => {
13265
+ const id = nextId;
13266
+ nextId += 1;
13267
+ return id;
13268
+ }
13269
+ });
12508
13270
  const failAll = (error) => {
12509
13271
  for (const waiter of pending.values()) {
12510
13272
  clearTimeout(waiter.timer);
@@ -12522,6 +13284,7 @@ function createParseService(options = {}) {
12522
13284
  const created = new Worker(workerPath);
12523
13285
  created.unref();
12524
13286
  created.on("message", (reply) => {
13287
+ if (collector.settle(reply.id)) return;
12525
13288
  const waiter = pending.get(reply.id);
12526
13289
  if (!waiter) return;
12527
13290
  pending.delete(reply.id);
@@ -12532,32 +13295,37 @@ function createParseService(options = {}) {
12532
13295
  created.on("error", () => {
12533
13296
  if (worker === created) worker = null;
12534
13297
  failAll(new VdeError("E_PARSE_FAILED", "The document could not be parsed.", { reason: "worker" }));
13298
+ collector.reset();
12535
13299
  });
12536
13300
  worker = created;
12537
13301
  return created;
12538
13302
  };
12539
13303
  const request = (work) => {
12540
13304
  if (closed) return Promise.reject(new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
12541
- return new Promise((resolve, reject) => {
12542
- const id = nextId;
12543
- nextId += 1;
12544
- const timer = setTimeout(() => {
12545
- discardWorker();
12546
- failAll(new VdeError("E_PARSE_FAILED", "Parsing the document did not finish in time.", {
12547
- reason: "timeout",
12548
- timeoutMs
12549
- }));
12550
- }, timeoutMs);
12551
- pending.set(id, {
12552
- resolve,
12553
- reject,
12554
- timer
12555
- });
12556
- ensureWorker().postMessage({
12557
- id,
12558
- ...work
12559
- });
13305
+ const result = new Promise((resolve, reject) => {
13306
+ collector.dispatch(() => {
13307
+ const id = nextId;
13308
+ nextId += 1;
13309
+ const timer = setTimeout(() => {
13310
+ discardWorker();
13311
+ failAll(new VdeError("E_PARSE_FAILED", "Parsing the document did not finish in time.", {
13312
+ reason: "timeout",
13313
+ timeoutMs
13314
+ }));
13315
+ }, timeoutMs);
13316
+ pending.set(id, {
13317
+ resolve,
13318
+ reject,
13319
+ timer
13320
+ });
13321
+ ensureWorker().postMessage({
13322
+ id,
13323
+ ...work
13324
+ });
13325
+ }, reject);
12560
13326
  });
13327
+ if (work.op !== "diagnostics") result.then(collector.touch, collector.touch);
13328
+ return result;
12561
13329
  };
12562
13330
  return {
12563
13331
  diagnostics: (collectGarbage, signal) => worker === null ? Promise.resolve(null) : untilAborted(request({
@@ -12580,6 +13348,7 @@ function createParseService(options = {}) {
12580
13348
  }),
12581
13349
  async close() {
12582
13350
  closed = true;
13351
+ collector.close(new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
12583
13352
  failAll(new VdeError("E_DAEMON_STOPPING", "The daemon is stopping."));
12584
13353
  const current = worker;
12585
13354
  worker = null;
@@ -12944,6 +13713,7 @@ async function startDaemon(options) {
12944
13713
  let feedback = null;
12945
13714
  let announceStopping = () => void 0;
12946
13715
  let lockTimer = null;
13716
+ let heapWatch = null;
12947
13717
  let stopping = null;
12948
13718
  const stopRequested = new AbortController();
12949
13719
  const owned = {
@@ -12977,6 +13747,7 @@ async function startDaemon(options) {
12977
13747
  };
12978
13748
  stopRequested.abort();
12979
13749
  if (lockTimer) clearInterval(lockTimer);
13750
+ heapWatch?.stop();
12980
13751
  await step("watcher", () => watcher?.close(), SHUTDOWN_CLOSE_TIMEOUT_MS);
12981
13752
  await step("feedback", () => feedback?.close());
12982
13753
  await step("drain", () => ipc?.drain());
@@ -13060,6 +13831,7 @@ async function startDaemon(options) {
13060
13831
  scan: (kind, text) => parser.scan(kind, text),
13061
13832
  emit
13062
13833
  });
13834
+ await documents.initializeRepositories();
13063
13835
  feedback = new FeedbackService({
13064
13836
  store: openedStore,
13065
13837
  documents,
@@ -13238,6 +14010,7 @@ async function startDaemon(options) {
13238
14010
  };
13239
14011
  lockTimer = setInterval(() => void checkLock(), LOCK_CHECK_INTERVAL_MS);
13240
14012
  lockTimer.unref();
14013
+ heapWatch = startHeapWatch();
13241
14014
  logger.log("daemon.started", { openDocuments: openedStore.payload.openOrder.length });
13242
14015
  } catch (error) {
13243
14016
  logger.log("daemon.start-failed", { code: error instanceof VdeError ? error.code : "E_INTERNAL" });