@harperfast/harper 5.3.0-alpha.1 → 5.3.0-beta.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/cliOperations.ts +12 -0
- package/bin/restart.ts +66 -6
- package/components/Application.ts +1134 -112
- package/components/OptionsWatcher.ts +368 -102
- package/components/Scope.ts +20 -1
- package/components/componentLoader.ts +43 -4
- package/components/deploymentOperations.ts +4 -1
- package/components/deploymentRecorder.ts +9 -2
- package/components/operations.js +284 -52
- package/components/operationsValidation.js +49 -2
- package/components/packageComponent.ts +25 -1
- package/components/requestRestart.ts +11 -0
- package/config/RootConfigWatcher.ts +191 -37
- package/config/configReadRetry.ts +62 -0
- package/config/configUtils.ts +78 -26
- package/config/parseConfigFile.ts +34 -0
- package/config/readConfigFileSync.ts +44 -0
- package/config/watcherArming.ts +59 -0
- package/config-root.schema.json +4 -0
- package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
- package/dist/bin/cliOperations.js +13 -0
- package/dist/bin/cliOperations.js.map +1 -1
- package/dist/bin/restart.js +42 -6
- package/dist/bin/restart.js.map +1 -1
- package/dist/components/Application.d.ts +104 -9
- package/dist/components/Application.js +954 -102
- package/dist/components/Application.js.map +1 -1
- package/dist/components/OptionsWatcher.d.ts +4 -1
- package/dist/components/OptionsWatcher.js +378 -104
- package/dist/components/OptionsWatcher.js.map +1 -1
- package/dist/components/Scope.js +15 -1
- package/dist/components/Scope.js.map +1 -1
- package/dist/components/componentLoader.js +35 -3
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deploymentOperations.js +4 -1
- package/dist/components/deploymentOperations.js.map +1 -1
- package/dist/components/deploymentRecorder.d.ts +4 -2
- package/dist/components/deploymentRecorder.js +1 -0
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/operations.d.ts +28 -0
- package/dist/components/operations.js +263 -54
- package/dist/components/operations.js.map +1 -1
- package/dist/components/operationsValidation.js +48 -2
- package/dist/components/operationsValidation.js.map +1 -1
- package/dist/components/packageComponent.js +24 -0
- package/dist/components/packageComponent.js.map +1 -1
- package/dist/components/requestRestart.d.ts +1 -0
- package/dist/components/requestRestart.js +7 -0
- package/dist/components/requestRestart.js.map +1 -1
- package/dist/config/RootConfigWatcher.d.ts +2 -0
- package/dist/config/RootConfigWatcher.js +189 -35
- package/dist/config/RootConfigWatcher.js.map +1 -1
- package/dist/config/configReadRetry.d.ts +8 -0
- package/dist/config/configReadRetry.js +62 -0
- package/dist/config/configReadRetry.js.map +1 -0
- package/dist/config/configUtils.d.ts +10 -9
- package/dist/config/configUtils.js +63 -27
- package/dist/config/configUtils.js.map +1 -1
- package/dist/config/parseConfigFile.d.ts +4 -0
- package/dist/config/parseConfigFile.js +35 -0
- package/dist/config/parseConfigFile.js.map +1 -0
- package/dist/config/readConfigFileSync.d.ts +1 -0
- package/dist/config/readConfigFileSync.js +47 -0
- package/dist/config/readConfigFileSync.js.map +1 -0
- package/dist/config/watcherArming.d.ts +15 -0
- package/dist/config/watcherArming.js +59 -0
- package/dist/config/watcherArming.js.map +1 -0
- package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
- package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +3 -0
- package/dist/resources/DatabaseTransaction.d.ts +25 -0
- package/dist/resources/DatabaseTransaction.js +224 -18
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +2 -1
- package/dist/resources/LMDBTransaction.js +22 -3
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/PrimaryRocksDatabase.js +22 -6
- package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
- package/dist/resources/RecordEncoder.d.ts +1 -1
- package/dist/resources/RecordEncoder.js +21 -5
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/Resource.js +97 -13
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +8 -0
- package/dist/resources/RocksIndexStore.js +2 -1
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
- package/dist/resources/RocksTransactionLogStore.js +104 -33
- package/dist/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/resources/Table.d.ts +58 -7
- package/dist/resources/Table.js +1095 -346
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +10 -3
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +170 -0
- package/dist/resources/auditStore.js +457 -11
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/dataLoader.js +3 -4
- package/dist/resources/dataLoader.js.map +1 -1
- package/dist/resources/databases.d.ts +16 -13
- package/dist/resources/databases.js +624 -176
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/derivedIndexRegistry.d.ts +5 -0
- package/dist/resources/derivedIndexRegistry.js +68 -0
- package/dist/resources/derivedIndexRegistry.js.map +1 -0
- package/dist/resources/derivedIndexRuntime.d.ts +215 -0
- package/dist/resources/derivedIndexRuntime.js +2027 -0
- package/dist/resources/derivedIndexRuntime.js.map +1 -0
- package/dist/resources/graphql.js +3 -2
- package/dist/resources/graphql.js.map +1 -1
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +848 -39
- package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
- package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
- package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
- package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
- package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
- package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
- package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
- package/dist/resources/nodeIdMapping.d.ts +5 -0
- package/dist/resources/nodeIdMapping.js +49 -0
- package/dist/resources/nodeIdMapping.js.map +1 -1
- package/dist/resources/recordLock.d.ts +47 -4
- package/dist/resources/recordLock.js +138 -7
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/resources/recordLockCoordinator.d.ts +557 -0
- package/dist/resources/recordLockCoordinator.js +2565 -0
- package/dist/resources/recordLockCoordinator.js.map +1 -0
- package/dist/resources/replayLogs.js +5 -0
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/replicatedApplyFailure.d.ts +16 -0
- package/dist/resources/replicatedApplyFailure.js +63 -0
- package/dist/resources/replicatedApplyFailure.js.map +1 -0
- package/dist/resources/scheduler/scheduler.js +3 -3
- package/dist/resources/scheduler/scheduler.js.map +1 -1
- package/dist/resources/search.d.ts +10 -4
- package/dist/resources/search.js +160 -40
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/tracked.d.ts +5 -1
- package/dist/resources/tracked.js +74 -23
- package/dist/resources/tracked.js.map +1 -1
- package/dist/security/jsLoader.js +6 -4
- package/dist/security/jsLoader.js.map +1 -1
- package/dist/server/REST.js +33 -2
- package/dist/server/REST.js.map +1 -1
- package/dist/server/http.d.ts +5 -1
- package/dist/server/http.js +34 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/serverHelpers/Headers.d.ts +2 -0
- package/dist/server/serverHelpers/Headers.js +6 -0
- package/dist/server/serverHelpers/Headers.js.map +1 -1
- package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
- package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
- package/dist/server/serverHelpers/Request.d.ts +5 -10
- package/dist/server/serverHelpers/Request.js +38 -136
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
- package/dist/server/serverHelpers/contentTypes.js +189 -15
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/serverUtilities.js +96 -17
- package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
- package/dist/server/storageReclamation.js +1 -1
- package/dist/server/storageReclamation.js.map +1 -1
- package/dist/server/threads/isolatedApplications.d.ts +47 -0
- package/dist/server/threads/isolatedApplications.js +171 -0
- package/dist/server/threads/isolatedApplications.js.map +1 -0
- package/dist/server/threads/logRotationTransport.d.ts +1 -0
- package/dist/server/threads/logRotationTransport.js +33 -0
- package/dist/server/threads/logRotationTransport.js.map +1 -0
- package/dist/server/threads/manageThreads.d.ts +64 -6
- package/dist/server/threads/manageThreads.js +261 -12
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/socketRouter.d.ts +1 -0
- package/dist/server/threads/socketRouter.js +196 -13
- package/dist/server/threads/socketRouter.js.map +1 -1
- package/dist/server/threads/threadServer.js +30 -7
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/utility/errors/hdbError.d.ts +24 -0
- package/dist/utility/errors/hdbError.js +58 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/hdbTerms.d.ts +2 -0
- package/dist/utility/hdbTerms.js +2 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/utility/logging/harper_logger.js +217 -38
- package/dist/utility/logging/harper_logger.js.map +1 -1
- package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
- package/dist/utility/logging/logGenerationCoordinator.js +184 -0
- package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
- package/dist/utility/logging/logRotation.d.ts +46 -0
- package/dist/utility/logging/logRotation.js +365 -0
- package/dist/utility/logging/logRotation.js.map +1 -0
- package/dist/utility/logging/logRotator.d.ts +1 -1
- package/dist/utility/logging/logRotator.js +172 -92
- package/dist/utility/logging/logRotator.js.map +1 -1
- package/dist/utility/npmUtilities.js +6 -4
- package/dist/utility/npmUtilities.js.map +1 -1
- package/dist/utility/watcherFallback.d.ts +0 -45
- package/dist/utility/watcherFallback.js +1 -125
- package/dist/utility/watcherFallback.js.map +1 -1
- package/dist/validation/configValidator.js +6 -3
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +6 -0
- package/json/systemSchema.json +3 -0
- package/npm-shrinkwrap.json +131 -41
- package/package.json +10 -3
- package/resources/DESIGN.md +124 -19
- package/resources/DatabaseTransaction.ts +230 -17
- package/resources/LMDBTransaction.ts +21 -3
- package/resources/PrimaryRocksDatabase.ts +20 -7
- package/resources/RecordEncoder.ts +25 -5
- package/resources/Resource.ts +97 -13
- package/resources/ResourceInterface.ts +8 -0
- package/resources/RocksIndexStore.ts +2 -1
- package/resources/RocksTransactionLogStore.ts +111 -31
- package/resources/Table.ts +1224 -393
- package/resources/analytics/write.ts +10 -3
- package/resources/auditStore.ts +460 -11
- package/resources/dataLoader.ts +3 -4
- package/resources/databases.ts +610 -146
- package/resources/derivedIndexRegistry.ts +56 -0
- package/resources/derivedIndexRuntime.ts +2292 -0
- package/resources/graphql.ts +3 -2
- package/resources/indexes/HierarchicalNavigableSmallWorld.ts +905 -46
- package/resources/indexes/hnswDerivedIndex.ts +531 -0
- package/resources/indexes/hnswPlaneBinding.ts +174 -0
- package/resources/nodeIdMapping.ts +50 -0
- package/resources/recordLock.ts +173 -7
- package/resources/recordLockCoordinator.ts +3043 -0
- package/resources/replayLogs.ts +5 -0
- package/resources/replicatedApplyFailure.ts +77 -0
- package/resources/scheduler/scheduler.ts +4 -4
- package/resources/search.ts +169 -49
- package/resources/tracked.ts +73 -22
- package/security/jsLoader.ts +6 -4
- package/server/DESIGN.md +11 -0
- package/server/REST.ts +36 -3
- package/server/http.ts +34 -2
- package/server/serverHelpers/Headers.ts +5 -1
- package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
- package/server/serverHelpers/Request.ts +33 -131
- package/server/serverHelpers/contentTypes.ts +188 -15
- package/server/serverHelpers/serverUtilities.ts +143 -24
- package/server/storageReclamation.ts +2 -2
- package/server/threads/isolatedApplications.ts +157 -0
- package/server/threads/logRotationTransport.ts +40 -0
- package/server/threads/manageThreads.js +254 -12
- package/server/threads/socketRouter.ts +217 -11
- package/server/threads/threadServer.js +30 -7
- package/studio/web/assets/{Chat-BnCBegQz.js → Chat-D3j-1yY1.js} +1 -1
- package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-BxJGYcfB.js} +3 -3
- package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-CT55oWOe.js} +1 -1
- package/studio/web/assets/{applications-DHxGi7JH.js → applications-D9Ct9_vm.js} +1 -1
- package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-DV8H7VwA.js} +1 -1
- package/studio/web/assets/{editor-DNcRHK54.js → editor-uatc0unt.js} +1 -1
- package/studio/web/assets/{html-Bdssedlg.js → html-Bm6D6paN.js} +1 -1
- package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CEn7tpLG.js} +1 -1
- package/studio/web/assets/{index-D6sxmFLR.js → index-BIXW6Pu4.js} +5 -5
- package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-UI7L-Vrk.js} +1 -1
- package/studio/web/assets/{javascript-B8meVSTH.js → javascript-CJ0G3AFZ.js} +1 -1
- package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-DQADAYEa.js} +1 -1
- package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-CAQJXWcI.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
- package/studio/web/assets/{notifications-CQf18QKb.js → notifications-BbxTU6Aw.js} +1 -1
- package/studio/web/assets/{notifications-CvZivSbh.js → notifications-Cvb3P1lB.js} +1 -1
- package/studio/web/assets/{profile-DdOwtntb.js → profile-Yyb7gsvL.js} +1 -1
- package/studio/web/assets/{regions-n69fwagr.js → regions-OgjGHlU5.js} +1 -1
- package/studio/web/assets/{register-PfWTCXWB.js → register-6qwNEOY3.js} +2 -2
- package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BilDMtgB.js} +1 -1
- package/studio/web/assets/{setup-CUx_aUDl.js → setup-J6qJ7OIU.js} +2 -2
- package/studio/web/assets/{status-D7BVKqX9.js → status-0RWGcfyD.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-BIn-vErT.js} +1 -1
- package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-DgUXku4d.js} +1 -1
- package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-C9orXcsM.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-BEoXXbUB.js} +1 -1
- package/studio/web/assets/{workers-pR3jRY9D.js → workers-JVzSDmgx.js} +1 -1
- package/studio/web/assets/{xml-2iRnMhQO.js → xml-Cq-S8S4X.js} +1 -1
- package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-sfoRdh1M.js} +1 -1
- package/studio/web/index.html +1 -1
- package/utility/errors/hdbError.ts +54 -0
- package/utility/hdbTerms.ts +2 -0
- package/utility/logging/harper_logger.ts +209 -30
- package/utility/logging/logGenerationCoordinator.ts +196 -0
- package/utility/logging/logRotation.ts +367 -0
- package/utility/logging/logRotator.ts +196 -91
- package/utility/npmUtilities.ts +6 -4
- package/utility/watcherFallback.ts +0 -122
- package/validation/configValidator.ts +6 -3
package/resources/replayLogs.ts
CHANGED
|
@@ -165,6 +165,11 @@ export function replayLogs(rootStore: RocksDatabase, tables: any, electedReplaye
|
|
|
165
165
|
noProgressRun++;
|
|
166
166
|
continue;
|
|
167
167
|
}
|
|
168
|
+
if (type === 'evict') {
|
|
169
|
+
noProgressRun = 0;
|
|
170
|
+
lastProgressTime = performance.now();
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
168
173
|
const Table = tableById.get(tableId);
|
|
169
174
|
if (!Table) {
|
|
170
175
|
// Entry for a table this node no longer has (dropped/foreign). Not an
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { onRemovedDB } from './databases.ts';
|
|
2
|
+
import { logger } from '../utility/logging/logger.ts';
|
|
3
|
+
import { contextStorage } from './transaction.ts';
|
|
4
|
+
|
|
5
|
+
export interface ReplicatedApplyFailure {
|
|
6
|
+
readonly database: string;
|
|
7
|
+
readonly table?: string;
|
|
8
|
+
readonly nodeId: number;
|
|
9
|
+
readonly position: number;
|
|
10
|
+
readonly localTime?: number;
|
|
11
|
+
readonly error: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ReplicatedApplyFailureListener = (failure: ReplicatedApplyFailure) => void | Promise<void>;
|
|
15
|
+
|
|
16
|
+
const listenersByDatabase = new Map<string, Set<ReplicatedApplyFailureListener>>();
|
|
17
|
+
let watchingDatabaseRemoval = false;
|
|
18
|
+
|
|
19
|
+
export function registerReplicatedApplyFailureListener(
|
|
20
|
+
database: string,
|
|
21
|
+
listener: ReplicatedApplyFailureListener
|
|
22
|
+
): void {
|
|
23
|
+
if (!watchingDatabaseRemoval) {
|
|
24
|
+
onRemovedDB((database) => listenersByDatabase.delete(database));
|
|
25
|
+
watchingDatabaseRemoval = true;
|
|
26
|
+
}
|
|
27
|
+
let listeners = listenersByDatabase.get(database);
|
|
28
|
+
if (!listeners) listenersByDatabase.set(database, (listeners = new Set()));
|
|
29
|
+
listeners.add(listener);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function unregisterReplicatedApplyFailureListener(
|
|
33
|
+
database: string,
|
|
34
|
+
listener: ReplicatedApplyFailureListener
|
|
35
|
+
): void {
|
|
36
|
+
const listeners = listenersByDatabase.get(database);
|
|
37
|
+
listeners?.delete(listener);
|
|
38
|
+
if (listeners?.size === 0) listenersByDatabase.delete(database);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function notifyReplicatedApplyFailure(
|
|
42
|
+
database: string,
|
|
43
|
+
event: { table?: string; nodeId?: number; localTime?: number },
|
|
44
|
+
position: number | undefined,
|
|
45
|
+
error: unknown,
|
|
46
|
+
table?: string
|
|
47
|
+
): Promise<void> {
|
|
48
|
+
if (listenersByDatabase.size === 0) return;
|
|
49
|
+
try {
|
|
50
|
+
if (typeof event?.nodeId !== 'number' || typeof position !== 'number') return;
|
|
51
|
+
const listeners = listenersByDatabase.get(database);
|
|
52
|
+
if (!listeners) return;
|
|
53
|
+
const failure: ReplicatedApplyFailure = Object.freeze({
|
|
54
|
+
database,
|
|
55
|
+
table: event.table ?? table,
|
|
56
|
+
nodeId: event.nodeId,
|
|
57
|
+
position,
|
|
58
|
+
localTime: event.localTime,
|
|
59
|
+
error,
|
|
60
|
+
});
|
|
61
|
+
for (const listener of [...listeners]) {
|
|
62
|
+
try {
|
|
63
|
+
await contextStorage.exit(() => listener(failure));
|
|
64
|
+
} catch (error) {
|
|
65
|
+
logListenerFailure(database, error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
logListenerFailure(database, error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function logListenerFailure(database: string, error: unknown): void {
|
|
74
|
+
try {
|
|
75
|
+
logger.error?.('replicated apply failure listener failed', database, error);
|
|
76
|
+
} catch {}
|
|
77
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isAbsolute, join } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { isApplicationPrimaryWorker } from '../../server/threads/manageThreads.js';
|
|
3
3
|
import { ClientError } from '../../utility/errors/hdbError.ts';
|
|
4
4
|
import { convertToMS } from '../../utility/common_utils.ts';
|
|
5
5
|
import harperLogger from '../../utility/logging/harper_logger.ts';
|
|
@@ -94,12 +94,12 @@ export async function handleApplication(scope): Promise<void> {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// Activation gates: one worker owns scheduling for the whole node
|
|
97
|
-
// (
|
|
97
|
+
// (the application's primary worker is correct in every threading mode, including
|
|
98
98
|
// threads:0 where the main thread acts as worker 0), and a deploy
|
|
99
99
|
// pre-flight validation scope must never touch the live engine — it can
|
|
100
100
|
// share a running component's identity, so registering from it would
|
|
101
|
-
// displace the real component's jobs
|
|
102
|
-
if (
|
|
101
|
+
// displace the real component's jobs.
|
|
102
|
+
if (!isApplicationPrimaryWorker(scope.applicationScope?.name)) {
|
|
103
103
|
schedulerLogger.debug?.('Scheduler config validated; activation skipped on non-primary worker');
|
|
104
104
|
return;
|
|
105
105
|
}
|
package/resources/search.ts
CHANGED
|
@@ -6,9 +6,10 @@ import { INVALIDATED, EVICTED, freezeRecord } from './Table.ts';
|
|
|
6
6
|
import type { DirectCondition, Id } from './ResourceInterface.ts';
|
|
7
7
|
import { RequestTarget } from './RequestTarget.ts';
|
|
8
8
|
import { lastMetadata } from './RecordEncoder.ts';
|
|
9
|
-
import { writeKeyId } from './DatabaseTransaction.ts';
|
|
9
|
+
import { writeKeyId, getReadTransactionGuard } from './DatabaseTransaction.ts';
|
|
10
10
|
import { recordAction } from './analytics/write';
|
|
11
11
|
import { RocksDatabase } from '@harperfast/rocksdb-js';
|
|
12
|
+
import { appendHeader } from '../server/serverHelpers/Headers.ts';
|
|
12
13
|
|
|
13
14
|
// these are ratios/percentages of overall table size
|
|
14
15
|
const OPEN_RANGE_ESTIMATE = 0.3;
|
|
@@ -141,16 +142,13 @@ export function executeConditions(
|
|
|
141
142
|
filtered
|
|
142
143
|
// recordAccess intentionally omitted: guards run once, at the top level (see above).
|
|
143
144
|
);
|
|
144
|
-
return searchByIndex(
|
|
145
|
-
|
|
146
|
-
txn,
|
|
147
|
-
condition.descending || request.reverse === true,
|
|
148
|
-
table,
|
|
149
|
-
request.allowFullScan,
|
|
145
|
+
return searchByIndex(condition, txn, condition.descending || request.reverse === true, table, {
|
|
146
|
+
allowFullScan: request.allowFullScan,
|
|
150
147
|
filtered,
|
|
151
148
|
context,
|
|
152
|
-
|
|
153
|
-
|
|
149
|
+
minResults:
|
|
150
|
+
request.limit === 0 ? 0 : request.limit !== undefined ? (request.offset || 0) + request.limit : undefined,
|
|
151
|
+
});
|
|
154
152
|
}
|
|
155
153
|
function mapConditionsToFilters(conditions, intersection, estimatedIncomingCount) {
|
|
156
154
|
return conditions
|
|
@@ -269,27 +267,35 @@ function distinctRecords(entries: any): AsyncIterable<Id> {
|
|
|
269
267
|
}
|
|
270
268
|
|
|
271
269
|
/**
|
|
272
|
-
* Search for records or keys, based on the search condition, using an index if available
|
|
270
|
+
* Search for records or keys, based on the search condition, using an index if available. The four
|
|
271
|
+
* leading parameters are required at every call site; everything optional is named, so a new
|
|
272
|
+
* capability can be added without shifting positions at callers that don't use it.
|
|
273
273
|
* @param searchCondition
|
|
274
274
|
* @param transaction
|
|
275
275
|
* @param reverse
|
|
276
276
|
* @param Table
|
|
277
|
-
* @param
|
|
278
|
-
* @param filtered
|
|
277
|
+
* @param options
|
|
279
278
|
*/
|
|
280
279
|
export function searchByIndex(
|
|
281
280
|
searchCondition: DirectCondition,
|
|
282
281
|
transaction: any,
|
|
283
282
|
reverse: boolean,
|
|
284
283
|
Table: any,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
284
|
+
options: {
|
|
285
|
+
allowFullScan?: boolean;
|
|
286
|
+
filtered?: any;
|
|
287
|
+
context?: any;
|
|
288
|
+
// How many rows the query will ultimately consume (offset + limit), when it is bounded. An
|
|
289
|
+
// approximate index returns a fixed-size candidate list, so without this a query asking for more
|
|
290
|
+
// rows than that list holds silently gets a short result set. Only custom indexes read it.
|
|
291
|
+
minResults?: number;
|
|
292
|
+
} = {}
|
|
292
293
|
): AsyncIterable<Id | { key: Id; value: any }> {
|
|
294
|
+
// A stale positional caller passes `allowFullScan` here. Type checking only covers .ts callers, so
|
|
295
|
+
// fail loud rather than silently reading every option as undefined.
|
|
296
|
+
if (typeof options !== 'object' || options === null)
|
|
297
|
+
throw new TypeError('searchByIndex: the 5th argument is an options object (#2165), not a positional value');
|
|
298
|
+
const { allowFullScan, filtered, context, minResults } = options;
|
|
293
299
|
let attribute_name = searchCondition[0] ?? searchCondition.attribute;
|
|
294
300
|
let value = searchCondition[1] ?? searchCondition.value;
|
|
295
301
|
const comparator = searchCondition.comparator;
|
|
@@ -323,8 +329,7 @@ export function searchByIndex(
|
|
|
323
329
|
transaction,
|
|
324
330
|
reverse,
|
|
325
331
|
relatedTable,
|
|
326
|
-
allowFullScan,
|
|
327
|
-
joined
|
|
332
|
+
{ allowFullScan, filtered: joined }
|
|
328
333
|
);
|
|
329
334
|
if (attribute.relationship.to) {
|
|
330
335
|
// this is one-to-many or many-to-many, so we need to track the filtering of related entries that match
|
|
@@ -341,8 +346,7 @@ export function searchByIndex(
|
|
|
341
346
|
transaction,
|
|
342
347
|
reverse,
|
|
343
348
|
Table,
|
|
344
|
-
allowFullScan,
|
|
345
|
-
joined
|
|
349
|
+
{ allowFullScan, filtered: joined }
|
|
346
350
|
);
|
|
347
351
|
};
|
|
348
352
|
if (attribute.elements) {
|
|
@@ -550,26 +554,117 @@ export function searchByIndex(
|
|
|
550
554
|
// exploring until it has enough MATCHING results, rather than post-filtering an under-filled
|
|
551
555
|
// candidate set. Only indexes that opt in (filteredSearch) receive it; others post-filter as before.
|
|
552
556
|
const recordFilter = index.customIndex.filteredSearch ? searchCondition.recordFilter : undefined;
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
557
|
+
const waiting = index.customIndex.filePrimary && searchCondition.waitForIndexMilliseconds > 0;
|
|
558
|
+
const start = waiting ? Promise.withResolvers<void>() : undefined;
|
|
559
|
+
const controller = waiting ? new AbortController() : undefined;
|
|
560
|
+
const checkActive = waiting ? getReadTransactionGuard(transaction) : undefined;
|
|
561
|
+
const signal = waiting
|
|
562
|
+
? context.signal
|
|
563
|
+
? AbortSignal.any([context.signal, controller.signal])
|
|
564
|
+
: controller.signal
|
|
565
|
+
: undefined;
|
|
566
|
+
const searchContext = waiting
|
|
567
|
+
? Object.create(context, {
|
|
568
|
+
signal: { value: signal },
|
|
569
|
+
indexSearchStart: { value: start.promise.then(() => checkActive?.()) },
|
|
570
|
+
})
|
|
571
|
+
: context;
|
|
572
|
+
const searched = index.customIndex.search(searchCondition, searchContext, {
|
|
573
|
+
filter:
|
|
574
|
+
waiting && recordFilter
|
|
575
|
+
? (id) => {
|
|
576
|
+
if (signal.aborted) return false;
|
|
577
|
+
checkActive?.();
|
|
578
|
+
return recordFilter(id);
|
|
579
|
+
}
|
|
580
|
+
: recordFilter,
|
|
581
|
+
minResults,
|
|
567
582
|
});
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
583
|
+
const coverage = (searched as any).indexCoverage;
|
|
584
|
+
if (!waiting && coverage && context?.responseHeaders) {
|
|
585
|
+
appendHeader(
|
|
586
|
+
context.responseHeaders,
|
|
587
|
+
'Harper-Index-Coverage',
|
|
588
|
+
`${coverage.state}; lag=${coverage.lagUpperBoundMilliseconds}; tolerance=${coverage.maxLagMilliseconds}`,
|
|
589
|
+
true
|
|
590
|
+
);
|
|
591
|
+
appendHeader(context.responseHeaders, 'Access-Control-Expose-Headers', 'Harper-Index-Coverage', true);
|
|
592
|
+
}
|
|
593
|
+
const processEntries = (entries: any[]) => {
|
|
594
|
+
const loaded = entries
|
|
595
|
+
.map((entry) => {
|
|
596
|
+
// if the custom index returns an entry with metadata, merge it with the loaded entry
|
|
597
|
+
if (typeof entry === 'object' && entry) {
|
|
598
|
+
const { key, ...otherProps } = entry;
|
|
599
|
+
if (key == null) return SKIP; // primaryKey missing from HNSW node — skip rather than crash
|
|
600
|
+
const loadedEntry = Table.primaryStore.getEntry(key, {
|
|
601
|
+
transaction: waiting ? transaction : context && Table._readTxnForContext(context),
|
|
602
|
+
});
|
|
603
|
+
if (!loadedEntry) return SKIP; // record was deleted/expired or not yet visible
|
|
604
|
+
freezeRecord(loadedEntry?.value);
|
|
605
|
+
recordRead(loadedEntry);
|
|
606
|
+
return { ...otherProps, ...loadedEntry };
|
|
607
|
+
}
|
|
608
|
+
return entry;
|
|
609
|
+
})
|
|
610
|
+
.filter((entry) => entry !== SKIP);
|
|
611
|
+
if (index.customIndex.rescoreResults) {
|
|
612
|
+
const rescored = index.customIndex.rescoreResults(loaded, searchCondition, comparator, attribute_name);
|
|
613
|
+
if (rescored != null) return rescored as any;
|
|
614
|
+
}
|
|
615
|
+
return loaded;
|
|
616
|
+
};
|
|
617
|
+
if (typeof (searched as any)?.then === 'function') {
|
|
618
|
+
let settled = false;
|
|
619
|
+
const pending = waiting
|
|
620
|
+
? (searched as Promise<any[]>)
|
|
621
|
+
.then((entries) => {
|
|
622
|
+
if (controller.signal.aborted) return [];
|
|
623
|
+
if (signal.aborted) throw signal.reason ?? new Error('Index search aborted', { cause: signal.reason });
|
|
624
|
+
checkActive?.();
|
|
625
|
+
return processEntries(entries);
|
|
626
|
+
})
|
|
627
|
+
.finally(() => {
|
|
628
|
+
settled = true;
|
|
629
|
+
})
|
|
630
|
+
: (searched as Promise<any[]>).then(processEntries);
|
|
631
|
+
// A consumer may abandon this lazy iterable without calling next().
|
|
632
|
+
pending.catch(() => {});
|
|
633
|
+
const results: any = new ExtendedIterable();
|
|
634
|
+
results.iterate = (options?: { async?: boolean }) => {
|
|
635
|
+
if (!options?.async) {
|
|
636
|
+
throw new Error(
|
|
637
|
+
'This index resolves search results asynchronously; the results must be consumed with async iteration'
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
// Overlapping next() calls share one cursor and cannot duplicate its first entry.
|
|
641
|
+
const iteratorPromise = pending.then((entries) => entries[Symbol.iterator]());
|
|
642
|
+
iteratorPromise.catch(() => {});
|
|
643
|
+
let closed = false;
|
|
644
|
+
return {
|
|
645
|
+
next() {
|
|
646
|
+
if (closed) return Promise.resolve({ done: true, value: undefined });
|
|
647
|
+
start?.resolve();
|
|
648
|
+
return iteratorPromise.then((inner) => {
|
|
649
|
+
if (closed) return { done: true, value: undefined };
|
|
650
|
+
checkActive?.();
|
|
651
|
+
return inner.next();
|
|
652
|
+
});
|
|
653
|
+
},
|
|
654
|
+
return(value?: any) {
|
|
655
|
+
closed = true;
|
|
656
|
+
if (!settled) controller?.abort();
|
|
657
|
+
iteratorPromise.then(
|
|
658
|
+
(inner) => (inner as any).return?.(value),
|
|
659
|
+
() => {}
|
|
660
|
+
);
|
|
661
|
+
return Promise.resolve({ done: true, value });
|
|
662
|
+
},
|
|
663
|
+
};
|
|
664
|
+
};
|
|
665
|
+
return results;
|
|
571
666
|
}
|
|
572
|
-
return
|
|
667
|
+
return processEntries(searched);
|
|
573
668
|
}
|
|
574
669
|
const scanned = index.getRange(rangeOptions).map(
|
|
575
670
|
filter
|
|
@@ -1103,7 +1198,7 @@ export function filterByType(searchCondition, Table, context, filtered, isPrimar
|
|
|
1103
1198
|
canUseIndex =
|
|
1104
1199
|
canUseIndex && // is it a comparator that makes sense to use index
|
|
1105
1200
|
!isPrimaryKey && // no need to use index for primary keys, since we will be iterating over the primary keys
|
|
1106
|
-
Table
|
|
1201
|
+
!!(Table && usableIndex(Table, attribute)) && // is there a usable (complete) index for this attribute
|
|
1107
1202
|
estimatedIncomingCount > 3; // do we have a valid estimate of multiple incoming records (that is worth using an index for)
|
|
1108
1203
|
if (canUseIndex) {
|
|
1109
1204
|
if (searchCondition.estimated_count == undefined) estimateCondition(Table)(searchCondition);
|
|
@@ -1169,7 +1264,7 @@ export function filterByType(searchCondition, Table, context, filtered, isPrimar
|
|
|
1169
1264
|
function estimateRangeCondition(table, condition, searchType, fraction) {
|
|
1170
1265
|
const attributeName = condition[0] ?? condition.attribute;
|
|
1171
1266
|
const isPrimaryKey = attributeName === table.primaryKey;
|
|
1172
|
-
const store = isPrimaryKey ? table.primaryStore : table
|
|
1267
|
+
const store = isPrimaryKey ? table.primaryStore : usableIndex(table, attributeName);
|
|
1173
1268
|
// LMDB-backed and custom index stores do not implement estimateCount
|
|
1174
1269
|
if (typeof store?.estimateCount !== 'function') return undefined;
|
|
1175
1270
|
let value = condition[1] ?? condition.value;
|
|
@@ -1245,6 +1340,28 @@ function estimateRangeCondition(table, condition, searchType, fraction) {
|
|
|
1245
1340
|
return Math.max(1, Math.round(confidence * count + (1 - confidence) * heuristic));
|
|
1246
1341
|
}
|
|
1247
1342
|
|
|
1343
|
+
/** The index a condition can be driven by; searchByIndex refuses a rebuilding one, so it reads as absent. */
|
|
1344
|
+
function usableIndex(table, attributeName): any {
|
|
1345
|
+
const index = attributeName == null ? undefined : table.indices[attributeName];
|
|
1346
|
+
return index?.isIndexing ? undefined : index;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
/**
|
|
1350
|
+
* True when an index this attribute would have to be driven by is still being built, following a
|
|
1351
|
+
* relationship path to the local join index and on to the related table's leaf index.
|
|
1352
|
+
*/
|
|
1353
|
+
function drivesRebuildingIndex(table, attributeName, relationshipOffset = 0): boolean {
|
|
1354
|
+
if (!Array.isArray(attributeName))
|
|
1355
|
+
return attributeName != null && attributeName !== table.primaryKey && !!table.indices[attributeName]?.isIndexing;
|
|
1356
|
+
if (relationshipOffset >= attributeName.length - 1)
|
|
1357
|
+
return drivesRebuildingIndex(table, attributeName[relationshipOffset]);
|
|
1358
|
+
const attribute = findAttribute(table.attributes, attributeName[relationshipOffset]);
|
|
1359
|
+
if (!attribute) return false;
|
|
1360
|
+
if (table.indices[attribute.relationship?.from]?.isIndexing) return true;
|
|
1361
|
+
const relatedTable = attribute.definition?.tableClass || attribute.elements?.definition?.tableClass;
|
|
1362
|
+
return !!relatedTable && drivesRebuildingIndex(relatedTable, attributeName, relationshipOffset + 1);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1248
1365
|
export function estimateCondition(table) {
|
|
1249
1366
|
function estimateConditionForTable(condition) {
|
|
1250
1367
|
if (condition.estimated_count === undefined) {
|
|
@@ -1274,7 +1391,10 @@ export function estimateCondition(table) {
|
|
|
1274
1391
|
// skip if it is cached
|
|
1275
1392
|
let searchType = condition.comparator || condition.search_type;
|
|
1276
1393
|
searchType = ALTERNATE_COMPARATOR_NAMES[searchType] || searchType;
|
|
1277
|
-
|
|
1394
|
+
// before comparator dispatch: several branches fall back to a finite table-fraction heuristic
|
|
1395
|
+
if (drivesRebuildingIndex(table, condition[0] ?? condition.attribute)) {
|
|
1396
|
+
condition.estimated_count = Infinity;
|
|
1397
|
+
} else if (condition.negated) {
|
|
1278
1398
|
// a negated condition always executes as a full scan (searchByIndex forces
|
|
1279
1399
|
// needFullScan), so follow the filter-only convention used by contains/ends_with:
|
|
1280
1400
|
// estimate Infinity so its positive-range estimate can never win the driving-condition
|
|
@@ -1307,12 +1427,12 @@ export function estimateCondition(table) {
|
|
|
1307
1427
|
}
|
|
1308
1428
|
} else {
|
|
1309
1429
|
// we only attempt to estimate count on equals operator because that's really all that LMDB supports (some other key-value stores like libmdbx could be considered if we need to do estimated counts of ranges at some point)
|
|
1310
|
-
const index = table
|
|
1430
|
+
const index = usableIndex(table, attribute_name);
|
|
1311
1431
|
condition.estimated_count = index ? index.getValuesCount(condition[1] ?? condition.value) : Infinity;
|
|
1312
1432
|
}
|
|
1313
1433
|
} else if (searchType === 'contains' || searchType === 'ends_with' || searchType === 'ne') {
|
|
1314
1434
|
const attribute_name = condition[0] ?? condition.attribute;
|
|
1315
|
-
const index = table
|
|
1435
|
+
const index = usableIndex(table, attribute_name);
|
|
1316
1436
|
if (condition.value === null && searchType === 'ne') {
|
|
1317
1437
|
condition.estimated_count = Math.max(
|
|
1318
1438
|
estimatedEntryCount(table.primaryStore) - (index ? index.getValuesCount(null) : 0),
|
|
@@ -1321,7 +1441,7 @@ export function estimateCondition(table) {
|
|
|
1321
1441
|
} else condition.estimated_count = Infinity;
|
|
1322
1442
|
} else if (searchType === 'in') {
|
|
1323
1443
|
const attribute_name = condition[0] ?? condition.attribute;
|
|
1324
|
-
const index = table
|
|
1444
|
+
const index = usableIndex(table, attribute_name);
|
|
1325
1445
|
if (Array.isArray(condition.value) && index) {
|
|
1326
1446
|
// Sum of per-value matches (over-counts duplicates but is a fine ceiling)
|
|
1327
1447
|
let estimate = 0;
|
|
@@ -1342,7 +1462,7 @@ export function estimateCondition(table) {
|
|
|
1342
1462
|
BETWEEN_ESTIMATE * estimatedEntryCount(table.primaryStore) + 1;
|
|
1343
1463
|
else if (searchType === 'sort') {
|
|
1344
1464
|
const attribute_name = condition[0] ?? condition.attribute;
|
|
1345
|
-
const index = table
|
|
1465
|
+
const index = usableIndex(table, attribute_name);
|
|
1346
1466
|
if (index?.customIndex?.estimateCountAsSort)
|
|
1347
1467
|
// allow custom index to define its own estimation of counts
|
|
1348
1468
|
condition.estimated_count = index.customIndex.estimateCountAsSort(condition);
|
|
@@ -1350,7 +1470,7 @@ export function estimateCondition(table) {
|
|
|
1350
1470
|
} else {
|
|
1351
1471
|
// for the search types that use the broadest range, try do them last
|
|
1352
1472
|
const attribute_name = condition[0] ?? condition.attribute;
|
|
1353
|
-
const index = table
|
|
1473
|
+
const index = usableIndex(table, attribute_name);
|
|
1354
1474
|
if (index?.customIndex?.estimateCount)
|
|
1355
1475
|
// allow custom index to define its own estimation of counts
|
|
1356
1476
|
condition.estimated_count = index.customIndex.estimateCount(condition.value);
|
package/resources/tracked.ts
CHANGED
|
@@ -3,7 +3,15 @@ import * as crdtOperations from './crdt.ts';
|
|
|
3
3
|
import { Blob } from './blob.ts';
|
|
4
4
|
import * as harperLogger from '../utility/logging/harper_logger.ts';
|
|
5
5
|
|
|
6
|
+
export const ASSERT_TRACKED_WRITABLE = Symbol('assert-tracked-writable');
|
|
7
|
+
export const GET_TRACKED_WRITE_GENERATION = Symbol('get-tracked-write-generation');
|
|
8
|
+
|
|
9
|
+
function assertWritable(target, generation?) {
|
|
10
|
+
target[ASSERT_TRACKED_WRITABLE]?.(generation);
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
function getChanges(target) {
|
|
14
|
+
assertWritable(target);
|
|
7
15
|
let changes = target.getChanges();
|
|
8
16
|
if (!changes) {
|
|
9
17
|
changes = Object.create(null);
|
|
@@ -49,6 +57,7 @@ export function assignTrackedAccessors(Target, typeDef, useFullPropertyProxy = f
|
|
|
49
57
|
// still load), this is a user assignment to a resolved attribute, and a read-only resolver
|
|
50
58
|
// (a scalar @computed has no attribute.set) has nowhere to put it (harper#2359).
|
|
51
59
|
if (!attribute.set) throw new ClientError(`${name} is a computed attribute and can not be assigned`);
|
|
60
|
+
assertWritable(this);
|
|
52
61
|
return attribute.set(this, related);
|
|
53
62
|
},
|
|
54
63
|
configurable: true,
|
|
@@ -188,7 +197,7 @@ export function assignTrackedAccessors(Target, typeDef, useFullPropertyProxy = f
|
|
|
188
197
|
}
|
|
189
198
|
const sourceValue = this.getRecord()?.[name];
|
|
190
199
|
if (sourceValue && typeof sourceValue === 'object') {
|
|
191
|
-
const updatedValue = trackObject(sourceValue, attribute);
|
|
200
|
+
const updatedValue = trackObject(sourceValue, attribute, this);
|
|
192
201
|
if (updatedValue) {
|
|
193
202
|
if (!changes) {
|
|
194
203
|
this._setChanges((changes = Object.create(null)));
|
|
@@ -288,7 +297,7 @@ function getProxiedProperty(target, name, receiver) {
|
|
|
288
297
|
}
|
|
289
298
|
const sourceValue = receiver.getRecord?.()?.[name];
|
|
290
299
|
if (sourceValue && typeof sourceValue === 'object') {
|
|
291
|
-
const updatedValue = trackObject(sourceValue);
|
|
300
|
+
const updatedValue = trackObject(sourceValue, undefined, receiver);
|
|
292
301
|
if (updatedValue) {
|
|
293
302
|
if (!changes) {
|
|
294
303
|
changes = Object.create(null);
|
|
@@ -303,6 +312,7 @@ function getProxiedProperty(target, name, receiver) {
|
|
|
303
312
|
}
|
|
304
313
|
function setProxiedProperty(target: any, name: string | symbol, value, receiver) {
|
|
305
314
|
if (typeof name === 'string') {
|
|
315
|
+
assertWritable(receiver);
|
|
306
316
|
let changes = receiver.getChanges?.();
|
|
307
317
|
if (!changes) {
|
|
308
318
|
changes = {};
|
|
@@ -315,7 +325,7 @@ function setProxiedProperty(target: any, name: string | symbol, value, receiver)
|
|
|
315
325
|
return true;
|
|
316
326
|
}
|
|
317
327
|
|
|
318
|
-
function trackObject(sourceObject: any, typeDef?: any) {
|
|
328
|
+
function trackObject(sourceObject: any, typeDef?: any, writableOwner?: any) {
|
|
319
329
|
// lazily instantiate in case of recursive structures
|
|
320
330
|
let TrackedObject;
|
|
321
331
|
switch (sourceObject.constructor) {
|
|
@@ -326,18 +336,41 @@ function trackObject(sourceObject: any, typeDef?: any) {
|
|
|
326
336
|
typeDef.TrackedObject = TrackedObject = class extends GenericTrackedObject {};
|
|
327
337
|
assignTrackedAccessors(TrackedObject, typeDef);
|
|
328
338
|
}
|
|
329
|
-
return new TrackedObject(sourceObject);
|
|
339
|
+
return new TrackedObject(sourceObject, writableOwner);
|
|
330
340
|
} else {
|
|
331
|
-
return new GenericTrackedObject(sourceObject);
|
|
341
|
+
return new GenericTrackedObject(sourceObject, writableOwner);
|
|
332
342
|
}
|
|
333
343
|
case Array:
|
|
334
|
-
const trackedArray = new TrackedArray(sourceObject.length, sourceObject);
|
|
344
|
+
const trackedArray = new TrackedArray(sourceObject.length, sourceObject, writableOwner);
|
|
335
345
|
for (let i = 0, l = sourceObject.length; i < l; i++) {
|
|
336
346
|
let element = sourceObject[i];
|
|
337
|
-
if (element && typeof element === 'object') element = trackObject(element, typeDef?.elements);
|
|
347
|
+
if (element && typeof element === 'object') element = trackObject(element, typeDef?.elements, trackedArray);
|
|
338
348
|
trackedArray[i] = element;
|
|
339
349
|
}
|
|
340
|
-
return trackedArray;
|
|
350
|
+
if (!writableOwner) return trackedArray;
|
|
351
|
+
return new Proxy(trackedArray, {
|
|
352
|
+
set(target, name, value) {
|
|
353
|
+
if (typeof name === 'string') {
|
|
354
|
+
assertWritable(target);
|
|
355
|
+
target[HAS_ARRAY_CHANGES] = true;
|
|
356
|
+
}
|
|
357
|
+
return Reflect.set(target, name, value, target);
|
|
358
|
+
},
|
|
359
|
+
defineProperty(target, name, descriptor) {
|
|
360
|
+
if (typeof name === 'string') {
|
|
361
|
+
assertWritable(target);
|
|
362
|
+
target[HAS_ARRAY_CHANGES] = true;
|
|
363
|
+
}
|
|
364
|
+
return Reflect.defineProperty(target, name, descriptor);
|
|
365
|
+
},
|
|
366
|
+
deleteProperty(target, name) {
|
|
367
|
+
if (typeof name === 'string') {
|
|
368
|
+
assertWritable(target);
|
|
369
|
+
target[HAS_ARRAY_CHANGES] = true;
|
|
370
|
+
}
|
|
371
|
+
return Reflect.deleteProperty(target, name);
|
|
372
|
+
},
|
|
373
|
+
});
|
|
341
374
|
// any other objects (like Date) are left unchanged
|
|
342
375
|
default:
|
|
343
376
|
return sourceObject;
|
|
@@ -346,10 +379,20 @@ function trackObject(sourceObject: any, typeDef?: any) {
|
|
|
346
379
|
export class GenericTrackedObject<T extends object = any> {
|
|
347
380
|
#record: T;
|
|
348
381
|
#changes: Partial<T>;
|
|
349
|
-
|
|
382
|
+
#writableOwner: any;
|
|
383
|
+
#writableGeneration: any;
|
|
384
|
+
constructor(sourceObject?: GenericTrackedObject<T> | T, writableOwner?: any) {
|
|
350
385
|
if ((sourceObject as GenericTrackedObject<T>)?.getRecord)
|
|
351
386
|
throw new Error('Can not track an already tracked object, check for circular references');
|
|
352
387
|
this.#record = sourceObject as any;
|
|
388
|
+
this.#writableOwner = writableOwner;
|
|
389
|
+
this.#writableGeneration = writableOwner?.[GET_TRACKED_WRITE_GENERATION]?.();
|
|
390
|
+
}
|
|
391
|
+
[ASSERT_TRACKED_WRITABLE]() {
|
|
392
|
+
this.#writableOwner?.[ASSERT_TRACKED_WRITABLE]?.(this.#writableGeneration);
|
|
393
|
+
}
|
|
394
|
+
[GET_TRACKED_WRITE_GENERATION]() {
|
|
395
|
+
return this.#writableGeneration;
|
|
353
396
|
}
|
|
354
397
|
getRecord(): T {
|
|
355
398
|
return this.#record;
|
|
@@ -404,18 +447,13 @@ export function updateAndFreeze(target, changes = target.getChanges?.()) {
|
|
|
404
447
|
let mergedUpdatedObject: any;
|
|
405
448
|
if (!target) return changes;
|
|
406
449
|
if (target.getRecord && target.constructor === Array && !Object.isFrozen(target)) {
|
|
407
|
-
|
|
408
|
-
|
|
450
|
+
if (!hasChanges(target)) return (target as any).getRecord();
|
|
451
|
+
// Materialize a plain array so the stored value retains neither the mutation proxy nor its owner.
|
|
452
|
+
mergedUpdatedObject = new Array(target.length);
|
|
409
453
|
for (let i = 0, l = target.length; i < l; i++) {
|
|
410
454
|
let value = target[i];
|
|
411
455
|
if (value && typeof value === 'object') {
|
|
412
|
-
|
|
413
|
-
if (newValue !== value && mergedUpdatedObject === target) {
|
|
414
|
-
// if we need to make any changes to the user's array, we make a copy so we don't modify
|
|
415
|
-
// an array that the user may be using with transient properties
|
|
416
|
-
mergedUpdatedObject = target.slice(0);
|
|
417
|
-
}
|
|
418
|
-
value = newValue;
|
|
456
|
+
value = updateAndFreeze(value);
|
|
419
457
|
}
|
|
420
458
|
mergedUpdatedObject[i] = value;
|
|
421
459
|
}
|
|
@@ -493,15 +531,28 @@ export function hasChanges(target) {
|
|
|
493
531
|
}
|
|
494
532
|
|
|
495
533
|
const HAS_ARRAY_CHANGES = Symbol.for('has-array-changes');
|
|
534
|
+
const TRACKED_ARRAY_RECORD = Symbol('tracked-array-record');
|
|
535
|
+
const TRACKED_WRITABLE_OWNER = Symbol('tracked-writable-owner');
|
|
536
|
+
const TRACKED_WRITABLE_GENERATION = Symbol('tracked-writable-generation');
|
|
496
537
|
class TrackedArray extends Array {
|
|
497
|
-
|
|
538
|
+
[TRACKED_ARRAY_RECORD]: any;
|
|
539
|
+
[TRACKED_WRITABLE_OWNER]: any;
|
|
540
|
+
[TRACKED_WRITABLE_GENERATION]: any;
|
|
498
541
|
[HAS_ARRAY_CHANGES]: boolean;
|
|
499
|
-
constructor(length, record) {
|
|
542
|
+
constructor(length, record, writableOwner) {
|
|
500
543
|
super(length);
|
|
501
|
-
this
|
|
544
|
+
this[TRACKED_ARRAY_RECORD] = record;
|
|
545
|
+
this[TRACKED_WRITABLE_OWNER] = writableOwner;
|
|
546
|
+
this[TRACKED_WRITABLE_GENERATION] = writableOwner?.[GET_TRACKED_WRITE_GENERATION]?.();
|
|
547
|
+
}
|
|
548
|
+
[ASSERT_TRACKED_WRITABLE]() {
|
|
549
|
+
this[TRACKED_WRITABLE_OWNER]?.[ASSERT_TRACKED_WRITABLE]?.(this[TRACKED_WRITABLE_GENERATION]);
|
|
550
|
+
}
|
|
551
|
+
[GET_TRACKED_WRITE_GENERATION]() {
|
|
552
|
+
return this[TRACKED_WRITABLE_GENERATION];
|
|
502
553
|
}
|
|
503
554
|
getRecord() {
|
|
504
|
-
return this
|
|
555
|
+
return this[TRACKED_ARRAY_RECORD];
|
|
505
556
|
}
|
|
506
557
|
splice(...args) {
|
|
507
558
|
this[HAS_ARRAY_CHANGES] = true;
|
package/security/jsLoader.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
} from 'node:fs';
|
|
33
33
|
import { EventEmitter } from 'node:events';
|
|
34
34
|
import { whenComponentsLoaded } from '../server/threads/threadServer.js';
|
|
35
|
+
import { thisThreadOwnsApplication } from '../server/threads/isolatedApplications.ts';
|
|
35
36
|
|
|
36
37
|
type Lockdown = 'none' | 'freeze' | 'ses' | 'freeze-after-load';
|
|
37
38
|
const APPLICATIONS_LOCKDOWN: Lockdown = env.get(CONFIG_PARAMS.APPLICATIONS_LOCKDOWN);
|
|
@@ -893,13 +894,14 @@ function scopedDatabaseBindings(scope: ApplicationScope): { databases: any; tabl
|
|
|
893
894
|
}
|
|
894
895
|
|
|
895
896
|
/**
|
|
896
|
-
* `defineTable` for a branched application registers through that application's table
|
|
897
|
-
* a branched name lands in its branch
|
|
897
|
+
* `defineTable` for a branched or isolated application registers through that application's table
|
|
898
|
+
* factory, so a branched name lands in its branch and an isolated schema is tagged with its owner.
|
|
898
899
|
*/
|
|
899
900
|
function scopedDefineTable(scope: ApplicationScope): typeof defineTable {
|
|
900
901
|
const branches = scope.branches;
|
|
901
|
-
|
|
902
|
-
|
|
902
|
+
const isolatedApplicationOwner = thisThreadOwnsApplication(scope.name);
|
|
903
|
+
if (!branches?.size && !isolatedApplicationOwner) return defineTable;
|
|
904
|
+
const declareTable = scopedTableFactory(branches, isolatedApplicationOwner);
|
|
903
905
|
return function (name: string, shape: any, options?: any) {
|
|
904
906
|
return defineTableUsing(declareTable, name, shape, options);
|
|
905
907
|
} as typeof defineTable;
|