@openclaw/feishu 2026.9.8 → 2026.10.1-beta.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.
- package/dist/.setup/{channel-DV4hM2Uf.mjs → channel-CX62T71S.mjs} +146 -245
- package/dist/.setup/{channel.runtime-CXQcH5zZ.mjs → channel.runtime-COiaq-Rv.mjs} +6 -12
- package/dist/.setup/{chat-D8MlFAZi.mjs → chat-BdyvImsY.mjs} +6 -9
- package/dist/.setup/{client-BYG-_IZl.mjs → client-BsBVWJzN.mjs} +1 -1
- package/dist/.setup/{doctor-contract-3XC0vbIw.mjs → doctor-contract-B1B9ogdd.mjs} +212 -27
- package/dist/.setup/{monitor-D_rsHBZ6.mjs → monitor-7DvwUs4Y.mjs} +5 -6
- package/dist/.setup/{monitor.account-DvvY7i6k.mjs → monitor.account-7xVu7XWY.mjs} +190 -393
- package/dist/.setup/{probe-_ViqSi0j.mjs → probe-B9o7iey6.mjs} +13 -39
- package/dist/.setup/{reply-delivery-result-COAjaukc.mjs → reply-delivery-result-Bt-BWBEA.mjs} +151 -297
- package/dist/.setup/{setup-api-C4S5I3ac.mjs → setup-api-DmzX9S01.mjs} +1 -1
- package/dist/.setup/{subagent-hooks-BaFoMu8W.mjs → subagent-hooks-CUXg0pC-.mjs} +5 -11
- package/dist/.setup/{thread-bindings-EdLXPXzw.mjs → thread-bindings-jW2x5VuN.mjs} +23 -41
- package/dist/api.js +234 -297
- package/dist/channel-plugin-api.js +1 -1
- package/dist/doctor-contract-api.js +2 -2
- package/dist/session-binding-contract-api.js +1 -1
- package/dist/setup-api.js +1 -1
- package/dist/setup-entry.js +1 -1
- package/dist/subagent-hooks-api.js +1 -1
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +95 -0
- package/node_modules/@openclaw/fs-safe/README.md +70 -379
- package/node_modules/@openclaw/fs-safe/dist/absolute-path.js +35 -51
- package/node_modules/@openclaw/fs-safe/dist/advanced.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/advanced.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-input.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/archive-staging.js +9 -10
- package/node_modules/@openclaw/fs-safe/dist/archive.js +2 -5
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.d.ts +51 -0
- package/node_modules/@openclaw/fs-safe/dist/atomic-io.js +242 -0
- package/node_modules/@openclaw/fs-safe/dist/config.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/config.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-file-input.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-tree-portable.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/create.js +12 -5
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.d.ts +11 -0
- package/node_modules/@openclaw/fs-safe/dist/directory-guard.js +4 -7
- package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.d.ts +55 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/entry-publication.js +262 -0
- package/node_modules/@openclaw/fs-safe/dist/exclusive-create.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/exclusive-create.js +22 -0
- package/node_modules/@openclaw/fs-safe/dist/file-cleanup.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/file-cleanup.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-identity.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-io.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root-mutation.js +9 -0
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-root.js +4 -6
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync-stale-admission.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/file-lock-sync.js +8 -0
- package/node_modules/@openclaw/fs-safe/dist/file-store-boundary.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/file-store-sync-write.js +20 -18
- package/node_modules/@openclaw/fs-safe/dist/file-store.js +4 -10
- package/node_modules/@openclaw/fs-safe/dist/fs.d.ts +2 -10
- package/node_modules/@openclaw/fs-safe/dist/fs.js +2 -10
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.d.ts +3 -4
- package/node_modules/@openclaw/fs-safe/dist/guarded-mkdir.js +6 -18
- package/node_modules/@openclaw/fs-safe/dist/guest-dispatch-python.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +0 -2
- package/node_modules/@openclaw/fs-safe/dist/guest.js +3 -9
- package/node_modules/@openclaw/fs-safe/dist/index.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/index.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/json-durable-queue-directory.js +1 -5
- package/node_modules/@openclaw/fs-safe/dist/json.js +19 -37
- package/node_modules/@openclaw/fs-safe/dist/move-path.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/mutation-authority.js +9 -1
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +47 -2
- package/node_modules/@openclaw/fs-safe/dist/native-config.d.ts +1 -9
- package/node_modules/@openclaw/fs-safe/dist/native-config.js +6 -40
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-parent-admission.js +5 -2
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write.js +11 -316
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.d.ts +7 -7
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent-windows.js +28 -175
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.d.ts +15 -0
- package/node_modules/@openclaw/fs-safe/dist/native-policy-parent.js +418 -0
- package/node_modules/@openclaw/fs-safe/dist/native-rename-outcome.js +9 -4
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +17 -13
- package/node_modules/@openclaw/fs-safe/dist/native-staged-symlink.js +6 -24
- package/node_modules/@openclaw/fs-safe/dist/native.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/path-case.js +10 -9
- package/node_modules/@openclaw/fs-safe/dist/path-scope-lexical.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/path-segment-route.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/path-segment-route.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/path.js +4 -1
- package/node_modules/@openclaw/fs-safe/dist/permissions-windows.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-mutation-admission.js +1 -3
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/pinned-write.js +9 -10
- package/node_modules/@openclaw/fs-safe/dist/private-producer-handoff.js +3 -6
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +4 -3
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.d.ts +3 -4
- package/node_modules/@openclaw/fs-safe/dist/replace-file-buffer.js +18 -32
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.d.ts +5 -17
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-fallback.js +92 -230
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.d.ts +4 -15
- package/node_modules/@openclaw/fs-safe/dist/replace-file-copy-source.js +22 -63
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.d.ts +9 -34
- package/node_modules/@openclaw/fs-safe/dist/replace-file-descriptor.js +78 -83
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.d.ts +18 -20
- package/node_modules/@openclaw/fs-safe/dist/replace-file-destination.js +64 -98
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.d.ts +16 -33
- package/node_modules/@openclaw/fs-safe/dist/replace-file-temp-owner.js +61 -200
- package/node_modules/@openclaw/fs-safe/dist/replace-file-types.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/replace-file.js +76 -216
- package/node_modules/@openclaw/fs-safe/dist/root-boundary.js +14 -24
- package/node_modules/@openclaw/fs-safe/dist/root-context.d.ts +1 -4
- package/node_modules/@openclaw/fs-safe/dist/root-context.js +4 -19
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.d.ts +52 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +350 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +35 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -27
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +31 -28
- package/node_modules/@openclaw/fs-safe/dist/root-directory.js +1 -4
- package/node_modules/@openclaw/fs-safe/dist/root-errors.js +34 -0
- package/node_modules/@openclaw/fs-safe/dist/root-file.js +9 -21
- package/node_modules/@openclaw/fs-safe/dist/root-impl.js +105 -37
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.d.ts +7 -2
- package/node_modules/@openclaw/fs-safe/dist/root-move-noreplace.js +67 -38
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-observed-path.js +0 -2
- package/node_modules/@openclaw/fs-safe/dist/root-options.d.ts +2 -3
- package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-path-stat.js +68 -85
- package/node_modules/@openclaw/fs-safe/dist/root-path.js +6 -1
- package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-paths-lexical.js +1 -1
- package/node_modules/@openclaw/fs-safe/dist/root-paths.js +4 -9
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove-native.js +311 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove.d.ts +11 -0
- package/node_modules/@openclaw/fs-safe/dist/root-remove.js +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-walk.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/root-walk.js +1 -3
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.d.ts +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-write-admission.js +8 -10
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.d.ts +0 -1
- package/node_modules/@openclaw/fs-safe/dist/safe-path-segment.js +0 -4
- package/node_modules/@openclaw/fs-safe/dist/secret-file.js +12 -9
- package/node_modules/@openclaw/fs-safe/dist/secure-temp-dir.d.ts +0 -2
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/sibling-temp.js +7 -4
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-acquire.js +13 -6
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.d.ts +0 -3
- package/node_modules/@openclaw/fs-safe/dist/sidecar-lock-reclaim.js +5 -23
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/staged-file-settlement.js +11 -6
- package/node_modules/@openclaw/fs-safe/dist/temp-cleanup.js +1 -8
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/temp-workspace-child-admission.js +2 -9
- package/node_modules/@openclaw/fs-safe/dist/text-atomic.d.ts +1 -6
- package/node_modules/@openclaw/fs-safe/dist/trash.js +6 -2
- package/node_modules/@openclaw/fs-safe/dist/walk.js +100 -46
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +7 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +235 -19
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +21 -2
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +21 -3
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +6 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +111 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +27 -4
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/watch.js +131 -40
- package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.d.ts +8 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-path-alias.js +24 -6
- package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.d.ts +13 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-path-syntax.js +51 -0
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +4 -0
- package/node_modules/@openclaw/fs-safe/docs/atomic.md +4 -1
- package/node_modules/@openclaw/fs-safe/docs/config.md +6 -14
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +4 -14
- package/node_modules/@openclaw/fs-safe/docs/durability.md +6 -29
- package/node_modules/@openclaw/fs-safe/docs/entry-publication.md +146 -0
- package/node_modules/@openclaw/fs-safe/docs/errors.md +16 -0
- package/node_modules/@openclaw/fs-safe/docs/file-store.md +28 -5
- package/node_modules/@openclaw/fs-safe/docs/index.md +2 -31
- package/node_modules/@openclaw/fs-safe/docs/install.md +10 -31
- package/node_modules/@openclaw/fs-safe/docs/json.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/local-roots.md +0 -1
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +12 -12
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +35 -96
- package/node_modules/@openclaw/fs-safe/docs/native.md +22 -20
- package/node_modules/@openclaw/fs-safe/docs/path.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/public-api.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/reading.md +1 -2
- package/node_modules/@openclaw/fs-safe/docs/root.md +26 -184
- package/node_modules/@openclaw/fs-safe/docs/secret-file.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +115 -3
- package/node_modules/@openclaw/fs-safe/docs/store.md +2 -2
- package/node_modules/@openclaw/fs-safe/docs/temp.md +29 -65
- package/node_modules/@openclaw/fs-safe/docs/testing.md +90 -58
- package/node_modules/@openclaw/fs-safe/docs/types.md +3 -16
- package/node_modules/@openclaw/fs-safe/docs/walk.md +33 -5
- package/node_modules/@openclaw/fs-safe/docs/watch.md +96 -14
- package/node_modules/@openclaw/fs-safe/docs/writing.md +172 -16
- package/node_modules/@openclaw/fs-safe/package.json +10 -10
- package/node_modules/@openclaw/fs-safe-darwin-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-darwin-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-darwin-x64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-arm64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-gnu/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-linux-x64-musl/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-x64-msvc/package.json +1 -1
- package/package.json +5 -5
- package/skills/feishu-wiki/SKILL.md +1 -1
- package/dist/.setup/accounts-wRqItHug.mjs +0 -206
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.d.ts +0 -6
- package/node_modules/@openclaw/fs-safe/dist/watch-alias.js +0 -88
- package/node_modules/@openclaw/fs-safe/docs/mutation-policy-proof.md +0 -69
- package/node_modules/@openclaw/fs-safe/docs/private-file-store.md +0 -68
- package/node_modules/@openclaw/fs-safe/docs/test-hooks.md +0 -110
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
const parent = (name) => path.dirname(name) === "." ? "" : path.dirname(name);
|
|
3
|
+
const below = (directory, name) => !directory || name.startsWith(directory + path.sep);
|
|
4
|
+
const identity = (fingerprint) => fingerprint?.split(":").slice(0, 3).join(":");
|
|
5
|
+
/** Only previously enumerated directories can be reconciled without discovering topology. */
|
|
6
|
+
export function watchRescanScopes(scopes, snapshot, changes) {
|
|
7
|
+
if (scopes.some((scope, index) => scope.kind === "tree" && scope.depth > 0 && scopes.some((other, next) => index !== next && (scope.path === other.path || below(scope.path, other.path)))))
|
|
8
|
+
return;
|
|
9
|
+
const directories = new Set();
|
|
10
|
+
for (const change of changes) {
|
|
11
|
+
// A directory hint can cover descendant edits and new native registrations.
|
|
12
|
+
if (snapshot.listed?.has(change.path))
|
|
13
|
+
return undefined;
|
|
14
|
+
if (scopes.some(scope => scope.path === change.path && scope.kind === "tree" && scope.depth > 0))
|
|
15
|
+
return undefined;
|
|
16
|
+
if (scopes.some(scope => scope.path === change.path))
|
|
17
|
+
continue;
|
|
18
|
+
const directory = parent(change.path);
|
|
19
|
+
if (!snapshot.listed?.has(directory))
|
|
20
|
+
return undefined;
|
|
21
|
+
directories.add(directory);
|
|
22
|
+
}
|
|
23
|
+
return [
|
|
24
|
+
...scopes.map(scope => ({ ...scope, depth: 0 })),
|
|
25
|
+
...[...directories].map(name => ({ path: name, kind: "tree", depth: 1 })),
|
|
26
|
+
];
|
|
27
|
+
}
|
|
28
|
+
/** Merge only freshly observed slices. Any topology/anchor ambiguity requires a full pass. */
|
|
29
|
+
export function mergeWatchRescan(scopes, before, slice, requested) {
|
|
30
|
+
if (slice.overflow || slice.structural?.size)
|
|
31
|
+
return;
|
|
32
|
+
// An exclusion or a blocked path may stop before a requested directory is listed.
|
|
33
|
+
for (const scope of requested)
|
|
34
|
+
if (scope.kind === "tree" && scope.depth > 0 && !slice.listed?.has(scope.path))
|
|
35
|
+
return;
|
|
36
|
+
for (const [name, current] of slice.directories) {
|
|
37
|
+
const prior = before.directories.get(name);
|
|
38
|
+
if (!prior || prior.dev !== current.dev || prior.ino !== current.ino)
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
for (const scope of scopes) {
|
|
42
|
+
if (identity(before.entries.get(scope.path)) !== identity(slice.entries.get(scope.path)))
|
|
43
|
+
return;
|
|
44
|
+
// A missing/blocked intermediate scope must not retain old descendant registrations.
|
|
45
|
+
for (const name of before.directories.keys())
|
|
46
|
+
if ((name === scope.path || below(name, scope.path)) &&
|
|
47
|
+
!slice.directories.has(name) && name !== scope.path)
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const directories = slice.listed;
|
|
51
|
+
const selected = new Set(scopes.map(scope => scope.path));
|
|
52
|
+
for (const directory of directories.keys()) {
|
|
53
|
+
for (const name of before.childPaths?.get(directory)?.values() ?? [])
|
|
54
|
+
selected.add(name);
|
|
55
|
+
for (const name of slice.childPaths?.get(directory)?.values() ?? [])
|
|
56
|
+
selected.add(name);
|
|
57
|
+
}
|
|
58
|
+
for (const [name, previous] of before.entries) {
|
|
59
|
+
if (!selected.has(name))
|
|
60
|
+
continue;
|
|
61
|
+
const current = slice.entries.get(name);
|
|
62
|
+
if ((previous.startsWith("directory:") || current?.startsWith("directory:")) && identity(previous) !== identity(current))
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const [name, current] of slice.entries) {
|
|
66
|
+
if (selected.has(name) && current.startsWith("directory:") && identity(current) !== identity(before.entries.get(name)))
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
// Exclusion changes can retire or admit a subtree, including callback-driven changes.
|
|
70
|
+
for (const [name, kind] of before.excluded ?? [])
|
|
71
|
+
if (selected.has(name) && kind === "directory" && slice.entries.has(name))
|
|
72
|
+
return;
|
|
73
|
+
const entries = new Map(before.entries);
|
|
74
|
+
for (const name of selected)
|
|
75
|
+
if (!slice.entries.has(name))
|
|
76
|
+
entries.delete(name);
|
|
77
|
+
for (const [name, value] of slice.entries)
|
|
78
|
+
if (selected.has(name))
|
|
79
|
+
entries.set(name, value);
|
|
80
|
+
const excluded = new Map(before.excluded), excludedDirectories = new Map(before.excludedDirectories);
|
|
81
|
+
for (const name of selected) {
|
|
82
|
+
excluded.delete(name);
|
|
83
|
+
excludedDirectories.delete(name);
|
|
84
|
+
}
|
|
85
|
+
for (const [name, kind] of slice.excluded ?? [])
|
|
86
|
+
if (selected.has(name)) {
|
|
87
|
+
excluded.set(name, kind);
|
|
88
|
+
const canonical = slice.excludedDirectories?.get(name);
|
|
89
|
+
if (canonical)
|
|
90
|
+
excludedDirectories.set(name, canonical);
|
|
91
|
+
}
|
|
92
|
+
const listed = new Map(before.listed);
|
|
93
|
+
let scanned = before.scanned;
|
|
94
|
+
for (const [name, count] of directories) {
|
|
95
|
+
scanned += count - (listed.get(name) ?? 0);
|
|
96
|
+
listed.set(name, count);
|
|
97
|
+
}
|
|
98
|
+
return { ...before, entries, excluded, excludedDirectories, listed, scanned,
|
|
99
|
+
entryAnchors: slice.entryAnchors,
|
|
100
|
+
scopeAnchors: new Map(scopes.flatMap(scope => {
|
|
101
|
+
const anchor = slice.scopeAnchors?.get(scope.path);
|
|
102
|
+
return anchor ? [[scope.path, anchor]] : [];
|
|
103
|
+
})),
|
|
104
|
+
directoryPaths: new Map([...before.directoryPaths ?? [], ...slice.directoryPaths ?? []]),
|
|
105
|
+
childPaths: new Map([...before.childPaths ?? [], ...slice.childPaths ?? []]),
|
|
106
|
+
listingPaths: new Map([...before.listingPaths ?? [], ...slice.listingPaths ?? []]),
|
|
107
|
+
targets: new Map(scopes.flatMap(scope => {
|
|
108
|
+
const target = slice.targets.get(scope.path);
|
|
109
|
+
return target ? [[scope.path, target]] : [];
|
|
110
|
+
})), structural: slice.structural, overflow: false, };
|
|
111
|
+
}
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { type RootContext } from "./root-context.js";
|
|
2
2
|
import { type RootDirectoryObservationGuard } from "./root-directory-list.js";
|
|
3
|
+
import type { RootDirectoryListingPaths } from "./root-directory-list-types.js";
|
|
3
4
|
import type { WatchEntry, WatchOptions, WatchScope } from "./watch-types.js";
|
|
4
5
|
export type DirectoryIdentity = Readonly<{
|
|
5
6
|
dev: bigint;
|
|
6
7
|
ino: bigint;
|
|
7
8
|
}>;
|
|
9
|
+
export type WatchEntryAnchor = {
|
|
10
|
+
directory: string;
|
|
11
|
+
name: string;
|
|
12
|
+
target?: DirectoryIdentity & {
|
|
13
|
+
kind: WatchEntry["kind"];
|
|
14
|
+
};
|
|
15
|
+
};
|
|
8
16
|
export type WatchSnapshot = {
|
|
9
17
|
entries: Map<string, string>;
|
|
18
|
+
entryAnchors?: Map<string, WatchEntryAnchor>;
|
|
10
19
|
excluded?: Map<string, WatchEntry["kind"]>;
|
|
11
20
|
excludedDirectories?: Map<string, string>;
|
|
12
21
|
directoryPaths?: Map<string, string>;
|
|
@@ -15,6 +24,10 @@ export type WatchSnapshot = {
|
|
|
15
24
|
scanned: number;
|
|
16
25
|
structural?: Set<string>;
|
|
17
26
|
overflow?: boolean;
|
|
27
|
+
scopeAnchors?: Map<string, WatchEntryAnchor>;
|
|
28
|
+
listed?: Map<string, number>;
|
|
29
|
+
childPaths?: Map<string, Map<string, string>>;
|
|
30
|
+
listingPaths?: Map<string, RootDirectoryListingPaths>;
|
|
18
31
|
};
|
|
19
32
|
export declare function watchScopes(input: readonly WatchScope[]): readonly WatchScope[];
|
|
20
33
|
/** A descendant's unavailable metadata never grants it authority or retires the Root. */
|
|
@@ -57,7 +57,10 @@ export function isWatchPathError(error) {
|
|
|
57
57
|
export async function scanWatch(root, scopes, options, signal, register, onCleanupFailure) {
|
|
58
58
|
const exclusions = new Map();
|
|
59
59
|
const excludedDirectories = new Map(), directoryPaths = new Map();
|
|
60
|
-
const
|
|
60
|
+
const entryAnchors = new Map();
|
|
61
|
+
const scopeAnchors = new Map();
|
|
62
|
+
const listed = new Map(), childPaths = new Map(), listingPaths = new Map();
|
|
63
|
+
const result = { entries: new Map(), entryAnchors, scopeAnchors, excluded: exclusions, excludedDirectories, directoryPaths, directories: new Map(), targets: new Map(), scanned: 0, listed, childPaths, listingPaths };
|
|
61
64
|
const attempts = new Map();
|
|
62
65
|
const guards = new Map();
|
|
63
66
|
const walked = new Map();
|
|
@@ -70,7 +73,11 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
70
73
|
result.overflow = true;
|
|
71
74
|
// Discard partially observed names when their enclosing directory lost admission.
|
|
72
75
|
const below = (name) => name === relative || !relative || name.startsWith(relative + path.sep);
|
|
73
|
-
for (const
|
|
76
|
+
for (const anchors of [entryAnchors, scopeAnchors])
|
|
77
|
+
for (const [scope, anchor] of anchors)
|
|
78
|
+
if (below(anchor.directory))
|
|
79
|
+
anchors.delete(scope);
|
|
80
|
+
for (const map of [result.entries, exclusions, excludedDirectories, directoryPaths, result.targets, result.directories, guards, walked, listed, childPaths, listingPaths]) {
|
|
74
81
|
for (const name of map.keys())
|
|
75
82
|
if (below(name))
|
|
76
83
|
map.delete(name);
|
|
@@ -167,8 +174,11 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
167
174
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
168
175
|
try {
|
|
169
176
|
guard = await directory(relative);
|
|
177
|
+
listed.set(relative, 0);
|
|
170
178
|
listing = await openRootDirectoryListing(root, guard.realPath, {
|
|
171
|
-
order: "filesystem", snapshot: false, signal, exactIdentity: true, skipVanished: true, onCleanupFailure,
|
|
179
|
+
order: "filesystem", snapshot: false, signal, exactIdentity: true, skipVanished: true, onCleanupFailure,
|
|
180
|
+
previousPaths: options.previous?.listingPaths?.get(relative),
|
|
181
|
+
admitEntry: () => { examined(); listed.set(relative, (listed.get(relative) ?? 0) + 1); return true; },
|
|
172
182
|
});
|
|
173
183
|
// The listing has its own guard. Both identities must agree before reading names.
|
|
174
184
|
await assertRootDirectoryObservationGuard(root, guard);
|
|
@@ -182,6 +192,10 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
182
192
|
}
|
|
183
193
|
if (!listing)
|
|
184
194
|
return;
|
|
195
|
+
listingPaths.set(relative, listing.paths);
|
|
196
|
+
const names = new Map();
|
|
197
|
+
childPaths.set(relative, names);
|
|
198
|
+
const previousNames = options.previous?.childPaths?.get(relative);
|
|
185
199
|
let failed = false;
|
|
186
200
|
let operationError;
|
|
187
201
|
try {
|
|
@@ -195,7 +209,8 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
195
209
|
if (!next.identity)
|
|
196
210
|
throw new FsSafeError("path-mismatch", "watch listing lacks exact identity");
|
|
197
211
|
const entry = next.entry;
|
|
198
|
-
const name = relative ? path.join(relative, entry.name) : entry.name;
|
|
212
|
+
const name = previousNames?.get(entry.name) ?? (relative ? path.join(relative, entry.name) : entry.name);
|
|
213
|
+
names.set(entry.name, name);
|
|
199
214
|
if (await excluded(name, entry))
|
|
200
215
|
continue;
|
|
201
216
|
result.entries.set(name, fingerprint(entry, next.identity));
|
|
@@ -231,6 +246,9 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
231
246
|
signal.throwIfAborted();
|
|
232
247
|
if (!scope.path) {
|
|
233
248
|
const guard = await directory("");
|
|
249
|
+
scopeAnchors.set(scope.path, { directory: "", name: "" });
|
|
250
|
+
if (scope.kind === "entry")
|
|
251
|
+
entryAnchors.set(scope.path, { directory: "", name: "" });
|
|
234
252
|
result.entries.set("", fingerprint({ name: "", ...pathStatFromStats(guard.stat) }, guard.stat));
|
|
235
253
|
if (scope.kind === "tree" && scope.depth > 0)
|
|
236
254
|
await tree("", scope.depth);
|
|
@@ -241,6 +259,10 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
241
259
|
try {
|
|
242
260
|
for (let i = 0; i < segments.length; i++) {
|
|
243
261
|
const guard = await directory(relative);
|
|
262
|
+
const anchor = { directory: relative, name: segments[i] };
|
|
263
|
+
scopeAnchors.set(scope.path, anchor);
|
|
264
|
+
if (scope.kind === "entry")
|
|
265
|
+
entryAnchors.set(scope.path, anchor);
|
|
244
266
|
examined();
|
|
245
267
|
// Filesystem lookup, not lowercase/prefix matching, owns case, Unicode and
|
|
246
268
|
// short-name aliases. It also preserves case-sensitive Windows directories.
|
|
@@ -251,6 +273,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
251
273
|
const name = relative ? path.join(relative, segments[i]) : segments[i];
|
|
252
274
|
if (await excluded(name, found.entry))
|
|
253
275
|
break;
|
|
276
|
+
anchor.target = { ...found.identity, kind: kind(found.entry) };
|
|
254
277
|
if (i === segments.length - 1) {
|
|
255
278
|
result.entries.set(name, fingerprint(found.entry, found.identity));
|
|
256
279
|
result.targets.set(name, found.identity);
|
|
@@ -15,6 +15,8 @@ function shallowest(paths, limit) {
|
|
|
15
15
|
export function watchStreamPaths(snapshot, scopes) {
|
|
16
16
|
const anchors = [];
|
|
17
17
|
for (const scope of scopes) {
|
|
18
|
+
if (scope.kind === "entry")
|
|
19
|
+
continue;
|
|
18
20
|
let name = scope.kind === "tree" && scope.depth !== 0 ? scope.path : path.dirname(scope.path);
|
|
19
21
|
if (name === ".")
|
|
20
22
|
name = "";
|
|
@@ -4,9 +4,9 @@ import { assertRootIdentityCurrent } from "./root-context.js";
|
|
|
4
4
|
import { rootHandleContext } from "./root-handle-context.js";
|
|
5
5
|
import { createSuppressedError } from "./suppressed-error.js";
|
|
6
6
|
import { getFsSafeTestHooks } from "./test-hooks.js";
|
|
7
|
-
import {
|
|
7
|
+
import { mergeWatchRescan, watchRescanScopes } from "./watch-rescan.js";
|
|
8
8
|
import { watchStreamPaths } from "./watch-stream.js";
|
|
9
|
-
import { changedEntries, excludedWatchPath, guardedHintChanges, scopedChanges } from "./watch-hints.js";
|
|
9
|
+
import { admittedNativeChanges, changedEntries, excludedWatchPath, guardedHintChanges, scopedChanges } from "./watch-hints.js";
|
|
10
10
|
import path from "node:path";
|
|
11
11
|
import { watchBinding, NativeWatchBackend } from "./watch-native.js";
|
|
12
12
|
import { getFsSafeNativeConfig } from "./native-config.js";
|
|
@@ -60,6 +60,7 @@ export function watch(root, input) {
|
|
|
60
60
|
const maxPendingPaths = budget(options.maxPendingPaths, 256, "maxPendingPaths", 4096);
|
|
61
61
|
if (typeof options.onInvalidate !== "function")
|
|
62
62
|
throw new TypeError("watch requires onInvalidate");
|
|
63
|
+
const callerSignal = options.signal ? AbortSignal.any([options.signal]) : undefined;
|
|
63
64
|
const makeGeneration = (scopes) => ({
|
|
64
65
|
scopes: watchScopes(scopes), abort: new AbortController(), waiter: deferred(),
|
|
65
66
|
});
|
|
@@ -80,6 +81,8 @@ export function watch(root, input) {
|
|
|
80
81
|
let resetRequested = false;
|
|
81
82
|
let refreshBackend = false;
|
|
82
83
|
let pending = false;
|
|
84
|
+
let fullRequested = false;
|
|
85
|
+
let pendingUncertain = false;
|
|
83
86
|
let pendingWaiter;
|
|
84
87
|
let runningWaiter;
|
|
85
88
|
let pendingHint = false;
|
|
@@ -126,6 +129,7 @@ export function watch(root, input) {
|
|
|
126
129
|
pendingHint = false;
|
|
127
130
|
pendingChanges = new Map();
|
|
128
131
|
pendingBackendOverflow = false;
|
|
132
|
+
pendingUncertain = false;
|
|
129
133
|
};
|
|
130
134
|
const notifyHealth = () => {
|
|
131
135
|
try {
|
|
@@ -203,7 +207,7 @@ export function watch(root, input) {
|
|
|
203
207
|
}
|
|
204
208
|
};
|
|
205
209
|
const scheduleInterval = () => {
|
|
206
|
-
if (terminal || failure !== undefined)
|
|
210
|
+
if (timer || fullRequested || terminal || failure !== undefined)
|
|
207
211
|
return;
|
|
208
212
|
timer = setTimeout(() => { timer = undefined; void request().catch(() => { }); }, intervalMs);
|
|
209
213
|
if (!persistent)
|
|
@@ -232,16 +236,27 @@ export function watch(root, input) {
|
|
|
232
236
|
pendingChanges = undefined;
|
|
233
237
|
else if (pendingChanges)
|
|
234
238
|
for (const hint of batch.hints) {
|
|
235
|
-
if (
|
|
239
|
+
if (hint.event === "rename")
|
|
240
|
+
pendingUncertain = true;
|
|
241
|
+
if (typeof hint.name === "string" && excludedWatchPath(snapshot, hint.directory ? path.join(hint.directory, hint.name) : hint.name)) {
|
|
242
|
+
pendingUncertain = true;
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
// Multiple bounded native batches can still fill the JS coalescing window.
|
|
246
|
+
// A Root fold retains discovery while letting the guarded diff own detail.
|
|
247
|
+
if (pendingChanges.get("subtree-root")?.event === "subtree")
|
|
236
248
|
continue;
|
|
237
|
-
const key = JSON.stringify([hint.directory, hint.name]);
|
|
249
|
+
const key = JSON.stringify([hint.directory, hint.name, hint.event === "children" || hint.event === "subtree" ? hint.event : "entry"]);
|
|
238
250
|
if (!pendingChanges.has(key) && pendingChanges.size >= maxPendingPaths) {
|
|
239
|
-
pendingChanges
|
|
251
|
+
pendingChanges.clear();
|
|
252
|
+
pendingChanges.set("subtree-root", { directory: "", name: "", event: "subtree" });
|
|
240
253
|
break;
|
|
241
254
|
}
|
|
242
255
|
const previous = pendingChanges.get(key);
|
|
243
256
|
pendingChanges.set(key, previous?.event === "rename" ? previous : hint);
|
|
244
257
|
}
|
|
258
|
+
if (!batch.hints.length)
|
|
259
|
+
pendingUncertain = true;
|
|
245
260
|
pendingHint = true;
|
|
246
261
|
pending = true;
|
|
247
262
|
if (hintTimer)
|
|
@@ -251,7 +266,7 @@ export function watch(root, input) {
|
|
|
251
266
|
if (terminal || current !== g || failure !== undefined)
|
|
252
267
|
return;
|
|
253
268
|
// Raw backend filenames stay private. Reconcile before publishing detail.
|
|
254
|
-
void request().catch(() => { });
|
|
269
|
+
void request(false).catch(() => { });
|
|
255
270
|
}, coalesceMs);
|
|
256
271
|
if (!persistent)
|
|
257
272
|
hintTimer.unref();
|
|
@@ -273,6 +288,7 @@ export function watch(root, input) {
|
|
|
273
288
|
await retireBackend();
|
|
274
289
|
check(g);
|
|
275
290
|
refreshBackend = false;
|
|
291
|
+
fullRequested = true;
|
|
276
292
|
}
|
|
277
293
|
if (resetRequested) {
|
|
278
294
|
await retireBackend();
|
|
@@ -280,17 +296,40 @@ export function watch(root, input) {
|
|
|
280
296
|
snapshot = undefined;
|
|
281
297
|
resetRequested = false;
|
|
282
298
|
}
|
|
283
|
-
state = snapshot ? "reconciling" : "starting";
|
|
284
|
-
notifyHealth();
|
|
285
|
-
check(g);
|
|
286
299
|
const hadHints = pendingHint;
|
|
287
300
|
const hadBackendOverflow = pendingBackendOverflow;
|
|
288
301
|
const hints = pendingChanges;
|
|
302
|
+
const full = fullRequested || pendingUncertain;
|
|
303
|
+
fullRequested = false;
|
|
304
|
+
pendingUncertain = false;
|
|
289
305
|
pendingHint = false;
|
|
290
306
|
pendingChanges = new Map();
|
|
291
307
|
pendingBackendOverflow = false;
|
|
292
308
|
clearTimeout(hintTimer);
|
|
293
309
|
hintTimer = undefined;
|
|
310
|
+
let scanScopes = g.scopes;
|
|
311
|
+
if (snapshot && hadHints && hints && !full) {
|
|
312
|
+
try {
|
|
313
|
+
const admitted = await admittedNativeChanges(context, g.scopes, snapshot, snapshot, {
|
|
314
|
+
hints: [...hints.values()], overflow: false,
|
|
315
|
+
}, g.abort.signal, maxPendingPaths, true);
|
|
316
|
+
check(g);
|
|
317
|
+
if (admitted?.length === 0)
|
|
318
|
+
return;
|
|
319
|
+
if (admitted)
|
|
320
|
+
scanScopes = watchRescanScopes(g.scopes, snapshot, admitted) ?? g.scopes;
|
|
321
|
+
}
|
|
322
|
+
catch (error) {
|
|
323
|
+
check(g);
|
|
324
|
+
await assertRootIdentityCurrent(context);
|
|
325
|
+
if (!isWatchPathError(error))
|
|
326
|
+
throw error;
|
|
327
|
+
// An uncertain alias or stale identity never suppresses reconciliation.
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
state = snapshot ? "reconciling" : "starting";
|
|
331
|
+
notifyHealth();
|
|
332
|
+
check(g);
|
|
294
333
|
if (mode === "events" && !backend && g.scopes.length) {
|
|
295
334
|
try {
|
|
296
335
|
const candidate = new NativeWatchBackend(binding, context, batch => {
|
|
@@ -312,38 +351,83 @@ export function watch(root, input) {
|
|
|
312
351
|
}
|
|
313
352
|
check(g);
|
|
314
353
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
354
|
+
let next;
|
|
355
|
+
for (let attempt = 0;; attempt++) {
|
|
356
|
+
if (scanScopes === g.scopes) {
|
|
357
|
+
clearTimeout(timer);
|
|
358
|
+
timer = undefined;
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
next = await scanWatch(context, scanScopes, { exclude: options.exclude, maxDirectories, maxEntries, maxPendingPaths, admitting: !snapshot, previous: snapshot }, g.abort.signal, async (name, identity, guard) => {
|
|
362
|
+
check(g);
|
|
363
|
+
const existing = registered.get(name);
|
|
364
|
+
const acquire = !existing || existing.dev !== identity.dev || existing.ino !== identity.ino;
|
|
365
|
+
await getFsSafeTestHooks()?.beforeWatchRegistration?.(guard.realPath);
|
|
366
|
+
check(g);
|
|
367
|
+
if (acquire) {
|
|
368
|
+
try {
|
|
369
|
+
backend?.add(name, identity);
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
if (snapshot || !fallBack(error))
|
|
373
|
+
throw error;
|
|
374
|
+
await retireBackend();
|
|
375
|
+
check(g);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
await getFsSafeTestHooks()?.afterWatchRegistration?.(guard.realPath);
|
|
329
379
|
check(g);
|
|
380
|
+
if (acquire)
|
|
381
|
+
registered.set(name, identity);
|
|
382
|
+
}, retainRetirement);
|
|
383
|
+
}
|
|
384
|
+
catch (error) {
|
|
385
|
+
// A slice can repeat ancestor lookups that a full traversal shares.
|
|
386
|
+
// Let the authoritative full pass decide whether the configured limit is exceeded.
|
|
387
|
+
if (scanScopes !== g.scopes && error instanceof FsSafeError && error.code === "too-large") {
|
|
388
|
+
scanScopes = g.scopes;
|
|
389
|
+
continue;
|
|
330
390
|
}
|
|
391
|
+
throw error;
|
|
331
392
|
}
|
|
332
|
-
await getFsSafeTestHooks()?.afterWatchRegistration?.(guard.realPath);
|
|
333
393
|
check(g);
|
|
334
|
-
if (
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
394
|
+
if (scanScopes !== g.scopes) {
|
|
395
|
+
const merged = mergeWatchRescan(g.scopes, snapshot, next, scanScopes);
|
|
396
|
+
if (!merged || merged.scanned > maxEntries || merged.directories.size > maxDirectories || (merged.excluded?.size ?? 0) > maxEntries) {
|
|
397
|
+
scanScopes = g.scopes;
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
next = merged;
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const entriesChanged = backend?.entries(next);
|
|
404
|
+
const streamsChanged = backend?.configure(watchStreamPaths(next, g.scopes));
|
|
405
|
+
if (entriesChanged || streamsChanged) {
|
|
406
|
+
// Complete a guarded pass with the new transport before publishing readiness.
|
|
407
|
+
// Bound handovers under churn; later passes still reconcile ongoing changes.
|
|
408
|
+
scanScopes = g.scopes;
|
|
409
|
+
if (attempt < 2)
|
|
410
|
+
continue;
|
|
411
|
+
pending = true;
|
|
412
|
+
fullRequested = true;
|
|
413
|
+
}
|
|
414
|
+
break;
|
|
415
|
+
}
|
|
416
|
+
catch (error) {
|
|
417
|
+
await assertRootIdentityCurrent(context);
|
|
418
|
+
check(g);
|
|
419
|
+
if (attempt >= 2 || !isWatchPathError(error))
|
|
420
|
+
throw error;
|
|
421
|
+
scanScopes = g.scopes;
|
|
422
|
+
// A replacement between scan and descriptor admission requires a fresh identity.
|
|
423
|
+
}
|
|
341
424
|
}
|
|
342
425
|
// Retire stale inventory before the next pass; that crawl installs fresh anchors first.
|
|
343
426
|
if ([...registered.keys()].some(name => !next.directories.has(name))) {
|
|
344
427
|
refreshBackend = true;
|
|
345
428
|
}
|
|
346
|
-
observedDirectories = next.directories.size
|
|
429
|
+
observedDirectories = backend?.directories === undefined ? next.directories.size
|
|
430
|
+
: backend.directories + (g.scopes.some(scope => scope.kind === "tree") ? next.directories.size : 0);
|
|
347
431
|
const initial = !snapshot;
|
|
348
432
|
let observed = next.overflow ? undefined : changedEntries(snapshot, next, maxPendingPaths);
|
|
349
433
|
if (observed) {
|
|
@@ -384,6 +468,8 @@ export function watch(root, input) {
|
|
|
384
468
|
state = "ready";
|
|
385
469
|
notifyHealth();
|
|
386
470
|
check(g);
|
|
471
|
+
// Rearm even when sustained hints keep the pump's coalesced request occupied.
|
|
472
|
+
scheduleInterval();
|
|
387
473
|
};
|
|
388
474
|
const pump = () => {
|
|
389
475
|
if (active || terminal || failure !== undefined)
|
|
@@ -435,13 +521,16 @@ export function watch(root, input) {
|
|
|
435
521
|
}
|
|
436
522
|
});
|
|
437
523
|
};
|
|
438
|
-
const request = () => {
|
|
524
|
+
const request = (full = true) => {
|
|
439
525
|
if (terminal)
|
|
440
526
|
return Promise.reject(retired());
|
|
441
527
|
if (failure !== undefined)
|
|
442
528
|
return Promise.reject(failure);
|
|
443
|
-
|
|
444
|
-
|
|
529
|
+
if (full) {
|
|
530
|
+
clearTimeout(timer);
|
|
531
|
+
timer = undefined;
|
|
532
|
+
fullRequested = true;
|
|
533
|
+
}
|
|
445
534
|
pending = true;
|
|
446
535
|
pendingWaiter ??= deferred();
|
|
447
536
|
const result = pendingWaiter.promise;
|
|
@@ -459,7 +548,8 @@ export function watch(root, input) {
|
|
|
459
548
|
pendingWaiter = undefined;
|
|
460
549
|
runningWaiter = undefined;
|
|
461
550
|
clearTimers();
|
|
462
|
-
|
|
551
|
+
if (callerSignal)
|
|
552
|
+
callerSignal.onabort = null;
|
|
463
553
|
// Start physical stop immediately, without waiting behind an in-flight scan.
|
|
464
554
|
try {
|
|
465
555
|
backend?.close();
|
|
@@ -485,7 +575,7 @@ export function watch(root, input) {
|
|
|
485
575
|
const abort = () => { void close(); };
|
|
486
576
|
const subscription = {
|
|
487
577
|
ready, health, close, [Symbol.asyncDispose]: close,
|
|
488
|
-
reconcile: request,
|
|
578
|
+
reconcile: () => request(),
|
|
489
579
|
setScopes(scopes) {
|
|
490
580
|
if (terminal)
|
|
491
581
|
return Promise.reject(retired());
|
|
@@ -521,8 +611,9 @@ export function watch(root, input) {
|
|
|
521
611
|
return next.waiter.promise;
|
|
522
612
|
},
|
|
523
613
|
};
|
|
524
|
-
|
|
525
|
-
|
|
614
|
+
if (callerSignal)
|
|
615
|
+
callerSignal.onabort = abort;
|
|
616
|
+
if (callerSignal?.aborted)
|
|
526
617
|
abort();
|
|
527
618
|
else
|
|
528
619
|
pump();
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { FsSafeError } from "./errors.js";
|
|
2
2
|
export type WindowsPathAliasKind = "filesystem" | "relative";
|
|
3
|
+
/**
|
|
4
|
+
* True when a Windows path could reach a UNC share or device namespace that
|
|
5
|
+
* none of the trusted boundary paths live on: its share or device differs from
|
|
6
|
+
* theirs or cannot be identified from its spelling, and it is not spelled
|
|
7
|
+
* exactly under one of them. Reject such input before any filesystem call:
|
|
8
|
+
* even lstat on `\\host\share\x` makes Windows contact host.
|
|
9
|
+
*/
|
|
10
|
+
export declare function isForeignWindowsShareOrDevicePath(value: string, trustedPaths: readonly (string | undefined)[], platform?: NodeJS.Platform | string): boolean;
|
|
3
11
|
/**
|
|
4
12
|
* Capture an ordinary Windows drive-relative path without normalizing its raw
|
|
5
13
|
* suffix. This is only for public APIs whose existing contract accepts such
|
|
@@ -1,6 +1,28 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { FsSafeError } from "./errors.js";
|
|
3
|
-
import { hasWindowsDrivePrefix, rootedWindowsDriveColonIndex, windowsNamespaceMarker, } from "./windows-path-syntax.js";
|
|
3
|
+
import { hasWindowsDrivePrefix, rootedWindowsDriveColonIndex, windowsNamespaceMarker, windowsSegmentsClimbAbove, windowsShareOrDeviceRoot, } from "./windows-path-syntax.js";
|
|
4
|
+
function isExactlyUnderWindowsPath(trusted, value) {
|
|
5
|
+
const root = trusted.replaceAll("/", "\\").replace(/(?<=.)\\+$/, "");
|
|
6
|
+
const candidate = value.replaceAll("/", "\\");
|
|
7
|
+
if (candidate !== root && !candidate.startsWith(root.endsWith("\\") ? root : `${root}\\`))
|
|
8
|
+
return false;
|
|
9
|
+
return !windowsSegmentsClimbAbove(candidate.slice(root.length).split("\\"));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* True when a Windows path could reach a UNC share or device namespace that
|
|
13
|
+
* none of the trusted boundary paths live on: its share or device differs from
|
|
14
|
+
* theirs or cannot be identified from its spelling, and it is not spelled
|
|
15
|
+
* exactly under one of them. Reject such input before any filesystem call:
|
|
16
|
+
* even lstat on `\\host\share\x` makes Windows contact host.
|
|
17
|
+
*/
|
|
18
|
+
export function isForeignWindowsShareOrDevicePath(value, trustedPaths, platform = process.platform) {
|
|
19
|
+
if (platform !== "win32")
|
|
20
|
+
return false;
|
|
21
|
+
const key = windowsShareOrDeviceRoot(value);
|
|
22
|
+
if (key === undefined)
|
|
23
|
+
return false;
|
|
24
|
+
return !trustedPaths.some(trusted => trusted !== undefined && ((key !== null && windowsShareOrDeviceRoot(trusted) === key) || isExactlyUnderWindowsPath(trusted, value)));
|
|
25
|
+
}
|
|
4
26
|
function isBareWindowsNamespaceDrive(value) {
|
|
5
27
|
return value.length === 6 && windowsNamespaceMarker(value) !== undefined &&
|
|
6
28
|
hasWindowsDrivePrefix(value, 4);
|
|
@@ -106,11 +128,7 @@ export function assertNoWindowsPathAliasForPlatform(value, kind, message, platfo
|
|
|
106
128
|
}
|
|
107
129
|
}
|
|
108
130
|
export function assertNoWindowsPathAlias(value, kind = "filesystem", message = "path uses a Windows filesystem namespace alias", platform = process.platform) {
|
|
109
|
-
|
|
110
|
-
throw new FsSafeError("invalid-path", message, {
|
|
111
|
-
details: { reason: "windows-path-alias" },
|
|
112
|
-
});
|
|
113
|
-
}
|
|
131
|
+
assertNoWindowsPathAliasForPlatform(value, kind, message, platform);
|
|
114
132
|
}
|
|
115
133
|
export function isWindowsPathAliasError(error) {
|
|
116
134
|
return error instanceof FsSafeError && error.details?.reason === "windows-path-alias";
|
|
@@ -3,3 +3,16 @@ export declare function hasWindowsDrivePrefix(value: string, offset?: number): b
|
|
|
3
3
|
/** Classify a raw prefix without normalizing any path components. */
|
|
4
4
|
export declare function windowsNamespaceMarker(value: string): "." | "?" | undefined;
|
|
5
5
|
export declare function rootedWindowsDriveColonIndex(value: string): number;
|
|
6
|
+
/**
|
|
7
|
+
* True when dot-like components climb above the start of `segments`. Win32
|
|
8
|
+
* trims trailing dots and spaces, so any all-dot or all-space component other
|
|
9
|
+
* than `.` is counted as a parent step.
|
|
10
|
+
*/
|
|
11
|
+
export declare function windowsSegmentsClimbAbove(segments: readonly string[]): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* Comparable share or device root of a path spelled with two leading
|
|
14
|
+
* separators, excluding namespaced drive roots such as `\\?\C:\`.
|
|
15
|
+
* Returns undefined for drive, rooted, and relative spellings, and null when
|
|
16
|
+
* the spelling alone cannot establish which share or device it reaches.
|
|
17
|
+
*/
|
|
18
|
+
export declare function windowsShareOrDeviceRoot(value: string): string | null | undefined;
|