@openclaw/signal 2026.11.3 → 2026.11.5-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/{accounts-B0Pba1-q.mjs → accounts-enJKL3ci.mjs} +4 -8
- package/dist/.setup/api-CrasQEKI.mjs +14 -0
- package/dist/.setup/{api-CbN3mye4.mjs → api-DljZsfUn.mjs} +9 -11
- package/dist/.setup/{approval-auth-DhUKkSkA.mjs → approval-auth-B_5Ytx8K.mjs} +7 -11
- package/dist/.setup/{approval-handler.runtime-CsEcBZoc.mjs → approval-handler.runtime-0RbVmTxx.mjs} +7 -8
- package/dist/.setup/{approval-reactions-JsqCEOHK.mjs → approval-reactions-CvqVSSx8.mjs} +16 -24
- package/dist/.setup/{channel-CM-rLLPN.mjs → channel-ByJj8fSh.mjs} +417 -372
- package/dist/.setup/{client-Duwqbgqi.mjs → client-CesmEjRc.mjs} +170 -186
- package/dist/.setup/{config-schema-DvxZ18SD.mjs → config-schema-BCmW0BLB.mjs} +2 -5
- package/dist/.setup/{doctor-contract-api-BIdVW0PF.mjs → doctor-contract-api-CcLwIhsD.mjs} +88 -95
- package/dist/.setup/{identity-B3JyPqGO.mjs → identity-vKtR3DrM.mjs} +39 -2
- package/dist/.setup/{install-signal-cli-hYVLYGXe.mjs → install-signal-cli-DvLDZfpP.mjs} +95 -109
- package/dist/.setup/{transport-url-CAC0JcuI.mjs → legacy-transport-D_cZC3bS.mjs} +18 -1
- package/dist/.setup/{monitor-C6u1JV2C.mjs → monitor-Cf5sihwl.mjs} +166 -239
- package/dist/.setup/{probe-D_93QCe0.mjs → probe-Caq5Y4kA.mjs} +3 -3
- package/dist/.setup/{send-CM7-016e.mjs → send-BJuOWLky.mjs} +34 -66
- package/dist/.setup/{setup-surface-DvKLwYrl.mjs → setup-surface-DQ1yFZmY.mjs} +3 -3
- package/dist/.setup/{transport-detection-CmlgOybe.mjs → transport-detection-DvR6X_UQ.mjs} +3 -3
- package/dist/.setup/{transport-policy-B6C_LogW.mjs → transport-policy-zIR3lRUj.mjs} +1 -1
- package/dist/.setup/transport-probes.runtime-CoH6UFtc.mjs +1 -0
- package/dist/.source-module-urls/b02c526df8b90083105b63e5b51d5c8bbfe02676df72f6be8dd8f825817b32b9.json +1 -0
- package/dist/api.js +9 -11
- package/dist/channel-config-api.js +1 -2
- package/dist/channel-plugin-api.js +1 -2
- package/dist/contract-api.js +3 -4
- package/dist/doctor-contract-api.js +1 -2
- package/dist/runtime-api.js +1 -2
- package/dist/setup-entry.js +1 -1
- package/node_modules/@openclaw/fs-safe/CHANGELOG.md +94 -0
- package/node_modules/@openclaw/fs-safe/dist/archive-parser.wasm +0 -0
- 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 +3 -1
- package/node_modules/@openclaw/fs-safe/dist/copy-path-admission.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/copy-root-file-sync.js +8 -1
- package/node_modules/@openclaw/fs-safe/dist/file-open-error.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/file-open-error.js +14 -0
- package/node_modules/@openclaw/fs-safe/dist/file-sync.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/file-sync.js +6 -1
- package/node_modules/@openclaw/fs-safe/dist/guest-native-python.js +3 -0
- package/node_modules/@openclaw/fs-safe/dist/native-binding.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/native-config.js +14 -3
- package/node_modules/@openclaw/fs-safe/dist/native-operations.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/native-pinned-write-windows.js +9 -2
- package/node_modules/@openclaw/fs-safe/dist/native-staged-file.js +40 -32
- package/node_modules/@openclaw/fs-safe/dist/output.js +6 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/owner-dacl.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/path.js +5 -1
- package/node_modules/@openclaw/fs-safe/dist/pinned-write-staged.js +25 -2
- package/node_modules/@openclaw/fs-safe/dist/publication-handle.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/publication-handle.js +32 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/publish-copy-stage.js +19 -9
- package/node_modules/@openclaw/fs-safe/dist/publish-file.js +23 -24
- package/node_modules/@openclaw/fs-safe/dist/publish-rename-fallback.d.ts +21 -0
- package/node_modules/@openclaw/fs-safe/dist/publish-rename-fallback.js +102 -0
- package/node_modules/@openclaw/fs-safe/dist/root-create-native.js +2 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-entry.js +8 -5
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list-types.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.d.ts +2 -1
- package/node_modules/@openclaw/fs-safe/dist/root-directory-list.js +19 -9
- package/node_modules/@openclaw/fs-safe/dist/root-file-final-admission.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/root-path-existing.d.ts +2 -2
- package/node_modules/@openclaw/fs-safe/dist/root-path-existing.js +27 -13
- package/node_modules/@openclaw/fs-safe/dist/root-path.js +2 -2
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.d.ts +3 -0
- package/node_modules/@openclaw/fs-safe/dist/sibling-staged-file.js +18 -3
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/watch-hints.js +68 -21
- package/node_modules/@openclaw/fs-safe/dist/watch-io-error.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-io-error.js +27 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-io-worker.d.ts +1 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-io-worker.js +93 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-io.d.ts +47 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-io.js +236 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-native.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/watch-native.js +3 -3
- package/node_modules/@openclaw/fs-safe/dist/watch-probe.d.ts +16 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-probe.js +135 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/watch-rescan.js +86 -24
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.d.ts +4 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-scan.js +78 -16
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.d.ts +2 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-stream.js +23 -0
- package/node_modules/@openclaw/fs-safe/dist/watch-types.d.ts +3 -1
- package/node_modules/@openclaw/fs-safe/dist/watch.js +60 -13
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.d.ts +1 -1
- package/node_modules/@openclaw/fs-safe/dist/windows-security-facts.js +1 -0
- package/node_modules/@openclaw/fs-safe/dist/windows-security-helper.exe +0 -0
- package/node_modules/@openclaw/fs-safe/docs/advanced.md +5 -2
- package/node_modules/@openclaw/fs-safe/docs/compiler-qualification.md +392 -0
- package/node_modules/@openclaw/fs-safe/docs/config.md +7 -1
- package/node_modules/@openclaw/fs-safe/docs/contributing.md +25 -5
- package/node_modules/@openclaw/fs-safe/docs/copy.md +10 -0
- package/node_modules/@openclaw/fs-safe/docs/durability.md +9 -3
- package/node_modules/@openclaw/fs-safe/docs/errors.md +13 -0
- package/node_modules/@openclaw/fs-safe/docs/migrating-to-0.5.md +6 -4
- package/node_modules/@openclaw/fs-safe/docs/native-helper.md +6 -4
- package/node_modules/@openclaw/fs-safe/docs/output.md +4 -2
- package/node_modules/@openclaw/fs-safe/docs/path.md +5 -2
- package/node_modules/@openclaw/fs-safe/docs/permissions.md +15 -0
- package/node_modules/@openclaw/fs-safe/docs/security-model.md +9 -0
- package/node_modules/@openclaw/fs-safe/docs/staged-file.md +2 -0
- package/node_modules/@openclaw/fs-safe/docs/testing.md +1 -1
- package/node_modules/@openclaw/fs-safe/docs/watch.md +62 -5
- package/node_modules/@openclaw/fs-safe/docs/writing.md +6 -1
- package/node_modules/@openclaw/fs-safe/package.json +18 -16
- 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-freebsd-arm64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-arm64/package.json +1 -1
- package/node_modules/@openclaw/fs-safe-freebsd-x64/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-freebsd-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-arm64-msvc/fs-safe-native.node +0 -0
- package/node_modules/@openclaw/fs-safe-win32-arm64-msvc/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/node_modules/zod/package.json +1 -1
- package/node_modules/zod/src/v4/classic/tests/array.test.ts +27 -0
- package/node_modules/zod/src/v4/classic/tests/map.test.ts +7 -0
- package/node_modules/zod/src/v4/classic/tests/set.test.ts +6 -0
- package/node_modules/zod/src/v4/core/compile.ts +9 -0
- package/node_modules/zod/src/v4/core/schemas.ts +35 -1
- package/node_modules/zod/src/v4/core/util.ts +13 -0
- package/node_modules/zod/src/v4/core/versions.ts +1 -1
- package/node_modules/zod/v4/core/compile.cjs +8 -0
- package/node_modules/zod/v4/core/compile.js +8 -0
- package/node_modules/zod/v4/core/schemas.cjs +30 -0
- package/node_modules/zod/v4/core/schemas.d.cts +3 -1
- package/node_modules/zod/v4/core/schemas.d.ts +3 -1
- package/node_modules/zod/v4/core/schemas.js +30 -0
- package/node_modules/zod/v4/core/util.cjs +15 -0
- package/node_modules/zod/v4/core/util.d.cts +2 -0
- package/node_modules/zod/v4/core/util.d.ts +2 -0
- package/node_modules/zod/v4/core/util.js +14 -0
- package/node_modules/zod/v4/core/versions.cjs +1 -1
- package/node_modules/zod/v4/core/versions.js +1 -1
- package/openclaw.plugin.json +0 -30
- package/package.json +6 -8
- package/dist/.setup/api-CJBtD3lT.mjs +0 -16
- package/dist/.setup/client-adapter-Chnbch_z.mjs +0 -30
- package/dist/.setup/legacy-transport-QIpaMNST.mjs +0 -18
- package/dist/.setup/normalize-D7kSomXt.mjs +0 -39
- package/dist/.setup/reaction-runtime-api-mdO4k0yv.mjs +0 -65
- package/dist/.setup/reaction-targets-BgPLDHBf.mjs +0 -72
- package/dist/.setup/rpc-context-BAXcX7dI.mjs +0 -13
- package/dist/.setup/transport-probes.runtime-D-xIMRcn.mjs +0 -2
- package/dist/reaction-runtime-api.js +0 -2
- package/node_modules/ws/LICENSE +0 -20
- package/node_modules/ws/README.md +0 -548
- package/node_modules/ws/browser.js +0 -8
- package/node_modules/ws/index.js +0 -22
- package/node_modules/ws/lib/buffer-util.js +0 -131
- package/node_modules/ws/lib/constants.js +0 -19
- package/node_modules/ws/lib/event-target.js +0 -292
- package/node_modules/ws/lib/extension.js +0 -203
- package/node_modules/ws/lib/limiter.js +0 -55
- package/node_modules/ws/lib/permessage-deflate.js +0 -530
- package/node_modules/ws/lib/receiver.js +0 -743
- package/node_modules/ws/lib/sender.js +0 -607
- package/node_modules/ws/lib/stream.js +0 -161
- package/node_modules/ws/lib/subprotocol.js +0 -62
- package/node_modules/ws/lib/validation.js +0 -152
- package/node_modules/ws/lib/websocket-server.js +0 -562
- package/node_modules/ws/lib/websocket.js +0 -1421
- package/node_modules/ws/package.json +0 -74
- package/node_modules/ws/wrapper.mjs +0 -21
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { watchYield } from "./watch-io.js";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
const parent = (name) => path.dirname(name) === "." ? "" : path.dirname(name);
|
|
3
4
|
const below = (directory, name) => !directory || name.startsWith(directory + path.sep);
|
|
@@ -26,14 +27,32 @@ export function watchRescanScopes(scopes, snapshot, changes) {
|
|
|
26
27
|
];
|
|
27
28
|
}
|
|
28
29
|
/** Merge only freshly observed slices. Any topology/anchor ambiguity requires a full pass. */
|
|
29
|
-
export function mergeWatchRescan(scopes, before, slice, requested) {
|
|
30
|
-
|
|
30
|
+
export async function mergeWatchRescan(scopes, before, slice, requested) {
|
|
31
|
+
const checkpoint = watchYield();
|
|
32
|
+
const copy = async (entries) => {
|
|
33
|
+
const result = new Map();
|
|
34
|
+
for (const [key, value] of entries ?? []) {
|
|
35
|
+
result.set(key, value);
|
|
36
|
+
await checkpoint();
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
const combine = async (left, right) => {
|
|
41
|
+
const result = await copy(left);
|
|
42
|
+
for (const [key, value] of right ?? []) {
|
|
43
|
+
result.set(key, value);
|
|
44
|
+
await checkpoint();
|
|
45
|
+
}
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
if (before.overflow || before.structural?.size || slice.overflow || slice.structural?.size)
|
|
31
49
|
return;
|
|
32
50
|
// An exclusion or a blocked path may stop before a requested directory is listed.
|
|
33
51
|
for (const scope of requested)
|
|
34
52
|
if (scope.kind === "tree" && scope.depth > 0 && !slice.listed?.has(scope.path))
|
|
35
53
|
return;
|
|
36
54
|
for (const [name, current] of slice.directories) {
|
|
55
|
+
await checkpoint();
|
|
37
56
|
const prior = before.directories.get(name);
|
|
38
57
|
if (!prior || prior.dev !== current.dev || prior.ino !== current.ino)
|
|
39
58
|
return;
|
|
@@ -42,62 +61,105 @@ export function mergeWatchRescan(scopes, before, slice, requested) {
|
|
|
42
61
|
if (identity(before.entries.get(scope.path)) !== identity(slice.entries.get(scope.path)))
|
|
43
62
|
return;
|
|
44
63
|
// A missing/blocked intermediate scope must not retain old descendant registrations.
|
|
45
|
-
for (const name of before.directories.keys())
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
for (const name of before.directories.keys()) {
|
|
65
|
+
await checkpoint();
|
|
66
|
+
if ((name === scope.path || below(name, scope.path)) && !slice.directories.has(name) && name !== scope.path)
|
|
48
67
|
return;
|
|
68
|
+
}
|
|
49
69
|
}
|
|
50
70
|
const directories = slice.listed;
|
|
51
71
|
const selected = new Set(scopes.map(scope => scope.path));
|
|
52
72
|
for (const directory of directories.keys()) {
|
|
53
|
-
for (const name of before.childPaths?.get(directory)?.values() ?? [])
|
|
73
|
+
for (const name of before.childPaths?.get(directory)?.values() ?? []) {
|
|
54
74
|
selected.add(name);
|
|
55
|
-
|
|
75
|
+
await checkpoint();
|
|
76
|
+
}
|
|
77
|
+
for (const name of slice.childPaths?.get(directory)?.values() ?? []) {
|
|
56
78
|
selected.add(name);
|
|
79
|
+
await checkpoint();
|
|
80
|
+
}
|
|
57
81
|
}
|
|
58
82
|
for (const name of selected) {
|
|
83
|
+
await checkpoint();
|
|
59
84
|
const previous = before.entries.get(name), current = slice.entries.get(name);
|
|
60
85
|
if ((previous?.startsWith("directory:") || current?.startsWith("directory:")) && identity(previous) !== identity(current))
|
|
61
86
|
return;
|
|
62
87
|
}
|
|
63
88
|
// Exclusion changes can retire or admit a subtree, including callback-driven changes.
|
|
64
|
-
for (const [name, kind] of before.excluded ?? [])
|
|
89
|
+
for (const [name, kind] of before.excluded ?? []) {
|
|
90
|
+
await checkpoint();
|
|
65
91
|
if (selected.has(name) && kind === "directory" && slice.entries.has(name))
|
|
66
92
|
return;
|
|
67
|
-
|
|
68
|
-
|
|
93
|
+
}
|
|
94
|
+
const entries = await copy(before.entries);
|
|
95
|
+
for (const name of selected) {
|
|
96
|
+
await checkpoint();
|
|
69
97
|
if (!slice.entries.has(name))
|
|
70
98
|
entries.delete(name);
|
|
71
|
-
|
|
99
|
+
}
|
|
100
|
+
for (const [name, value] of slice.entries) {
|
|
101
|
+
await checkpoint();
|
|
72
102
|
if (selected.has(name))
|
|
73
103
|
entries.set(name, value);
|
|
74
|
-
|
|
104
|
+
}
|
|
105
|
+
const excluded = await copy(before.excluded), excludedDirectories = await copy(before.excludedDirectories);
|
|
75
106
|
for (const name of selected) {
|
|
107
|
+
await checkpoint();
|
|
76
108
|
excluded.delete(name);
|
|
77
109
|
excludedDirectories.delete(name);
|
|
78
110
|
}
|
|
79
|
-
for (const [name, kind] of slice.excluded ?? [])
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
111
|
+
for (const [name, kind] of slice.excluded ?? []) {
|
|
112
|
+
await checkpoint();
|
|
113
|
+
if (!selected.has(name))
|
|
114
|
+
continue;
|
|
115
|
+
excluded.set(name, kind);
|
|
116
|
+
const canonical = slice.excludedDirectories?.get(name);
|
|
117
|
+
if (canonical)
|
|
118
|
+
excludedDirectories.set(name, canonical);
|
|
119
|
+
}
|
|
120
|
+
const listed = await copy(before.listed);
|
|
87
121
|
let scanned = before.scanned;
|
|
88
122
|
for (const [name, count] of directories) {
|
|
123
|
+
await checkpoint();
|
|
89
124
|
scanned += count - (listed.get(name) ?? 0);
|
|
90
125
|
listed.set(name, count);
|
|
91
126
|
}
|
|
92
|
-
|
|
127
|
+
const observations = before.observations && slice.observations ? await copy(before.observations) : undefined;
|
|
128
|
+
if (observations) {
|
|
129
|
+
for (const name of selected) {
|
|
130
|
+
await checkpoint();
|
|
131
|
+
if (!slice.observations.has(name))
|
|
132
|
+
observations.delete(name);
|
|
133
|
+
}
|
|
134
|
+
for (const [name, observation] of slice.observations) {
|
|
135
|
+
await checkpoint();
|
|
136
|
+
// A parent's listing can observe a child's new timestamps without reading
|
|
137
|
+
// its children. Keep the old stamp until that directory is actually listed.
|
|
138
|
+
if (before.listed?.has(name) && !directories.has(name))
|
|
139
|
+
continue;
|
|
140
|
+
observations.set(name, observation);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const unsettled = new Set();
|
|
144
|
+
for (const name of before.unsettled ?? []) {
|
|
145
|
+
await checkpoint();
|
|
146
|
+
if (!directories.has(name))
|
|
147
|
+
unsettled.add(name);
|
|
148
|
+
}
|
|
149
|
+
for (const name of slice.unsettled ?? []) {
|
|
150
|
+
await checkpoint();
|
|
151
|
+
unsettled.add(name);
|
|
152
|
+
}
|
|
153
|
+
return { ...before, entries, excluded, excludedDirectories, listed, scanned, unsettled,
|
|
93
154
|
entryAnchors: slice.entryAnchors,
|
|
94
155
|
scopeAnchors: new Map(scopes.flatMap(scope => {
|
|
95
156
|
const anchor = slice.scopeAnchors?.get(scope.path);
|
|
96
157
|
return anchor ? [[scope.path, anchor]] : [];
|
|
97
158
|
})),
|
|
98
|
-
directoryPaths:
|
|
99
|
-
childPaths:
|
|
100
|
-
listingPaths:
|
|
159
|
+
directoryPaths: await combine(before.directoryPaths, slice.directoryPaths),
|
|
160
|
+
childPaths: await combine(before.childPaths, slice.childPaths),
|
|
161
|
+
listingPaths: await combine(before.listingPaths, slice.listingPaths),
|
|
162
|
+
observations,
|
|
101
163
|
targets: new Map(scopes.flatMap(scope => {
|
|
102
164
|
const target = slice.targets.get(scope.path);
|
|
103
165
|
return target ? [[scope.path, target]] : [];
|
|
@@ -2,6 +2,7 @@ import { type RootContext } from "./root-context.js";
|
|
|
2
2
|
import { type RootDirectoryObservationGuard } from "./root-directory-list.js";
|
|
3
3
|
import type { RootDirectoryListingPaths } from "./root-directory-list-types.js";
|
|
4
4
|
import type { WatchEntry, WatchOptions, WatchScope } from "./watch-types.js";
|
|
5
|
+
import { type WatchObservation } from "./watch-probe.js";
|
|
5
6
|
export type DirectoryIdentity = Readonly<{
|
|
6
7
|
dev: bigint;
|
|
7
8
|
ino: bigint;
|
|
@@ -28,6 +29,8 @@ export type WatchSnapshot = {
|
|
|
28
29
|
listed?: Map<string, number>;
|
|
29
30
|
childPaths?: Map<string, Map<string, string>>;
|
|
30
31
|
listingPaths?: Map<string, RootDirectoryListingPaths>;
|
|
32
|
+
observations?: Map<string, WatchObservation>;
|
|
33
|
+
unsettled?: Set<string>;
|
|
31
34
|
};
|
|
32
35
|
export declare function watchScopes(input: readonly WatchScope[]): readonly WatchScope[];
|
|
33
36
|
/** A descendant's unavailable metadata never grants it authority or retires the Root. */
|
|
@@ -38,4 +41,5 @@ export declare function scanWatch(root: RootContext, scopes: readonly WatchScope
|
|
|
38
41
|
maxPendingPaths: number;
|
|
39
42
|
admitting: boolean;
|
|
40
43
|
previous?: WatchSnapshot;
|
|
44
|
+
reuse?: Set<string>;
|
|
41
45
|
}, signal: AbortSignal, register: (name: string, identity: DirectoryIdentity, guard: RootDirectoryObservationGuard) => Promise<void>, onCleanupFailure?: (error: unknown) => void): Promise<WatchSnapshot>;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { watchRootCurrent as assertRootIdentityCurrent, watchResolve as resolvePathInRoot, watchGuard as createRootDirectoryObservationGuard, watchAssert as assertRootDirectoryObservationGuard, watchListing as openRootDirectoryListing, watchLookup as lookupRootDirectoryEntry, watchYield } from "./watch-io.js";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { isWindowsReservedDeviceName } from "./device-path.js";
|
|
3
4
|
import { isNotFoundPathError } from "./path.js";
|
|
4
5
|
import { FsSafeError } from "./errors.js";
|
|
5
6
|
import { assertSynchronousCallbackResult } from "./mutation-authority.js";
|
|
6
7
|
import { validatePinnedRelativePath } from "./pinned-operation.js";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
8
|
+
import { assertValidRootRelativePath } from "./root-context.js";
|
|
9
|
+
import { pathStatFromStats } from "./root-directory-list.js";
|
|
9
10
|
import { createSuppressedError } from "./suppressed-error.js";
|
|
10
|
-
import {
|
|
11
|
+
import { watchObservation } from "./watch-probe.js";
|
|
11
12
|
export function watchScopes(input) {
|
|
12
13
|
if (!Array.isArray(input) || input.length > 128)
|
|
13
14
|
throw new RangeError("watch accepts at most 128 scopes");
|
|
@@ -35,13 +36,13 @@ export function watchScopes(input) {
|
|
|
35
36
|
function kind(entry) {
|
|
36
37
|
return entry.isSymbolicLink ? "symlink" : entry.isDirectory ? "directory" : entry.isFile ? "file" : "other";
|
|
37
38
|
}
|
|
38
|
-
function fingerprint(entry, identity) {
|
|
39
|
+
function fingerprint(entry, identity, observation) {
|
|
39
40
|
const { dev, ino } = identity;
|
|
40
41
|
// Metadata is advisory; identity bits are never rounded through PathStat. Directory size/mtime
|
|
41
42
|
// reflect children, not changes to an entry-only scope.
|
|
42
43
|
return entry.isDirectory
|
|
43
44
|
? [kind(entry), dev, ino, entry.mode].join(":")
|
|
44
|
-
: [kind(entry), dev, ino, entry.size, entry.mtimeMs, entry.mode].join(":");
|
|
45
|
+
: [kind(entry), dev, ino, entry.size, entry.mtimeMs, entry.mode, entry.nlink, observation?.ctimeNs].join(":");
|
|
45
46
|
}
|
|
46
47
|
/** A descendant's unavailable metadata never grants it authority or retires the Root. */
|
|
47
48
|
export function isWatchPathError(error) {
|
|
@@ -52,21 +53,34 @@ export function isWatchPathError(error) {
|
|
|
52
53
|
return true;
|
|
53
54
|
return ["not-found", "path-mismatch", "not-file", "symlink", "outside-workspace", "path-alias"].includes(error.code);
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
const failure = error;
|
|
57
|
+
// Windows realpath can lose its temporary handle during directory removal.
|
|
58
|
+
if (failure?.code === "EBADF" && failure.syscall === "realpath")
|
|
59
|
+
return true;
|
|
60
|
+
return ["ENOTDIR", "EACCES", "EPERM", "EBUSY", "ESTALE", "EIO", "ELOOP"].includes(failure?.code ?? "");
|
|
56
61
|
}
|
|
57
62
|
export async function scanWatch(root, scopes, options, signal, register, onCleanupFailure) {
|
|
63
|
+
const checkpoint = watchYield();
|
|
58
64
|
const exclusions = new Map();
|
|
59
65
|
const excludedDirectories = new Map(), directoryPaths = new Map();
|
|
60
66
|
const entryAnchors = new Map();
|
|
61
67
|
const scopeAnchors = new Map();
|
|
62
68
|
const listed = new Map(), childPaths = new Map(), listingPaths = new Map();
|
|
63
|
-
const
|
|
69
|
+
const observations = new Map();
|
|
70
|
+
const unsettled = new Set();
|
|
71
|
+
const record = (name, observation, directory) => {
|
|
72
|
+
observations.set(name, watchObservation(observation));
|
|
73
|
+
const settledBefore = BigInt(Date.now() - 1000) * 1000000n;
|
|
74
|
+
if (observation.mtimeNs > settledBefore || observation.ctimeNs > settledBefore)
|
|
75
|
+
unsettled.add(directory);
|
|
76
|
+
};
|
|
77
|
+
const result = { entries: new Map(), entryAnchors, scopeAnchors, excluded: exclusions, excludedDirectories, directoryPaths, directories: new Map(), targets: new Map(), scanned: 0, listed, childPaths, listingPaths, observations, unsettled };
|
|
64
78
|
const attempts = new Map();
|
|
65
79
|
const guards = new Map();
|
|
66
80
|
const walked = new Map();
|
|
67
81
|
const structural = new Set();
|
|
68
82
|
result.structural = structural;
|
|
69
|
-
const invalidate = (relative) => {
|
|
83
|
+
const invalidate = async (relative) => {
|
|
70
84
|
if (structural.size < options.maxPendingPaths)
|
|
71
85
|
structural.add(relative);
|
|
72
86
|
else
|
|
@@ -77,10 +91,12 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
77
91
|
for (const [scope, anchor] of anchors)
|
|
78
92
|
if (below(anchor.directory))
|
|
79
93
|
anchors.delete(scope);
|
|
80
|
-
for (const map of [result.entries, exclusions, excludedDirectories, directoryPaths, result.targets, result.directories, guards, walked, listed, childPaths, listingPaths]) {
|
|
81
|
-
for (const name of map.keys())
|
|
94
|
+
for (const map of [result.entries, exclusions, excludedDirectories, directoryPaths, result.targets, result.directories, guards, walked, listed, childPaths, listingPaths, observations, unsettled]) {
|
|
95
|
+
for (const name of map.keys()) {
|
|
96
|
+
await checkpoint();
|
|
82
97
|
if (below(name))
|
|
83
98
|
map.delete(name);
|
|
99
|
+
}
|
|
84
100
|
}
|
|
85
101
|
};
|
|
86
102
|
const recover = async (relative, error) => {
|
|
@@ -89,7 +105,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
89
105
|
const code = error instanceof FsSafeError ? error.details?.code ?? error.code : error?.code;
|
|
90
106
|
if (!isWatchPathError(error) || (!relative && (code === "EACCES" || code === "EPERM")))
|
|
91
107
|
throw error;
|
|
92
|
-
invalidate(relative);
|
|
108
|
+
await invalidate(relative);
|
|
93
109
|
};
|
|
94
110
|
const examined = () => {
|
|
95
111
|
if (++result.scanned > options.maxEntries)
|
|
@@ -147,6 +163,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
147
163
|
const guard = await createRootDirectoryObservationGuard(root, resolved.resolved);
|
|
148
164
|
const identity = { dev: guard.stat.dev, ino: guard.stat.ino };
|
|
149
165
|
result.directories.set(relative, identity);
|
|
166
|
+
record(relative, guard.stat, relative);
|
|
150
167
|
directoryPaths.set(relative, guard.realPath);
|
|
151
168
|
await register(relative, identity, guard);
|
|
152
169
|
signal.throwIfAborted();
|
|
@@ -166,9 +183,44 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
166
183
|
throw new FsSafeError("path-mismatch", "watch directory changed during registration");
|
|
167
184
|
};
|
|
168
185
|
const tree = async (relative, depth) => {
|
|
186
|
+
await checkpoint();
|
|
169
187
|
if ((walked.get(relative) ?? 0) >= depth)
|
|
170
188
|
return;
|
|
171
189
|
walked.set(relative, depth);
|
|
190
|
+
const previous = options.previous;
|
|
191
|
+
if (options.reuse?.has(relative) && previous?.childPaths?.has(relative)) {
|
|
192
|
+
// Copy only a previously guarded listing, never metadata obtained by the probe.
|
|
193
|
+
const names = previous.childPaths.get(relative);
|
|
194
|
+
childPaths.set(relative, names);
|
|
195
|
+
listed.set(relative, previous.listed.get(relative));
|
|
196
|
+
listingPaths.set(relative, previous.listingPaths.get(relative));
|
|
197
|
+
result.directories.set(relative, previous.directories.get(relative));
|
|
198
|
+
directoryPaths.set(relative, previous.directoryPaths.get(relative));
|
|
199
|
+
observations.set(relative, previous.observations.get(relative));
|
|
200
|
+
if (previous.unsettled?.has(relative))
|
|
201
|
+
unsettled.add(relative);
|
|
202
|
+
result.scanned += previous.listed.get(relative);
|
|
203
|
+
for (const name of names.values()) {
|
|
204
|
+
await checkpoint();
|
|
205
|
+
const observation = previous.observations?.get(name);
|
|
206
|
+
if (observation)
|
|
207
|
+
observations.set(name, observation);
|
|
208
|
+
const excludedKind = previous.excluded?.get(name);
|
|
209
|
+
if (excludedKind) {
|
|
210
|
+
exclusions.set(name, excludedKind);
|
|
211
|
+
const canonical = previous.excludedDirectories?.get(name);
|
|
212
|
+
if (canonical)
|
|
213
|
+
excludedDirectories.set(name, canonical);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
const entry = previous.entries.get(name);
|
|
217
|
+
if (entry)
|
|
218
|
+
result.entries.set(name, entry);
|
|
219
|
+
if (entry?.startsWith("directory:") && depth > 1)
|
|
220
|
+
await tree(name, depth - 1);
|
|
221
|
+
}
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
172
224
|
let guard;
|
|
173
225
|
let listing;
|
|
174
226
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
@@ -179,7 +231,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
179
231
|
order: "filesystem", snapshot: false, signal, exactIdentity: true, skipVanished: true, onCleanupFailure,
|
|
180
232
|
previousPaths: options.previous?.listingPaths?.get(relative),
|
|
181
233
|
admitEntry: () => { examined(); listed.set(relative, (listed.get(relative) ?? 0) + 1); return true; },
|
|
182
|
-
});
|
|
234
|
+
}, () => options.maxEntries - result.scanned);
|
|
183
235
|
// The listing has its own guard. Both identities must agree before reading names.
|
|
184
236
|
await assertRootDirectoryObservationGuard(root, guard);
|
|
185
237
|
break;
|
|
@@ -200,6 +252,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
200
252
|
let operationError;
|
|
201
253
|
try {
|
|
202
254
|
while (true) {
|
|
255
|
+
await checkpoint();
|
|
203
256
|
const next = await listing.next();
|
|
204
257
|
signal.throwIfAborted();
|
|
205
258
|
if (!next)
|
|
@@ -211,9 +264,11 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
211
264
|
const entry = next.entry;
|
|
212
265
|
const name = previousNames?.get(entry.name) ?? (relative ? path.join(relative, entry.name) : entry.name);
|
|
213
266
|
names.set(entry.name, name);
|
|
267
|
+
if (next.observation)
|
|
268
|
+
record(name, next.observation, relative);
|
|
214
269
|
if (await excluded(name, entry))
|
|
215
270
|
continue;
|
|
216
|
-
result.entries.set(name, fingerprint(entry, next.identity));
|
|
271
|
+
result.entries.set(name, fingerprint(entry, next.identity, next.observation));
|
|
217
272
|
if (entry.isDirectory && !entry.isSymbolicLink && depth > 1)
|
|
218
273
|
await tree(name, depth - 1);
|
|
219
274
|
}
|
|
@@ -266,16 +321,18 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
266
321
|
examined();
|
|
267
322
|
// Filesystem lookup, not lowercase/prefix matching, owns case, Unicode and
|
|
268
323
|
// short-name aliases. It also preserves case-sensitive Windows directories.
|
|
269
|
-
const found = await lookupRootDirectoryEntry(root, guard, segments[i]);
|
|
324
|
+
const found = await lookupRootDirectoryEntry(root, guard, segments[i], true);
|
|
270
325
|
signal.throwIfAborted();
|
|
271
326
|
if (!found)
|
|
272
327
|
break;
|
|
273
328
|
const name = relative ? path.join(relative, segments[i]) : segments[i];
|
|
329
|
+
if (found.observation)
|
|
330
|
+
record(name, found.observation, relative);
|
|
274
331
|
if (await excluded(name, found.entry))
|
|
275
332
|
break;
|
|
276
333
|
anchor.target = { ...found.identity, kind: kind(found.entry) };
|
|
277
334
|
if (i === segments.length - 1) {
|
|
278
|
-
result.entries.set(name, fingerprint(found.entry, found.identity));
|
|
335
|
+
result.entries.set(name, fingerprint(found.entry, found.identity, found.observation));
|
|
279
336
|
result.targets.set(name, found.identity);
|
|
280
337
|
if (scope.kind === "tree" && found.entry.isDirectory && !found.entry.isSymbolicLink && scope.depth > 0)
|
|
281
338
|
await tree(name, scope.depth);
|
|
@@ -284,7 +341,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
284
341
|
if (found.entry.isSymbolicLink) {
|
|
285
342
|
if (options.admitting)
|
|
286
343
|
throw new FsSafeError("symlink", "watch scope traverses a symbolic link; admit its target separately", { details: { operation: "scope" } });
|
|
287
|
-
invalidate(name);
|
|
344
|
+
await invalidate(name);
|
|
288
345
|
break;
|
|
289
346
|
}
|
|
290
347
|
if (!found.entry.isDirectory)
|
|
@@ -299,6 +356,7 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
299
356
|
}
|
|
300
357
|
}
|
|
301
358
|
for (const [relative, guard] of guards) {
|
|
359
|
+
await checkpoint();
|
|
302
360
|
try {
|
|
303
361
|
await assertRootDirectoryObservationGuard(root, guard);
|
|
304
362
|
}
|
|
@@ -308,9 +366,13 @@ export async function scanWatch(root, scopes, options, signal, register, onClean
|
|
|
308
366
|
}
|
|
309
367
|
await assertRootIdentityCurrent(root);
|
|
310
368
|
signal.throwIfAborted();
|
|
369
|
+
if (result.directories.size > options.maxDirectories || result.scanned > options.maxEntries) {
|
|
370
|
+
throw new FsSafeError("too-large", "watch observation budget exceeded", { details: { operation: "scan" } });
|
|
371
|
+
}
|
|
311
372
|
// Native deletion hints can arrive after a later scan. Keep bounded tombstones
|
|
312
373
|
// until the name is admitted again; they never grant authority to publish it.
|
|
313
374
|
for (const [name, kind] of options.previous?.excluded ?? []) {
|
|
375
|
+
await checkpoint();
|
|
314
376
|
if (exclusions.size >= options.maxEntries)
|
|
315
377
|
break;
|
|
316
378
|
if (!result.entries.has(name) && !result.directories.has(name) && !exclusions.has(name))
|
|
@@ -6,3 +6,5 @@ export type WatchStreamPaths = {
|
|
|
6
6
|
};
|
|
7
7
|
/** Canonical names come only from guarded directory observations, never backend hints. */
|
|
8
8
|
export declare function watchStreamPaths(snapshot: WatchSnapshot, scopes: readonly WatchScope[]): WatchStreamPaths;
|
|
9
|
+
/** Avoid a tree-sized sort on the host when many directories are excluded. */
|
|
10
|
+
export declare function watchStreamPathsAsync(snapshot: WatchSnapshot, scopes: readonly WatchScope[]): Promise<WatchStreamPaths>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { watchYield } from "./watch-io.js";
|
|
2
3
|
function shallowest(paths, limit) {
|
|
3
4
|
const result = [];
|
|
4
5
|
const sorted = [...new Set(paths)].sort((a, b) => a.split(path.sep).length - b.split(path.sep).length || a.localeCompare(b));
|
|
@@ -32,3 +33,25 @@ export function watchStreamPaths(snapshot, scopes) {
|
|
|
32
33
|
}
|
|
33
34
|
return { anchors: shallowest(anchors, 128), exclusions: shallowest(snapshot.excludedDirectories?.values() ?? [], 8) };
|
|
34
35
|
}
|
|
36
|
+
/** Avoid a tree-sized sort on the host when many directories are excluded. */
|
|
37
|
+
export async function watchStreamPathsAsync(snapshot, scopes) {
|
|
38
|
+
const result = watchStreamPaths({ ...snapshot, excludedDirectories: undefined }, scopes);
|
|
39
|
+
const checkpoint = watchYield();
|
|
40
|
+
for (let n = 0; n < 8; n++) {
|
|
41
|
+
let selected, depth = Infinity;
|
|
42
|
+
for (const name of snapshot.excludedDirectories?.values() ?? []) {
|
|
43
|
+
await checkpoint();
|
|
44
|
+
if (result.exclusions.some(parent => name === parent || name.startsWith(parent.endsWith(path.sep) ? parent : parent + path.sep)))
|
|
45
|
+
continue;
|
|
46
|
+
const nextDepth = name.split(path.sep).length;
|
|
47
|
+
if (nextDepth < depth || (nextDepth === depth && name.localeCompare(selected) < 0)) {
|
|
48
|
+
selected = name;
|
|
49
|
+
depth = nextDepth;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (selected === undefined)
|
|
53
|
+
break;
|
|
54
|
+
result.exclusions.push(selected);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
@@ -35,8 +35,10 @@ export type WatchOptions = {
|
|
|
35
35
|
mode: "auto" | "events" | "poll";
|
|
36
36
|
/** Keep the Node event loop alive while open. Defaults to true. */
|
|
37
37
|
persistent?: boolean;
|
|
38
|
-
/**
|
|
38
|
+
/** Incremental metadata check interval: events 30000, poll 1000; minimum 20 ms. */
|
|
39
39
|
intervalMs?: number;
|
|
40
|
+
/** Full guarded safety-net reconciliation interval. Defaults to 300000 ms; minimum 20 ms. */
|
|
41
|
+
fullReconcileIntervalMs?: number;
|
|
40
42
|
/** Polling transport interval; overrides intervalMs in poll mode or auto fallback. Minimum 20 ms. */
|
|
41
43
|
pollIntervalMs?: number;
|
|
42
44
|
exclude?: (entry: WatchEntry) => boolean;
|