@harperfast/harper 5.2.0-beta.4 → 5.2.1
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/bin/backup.ts +169 -0
- package/bin/cliOperations.ts +92 -73
- package/bin/copyDb.ts +21 -4
- package/bin/harper.ts +43 -55
- package/bin/help.ts +216 -0
- package/components/Application.ts +236 -46
- package/components/ApplicationScope.ts +26 -0
- package/components/EntryHandler.ts +410 -105
- package/components/RuntimeModuleTracker.ts +189 -0
- package/components/Scope.ts +120 -30
- package/components/componentLoader.ts +177 -25
- package/components/deployLifecycle.ts +119 -33
- package/components/mcp/toolRegistry.ts +10 -0
- package/components/mcp/tools/application.ts +12 -5
- package/components/mcp/tools/operations.ts +9 -0
- package/components/mcp/tools/schemas/operationDescriptions.ts +3 -0
- package/components/mcp/tools/schemas/operations.ts +9 -0
- package/components/operations.js +5 -6
- package/components/operationsValidation.js +32 -2
- package/components/scopeMount.ts +150 -0
- package/config/configUtils.ts +12 -9
- package/config-root.schema.json +14 -0
- package/dataLayer/backupManifest.ts +102 -0
- package/dataLayer/blobBackup.ts +286 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +69 -5
- package/dataLayer/hdbInfoController.ts +8 -0
- package/dataLayer/restoreMarker.ts +276 -0
- package/dataLayer/rocksdbBackup.ts +1100 -0
- package/dataLayer/schemaDescribe.ts +2 -1
- package/dist/bin/backup.d.ts +9 -0
- package/dist/bin/backup.js +192 -0
- package/dist/bin/backup.js.map +1 -0
- package/dist/bin/cliOperations.d.ts +13 -0
- package/dist/bin/cliOperations.js +89 -70
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/copyDb.js +13 -2
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/harper.d.ts +13 -0
- package/dist/bin/harper.js +45 -56
- package/dist/bin/harper.js.map +1 -1
- package/dist/bin/help.d.ts +8 -0
- package/dist/bin/help.js +192 -0
- package/dist/bin/help.js.map +1 -0
- package/dist/components/Application.d.ts +16 -1
- package/dist/components/Application.js +210 -38
- package/dist/components/Application.js.map +1 -1
- package/dist/components/ApplicationScope.d.ts +7 -0
- package/dist/components/ApplicationScope.js +22 -0
- package/dist/components/ApplicationScope.js.map +1 -1
- package/dist/components/EntryHandler.d.ts +4 -4
- package/dist/components/EntryHandler.js +386 -95
- package/dist/components/EntryHandler.js.map +1 -1
- package/dist/components/RuntimeModuleTracker.d.ts +11 -0
- package/dist/components/RuntimeModuleTracker.js +189 -0
- package/dist/components/RuntimeModuleTracker.js.map +1 -0
- package/dist/components/Scope.d.ts +38 -1
- package/dist/components/Scope.js +117 -28
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.d.ts +2 -6
- package/dist/components/componentLoader.js +160 -18
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deployLifecycle.d.ts +6 -2
- package/dist/components/deployLifecycle.js +109 -31
- package/dist/components/deployLifecycle.js.map +1 -1
- package/dist/components/mcp/toolRegistry.d.ts +7 -0
- package/dist/components/mcp/toolRegistry.js +10 -0
- package/dist/components/mcp/toolRegistry.js.map +1 -1
- package/dist/components/mcp/tools/application.js +11 -5
- package/dist/components/mcp/tools/application.js.map +1 -1
- package/dist/components/mcp/tools/operations.js +9 -0
- package/dist/components/mcp/tools/operations.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operationDescriptions.js +2 -0
- package/dist/components/mcp/tools/schemas/operationDescriptions.js.map +1 -1
- package/dist/components/mcp/tools/schemas/operations.js +9 -0
- package/dist/components/mcp/tools/schemas/operations.js.map +1 -1
- package/dist/components/operations.js +6 -6
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +34 -2
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/scopeMount.d.ts +86 -0
- package/dist/components/scopeMount.js +131 -0
- package/dist/components/scopeMount.js.map +1 -0
- package/dist/config/configUtils.js +13 -9
- package/dist/config/configUtils.js.map +1 -1
- package/dist/dataLayer/backupManifest.d.ts +26 -0
- package/dist/dataLayer/backupManifest.js +97 -0
- package/dist/dataLayer/backupManifest.js.map +1 -0
- package/dist/dataLayer/blobBackup.d.ts +87 -0
- package/dist/dataLayer/blobBackup.js +282 -0
- package/dist/dataLayer/blobBackup.js.map +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.d.ts +3 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +45 -5
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/dataLayer/hdbInfoController.js +4 -0
- package/dist/dataLayer/hdbInfoController.js.map +1 -1
- package/dist/dataLayer/restoreMarker.d.ts +122 -0
- package/dist/dataLayer/restoreMarker.js +261 -0
- package/dist/dataLayer/restoreMarker.js.map +1 -0
- package/dist/dataLayer/rocksdbBackup.d.ts +127 -0
- package/dist/dataLayer/rocksdbBackup.js +1039 -0
- package/dist/dataLayer/rocksdbBackup.js.map +1 -0
- package/dist/dataLayer/schemaDescribe.js +2 -1
- package/dist/dataLayer/schemaDescribe.js.map +1 -1
- package/dist/resources/DatabaseTransaction.d.ts +55 -0
- package/dist/resources/DatabaseTransaction.js +282 -109
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -5
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/Resources.js +22 -4
- package/dist/resources/Resources.js.map +1 -1
- package/dist/resources/Table.d.ts +6 -5
- package/dist/resources/Table.js +114 -37
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +6 -6
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/blob.d.ts +8 -1
- package/dist/resources/blob.js +31 -14
- package/dist/resources/blob.js.map +1 -1
- package/dist/resources/databases.d.ts +59 -1
- package/dist/resources/databases.js +386 -41
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/jsResource.d.ts +4 -26
- package/dist/resources/jsResource.js +5 -59
- package/dist/resources/jsResource.js.map +1 -1
- package/dist/resources/models/Models.d.ts +11 -1
- package/dist/resources/models/Models.js +10 -1
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +9 -0
- package/dist/resources/models/backendRegistry.js +10 -0
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/openaiStream.d.ts +16 -1
- package/dist/resources/models/openaiStream.js +113 -21
- package/dist/resources/models/openaiStream.js.map +1 -1
- package/dist/resources/models/v1/chatCompletions.d.ts +54 -0
- package/dist/resources/models/v1/chatCompletions.js +115 -0
- package/dist/resources/models/v1/chatCompletions.js.map +1 -0
- package/dist/resources/models/v1/embeddings.d.ts +11 -0
- package/dist/resources/models/v1/embeddings.js +71 -0
- package/dist/resources/models/v1/embeddings.js.map +1 -0
- package/dist/resources/models/v1/errors.d.ts +54 -0
- package/dist/resources/models/v1/errors.js +130 -0
- package/dist/resources/models/v1/errors.js.map +1 -0
- package/dist/resources/models/v1/index.d.ts +36 -0
- package/dist/resources/models/v1/index.js +75 -0
- package/dist/resources/models/v1/index.js.map +1 -0
- package/dist/resources/models/v1/models.d.ts +26 -0
- package/dist/resources/models/v1/models.js +44 -0
- package/dist/resources/models/v1/models.js.map +1 -0
- package/dist/resources/models/v1/translation.d.ts +133 -0
- package/dist/resources/models/v1/translation.js +298 -0
- package/dist/resources/models/v1/translation.js.map +1 -0
- package/dist/resources/roles.d.ts +1 -1
- package/dist/resources/roles.js +54 -7
- package/dist/resources/roles.js.map +1 -1
- package/dist/resources/transaction.js +0 -3
- package/dist/resources/transaction.js.map +1 -1
- package/dist/security/jsLoader.js +84 -33
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/security/role.js +4 -0
- package/dist/security/role.js.map +1 -1
- package/dist/security/superUserGuard.d.ts +7 -0
- package/dist/security/superUserGuard.js +23 -0
- package/dist/security/superUserGuard.js.map +1 -0
- package/dist/security/tokenAuthentication.d.ts +0 -1
- package/dist/security/tokenAuthentication.js +6 -2
- package/dist/security/tokenAuthentication.js.map +1 -1
- package/dist/security/user.d.ts +6 -1
- package/dist/security/user.js +23 -1
- package/dist/security/user.js.map +1 -1
- package/dist/server/REST.js +25 -9
- package/dist/server/REST.js.map +1 -1
- package/dist/server/fastifyRoutes.js +15 -1
- package/dist/server/fastifyRoutes.js.map +1 -1
- package/dist/server/http.d.ts +23 -2
- package/dist/server/http.js +99 -12
- package/dist/server/http.js.map +1 -1
- package/dist/server/itc/serverHandlers.js +7 -1
- package/dist/server/itc/serverHandlers.js.map +1 -1
- package/dist/server/jobs/jobProcess.js +20 -1
- package/dist/server/jobs/jobProcess.js.map +1 -1
- package/dist/server/jobs/jobRunner.js +10 -0
- package/dist/server/jobs/jobRunner.js.map +1 -1
- package/dist/server/jobs/jobs.js +11 -0
- package/dist/server/jobs/jobs.js.map +1 -1
- package/dist/server/middlewareChain.d.ts +10 -1
- package/dist/server/middlewareChain.js +81 -21
- package/dist/server/middlewareChain.js.map +1 -1
- package/dist/server/operationsServer.d.ts +0 -2
- package/dist/server/operationsServer.js.map +1 -1
- package/dist/server/serverHelpers/multipartParser.js +9 -0
- package/dist/server/serverHelpers/multipartParser.js.map +1 -1
- package/dist/server/serverHelpers/operationAuthorizationState.d.ts +2 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js +13 -0
- package/dist/server/serverHelpers/operationAuthorizationState.js.map +1 -0
- package/dist/server/serverHelpers/registeredOperations.d.ts +3 -2
- package/dist/server/serverHelpers/registeredOperations.js +14 -13
- package/dist/server/serverHelpers/registeredOperations.js.map +1 -1
- package/dist/server/serverHelpers/serverHandlers.js +27 -7
- package/dist/server/serverHelpers/serverHandlers.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.d.ts +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +21 -7
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/static.js +68 -21
- package/dist/server/static.js.map +1 -1
- package/dist/server/storageReclamation.d.ts +17 -0
- package/dist/server/storageReclamation.js +90 -6
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/socketRouter.js +20 -0
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +12 -0
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/sqlEngine/diff/differential.js +7 -3
- package/dist/sqlEngine/diff/differential.js.map +1 -1
- package/dist/sqlTranslator/index.js +6 -1
- package/dist/sqlTranslator/index.js.map +1 -1
- package/dist/upgrade/upgradePrompt.d.ts +2 -2
- package/dist/upgrade/upgradePrompt.js +22 -3
- package/dist/upgrade/upgradePrompt.js.map +1 -1
- package/dist/utility/OperationFunctionCaller.js +25 -6
- package/dist/utility/OperationFunctionCaller.js.map +1 -1
- package/dist/utility/errors/commonErrors.d.ts +1 -0
- package/dist/utility/errors/commonErrors.js +1 -0
- package/dist/utility/errors/commonErrors.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +13 -1
- package/dist/utility/hdbTerms.js +13 -1
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/install/installer.js +32 -0
- package/dist/utility/install/installer.js.map +1 -1
- package/dist/utility/logging/harper_logger.d.ts +47 -0
- package/dist/utility/logging/harper_logger.js +803 -38
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/operation_authorization.js +13 -3
- package/dist/utility/operation_authorization.js.map +1 -1
- package/npm-shrinkwrap.json +198 -193
- package/package.json +12 -7
- package/resources/DESIGN.md +2 -0
- package/resources/DatabaseTransaction.ts +310 -104
- package/resources/ResourceInterface.ts +8 -5
- package/resources/Resources.ts +22 -4
- package/resources/Table.ts +341 -263
- package/resources/analytics/write.ts +22 -20
- package/resources/blob.ts +31 -14
- package/resources/databases.ts +387 -46
- package/resources/jsResource.ts +5 -62
- package/resources/models/Models.ts +14 -1
- package/resources/models/backendRegistry.ts +10 -0
- package/resources/models/openaiStream.ts +131 -19
- package/resources/models/v1/chatCompletions.ts +128 -0
- package/resources/models/v1/embeddings.ts +70 -0
- package/resources/models/v1/errors.ts +141 -0
- package/resources/models/v1/index.ts +72 -0
- package/resources/models/v1/models.ts +53 -0
- package/resources/models/v1/translation.ts +362 -0
- package/resources/roles.ts +67 -7
- package/resources/transaction.ts +0 -3
- package/security/jsLoader.ts +84 -30
- package/security/role.ts +7 -0
- package/security/superUserGuard.ts +20 -0
- package/security/tokenAuthentication.ts +6 -3
- package/security/user.ts +26 -1
- package/server/DESIGN.md +61 -34
- package/server/REST.ts +25 -9
- package/server/fastifyRoutes.ts +20 -1
- package/server/http.ts +100 -13
- package/server/itc/serverHandlers.js +7 -1
- package/server/jobs/jobProcess.ts +18 -1
- package/server/jobs/jobRunner.ts +10 -0
- package/server/jobs/jobs.ts +11 -0
- package/server/middlewareChain.ts +79 -20
- package/server/operationsServer.ts +0 -2
- package/server/serverHelpers/multipartParser.ts +9 -0
- package/server/serverHelpers/operationAuthorizationState.ts +11 -0
- package/server/serverHelpers/registeredOperations.ts +19 -15
- package/server/serverHelpers/serverHandlers.js +28 -7
- package/server/serverHelpers/serverUtilities.ts +29 -7
- package/server/static.ts +87 -23
- package/server/storageReclamation.ts +104 -8
- package/server/threads/socketRouter.ts +20 -0
- package/server/threads/threadServer.js +11 -0
- package/sqlTranslator/index.ts +6 -1
- package/static/defaultConfig.yaml +3 -0
- package/studio/web/assets/{Chat-DK3GlWEb.js → Chat-aApwhRmz.js} +2 -2
- package/studio/web/assets/{Chat-DK3GlWEb.js.map → Chat-aApwhRmz.js.map} +1 -1
- package/studio/web/assets/{FloatingChat-fBcC1Ew_.js → FloatingChat-sC0H91n9.js} +4 -4
- package/studio/web/assets/{FloatingChat-fBcC1Ew_.js.map → FloatingChat-sC0H91n9.js.map} +1 -1
- package/studio/web/assets/{apiToken-DJo1nakA.js → apiToken-DZ8JhHOI.js} +2 -2
- package/studio/web/assets/{apiToken-DJo1nakA.js.map → apiToken-DZ8JhHOI.js.map} +1 -1
- package/studio/web/assets/{applications-BDfH8urd.js → applications-9SEVLIO5.js} +2 -2
- package/studio/web/assets/{applications-BDfH8urd.js.map → applications-9SEVLIO5.js.map} +1 -1
- package/studio/web/assets/{index-BHo3c2Gk.js → index-Dy3uDGXb.js} +6 -6
- package/studio/web/assets/index-Dy3uDGXb.js.map +1 -0
- package/studio/web/assets/{index.lazy-BTo0y6UM.js → index.lazy-CN1zq4I4.js} +4 -4
- package/studio/web/assets/{index.lazy-BTo0y6UM.js.map → index.lazy-CN1zq4I4.js.map} +1 -1
- package/studio/web/assets/{notifications-CMxvWNnz.js → notifications-BFAF07xr.js} +2 -2
- package/studio/web/assets/{notifications-CMxvWNnz.js.map → notifications-BFAF07xr.js.map} +1 -1
- package/studio/web/assets/{notifications-D3GoB26g.js → notifications-CKlYVvVN.js} +2 -2
- package/studio/web/assets/{notifications-D3GoB26g.js.map → notifications-CKlYVvVN.js.map} +1 -1
- package/studio/web/assets/{profile-Doj5FVDE.js → profile-A1zhEdFG.js} +2 -2
- package/studio/web/assets/{profile-Doj5FVDE.js.map → profile-A1zhEdFG.js.map} +1 -1
- package/studio/web/assets/{setComponentFile-yinsqJy0.js → setComponentFile-SEtBt_GV.js} +2 -2
- package/studio/web/assets/{setComponentFile-yinsqJy0.js.map → setComponentFile-SEtBt_GV.js.map} +1 -1
- package/studio/web/assets/{setup-DJwR0BHd.js → setup-BisINqdH.js} +2 -2
- package/studio/web/assets/{setup-DJwR0BHd.js.map → setup-BisINqdH.js.map} +1 -1
- package/studio/web/assets/{status-Br_AbsJs.js → status-BuQoCc7l.js} +2 -2
- package/studio/web/assets/{status-Br_AbsJs.js.map → status-BuQoCc7l.js.map} +1 -1
- package/studio/web/assets/{swagger-ui-react-02XH5sVf.js → swagger-ui-react-CAi_s1PC.js} +2 -2
- package/studio/web/assets/{swagger-ui-react-02XH5sVf.js.map → swagger-ui-react-CAi_s1PC.js.map} +1 -1
- package/studio/web/assets/{tsMode-D1DMKY7h.js → tsMode-DT74tlkM.js} +2 -2
- package/studio/web/assets/{tsMode-D1DMKY7h.js.map → tsMode-DT74tlkM.js.map} +1 -1
- package/studio/web/assets/{useEntityRestURL-DtRblRgw.js → useEntityRestURL-CGRGc1n7.js} +2 -2
- package/studio/web/assets/{useEntityRestURL-DtRblRgw.js.map → useEntityRestURL-CGRGc1n7.js.map} +1 -1
- package/studio/web/index.html +1 -1
- package/upgrade/upgradePrompt.ts +22 -3
- package/utility/OperationFunctionCaller.ts +24 -3
- package/utility/errors/commonErrors.ts +2 -0
- package/utility/hdbTerms.ts +13 -1
- package/utility/install/installer.ts +37 -0
- package/utility/logging/harper_logger.ts +786 -41
- package/utility/operation_authorization.ts +31 -3
- package/studio/web/assets/index-BHo3c2Gk.js.map +0 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type Logger } from '../utility/logging/logger.ts';
|
|
2
2
|
import { loggerWithTag } from '../utility/logging/harper_logger.ts';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
3
4
|
import type { Stats } from 'node:fs';
|
|
4
|
-
import { EventEmitter
|
|
5
|
+
import { EventEmitter } from 'node:events';
|
|
5
6
|
import { Component, FileAndURLPathConfig } from './Component.ts';
|
|
6
7
|
import chokidar, { FSWatcher, FSWatcherEventMap } from 'chokidar';
|
|
7
8
|
import { join } from 'node:path';
|
|
@@ -74,25 +75,39 @@ export type EntryHandlerEventMap = {
|
|
|
74
75
|
unlinkDir: [entry: UnlinkDirectoryEvent];
|
|
75
76
|
};
|
|
76
77
|
|
|
78
|
+
type EntrySnapshot =
|
|
79
|
+
{ entryType: 'file'; urlPath: string; digest: Buffer } | { entryType: 'directory'; urlPath: string };
|
|
80
|
+
|
|
81
|
+
type RedeployScan = {
|
|
82
|
+
generation: number;
|
|
83
|
+
previousEntries: Map<string, EntrySnapshot>;
|
|
84
|
+
currentEntries: Map<string, EntrySnapshot>;
|
|
85
|
+
removalsEmitted: Set<string>;
|
|
86
|
+
readFailures: Map<string, unknown>;
|
|
87
|
+
};
|
|
88
|
+
|
|
77
89
|
export class EntryHandler extends EventEmitter<EntryHandlerEventMap> {
|
|
78
90
|
#component: Component;
|
|
79
91
|
#watcher?: FSWatcher;
|
|
80
92
|
#logger: Logger;
|
|
81
93
|
#pendingFileReads: Set<Promise<void>>;
|
|
94
|
+
#pendingFileReadsByGeneration: Map<number, Set<Promise<void>>>;
|
|
82
95
|
#isInitialScanComplete: boolean;
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
#
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// and setup, which under inotify pressure can produce spurious EMFILE.
|
|
96
|
+
#readyPending = false;
|
|
97
|
+
#readyGeneration = 0;
|
|
98
|
+
#watchGeneration = 0;
|
|
99
|
+
#eventSequence = 0;
|
|
100
|
+
#latestPathSequence = new Map<string, number>();
|
|
101
|
+
#entries = new Map<string, EntrySnapshot>();
|
|
102
|
+
#redeployScan?: RedeployScan;
|
|
103
|
+
#paused = false;
|
|
92
104
|
#pausedClose?: Promise<void>;
|
|
93
|
-
|
|
94
|
-
#
|
|
95
|
-
#
|
|
105
|
+
// Serializes replacements so concurrent update/recovery calls cannot orphan an intervening watcher.
|
|
106
|
+
#watchInstall?: Promise<void>;
|
|
107
|
+
#usingPolling = false;
|
|
108
|
+
#closed = false;
|
|
109
|
+
#openCount = 0;
|
|
110
|
+
#liveWatchers = new Set<FSWatcher>();
|
|
96
111
|
ready: Promise<any[]>;
|
|
97
112
|
|
|
98
113
|
constructor(name: string, directory: string, config: FilesOption | FileAndURLPathConfig, logger?: Logger) {
|
|
@@ -101,9 +116,40 @@ export class EntryHandler extends EventEmitter<EntryHandlerEventMap> {
|
|
|
101
116
|
this.#component = new Component(name, directory, castConfig(config));
|
|
102
117
|
this.#logger = logger || loggerWithTag(name);
|
|
103
118
|
this.#pendingFileReads = new Set();
|
|
119
|
+
this.#pendingFileReadsByGeneration = new Map();
|
|
104
120
|
this.#isInitialScanComplete = false;
|
|
105
|
-
this.ready =
|
|
106
|
-
this.#watch();
|
|
121
|
+
this.ready = this.#createReadyLatch();
|
|
122
|
+
this.#watch().catch(() => {});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#createReadyLatch(): Promise<any[]> {
|
|
126
|
+
if (this.#readyPending) return this.ready;
|
|
127
|
+
this.#readyPending = true;
|
|
128
|
+
const ready = new Promise<any[]>((resolve, reject) => {
|
|
129
|
+
const cleanup = () => {
|
|
130
|
+
this.#readyPending = false;
|
|
131
|
+
this.off('ready', handleReady);
|
|
132
|
+
this.off('error', handleError);
|
|
133
|
+
this.off('close', handleClose);
|
|
134
|
+
};
|
|
135
|
+
const handleReady = () => {
|
|
136
|
+
cleanup();
|
|
137
|
+
resolve([]);
|
|
138
|
+
};
|
|
139
|
+
const handleError = (error: unknown) => {
|
|
140
|
+
cleanup();
|
|
141
|
+
reject(error);
|
|
142
|
+
};
|
|
143
|
+
const handleClose = () => {
|
|
144
|
+
cleanup();
|
|
145
|
+
reject(new Error(`Entry handler for ${this.name} closed before becoming ready`));
|
|
146
|
+
};
|
|
147
|
+
this.once('ready', handleReady);
|
|
148
|
+
this.once('error', handleError);
|
|
149
|
+
this.once('close', handleClose);
|
|
150
|
+
});
|
|
151
|
+
void ready.catch(() => {});
|
|
152
|
+
return ready;
|
|
107
153
|
}
|
|
108
154
|
|
|
109
155
|
get name(): string {
|
|
@@ -114,7 +160,8 @@ export class EntryHandler extends EventEmitter<EntryHandlerEventMap> {
|
|
|
114
160
|
return this.#component.directory;
|
|
115
161
|
}
|
|
116
162
|
|
|
117
|
-
#handleAll(...[event, path, stats]: FSWatcherEventMap['all']): void {
|
|
163
|
+
#handleAll(generation: number, ...[event, path, stats]: FSWatcherEventMap['all']): void {
|
|
164
|
+
if (generation !== this.#watchGeneration) return;
|
|
118
165
|
if (path === '') path = '/';
|
|
119
166
|
|
|
120
167
|
if (!isMatch(path, this.#component.globOptions.source, { ignore: this.#component.globOptions.ignore })) return;
|
|
@@ -124,26 +171,7 @@ export class EntryHandler extends EventEmitter<EntryHandlerEventMap> {
|
|
|
124
171
|
switch (event) {
|
|
125
172
|
case 'add':
|
|
126
173
|
case 'change': {
|
|
127
|
-
|
|
128
|
-
const fileReadPromise = readFile(absolutePath)
|
|
129
|
-
.then((contents) => {
|
|
130
|
-
const entry: AddFileEvent | ChangeFileEvent = {
|
|
131
|
-
eventType: event,
|
|
132
|
-
entryType: 'file' as const,
|
|
133
|
-
contents,
|
|
134
|
-
stats,
|
|
135
|
-
absolutePath,
|
|
136
|
-
urlPath,
|
|
137
|
-
};
|
|
138
|
-
this.emit('all', entry);
|
|
139
|
-
this.emit(event, entry as any);
|
|
140
|
-
})
|
|
141
|
-
.finally(() => {
|
|
142
|
-
this.#pendingFileReads.delete(fileReadPromise);
|
|
143
|
-
this.#checkIfAllComplete();
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
this.#pendingFileReads.add(fileReadPromise);
|
|
174
|
+
this.#queueFileRead(generation, event, path, absolutePath, stats, readFile(absolutePath));
|
|
147
175
|
break;
|
|
148
176
|
}
|
|
149
177
|
case 'unlink': {
|
|
@@ -155,28 +183,214 @@ export class EntryHandler extends EventEmitter<EntryHandlerEventMap> {
|
|
|
155
183
|
absolutePath,
|
|
156
184
|
urlPath,
|
|
157
185
|
};
|
|
158
|
-
this
|
|
159
|
-
this.emit(event, entry);
|
|
186
|
+
this.#handleRemovedEntry(generation, entry);
|
|
160
187
|
break;
|
|
161
188
|
}
|
|
162
189
|
case 'addDir':
|
|
163
190
|
case 'unlinkDir': {
|
|
164
191
|
const urlPath = deriveURLPath(this.#component, path, 'directory');
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
192
|
+
if (event === 'addDir') {
|
|
193
|
+
const entry: AddDirectoryEvent = {
|
|
194
|
+
eventType: event,
|
|
195
|
+
entryType: 'directory',
|
|
196
|
+
stats,
|
|
197
|
+
absolutePath,
|
|
198
|
+
urlPath,
|
|
199
|
+
};
|
|
200
|
+
this.#handleAddedEntry(generation, entry, this.#snapshot(entry));
|
|
201
|
+
} else {
|
|
202
|
+
const entry: UnlinkDirectoryEvent = {
|
|
203
|
+
eventType: event,
|
|
204
|
+
entryType: 'directory',
|
|
205
|
+
stats,
|
|
206
|
+
absolutePath,
|
|
207
|
+
urlPath,
|
|
208
|
+
};
|
|
209
|
+
this.#handleRemovedEntry(generation, entry);
|
|
210
|
+
}
|
|
174
211
|
break;
|
|
175
212
|
}
|
|
176
213
|
}
|
|
177
214
|
}
|
|
178
215
|
|
|
179
|
-
#
|
|
216
|
+
#queueFileRead(
|
|
217
|
+
generation: number,
|
|
218
|
+
event: 'add' | 'change',
|
|
219
|
+
path: string,
|
|
220
|
+
absolutePath: string,
|
|
221
|
+
stats: Stats | undefined,
|
|
222
|
+
contentsPromise: Promise<Buffer>
|
|
223
|
+
): Promise<void> {
|
|
224
|
+
const sequence = ++this.#eventSequence;
|
|
225
|
+
this.#latestPathSequence.set(absolutePath, sequence);
|
|
226
|
+
|
|
227
|
+
const fileReadPromise = contentsPromise
|
|
228
|
+
.then(
|
|
229
|
+
(contents) => {
|
|
230
|
+
if (generation !== this.#watchGeneration || this.#latestPathSequence.get(absolutePath) !== sequence) return;
|
|
231
|
+
if (this.#redeployScan?.generation === generation) this.#redeployScan.readFailures.delete(absolutePath);
|
|
232
|
+
const entry: AddFileEvent | ChangeFileEvent = {
|
|
233
|
+
eventType: event,
|
|
234
|
+
entryType: 'file',
|
|
235
|
+
contents,
|
|
236
|
+
stats,
|
|
237
|
+
absolutePath,
|
|
238
|
+
urlPath: deriveURLPath(this.#component, path, 'file'),
|
|
239
|
+
};
|
|
240
|
+
try {
|
|
241
|
+
this.#handleAddedEntry(generation, entry, this.#snapshot(entry));
|
|
242
|
+
} catch (error) {
|
|
243
|
+
this.#reportConsumerError(error);
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
(error) => {
|
|
247
|
+
if (
|
|
248
|
+
generation !== this.#watchGeneration ||
|
|
249
|
+
this.#latestPathSequence.get(absolutePath) !== sequence ||
|
|
250
|
+
(error as NodeJS.ErrnoException | null | undefined)?.code === 'ENOENT'
|
|
251
|
+
)
|
|
252
|
+
return;
|
|
253
|
+
if (this.#redeployScan?.generation === generation) {
|
|
254
|
+
this.#redeployScan.readFailures.set(absolutePath, error);
|
|
255
|
+
} else {
|
|
256
|
+
this.#handleWatcherError(error);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
)
|
|
260
|
+
.finally(() => {
|
|
261
|
+
if (this.#latestPathSequence.get(absolutePath) === sequence) this.#latestPathSequence.delete(absolutePath);
|
|
262
|
+
this.#pendingFileReads.delete(fileReadPromise);
|
|
263
|
+
const generationReads = this.#pendingFileReadsByGeneration.get(generation);
|
|
264
|
+
generationReads?.delete(fileReadPromise);
|
|
265
|
+
if (generationReads?.size === 0) this.#pendingFileReadsByGeneration.delete(generation);
|
|
266
|
+
this.#checkIfAllComplete(generation);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
this.#pendingFileReads.add(fileReadPromise);
|
|
270
|
+
let generationReads = this.#pendingFileReadsByGeneration.get(generation);
|
|
271
|
+
if (!generationReads) this.#pendingFileReadsByGeneration.set(generation, (generationReads = new Set()));
|
|
272
|
+
generationReads.add(fileReadPromise);
|
|
273
|
+
return fileReadPromise;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#snapshot(entry: AddFileEvent | ChangeFileEvent | AddDirectoryEvent): EntrySnapshot {
|
|
277
|
+
if (entry.entryType === 'file')
|
|
278
|
+
return {
|
|
279
|
+
entryType: entry.entryType,
|
|
280
|
+
urlPath: entry.urlPath,
|
|
281
|
+
digest: createHash('sha256').update(entry.contents).digest(),
|
|
282
|
+
};
|
|
283
|
+
return { entryType: entry.entryType, urlPath: entry.urlPath };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#handleAddedEntry(
|
|
287
|
+
generation: number,
|
|
288
|
+
entry: AddFileEvent | ChangeFileEvent | AddDirectoryEvent,
|
|
289
|
+
snapshot: EntrySnapshot
|
|
290
|
+
): void {
|
|
291
|
+
if (generation !== this.#watchGeneration) return;
|
|
292
|
+
const scan = this.#redeployScan?.generation === generation ? this.#redeployScan : undefined;
|
|
293
|
+
if (!scan) {
|
|
294
|
+
const wasKnown = this.#entries.has(entry.absolutePath);
|
|
295
|
+
this.#entries.set(entry.absolutePath, snapshot);
|
|
296
|
+
// A newer change read can supersede an initial add read for the same path. The consumer still
|
|
297
|
+
// needs its first event to be an add so it can load the entry before handling later changes.
|
|
298
|
+
if (entry.entryType === 'file' && entry.eventType === 'change' && !wasKnown)
|
|
299
|
+
this.#emitEntrySafely({ ...entry, eventType: 'add' });
|
|
300
|
+
else this.#emitEntrySafely(entry);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const wasRemovedDuringScan = scan.removalsEmitted.delete(entry.absolutePath);
|
|
305
|
+
const currentEntry = scan.currentEntries.get(entry.absolutePath);
|
|
306
|
+
const previousEntry = wasRemovedDuringScan
|
|
307
|
+
? undefined
|
|
308
|
+
: (currentEntry ?? scan.previousEntries.get(entry.absolutePath));
|
|
309
|
+
scan.currentEntries.set(entry.absolutePath, snapshot);
|
|
310
|
+
|
|
311
|
+
if (!previousEntry) {
|
|
312
|
+
this.#emitAddition(entry);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (previousEntry.entryType !== snapshot.entryType || previousEntry.urlPath !== snapshot.urlPath) {
|
|
316
|
+
try {
|
|
317
|
+
this.#emitRemoval(previousEntry, entry.absolutePath);
|
|
318
|
+
} catch (error) {
|
|
319
|
+
this.#reportConsumerError(error);
|
|
320
|
+
}
|
|
321
|
+
this.#emitAddition(entry);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (
|
|
325
|
+
snapshot.entryType === 'file' &&
|
|
326
|
+
previousEntry.entryType === 'file' &&
|
|
327
|
+
!previousEntry.digest.equals(snapshot.digest)
|
|
328
|
+
) {
|
|
329
|
+
this.#emitEntrySafely({ ...entry, eventType: 'change' } as ChangeFileEvent);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#emitAddition(entry: AddFileEvent | ChangeFileEvent | AddDirectoryEvent): void {
|
|
334
|
+
if (entry.entryType === 'file') this.#emitEntrySafely({ ...entry, eventType: 'add' });
|
|
335
|
+
else this.#emitEntrySafely({ ...entry, eventType: 'addDir' });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
#handleRemovedEntry(generation: number, entry: UnlinkFileEvent | UnlinkDirectoryEvent): void {
|
|
339
|
+
if (generation !== this.#watchGeneration) return;
|
|
340
|
+
this.#latestPathSequence.delete(entry.absolutePath);
|
|
341
|
+
const scan = this.#redeployScan?.generation === generation ? this.#redeployScan : undefined;
|
|
342
|
+
if (!scan) {
|
|
343
|
+
this.#entries.delete(entry.absolutePath);
|
|
344
|
+
this.#emitEntrySafely(entry);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const knownEntry = scan.currentEntries.get(entry.absolutePath) ?? scan.previousEntries.get(entry.absolutePath);
|
|
349
|
+
scan.currentEntries.delete(entry.absolutePath);
|
|
350
|
+
if (!knownEntry || scan.removalsEmitted.has(entry.absolutePath)) return;
|
|
351
|
+
try {
|
|
352
|
+
this.#emitRemoval(knownEntry, entry.absolutePath);
|
|
353
|
+
} catch (error) {
|
|
354
|
+
this.#reportConsumerError(error);
|
|
355
|
+
}
|
|
356
|
+
scan.removalsEmitted.add(entry.absolutePath);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
#emitRemoval(snapshot: EntrySnapshot, absolutePath: string): void {
|
|
360
|
+
if (snapshot.entryType === 'file')
|
|
361
|
+
this.#emitEntry({ eventType: 'unlink', entryType: 'file', absolutePath, urlPath: snapshot.urlPath });
|
|
362
|
+
else this.#emitEntry({ eventType: 'unlinkDir', entryType: 'directory', absolutePath, urlPath: snapshot.urlPath });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
#emitEntry(entry: FileEntryEvent | DirectoryEntryEvent): void {
|
|
366
|
+
this.emit('all', entry);
|
|
367
|
+
switch (entry.eventType) {
|
|
368
|
+
case 'add':
|
|
369
|
+
this.emit('add', entry);
|
|
370
|
+
break;
|
|
371
|
+
case 'change':
|
|
372
|
+
this.emit('change', entry);
|
|
373
|
+
break;
|
|
374
|
+
case 'unlink':
|
|
375
|
+
this.emit('unlink', entry);
|
|
376
|
+
break;
|
|
377
|
+
case 'addDir':
|
|
378
|
+
this.emit('addDir', entry);
|
|
379
|
+
break;
|
|
380
|
+
case 'unlinkDir':
|
|
381
|
+
this.emit('unlinkDir', entry);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
#emitEntrySafely(entry: FileEntryEvent | DirectoryEntryEvent): void {
|
|
386
|
+
try {
|
|
387
|
+
this.#emitEntry(entry);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
this.#reportConsumerError(error);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#handleWatcherError(error: unknown): void {
|
|
180
394
|
if (isWatcherExhaustionError(error)) {
|
|
181
395
|
// Swallow every exhaustion error — chokidar can emit several before the
|
|
182
396
|
// failed native watcher closes, and we don't want a flurry of ENOSPC to
|
|
@@ -196,55 +410,136 @@ export class EntryHandler extends EventEmitter<EntryHandlerEventMap> {
|
|
|
196
410
|
}
|
|
197
411
|
return;
|
|
198
412
|
}
|
|
199
|
-
this
|
|
413
|
+
this.#reportConsumerError(error);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
#reportConsumerError(error: unknown): void {
|
|
417
|
+
if (this.listenerCount('error') > 0) this.emit('error', error);
|
|
418
|
+
else this.#logger.error?.('Error in entry listener:', error);
|
|
200
419
|
}
|
|
201
420
|
|
|
202
|
-
#handleReady(): void {
|
|
421
|
+
#handleReady(generation: number): void {
|
|
422
|
+
if (generation !== this.#watchGeneration) return;
|
|
203
423
|
this.#isInitialScanComplete = true;
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
);
|
|
424
|
+
const pendingReads = this.#pendingFileReadsByGeneration.get(generation)?.size ?? 0;
|
|
425
|
+
if (pendingReads > 0) {
|
|
426
|
+
this.#logger.debug?.(`Initial scan complete, still waiting for ${pendingReads} pending file reads`);
|
|
208
427
|
}
|
|
209
|
-
this.#checkIfAllComplete();
|
|
428
|
+
this.#checkIfAllComplete(generation);
|
|
210
429
|
}
|
|
211
430
|
|
|
212
|
-
#checkIfAllComplete(): void {
|
|
213
|
-
|
|
214
|
-
|
|
431
|
+
#checkIfAllComplete(generation: number): void {
|
|
432
|
+
if (
|
|
433
|
+
generation === this.#watchGeneration &&
|
|
434
|
+
this.#readyGeneration !== generation &&
|
|
435
|
+
this.#isInitialScanComplete &&
|
|
436
|
+
!this.#pendingFileReadsByGeneration.has(generation)
|
|
437
|
+
) {
|
|
438
|
+
this.#readyGeneration = generation;
|
|
439
|
+
const { listenerErrors, readErrors } = this.#finishRedeployScan(generation);
|
|
215
440
|
this.emit('ready');
|
|
441
|
+
// The readiness latch rejects if `error` is emitted first. Surface consumer failures only
|
|
442
|
+
// after resolving the generation's ready latch so one bad removal listener cannot strand startup.
|
|
443
|
+
for (const error of listenerErrors) this.#reportConsumerError(error);
|
|
444
|
+
for (const error of readErrors) this.#handleWatcherError(error);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
#finishRedeployScan(generation: number): { listenerErrors: unknown[]; readErrors: unknown[] } {
|
|
449
|
+
const scan = this.#redeployScan;
|
|
450
|
+
if (!scan || scan.generation !== generation) return { listenerErrors: [], readErrors: [] };
|
|
451
|
+
const listenerErrors: unknown[] = [];
|
|
452
|
+
for (const absolutePath of scan.readFailures.keys()) {
|
|
453
|
+
const previousEntry = scan.previousEntries.get(absolutePath);
|
|
454
|
+
if (previousEntry && !scan.currentEntries.has(absolutePath)) scan.currentEntries.set(absolutePath, previousEntry);
|
|
455
|
+
}
|
|
456
|
+
const missingEntries = [...scan.previousEntries].filter(
|
|
457
|
+
([absolutePath]) => !scan.currentEntries.has(absolutePath) && !scan.removalsEmitted.has(absolutePath)
|
|
458
|
+
);
|
|
459
|
+
// Match watcher deletion semantics: descendants disappear before the directory that contained them.
|
|
460
|
+
missingEntries.sort(([a], [b]) => b.length - a.length);
|
|
461
|
+
// Commit the generation before invoking consumer code. A synchronously throwing removal listener must
|
|
462
|
+
// not leave the old snapshot installed or keep the readiness latch permanently unresolved.
|
|
463
|
+
this.#entries = scan.currentEntries;
|
|
464
|
+
this.#redeployScan = undefined;
|
|
465
|
+
for (const [absolutePath, snapshot] of missingEntries) {
|
|
466
|
+
try {
|
|
467
|
+
this.#emitRemoval(snapshot, absolutePath);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
listenerErrors.push(error);
|
|
470
|
+
}
|
|
216
471
|
}
|
|
472
|
+
return { listenerErrors, readErrors: [...scan.readFailures.values()] };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
#observedEntries(): Map<string, EntrySnapshot> {
|
|
476
|
+
const scan = this.#redeployScan;
|
|
477
|
+
if (!scan) return this.#entries;
|
|
478
|
+
// A generation can be replaced before `ready`, after some events have already reached consumers.
|
|
479
|
+
// Carry exactly that observed state forward: retain previously known paths that this partial scan
|
|
480
|
+
// has not reached, apply additions/changes it emitted, and remove paths whose unlink was emitted.
|
|
481
|
+
const observedEntries = new Map(scan.previousEntries);
|
|
482
|
+
for (const absolutePath of scan.removalsEmitted) observedEntries.delete(absolutePath);
|
|
483
|
+
for (const [absolutePath, snapshot] of scan.currentEntries) observedEntries.set(absolutePath, snapshot);
|
|
484
|
+
return observedEntries;
|
|
217
485
|
}
|
|
218
486
|
|
|
219
487
|
async #watch() {
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
488
|
+
const install = this.#watchInstall ? this.#watchInstall.then(() => this.#installWatcher()) : this.#installWatcher();
|
|
489
|
+
const settled: Promise<void> = install.then(
|
|
490
|
+
() => {
|
|
491
|
+
if (this.#watchInstall === settled) this.#watchInstall = undefined;
|
|
492
|
+
},
|
|
493
|
+
() => {
|
|
494
|
+
if (this.#watchInstall === settled) this.#watchInstall = undefined;
|
|
495
|
+
}
|
|
496
|
+
);
|
|
497
|
+
this.#watchInstall = settled;
|
|
498
|
+
await install;
|
|
499
|
+
|
|
500
|
+
return this.ready;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async #installWatcher(): Promise<void> {
|
|
223
504
|
if (this.#pausedClose) {
|
|
224
505
|
await this.#pausedClose;
|
|
225
506
|
this.#pausedClose = undefined;
|
|
226
507
|
}
|
|
227
508
|
|
|
228
|
-
|
|
229
|
-
|
|
509
|
+
const outgoing = this.#watcher;
|
|
510
|
+
if (outgoing) {
|
|
511
|
+
// Stays installed across the await so a close() landing in the gap still includes this
|
|
512
|
+
// teardown in its own settle set; only clear it if close() didn't already replace it.
|
|
513
|
+
try {
|
|
514
|
+
await outgoing.close();
|
|
515
|
+
} catch (error) {
|
|
516
|
+
this.#logger.warn?.(`Failed to close the previous watcher for ${this.name}; replacing it anyway:`, error);
|
|
517
|
+
} finally {
|
|
518
|
+
this.#liveWatchers.delete(outgoing);
|
|
519
|
+
}
|
|
520
|
+
if (this.#watcher === outgoing) this.#watcher = undefined;
|
|
521
|
+
}
|
|
230
522
|
|
|
231
|
-
|
|
232
|
-
// install a fresh watcher — it would outlive the EntryHandler.
|
|
233
|
-
if (this.#closed) return this.ready;
|
|
523
|
+
if (this.#closed) return;
|
|
234
524
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (this.#paused) return this.ready;
|
|
525
|
+
if (this.#paused) return;
|
|
526
|
+
const generation = ++this.#watchGeneration;
|
|
238
527
|
|
|
239
|
-
// When a fresh watcher is installed (after pause+resume, or update), the
|
|
240
|
-
// initial scan emits add events anew, so reset the readiness latch so
|
|
241
|
-
// `ready` resolves after the new scan completes.
|
|
242
528
|
this.#isInitialScanComplete = false;
|
|
529
|
+
const previousEntries = this.#observedEntries();
|
|
530
|
+
this.#entries = previousEntries;
|
|
531
|
+
this.#redeployScan = {
|
|
532
|
+
generation,
|
|
533
|
+
previousEntries,
|
|
534
|
+
currentEntries: new Map(),
|
|
535
|
+
removalsEmitted: new Set(),
|
|
536
|
+
readFailures: new Map(),
|
|
537
|
+
};
|
|
243
538
|
|
|
244
539
|
const allowedBases = this.#component.patternBases.map((base) => join(this.#component.directory, base));
|
|
245
540
|
|
|
246
541
|
this.#openCount++;
|
|
247
|
-
this.#watcher = chokidar
|
|
542
|
+
const watcher = (this.#watcher = chokidar
|
|
248
543
|
.watch(this.#component.commonPatternBase, {
|
|
249
544
|
cwd: this.#component.directory,
|
|
250
545
|
persistent: false,
|
|
@@ -280,39 +575,48 @@ export class EntryHandler extends EventEmitter<EntryHandlerEventMap> {
|
|
|
280
575
|
);
|
|
281
576
|
},
|
|
282
577
|
})
|
|
283
|
-
.on('all', this.#handleAll
|
|
284
|
-
.on('error',
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
578
|
+
.on('all', (...args) => this.#handleAll(generation, ...args))
|
|
579
|
+
.on('error', (error) => {
|
|
580
|
+
if (generation === this.#watchGeneration) this.#handleWatcherError(error);
|
|
581
|
+
})
|
|
582
|
+
.on('ready', () => this.#handleReady(generation)));
|
|
583
|
+
this.#liveWatchers.add(watcher);
|
|
288
584
|
}
|
|
289
585
|
|
|
290
|
-
// Test-only: simulate the underlying chokidar watcher emitting an error.
|
|
291
|
-
// Exposed so the polling-fallback path can be exercised without triggering a
|
|
292
|
-
// real ENOSPC/EMFILE on the host.
|
|
293
586
|
_simulateWatcherErrorForTests(error: unknown): void {
|
|
294
|
-
this.#
|
|
587
|
+
this.#handleWatcherError(error);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
_simulateFileReadForTests(event: 'add' | 'change', path: string, contents: Promise<Buffer>): Promise<void> {
|
|
591
|
+
const absolutePath = join(this.directory, path);
|
|
592
|
+
return this.#queueFileRead(this.#watchGeneration, event, path, absolutePath, undefined, contents);
|
|
295
593
|
}
|
|
296
594
|
|
|
297
|
-
// Test-only: whether the watcher has fallen back to polling.
|
|
298
595
|
get _usingPollingForTests(): boolean {
|
|
299
596
|
return this.#usingPolling;
|
|
300
597
|
}
|
|
301
598
|
|
|
302
|
-
// Test-only: number of times the underlying watcher has been (re)opened.
|
|
303
|
-
// Used to assert that a close()-during-fallback race didn't install a
|
|
304
|
-
// replacement watcher.
|
|
305
599
|
get _openCountForTests(): number {
|
|
306
600
|
return this.#openCount;
|
|
307
601
|
}
|
|
308
602
|
|
|
603
|
+
get _liveWatcherCountForTests(): number {
|
|
604
|
+
return this.#liveWatchers.size;
|
|
605
|
+
}
|
|
606
|
+
|
|
309
607
|
close(): Promise<this> {
|
|
608
|
+
this.#watchGeneration++;
|
|
310
609
|
this.#closed = true;
|
|
311
610
|
const pendingReads = [...this.#pendingFileReads];
|
|
312
|
-
const
|
|
611
|
+
const outgoing = this.#watcher;
|
|
612
|
+
const watcherClose = outgoing
|
|
613
|
+
? Promise.resolve(outgoing.close())
|
|
614
|
+
.catch(() => {})
|
|
615
|
+
.then(() => {
|
|
616
|
+
this.#liveWatchers.delete(outgoing);
|
|
617
|
+
})
|
|
618
|
+
: Promise.resolve();
|
|
313
619
|
this.#watcher = undefined;
|
|
314
|
-
// If paused, there may be an in-flight close from pause() that hasn't settled yet.
|
|
315
|
-
// Include it so close() doesn't resolve while inotify handles are still releasing.
|
|
316
620
|
const pausedClose = this.#pausedClose ?? Promise.resolve();
|
|
317
621
|
this.#pausedClose = undefined;
|
|
318
622
|
|
|
@@ -332,41 +636,42 @@ export class EntryHandler extends EventEmitter<EntryHandlerEventMap> {
|
|
|
332
636
|
* Idempotent. Awaiting `ready` while paused will not resolve until resume().
|
|
333
637
|
*/
|
|
334
638
|
pause(): void {
|
|
639
|
+
if (this.#paused || this.#closed) return;
|
|
640
|
+
this.#watchGeneration++;
|
|
335
641
|
this.#paused = true;
|
|
336
|
-
|
|
337
|
-
// ready while paused will not resolve until resume()" contract holds even
|
|
338
|
-
// when the watcher had already become ready before pause(). The next
|
|
339
|
-
// 'ready' emit will come from the chokidar instance installed by resume().
|
|
340
|
-
this.ready = once(this, 'ready');
|
|
642
|
+
this.ready = this.#createReadyLatch();
|
|
341
643
|
if (this.#watcher) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
644
|
+
const outgoing = this.#watcher;
|
|
645
|
+
this.#pausedClose = Promise.resolve(outgoing.close())
|
|
646
|
+
.catch(() => {})
|
|
647
|
+
.then(() => {
|
|
648
|
+
this.#liveWatchers.delete(outgoing);
|
|
649
|
+
});
|
|
347
650
|
this.#watcher = undefined;
|
|
348
651
|
}
|
|
349
652
|
}
|
|
350
653
|
|
|
351
654
|
/**
|
|
352
655
|
* Reinstate the watcher previously stopped by pause(). The fresh chokidar
|
|
353
|
-
* instance
|
|
354
|
-
*
|
|
355
|
-
* caller (Scope, on deploy:end) wants plugins to see the post-deploy tree
|
|
356
|
-
* as if loading cold.
|
|
656
|
+
* instance scans the post-pause tree, compares it with the retained snapshot,
|
|
657
|
+
* and emits only the logical add, change, unlink, addDir, and unlinkDir events.
|
|
357
658
|
*
|
|
358
659
|
* No-op if not currently paused.
|
|
359
660
|
*/
|
|
360
661
|
resume(): Promise<any[]> {
|
|
361
662
|
if (!this.#paused) return this.ready;
|
|
362
663
|
this.#paused = false;
|
|
363
|
-
// `this.ready` was already reset to a pending promise in pause(); just
|
|
364
|
-
// trigger the watcher recreation and let its 'ready' emit resolve it.
|
|
365
664
|
return this.#watch();
|
|
366
665
|
}
|
|
367
666
|
|
|
368
667
|
update(config: FilesOption | FileAndURLPathConfig) {
|
|
668
|
+
if (this.#closed) return this.ready;
|
|
669
|
+
this.#watchGeneration++;
|
|
369
670
|
this.#component = new Component(this.name, this.directory, castConfig(config));
|
|
671
|
+
// Re-arm the readiness latch for parity with pause(). Without this, awaiting update() on an
|
|
672
|
+
// already-ready handler resolves against the outgoing generation's 'ready' — before the
|
|
673
|
+
// replacement generation has scanned, digested, and emitted its logical events.
|
|
674
|
+
this.ready = this.#createReadyLatch();
|
|
370
675
|
|
|
371
676
|
return this.#watch();
|
|
372
677
|
}
|