@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.
Files changed (291) hide show
  1. package/bin/cliOperations.ts +12 -0
  2. package/bin/restart.ts +66 -6
  3. package/components/Application.ts +1134 -112
  4. package/components/OptionsWatcher.ts +368 -102
  5. package/components/Scope.ts +20 -1
  6. package/components/componentLoader.ts +43 -4
  7. package/components/deploymentOperations.ts +4 -1
  8. package/components/deploymentRecorder.ts +9 -2
  9. package/components/operations.js +284 -52
  10. package/components/operationsValidation.js +49 -2
  11. package/components/packageComponent.ts +25 -1
  12. package/components/requestRestart.ts +11 -0
  13. package/config/RootConfigWatcher.ts +191 -37
  14. package/config/configReadRetry.ts +62 -0
  15. package/config/configUtils.ts +78 -26
  16. package/config/parseConfigFile.ts +34 -0
  17. package/config/readConfigFileSync.ts +44 -0
  18. package/config/watcherArming.ts +59 -0
  19. package/config-root.schema.json +4 -0
  20. package/dataLayer/harperBridge/ResourceBridge.ts +28 -2
  21. package/dist/bin/cliOperations.js +13 -0
  22. package/dist/bin/cliOperations.js.map +1 -1
  23. package/dist/bin/restart.js +42 -6
  24. package/dist/bin/restart.js.map +1 -1
  25. package/dist/components/Application.d.ts +104 -9
  26. package/dist/components/Application.js +954 -102
  27. package/dist/components/Application.js.map +1 -1
  28. package/dist/components/OptionsWatcher.d.ts +4 -1
  29. package/dist/components/OptionsWatcher.js +378 -104
  30. package/dist/components/OptionsWatcher.js.map +1 -1
  31. package/dist/components/Scope.js +15 -1
  32. package/dist/components/Scope.js.map +1 -1
  33. package/dist/components/componentLoader.js +35 -3
  34. package/dist/components/componentLoader.js.map +1 -1
  35. package/dist/components/deploymentOperations.js +4 -1
  36. package/dist/components/deploymentOperations.js.map +1 -1
  37. package/dist/components/deploymentRecorder.d.ts +4 -2
  38. package/dist/components/deploymentRecorder.js +1 -0
  39. package/dist/components/deploymentRecorder.js.map +1 -1
  40. package/dist/components/operations.d.ts +28 -0
  41. package/dist/components/operations.js +263 -54
  42. package/dist/components/operations.js.map +1 -1
  43. package/dist/components/operationsValidation.js +48 -2
  44. package/dist/components/operationsValidation.js.map +1 -1
  45. package/dist/components/packageComponent.js +24 -0
  46. package/dist/components/packageComponent.js.map +1 -1
  47. package/dist/components/requestRestart.d.ts +1 -0
  48. package/dist/components/requestRestart.js +7 -0
  49. package/dist/components/requestRestart.js.map +1 -1
  50. package/dist/config/RootConfigWatcher.d.ts +2 -0
  51. package/dist/config/RootConfigWatcher.js +189 -35
  52. package/dist/config/RootConfigWatcher.js.map +1 -1
  53. package/dist/config/configReadRetry.d.ts +8 -0
  54. package/dist/config/configReadRetry.js +62 -0
  55. package/dist/config/configReadRetry.js.map +1 -0
  56. package/dist/config/configUtils.d.ts +10 -9
  57. package/dist/config/configUtils.js +63 -27
  58. package/dist/config/configUtils.js.map +1 -1
  59. package/dist/config/parseConfigFile.d.ts +4 -0
  60. package/dist/config/parseConfigFile.js +35 -0
  61. package/dist/config/parseConfigFile.js.map +1 -0
  62. package/dist/config/readConfigFileSync.d.ts +1 -0
  63. package/dist/config/readConfigFileSync.js +47 -0
  64. package/dist/config/readConfigFileSync.js.map +1 -0
  65. package/dist/config/watcherArming.d.ts +15 -0
  66. package/dist/config/watcherArming.js +59 -0
  67. package/dist/config/watcherArming.js.map +1 -0
  68. package/dist/dataLayer/harperBridge/ResourceBridge.js +21 -2
  69. package/dist/dataLayer/harperBridge/ResourceBridge.js.map +1 -1
  70. package/dist/index.d.ts +1 -0
  71. package/dist/index.js +4 -1
  72. package/dist/index.js.map +1 -1
  73. package/dist/json/systemSchema.json +3 -0
  74. package/dist/resources/DatabaseTransaction.d.ts +25 -0
  75. package/dist/resources/DatabaseTransaction.js +224 -18
  76. package/dist/resources/DatabaseTransaction.js.map +1 -1
  77. package/dist/resources/LMDBTransaction.d.ts +2 -1
  78. package/dist/resources/LMDBTransaction.js +22 -3
  79. package/dist/resources/LMDBTransaction.js.map +1 -1
  80. package/dist/resources/PrimaryRocksDatabase.js +22 -6
  81. package/dist/resources/PrimaryRocksDatabase.js.map +1 -1
  82. package/dist/resources/RecordEncoder.d.ts +1 -1
  83. package/dist/resources/RecordEncoder.js +21 -5
  84. package/dist/resources/RecordEncoder.js.map +1 -1
  85. package/dist/resources/Resource.js +97 -13
  86. package/dist/resources/Resource.js.map +1 -1
  87. package/dist/resources/ResourceInterface.d.ts +8 -0
  88. package/dist/resources/RocksIndexStore.js +2 -1
  89. package/dist/resources/RocksIndexStore.js.map +1 -1
  90. package/dist/resources/RocksTransactionLogStore.d.ts +10 -0
  91. package/dist/resources/RocksTransactionLogStore.js +104 -33
  92. package/dist/resources/RocksTransactionLogStore.js.map +1 -1
  93. package/dist/resources/Table.d.ts +58 -7
  94. package/dist/resources/Table.js +1095 -346
  95. package/dist/resources/Table.js.map +1 -1
  96. package/dist/resources/analytics/write.js +10 -3
  97. package/dist/resources/analytics/write.js.map +1 -1
  98. package/dist/resources/auditStore.d.ts +170 -0
  99. package/dist/resources/auditStore.js +457 -11
  100. package/dist/resources/auditStore.js.map +1 -1
  101. package/dist/resources/dataLoader.js +3 -4
  102. package/dist/resources/dataLoader.js.map +1 -1
  103. package/dist/resources/databases.d.ts +16 -13
  104. package/dist/resources/databases.js +624 -176
  105. package/dist/resources/databases.js.map +1 -1
  106. package/dist/resources/derivedIndexRegistry.d.ts +5 -0
  107. package/dist/resources/derivedIndexRegistry.js +68 -0
  108. package/dist/resources/derivedIndexRegistry.js.map +1 -0
  109. package/dist/resources/derivedIndexRuntime.d.ts +215 -0
  110. package/dist/resources/derivedIndexRuntime.js +2027 -0
  111. package/dist/resources/derivedIndexRuntime.js.map +1 -0
  112. package/dist/resources/graphql.js +3 -2
  113. package/dist/resources/graphql.js.map +1 -1
  114. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.d.ts +102 -9
  115. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js +848 -39
  116. package/dist/resources/indexes/HierarchicalNavigableSmallWorld.js.map +1 -1
  117. package/dist/resources/indexes/hnswDerivedIndex.d.ts +67 -0
  118. package/dist/resources/indexes/hnswDerivedIndex.js +464 -0
  119. package/dist/resources/indexes/hnswDerivedIndex.js.map +1 -0
  120. package/dist/resources/indexes/hnswPlaneBinding.d.ts +65 -0
  121. package/dist/resources/indexes/hnswPlaneBinding.js +91 -0
  122. package/dist/resources/indexes/hnswPlaneBinding.js.map +1 -0
  123. package/dist/resources/nodeIdMapping.d.ts +5 -0
  124. package/dist/resources/nodeIdMapping.js +49 -0
  125. package/dist/resources/nodeIdMapping.js.map +1 -1
  126. package/dist/resources/recordLock.d.ts +47 -4
  127. package/dist/resources/recordLock.js +138 -7
  128. package/dist/resources/recordLock.js.map +1 -1
  129. package/dist/resources/recordLockCoordinator.d.ts +557 -0
  130. package/dist/resources/recordLockCoordinator.js +2565 -0
  131. package/dist/resources/recordLockCoordinator.js.map +1 -0
  132. package/dist/resources/replayLogs.js +5 -0
  133. package/dist/resources/replayLogs.js.map +1 -1
  134. package/dist/resources/replicatedApplyFailure.d.ts +16 -0
  135. package/dist/resources/replicatedApplyFailure.js +63 -0
  136. package/dist/resources/replicatedApplyFailure.js.map +1 -0
  137. package/dist/resources/scheduler/scheduler.js +3 -3
  138. package/dist/resources/scheduler/scheduler.js.map +1 -1
  139. package/dist/resources/search.d.ts +10 -4
  140. package/dist/resources/search.js +160 -40
  141. package/dist/resources/search.js.map +1 -1
  142. package/dist/resources/tracked.d.ts +5 -1
  143. package/dist/resources/tracked.js +74 -23
  144. package/dist/resources/tracked.js.map +1 -1
  145. package/dist/security/jsLoader.js +6 -4
  146. package/dist/security/jsLoader.js.map +1 -1
  147. package/dist/server/REST.js +33 -2
  148. package/dist/server/REST.js.map +1 -1
  149. package/dist/server/http.d.ts +5 -1
  150. package/dist/server/http.js +34 -2
  151. package/dist/server/http.js.map +1 -1
  152. package/dist/server/serverHelpers/Headers.d.ts +2 -0
  153. package/dist/server/serverHelpers/Headers.js +6 -0
  154. package/dist/server/serverHelpers/Headers.js.map +1 -1
  155. package/dist/server/serverHelpers/NodeAdapterResponse.d.ts +48 -0
  156. package/dist/server/serverHelpers/NodeAdapterResponse.js +220 -0
  157. package/dist/server/serverHelpers/NodeAdapterResponse.js.map +1 -0
  158. package/dist/server/serverHelpers/Request.d.ts +5 -10
  159. package/dist/server/serverHelpers/Request.js +38 -136
  160. package/dist/server/serverHelpers/Request.js.map +1 -1
  161. package/dist/server/serverHelpers/contentTypes.d.ts +2 -0
  162. package/dist/server/serverHelpers/contentTypes.js +189 -15
  163. package/dist/server/serverHelpers/contentTypes.js.map +1 -1
  164. package/dist/server/serverHelpers/serverUtilities.js +96 -17
  165. package/dist/server/serverHelpers/serverUtilities.js.map +1 -1
  166. package/dist/server/storageReclamation.js +1 -1
  167. package/dist/server/storageReclamation.js.map +1 -1
  168. package/dist/server/threads/isolatedApplications.d.ts +47 -0
  169. package/dist/server/threads/isolatedApplications.js +171 -0
  170. package/dist/server/threads/isolatedApplications.js.map +1 -0
  171. package/dist/server/threads/logRotationTransport.d.ts +1 -0
  172. package/dist/server/threads/logRotationTransport.js +33 -0
  173. package/dist/server/threads/logRotationTransport.js.map +1 -0
  174. package/dist/server/threads/manageThreads.d.ts +64 -6
  175. package/dist/server/threads/manageThreads.js +261 -12
  176. package/dist/server/threads/manageThreads.js.map +1 -1
  177. package/dist/server/threads/socketRouter.d.ts +1 -0
  178. package/dist/server/threads/socketRouter.js +196 -13
  179. package/dist/server/threads/socketRouter.js.map +1 -1
  180. package/dist/server/threads/threadServer.js +30 -7
  181. package/dist/server/threads/threadServer.js.map +1 -1
  182. package/dist/utility/errors/hdbError.d.ts +24 -0
  183. package/dist/utility/errors/hdbError.js +58 -1
  184. package/dist/utility/errors/hdbError.js.map +1 -1
  185. package/dist/utility/hdbTerms.d.ts +2 -0
  186. package/dist/utility/hdbTerms.js +2 -0
  187. package/dist/utility/hdbTerms.js.map +1 -1
  188. package/dist/utility/logging/harper_logger.js +217 -38
  189. package/dist/utility/logging/harper_logger.js.map +1 -1
  190. package/dist/utility/logging/logGenerationCoordinator.d.ts +35 -0
  191. package/dist/utility/logging/logGenerationCoordinator.js +184 -0
  192. package/dist/utility/logging/logGenerationCoordinator.js.map +1 -0
  193. package/dist/utility/logging/logRotation.d.ts +46 -0
  194. package/dist/utility/logging/logRotation.js +365 -0
  195. package/dist/utility/logging/logRotation.js.map +1 -0
  196. package/dist/utility/logging/logRotator.d.ts +1 -1
  197. package/dist/utility/logging/logRotator.js +172 -92
  198. package/dist/utility/logging/logRotator.js.map +1 -1
  199. package/dist/utility/npmUtilities.js +6 -4
  200. package/dist/utility/npmUtilities.js.map +1 -1
  201. package/dist/utility/watcherFallback.d.ts +0 -45
  202. package/dist/utility/watcherFallback.js +1 -125
  203. package/dist/utility/watcherFallback.js.map +1 -1
  204. package/dist/validation/configValidator.js +6 -3
  205. package/dist/validation/configValidator.js.map +1 -1
  206. package/index.ts +6 -0
  207. package/json/systemSchema.json +3 -0
  208. package/npm-shrinkwrap.json +131 -41
  209. package/package.json +10 -3
  210. package/resources/DESIGN.md +124 -19
  211. package/resources/DatabaseTransaction.ts +230 -17
  212. package/resources/LMDBTransaction.ts +21 -3
  213. package/resources/PrimaryRocksDatabase.ts +20 -7
  214. package/resources/RecordEncoder.ts +25 -5
  215. package/resources/Resource.ts +97 -13
  216. package/resources/ResourceInterface.ts +8 -0
  217. package/resources/RocksIndexStore.ts +2 -1
  218. package/resources/RocksTransactionLogStore.ts +111 -31
  219. package/resources/Table.ts +1224 -393
  220. package/resources/analytics/write.ts +10 -3
  221. package/resources/auditStore.ts +460 -11
  222. package/resources/dataLoader.ts +3 -4
  223. package/resources/databases.ts +610 -146
  224. package/resources/derivedIndexRegistry.ts +56 -0
  225. package/resources/derivedIndexRuntime.ts +2292 -0
  226. package/resources/graphql.ts +3 -2
  227. package/resources/indexes/HierarchicalNavigableSmallWorld.ts +905 -46
  228. package/resources/indexes/hnswDerivedIndex.ts +531 -0
  229. package/resources/indexes/hnswPlaneBinding.ts +174 -0
  230. package/resources/nodeIdMapping.ts +50 -0
  231. package/resources/recordLock.ts +173 -7
  232. package/resources/recordLockCoordinator.ts +3043 -0
  233. package/resources/replayLogs.ts +5 -0
  234. package/resources/replicatedApplyFailure.ts +77 -0
  235. package/resources/scheduler/scheduler.ts +4 -4
  236. package/resources/search.ts +169 -49
  237. package/resources/tracked.ts +73 -22
  238. package/security/jsLoader.ts +6 -4
  239. package/server/DESIGN.md +11 -0
  240. package/server/REST.ts +36 -3
  241. package/server/http.ts +34 -2
  242. package/server/serverHelpers/Headers.ts +5 -1
  243. package/server/serverHelpers/NodeAdapterResponse.ts +221 -0
  244. package/server/serverHelpers/Request.ts +33 -131
  245. package/server/serverHelpers/contentTypes.ts +188 -15
  246. package/server/serverHelpers/serverUtilities.ts +143 -24
  247. package/server/storageReclamation.ts +2 -2
  248. package/server/threads/isolatedApplications.ts +157 -0
  249. package/server/threads/logRotationTransport.ts +40 -0
  250. package/server/threads/manageThreads.js +254 -12
  251. package/server/threads/socketRouter.ts +217 -11
  252. package/server/threads/threadServer.js +30 -7
  253. package/studio/web/assets/{Chat-BnCBegQz.js → Chat-D3j-1yY1.js} +1 -1
  254. package/studio/web/assets/{FloatingChat-CoDW1ySS.js → FloatingChat-BxJGYcfB.js} +3 -3
  255. package/studio/web/assets/{apiToken-Bwk5BLXW.js → apiToken-CT55oWOe.js} +1 -1
  256. package/studio/web/assets/{applications-DHxGi7JH.js → applications-D9Ct9_vm.js} +1 -1
  257. package/studio/web/assets/{cssMode-s0cWI-_M.js → cssMode-DV8H7VwA.js} +1 -1
  258. package/studio/web/assets/{editor-DNcRHK54.js → editor-uatc0unt.js} +1 -1
  259. package/studio/web/assets/{html-Bdssedlg.js → html-Bm6D6paN.js} +1 -1
  260. package/studio/web/assets/{htmlMode-CoDlJ3fw.js → htmlMode-CEn7tpLG.js} +1 -1
  261. package/studio/web/assets/{index-D6sxmFLR.js → index-BIXW6Pu4.js} +5 -5
  262. package/studio/web/assets/{index.lazy-tmU5BS8s.js → index.lazy-UI7L-Vrk.js} +1 -1
  263. package/studio/web/assets/{javascript-B8meVSTH.js → javascript-CJ0G3AFZ.js} +1 -1
  264. package/studio/web/assets/{jsonMode-DpIPd35T.js → jsonMode-DQADAYEa.js} +1 -1
  265. package/studio/web/assets/{languageServices-C_5FMJzQ.js → languageServices-CAQJXWcI.js} +1 -1
  266. package/studio/web/assets/{lspLanguageFeatures-BIzNBzPK.js → lspLanguageFeatures-CCQ8P5sY.js} +1 -1
  267. package/studio/web/assets/{notifications-CQf18QKb.js → notifications-BbxTU6Aw.js} +1 -1
  268. package/studio/web/assets/{notifications-CvZivSbh.js → notifications-Cvb3P1lB.js} +1 -1
  269. package/studio/web/assets/{profile-DdOwtntb.js → profile-Yyb7gsvL.js} +1 -1
  270. package/studio/web/assets/{regions-n69fwagr.js → regions-OgjGHlU5.js} +1 -1
  271. package/studio/web/assets/{register-PfWTCXWB.js → register-6qwNEOY3.js} +2 -2
  272. package/studio/web/assets/{setComponentFile-Bg6O7X0S.js → setComponentFile-BilDMtgB.js} +1 -1
  273. package/studio/web/assets/{setup-CUx_aUDl.js → setup-J6qJ7OIU.js} +2 -2
  274. package/studio/web/assets/{status-D7BVKqX9.js → status-0RWGcfyD.js} +1 -1
  275. package/studio/web/assets/{toggleHighContrast-DBSyXzMr.js → toggleHighContrast-BIn-vErT.js} +1 -1
  276. package/studio/web/assets/{tsMode-BByKCjBS.js → tsMode-DgUXku4d.js} +1 -1
  277. package/studio/web/assets/{typescript-DDLnLpw9.js → typescript-C9orXcsM.js} +1 -1
  278. package/studio/web/assets/{useEntityRestURL-31CHGaHk.js → useEntityRestURL-BEoXXbUB.js} +1 -1
  279. package/studio/web/assets/{workers-pR3jRY9D.js → workers-JVzSDmgx.js} +1 -1
  280. package/studio/web/assets/{xml-2iRnMhQO.js → xml-Cq-S8S4X.js} +1 -1
  281. package/studio/web/assets/{yaml-Bf92gJpd.js → yaml-sfoRdh1M.js} +1 -1
  282. package/studio/web/index.html +1 -1
  283. package/utility/errors/hdbError.ts +54 -0
  284. package/utility/hdbTerms.ts +2 -0
  285. package/utility/logging/harper_logger.ts +209 -30
  286. package/utility/logging/logGenerationCoordinator.ts +196 -0
  287. package/utility/logging/logRotation.ts +367 -0
  288. package/utility/logging/logRotator.ts +196 -91
  289. package/utility/npmUtilities.ts +6 -4
  290. package/utility/watcherFallback.ts +0 -122
  291. package/validation/configValidator.ts +6 -3
@@ -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 { getWorkerIndex } from '../../server/threads/manageThreads.js';
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
- // (getWorkerIndex() === 0 is correct in every threading mode, including
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 (review finding).
102
- if (getWorkerIndex() !== 0) {
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
  }
@@ -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
- condition,
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
- request.limit !== undefined ? (request.offset || 0) + request.limit : undefined
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 allowFullScan
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
- 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
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 loaded = index.customIndex.search(searchCondition, context, recordFilter, minResults).map((entry) => {
554
- // if the custom index returns an entry with metadata, merge it with the loaded entry
555
- if (typeof entry === 'object' && entry) {
556
- const { key, ...otherProps } = entry;
557
- if (key == null) return SKIP; // primaryKey missing from HNSW node — skip rather than crash
558
- const loadedEntry = Table.primaryStore.getEntry(key, {
559
- transaction: context && Table._readTxnForContext(context),
560
- });
561
- if (!loadedEntry) return SKIP; // record was deleted/expired or not yet visible
562
- freezeRecord(loadedEntry?.value);
563
- recordRead(loadedEntry);
564
- return { ...otherProps, ...loadedEntry };
565
- }
566
- return entry;
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
- if (index.customIndex.rescoreResults) {
569
- const rescored = index.customIndex.rescoreResults(loaded, searchCondition, comparator, attribute_name);
570
- if (rescored != null) return rescored as any;
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 loaded;
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?.indices[attribute] && // is there an index for this attribute
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.indices[attributeName];
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
- if (condition.negated) {
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.indices[attribute_name];
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.indices[attribute_name];
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.indices[attribute_name];
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.indices[attribute_name];
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.indices[attribute_name];
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);
@@ -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
- constructor(sourceObject?: GenericTrackedObject<T> | T) {
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
- // a tracked array, by default we can freeze the tracked array itself
408
- mergedUpdatedObject = target;
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
- const newValue = updateAndFreeze(value);
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
- #record: any;
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.#record = record;
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.#record;
555
+ return this[TRACKED_ARRAY_RECORD];
505
556
  }
506
557
  splice(...args) {
507
558
  this[HAS_ARRAY_CHANGES] = true;
@@ -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 factory, so
897
- * a branched name lands in its branch; an unbranched application gets `defineTable` itself.
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
- if (!branches?.size) return defineTable;
902
- const declareTable = scopedTableFactory(branches);
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;