@harperfast/harper 5.3.0 → 5.3.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/copyDb.ts +5 -1
- package/bin/deployRenderer.ts +3 -0
- package/components/Application.ts +66 -22
- package/components/DESIGN.md +89 -36
- package/components/componentLoader.ts +2 -0
- package/components/deploymentRecorder.ts +18 -1
- package/components/installFingerprint.ts +123 -0
- package/components/mcp/DESIGN.md +4 -4
- package/components/operations.js +29 -0
- package/config/DESIGN.md +5 -5
- package/dataLayer/DESIGN.md +37 -22
- package/dataLayer/backupRepository.ts +248 -0
- package/dataLayer/restoreMarker.ts +30 -75
- package/dataLayer/rocksdbBackup.ts +253 -110
- package/dist/bin/copyDb.js +3 -0
- package/dist/bin/copyDb.js.map +1 -1
- package/dist/bin/deployRenderer.js +3 -0
- package/dist/bin/deployRenderer.js.map +1 -1
- package/dist/components/Application.d.ts +5 -0
- package/dist/components/Application.js +51 -19
- package/dist/components/Application.js.map +1 -1
- package/dist/components/componentLoader.js +2 -0
- package/dist/components/componentLoader.js.map +1 -1
- package/dist/components/deploymentRecorder.js +18 -1
- package/dist/components/deploymentRecorder.js.map +1 -1
- package/dist/components/installFingerprint.d.ts +39 -0
- package/dist/components/installFingerprint.js +113 -0
- package/dist/components/installFingerprint.js.map +1 -0
- package/dist/components/operations.d.ts +2 -0
- package/dist/components/operations.js +31 -0
- package/dist/components/operations.js.map +1 -1
- package/dist/dataLayer/backupRepository.d.ts +33 -0
- package/dist/dataLayer/backupRepository.js +232 -0
- package/dist/dataLayer/backupRepository.js.map +1 -0
- package/dist/dataLayer/restoreMarker.d.ts +4 -1
- package/dist/dataLayer/restoreMarker.js +29 -68
- package/dist/dataLayer/restoreMarker.js.map +1 -1
- package/dist/dataLayer/rocksdbBackup.js +221 -112
- package/dist/dataLayer/rocksdbBackup.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js.map +1 -1
- package/dist/json/systemSchema.json +14 -0
- package/dist/resources/DatabaseTransaction.d.ts +140 -11
- package/dist/resources/DatabaseTransaction.js +1055 -464
- package/dist/resources/DatabaseTransaction.js.map +1 -1
- package/dist/resources/IterableEventQueue.d.ts +2 -1
- package/dist/resources/IterableEventQueue.js +16 -8
- package/dist/resources/IterableEventQueue.js.map +1 -1
- package/dist/resources/LMDBTransaction.d.ts +13 -2
- package/dist/resources/LMDBTransaction.js +219 -93
- package/dist/resources/LMDBTransaction.js.map +1 -1
- package/dist/resources/RecordEncoder.js +2 -3
- package/dist/resources/RecordEncoder.js.map +1 -1
- package/dist/resources/Resource.js +5 -6
- package/dist/resources/Resource.js.map +1 -1
- package/dist/resources/ResourceInterface.d.ts +3 -1
- package/dist/resources/ResourceInterface.js.map +1 -1
- package/dist/resources/RocksIndexStore.d.ts +2 -1
- package/dist/resources/RocksIndexStore.js +3 -0
- package/dist/resources/RocksIndexStore.js.map +1 -1
- package/dist/resources/Table.js +116 -12
- package/dist/resources/Table.js.map +1 -1
- package/dist/resources/analytics/write.js +6 -2
- package/dist/resources/analytics/write.js.map +1 -1
- package/dist/resources/auditStore.d.ts +71 -17
- package/dist/resources/auditStore.js +248 -64
- package/dist/resources/auditStore.js.map +1 -1
- package/dist/resources/branchDatabase.js +2 -0
- package/dist/resources/branchDatabase.js.map +1 -1
- package/dist/resources/databases.js +8 -2
- package/dist/resources/databases.js.map +1 -1
- package/dist/resources/indexes/fullTextNativeBinding.d.ts +6 -2
- package/dist/resources/indexes/fullTextNativeBinding.js +12 -2
- package/dist/resources/indexes/fullTextNativeBinding.js.map +1 -1
- package/dist/resources/indexes/fullTextQueryIndex.d.ts +4 -0
- package/dist/resources/indexes/fullTextQueryIndex.js +207 -10
- package/dist/resources/indexes/fullTextQueryIndex.js.map +1 -1
- package/dist/resources/models/Models.d.ts +11 -0
- package/dist/resources/models/Models.js +95 -4
- package/dist/resources/models/Models.js.map +1 -1
- package/dist/resources/models/backendRegistry.d.ts +8 -0
- package/dist/resources/models/backendRegistry.js +15 -1
- package/dist/resources/models/backendRegistry.js.map +1 -1
- package/dist/resources/models/bootstrap.d.ts +2 -0
- package/dist/resources/models/bootstrap.js +6 -0
- package/dist/resources/models/bootstrap.js.map +1 -1
- package/dist/resources/models/calibration.d.ts +88 -0
- package/dist/resources/models/calibration.js +244 -0
- package/dist/resources/models/calibration.js.map +1 -0
- package/dist/resources/models/calibrationStore.d.ts +163 -0
- package/dist/resources/models/calibrationStore.js +986 -0
- package/dist/resources/models/calibrationStore.js.map +1 -0
- package/dist/resources/models/decisionStore.d.ts +24 -0
- package/dist/resources/models/decisionStore.js +45 -0
- package/dist/resources/models/decisionStore.js.map +1 -1
- package/dist/resources/models/generativeDecision.js +44 -15
- package/dist/resources/models/generativeDecision.js.map +1 -1
- package/dist/resources/models/types.d.ts +7 -0
- package/dist/resources/recordLock.d.ts +8 -5
- package/dist/resources/recordLock.js +6 -4
- package/dist/resources/recordLock.js.map +1 -1
- package/dist/resources/replayLogs.js +8 -1
- package/dist/resources/replayLogs.js.map +1 -1
- package/dist/resources/scheduler/engine.d.ts +4 -0
- package/dist/resources/scheduler/engine.js +30 -2
- package/dist/resources/scheduler/engine.js.map +1 -1
- package/dist/resources/search.d.ts +16 -4
- package/dist/resources/search.js +117 -13
- package/dist/resources/search.js.map +1 -1
- package/dist/resources/transaction.js +85 -5
- package/dist/resources/transaction.js.map +1 -1
- package/dist/resources/transactionBroadcast.d.ts +8 -0
- package/dist/resources/transactionBroadcast.js +65 -0
- package/dist/resources/transactionBroadcast.js.map +1 -1
- package/dist/security/keys.d.ts +1 -1
- package/dist/security/keys.js +170 -120
- package/dist/security/keys.js.map +1 -1
- package/dist/server/http.js +10 -0
- package/dist/server/http.js.map +1 -1
- package/dist/server/serverHelpers/Request.js +11 -2
- package/dist/server/serverHelpers/Request.js.map +1 -1
- package/dist/server/serverHelpers/contentTypes.js +8 -3
- package/dist/server/serverHelpers/contentTypes.js.map +1 -1
- package/dist/server/serverHelpers/uwsServer.js +38 -7
- package/dist/server/serverHelpers/uwsServer.js.map +1 -1
- package/dist/server/threads/manageThreads.d.ts +3 -3
- package/dist/server/threads/manageThreads.js +15 -13
- package/dist/server/threads/manageThreads.js.map +1 -1
- package/dist/server/threads/threadServer.d.ts +3 -0
- package/dist/server/threads/threadServer.js +24 -6
- package/dist/server/threads/threadServer.js.map +1 -1
- package/dist/upgrade/directives/5-3-0.js +9 -3
- package/dist/upgrade/directives/5-3-0.js.map +1 -1
- package/dist/utility/durableFile.d.ts +36 -0
- package/dist/utility/durableFile.js +106 -0
- package/dist/utility/durableFile.js.map +1 -0
- package/dist/utility/errors/hdbError.d.ts +4 -0
- package/dist/utility/errors/hdbError.js +10 -1
- package/dist/utility/errors/hdbError.js.map +1 -1
- package/dist/utility/globalSchema.d.ts +9 -0
- package/dist/utility/hdbTerms.d.ts +1 -0
- package/dist/utility/hdbTerms.js +1 -0
- package/dist/utility/hdbTerms.js.map +1 -1
- package/dist/validation/configValidator.js +29 -0
- package/dist/validation/configValidator.js.map +1 -1
- package/index.ts +7 -0
- package/json/systemSchema.json +14 -0
- package/npm-shrinkwrap.json +22 -22
- package/package.json +2 -2
- package/resources/DESIGN.md +57 -272
- package/resources/DatabaseTransaction.ts +708 -87
- package/resources/FULL_TEXT.md +6 -2
- package/resources/IterableEventQueue.ts +14 -7
- package/resources/LMDBTransaction.ts +212 -84
- package/resources/RecordEncoder.ts +2 -3
- package/resources/Resource.ts +5 -6
- package/resources/ResourceInterface.ts +3 -1
- package/resources/RocksIndexStore.ts +5 -0
- package/resources/Table.ts +118 -15
- package/resources/analytics/write.ts +6 -2
- package/resources/audit-retention.md +203 -0
- package/resources/auditStore.ts +306 -84
- package/resources/branchDatabase.ts +2 -0
- package/resources/databases.ts +6 -2
- package/resources/indexes/DESIGN.md +120 -177
- package/resources/indexes/fullTextNativeBinding.ts +22 -3
- package/resources/indexes/fullTextQueryIndex.ts +274 -11
- package/resources/models/DESIGN.md +11 -3
- package/resources/models/Models.ts +127 -4
- package/resources/models/backendRegistry.ts +17 -0
- package/resources/models/bootstrap.ts +9 -1
- package/resources/models/calibration.ts +298 -0
- package/resources/models/calibrationStore.ts +1167 -0
- package/resources/models/decisionStore.ts +57 -0
- package/resources/models/generativeDecision.ts +54 -16
- package/resources/models/types.ts +6 -0
- package/resources/record-locks.md +155 -141
- package/resources/recordLock.ts +12 -6
- package/resources/replayLogs.ts +9 -2
- package/resources/scheduler/DESIGN.md +4 -3
- package/resources/scheduler/engine.ts +35 -2
- package/resources/search.ts +154 -16
- package/resources/transaction.ts +50 -5
- package/resources/transactionBroadcast.ts +69 -0
- package/security/DESIGN.md +9 -30
- package/security/keys.ts +175 -127
- package/server/DESIGN.md +70 -36
- package/server/http.ts +10 -0
- package/server/serverHelpers/Request.ts +10 -2
- package/server/serverHelpers/contentTypes.ts +5 -3
- package/server/serverHelpers/uwsServer.ts +34 -7
- package/server/threads/manageThreads.js +16 -14
- package/server/threads/threadServer.js +24 -6
- package/studio/web/assets/{Chat-CNEKMllJ.js → Chat-4LMxI0yB.js} +1 -1
- package/studio/web/assets/{FloatingChat-CEnWhHeR.js → FloatingChat-kWHApEtF.js} +3 -3
- package/studio/web/assets/{SearchDialog-CwhJdEdY.js → SearchDialog-B7ymna97.js} +1 -1
- package/studio/web/assets/{apiToken-DC5vsaaO.js → apiToken-CE3AAt9f.js} +1 -1
- package/studio/web/assets/{applications-DQZxDy0x.js → applications-DdN7CqsB.js} +1 -1
- package/studio/web/assets/{cssMode-Dz2DxF4e.js → cssMode-BpySZUSt.js} +1 -1
- package/studio/web/assets/{editor-B3mjd3tz.js → editor-myLCVv9C.js} +1 -1
- package/studio/web/assets/{html-BsRGM7tp.js → html-DGNYO55O.js} +1 -1
- package/studio/web/assets/{htmlMode-tUKJaA3o.js → htmlMode-BUgpQr41.js} +1 -1
- package/studio/web/assets/{index-BuqFjN5K.js → index-FN4KC9va.js} +39 -39
- package/studio/web/assets/index-QLZw6xrU.css +1 -0
- package/studio/web/assets/{index.lazy-CcpfQFY_.js → index.lazy-DZFuaumG.js} +1 -1
- package/studio/web/assets/{javascript-BZ_uRGHl.js → javascript-DiBzh4pj.js} +1 -1
- package/studio/web/assets/{jsonMode-DgB-aFMj.js → jsonMode-Bm312nmu.js} +1 -1
- package/studio/web/assets/{languageServices-BleuBw17.js → languageServices-Dz2K9rZN.js} +1 -1
- package/studio/web/assets/{lspLanguageFeatures-ByVYLSsQ.js → lspLanguageFeatures-CNu1L5xn.js} +1 -1
- package/studio/web/assets/{notifications-TuzN0IMi.js → notifications--nwFLGmM.js} +1 -1
- package/studio/web/assets/{notifications-D1Nf-Edn.js → notifications-BelKw5bR.js} +1 -1
- package/studio/web/assets/{profile-D7C7Lupz.js → profile-AU5FjOBx.js} +1 -1
- package/studio/web/assets/{regions-nGQCm-v8.js → regions-C0sTSEHH.js} +1 -1
- package/studio/web/assets/{register-C3x9Ze6P.js → register-DyNR2rGk.js} +2 -2
- package/studio/web/assets/{setComponentFile-DpjrRqeG.js → setComponentFile-BkWx8AN3.js} +1 -1
- package/studio/web/assets/{setup-CGiKLZ9e.js → setup-D6LZS78d.js} +2 -2
- package/studio/web/assets/{status-CGJwrNPH.js → status-BCWnn70o.js} +1 -1
- package/studio/web/assets/{toggleHighContrast-C-Ne6P7T.js → toggleHighContrast-pyeGJ6W1.js} +1 -1
- package/studio/web/assets/{tsMode-CSreYTys.js → tsMode-CGV3iaWC.js} +1 -1
- package/studio/web/assets/{typescript-MnxBk89W.js → typescript-Ci7Khvwo.js} +1 -1
- package/studio/web/assets/{useEntityRestURL-TNrh77-s.js → useEntityRestURL-DNiz3aZ0.js} +1 -1
- package/studio/web/assets/{workers-BfZlUNVI.js → workers-byzxjbqy.js} +1 -1
- package/studio/web/assets/{xml-BHd6qGH7.js → xml-DGnHQ3CX.js} +1 -1
- package/studio/web/assets/{yaml-C53R8OQq.js → yaml-BUoTnPWb.js} +1 -1
- package/studio/web/index.html +2 -2
- package/upgrade/directives/5-3-0.ts +10 -3
- package/utility/durableFile.ts +96 -0
- package/utility/errors/hdbError.ts +9 -0
- package/utility/hdbTerms.ts +1 -0
- package/validation/configValidator.ts +31 -0
- package/studio/web/assets/index-72cEH474.css +0 -1
package/resources/auditStore.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
1
2
|
import { readKey, writeKey } from 'ordered-binary';
|
|
2
3
|
import { initSync, get as envGet } from '../utility/environment/environmentManager.ts';
|
|
3
4
|
import { AUDIT_STORE_NAME } from '../utility/lmdb/terms.ts';
|
|
@@ -12,7 +13,8 @@ import { onStorageReclamation } from '../server/storageReclamation.ts';
|
|
|
12
13
|
import { RocksDatabase } from '@harperfast/rocksdb-js';
|
|
13
14
|
import { asBinary } from 'lmdb';
|
|
14
15
|
import { RocksTransactionLogStore } from './RocksTransactionLogStore.ts';
|
|
15
|
-
import {
|
|
16
|
+
import { endSubscriptionsFromEarlierHandles } from './transactionBroadcast.ts';
|
|
17
|
+
import { isReadOnlyMode, openRocksDatabase } from './databases.ts';
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* This module is responsible for the binary representation of audit records in an efficient form.
|
|
@@ -138,9 +140,10 @@ const FLOAT_BUFFER = new Uint8Array(FLOAT_TARGET.buffer);
|
|
|
138
140
|
const AUDIT_FLOOR_KEY = Symbol.for('audit-floor');
|
|
139
141
|
// The epoch `establishAuditFloor` stamped, never raised or removed. Its PRESENCE marks the floor as
|
|
140
142
|
// unverified provenance — a guess bounded by what survived, blind to history a legacy prune removed
|
|
141
|
-
// before tracking began
|
|
142
|
-
//
|
|
143
|
-
//
|
|
143
|
+
// before tracking began. No comparison retires the mark: a later prune certifies only what it
|
|
144
|
+
// removed, so `floor > bootstrap` says nothing about the older gap. Resume never relies on it, since a
|
|
145
|
+
// position is bound to a database generation and everything after its cursor was written after
|
|
146
|
+
// tracking began. The value records how far the guess reached. See `establishAuditFloor`.
|
|
144
147
|
const AUDIT_FLOOR_BOOTSTRAP_KEY = Symbol.for('audit-floor-bootstrap');
|
|
145
148
|
/**
|
|
146
149
|
* The floor's own eight bytes, deliberately NOT the FLOAT_TARGET/FLOAT_BUFFER pair the `last-removed`
|
|
@@ -152,6 +155,25 @@ const FLOOR_TARGET = new Float64Array(1);
|
|
|
152
155
|
const FLOOR_BUFFER = new Uint8Array(FLOOR_TARGET.buffer);
|
|
153
156
|
/** No trustworthy floor: the highest possible floor, so every cursor compares as stale. */
|
|
154
157
|
const AUDIT_FLOOR_UNKNOWN = Infinity;
|
|
158
|
+
/**
|
|
159
|
+
* Which copy of this database's history this is: a sixteen-byte random id, then the float64 time the
|
|
160
|
+
* generation began (0 for genesis). Every path that publishes a copy stamps a fresh one first
|
|
161
|
+
* (`stampDatabaseGeneration`), so a position naming an older id is refused (harper#2451). RocksDB only,
|
|
162
|
+
* like the resume floor: an LMDB database carries neither, so no position is resumable against it.
|
|
163
|
+
*/
|
|
164
|
+
const DATABASE_GENERATION_KEY = Symbol.for('database-generation');
|
|
165
|
+
const GENERATION_ID_BYTES = 16;
|
|
166
|
+
const GENERATION_RECORD_BYTES = GENERATION_ID_BYTES + 8;
|
|
167
|
+
/**
|
|
168
|
+
* The highest prune cutoff within the current generation, encoded like the floor. Kept apart from the
|
|
169
|
+
* floor, which also steers `Table.commit`'s reconciliation: resume validity must restart at a copy and
|
|
170
|
+
* must not be absorbed by the floor's unknown sentinel, and doing either to the floor changes merges.
|
|
171
|
+
*/
|
|
172
|
+
const AUDIT_RESUME_FLOOR_KEY = Symbol.for('audit-resume-floor');
|
|
173
|
+
|
|
174
|
+
function isRocksStore(store: any): boolean {
|
|
175
|
+
return store instanceof RocksTransactionLogStore || store instanceof RocksDatabase;
|
|
176
|
+
}
|
|
155
177
|
|
|
156
178
|
/** Last resort on a detached path: a failing log sink must not itself become an unhandled rejection. */
|
|
157
179
|
function warnContained(message: string, error: unknown) {
|
|
@@ -191,6 +213,8 @@ export function openAuditStore(rootStore) {
|
|
|
191
213
|
rootStore.auditStore = auditStore;
|
|
192
214
|
auditStore.rootStore = rootStore;
|
|
193
215
|
establishAuditFloor(auditStore);
|
|
216
|
+
establishDatabaseGeneration(auditStore);
|
|
217
|
+
endSubscriptionsFromEarlierHandles(auditStore, isRocksStore(auditStore));
|
|
194
218
|
auditStore.tableStores = [];
|
|
195
219
|
const deleteCallbacks = [];
|
|
196
220
|
auditStore.addDeleteRemovalCallback = function (tableId, table, callback) {
|
|
@@ -508,13 +532,13 @@ function decodeAuditFloor(stored: any): number {
|
|
|
508
532
|
}
|
|
509
533
|
|
|
510
534
|
/**
|
|
511
|
-
* Did
|
|
512
|
-
* `decodeAuditFloor(undefined)` is the unknown sentinel too, so on a
|
|
513
|
-
* resolver writes exactly that sentinel — comparing decoded values alone
|
|
514
|
-
* write that never happened, and the caller pruned with nothing persisted.
|
|
535
|
+
* Did a metadata write land? A record has to be PRESENT with exactly the bytes written, not merely
|
|
536
|
+
* decode to the same value: `decodeAuditFloor(undefined)` is the unknown sentinel too, so on a
|
|
537
|
+
* floorless store — where the resolver writes exactly that sentinel — comparing decoded values alone
|
|
538
|
+
* reported a commit for a write that never happened, and the caller pruned with nothing persisted.
|
|
515
539
|
*/
|
|
516
|
-
function
|
|
517
|
-
return stored !== undefined &&
|
|
540
|
+
function writeLanded(stored: any, written: Uint8Array): boolean {
|
|
541
|
+
return stored !== undefined && stored.byteLength === written.byteLength && Buffer.compare(stored, written) === 0;
|
|
518
542
|
}
|
|
519
543
|
|
|
520
544
|
/** Own eight bytes per write: the store must never be handed a live view of the reused module buffer. */
|
|
@@ -538,50 +562,66 @@ function updateAuditFloor(
|
|
|
538
562
|
resolve: (current: number, recorded: boolean) => number | undefined,
|
|
539
563
|
key: symbol = AUDIT_FLOOR_KEY
|
|
540
564
|
): void {
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
565
|
+
commitAuditMetadata(
|
|
566
|
+
auditStore,
|
|
567
|
+
(read) => {
|
|
568
|
+
const stored = read(key);
|
|
569
|
+
const floor = resolve(decodeAuditFloor(stored), stored !== undefined);
|
|
570
|
+
return floor === undefined ? undefined : [[key, encodeAuditFloor(floor)]];
|
|
571
|
+
},
|
|
572
|
+
'audit retention floor'
|
|
573
|
+
);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Write audit metadata records under one store transaction, all or none: every write is read back,
|
|
578
|
+
* and a mismatch throws inside the transaction so it aborts — returning false commits on LMDB.
|
|
579
|
+
*/
|
|
580
|
+
function commitAuditMetadata(
|
|
581
|
+
store: any,
|
|
582
|
+
plan: (read: (key: symbol) => any) => Array<[symbol, Uint8Array]> | undefined,
|
|
583
|
+
what: string
|
|
584
|
+
): void {
|
|
585
|
+
// A copy being stamped passes its RocksDB root directly. A legacy `auditPath` layout is opened as its
|
|
586
|
+
// own standalone LMDB root (databases.ts) and has no `.rootStore`, so it owns the transaction itself.
|
|
587
|
+
const onRocksDB = isRocksStore(store);
|
|
588
|
+
const transactionOwner = store instanceof RocksDatabase ? store : (store?.rootStore ?? store);
|
|
544
589
|
if (!transactionOwner?.transactionSync)
|
|
545
|
-
throw new Error(
|
|
546
|
-
|
|
547
|
-
//
|
|
548
|
-
// swallowed abort rather than throwing (see RecordEncoder.saveStructures)
|
|
549
|
-
//
|
|
550
|
-
//
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
(
|
|
556
|
-
const
|
|
557
|
-
const
|
|
558
|
-
if (floor !== undefined) {
|
|
559
|
-
txn.putSync(key, asBinary(encodeAuditFloor(floor)));
|
|
560
|
-
if (!floorWriteLanded(txn.getBinarySync(key), floor)) return false;
|
|
561
|
-
}
|
|
562
|
-
return true;
|
|
563
|
-
},
|
|
564
|
-
{ retryOnBusy: true }
|
|
565
|
-
)
|
|
566
|
-
: transactionOwner.transactionSync(() => {
|
|
567
|
-
const stored = auditStore.getBinary(key);
|
|
568
|
-
const floor = resolve(decodeAuditFloor(stored), stored !== undefined);
|
|
569
|
-
// `put` rather than `putSync`, and inside the transaction: lmdb's putSync is
|
|
570
|
-
// `put(...) === SYNC_PROMISE_SUCCESS`, so it drops whatever put returns, and a rejected put
|
|
571
|
-
// would leak with no owner. Within a write transaction put writes synchronously and returns
|
|
572
|
-
// an already-resolved sentinel, so the value is visible immediately either way and this
|
|
573
|
-
// only takes ownership of the failure case.
|
|
574
|
-
// asBinary: a legacy standalone audit root's encoder has no Uint8Array passthrough, so raw
|
|
575
|
-
// bytes would reach createAuditEntry and throw. This bypasses both encoders.
|
|
576
|
-
if (floor !== undefined) {
|
|
577
|
-
auditStore
|
|
578
|
-
.put(key, asBinary(encodeAuditFloor(floor)))
|
|
579
|
-
?.catch?.((error) => warnContained('Error writing the audit retention floor', error));
|
|
580
|
-
if (!floorWriteLanded(auditStore.getBinary(key), floor)) return false;
|
|
590
|
+
throw new Error(`Cannot record the ${what}: this database has no audit store`);
|
|
591
|
+
const notCommitted = () => new Error(`The ${what} transaction did not commit`);
|
|
592
|
+
// The caller still demands an explicit `true`: a RocksDB transactionSync returns undefined for a
|
|
593
|
+
// swallowed abort rather than throwing (see RecordEncoder.saveStructures). Reads inside a write
|
|
594
|
+
// transaction see their own writes on both engines, so the read-back observes what commit will make
|
|
595
|
+
// durable.
|
|
596
|
+
const committed = onRocksDB
|
|
597
|
+
? transactionOwner.transactionSync(
|
|
598
|
+
(txn) => {
|
|
599
|
+
const writes = plan((key) => txn.getBinarySync(key));
|
|
600
|
+
if (writes) {
|
|
601
|
+
for (const [key, bytes] of writes) txn.putSync(key, asBinary(bytes));
|
|
602
|
+
for (const [key, bytes] of writes) if (!writeLanded(txn.getBinarySync(key), bytes)) throw notCommitted();
|
|
581
603
|
}
|
|
582
604
|
return true;
|
|
583
|
-
}
|
|
584
|
-
|
|
605
|
+
},
|
|
606
|
+
{ retryOnBusy: true }
|
|
607
|
+
)
|
|
608
|
+
: transactionOwner.transactionSync(() => {
|
|
609
|
+
const writes = plan((key) => store.getBinary(key));
|
|
610
|
+
// `put` rather than `putSync`, and inside the transaction: lmdb's putSync is
|
|
611
|
+
// `put(...) === SYNC_PROMISE_SUCCESS`, so it drops whatever put returns, and a rejected put
|
|
612
|
+
// would leak with no owner. Within a write transaction put writes synchronously and returns
|
|
613
|
+
// an already-resolved sentinel, so the value is visible immediately either way and this
|
|
614
|
+
// only takes ownership of the failure case.
|
|
615
|
+
// asBinary: a legacy standalone audit root's encoder has no Uint8Array passthrough, so raw
|
|
616
|
+
// bytes would reach createAuditEntry and throw. This bypasses both encoders.
|
|
617
|
+
if (writes) {
|
|
618
|
+
for (const [key, bytes] of writes)
|
|
619
|
+
store.put(key, asBinary(bytes))?.catch?.((error) => warnContained(`Error writing the ${what}`, error));
|
|
620
|
+
for (const [key, bytes] of writes) if (!writeLanded(store.getBinary(key), bytes)) throw notCommitted();
|
|
621
|
+
}
|
|
622
|
+
return true;
|
|
623
|
+
});
|
|
624
|
+
if (committed !== true) throw notCommitted();
|
|
585
625
|
}
|
|
586
626
|
|
|
587
627
|
/**
|
|
@@ -666,25 +706,42 @@ export function raiseAuditFloor(auditStore: any, cutoff: number): void {
|
|
|
666
706
|
// database, a cutoff below one a wider prune already set), and taking the env write lock to
|
|
667
707
|
// discover that serializes every worker's boot and reclamation on it. The in-transaction guards
|
|
668
708
|
// below stay authoritative.
|
|
669
|
-
// Skips only the case it can prove is a no-op:
|
|
670
|
-
// the cutoff. An absent record is NOT decided here — the presence question is
|
|
671
|
-
// transaction below, because another worker's establishAuditFloor can land between
|
|
672
|
-
// that write.
|
|
709
|
+
// Skips only the case it can prove is a no-op: the floor, and on RocksDB the resume record, exist and
|
|
710
|
+
// already sit at or above the cutoff. An absent record is NOT decided here — the presence question is
|
|
711
|
+
// settled inside the transaction below, because another worker's establishAuditFloor can land between
|
|
712
|
+
// this read and that write.
|
|
713
|
+
const resumeTracked = isRocksStore(auditStore);
|
|
673
714
|
if (auditStore?.getBinary) {
|
|
674
715
|
const stored = auditStore.getBinary(AUDIT_FLOOR_KEY);
|
|
675
|
-
|
|
716
|
+
const resume = resumeTracked ? auditStore.getBinary(AUDIT_RESUME_FLOOR_KEY) : undefined;
|
|
717
|
+
if (
|
|
718
|
+
stored !== undefined &&
|
|
719
|
+
!(cutoff > decodeAuditFloor(stored)) &&
|
|
720
|
+
(!resumeTracked || (resume !== undefined && !(cutoff > decodeAuditFloor(resume))))
|
|
721
|
+
)
|
|
722
|
+
return;
|
|
676
723
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
724
|
+
commitAuditMetadata(
|
|
725
|
+
auditStore,
|
|
726
|
+
(read) => {
|
|
727
|
+
const writes: Array<[symbol, Uint8Array]> = [];
|
|
728
|
+
const stored = read(AUDIT_FLOOR_KEY);
|
|
729
|
+
// Still no record, and we are about to prune: persist the unknown sentinel. Leaving no marker
|
|
730
|
+
// lets the next open stamp a FINITE epoch, and a prune bound above that epoch (a future
|
|
731
|
+
// `endTime`, or a rolled-back clock) then certifies cursors whose history this prune deleted.
|
|
732
|
+
// Unknown is the honest value, because a store with no record may have been pruned before this
|
|
733
|
+
// run too.
|
|
734
|
+
if (stored === undefined) writes.push([AUDIT_FLOOR_KEY, encodeAuditFloor(AUDIT_FLOOR_UNKNOWN)]);
|
|
735
|
+
else if (cutoff > decodeAuditFloor(stored)) writes.push([AUDIT_FLOOR_KEY, encodeAuditFloor(cutoff)]);
|
|
736
|
+
if (resumeTracked) {
|
|
737
|
+
const resume = read(AUDIT_RESUME_FLOOR_KEY);
|
|
738
|
+
if (resume === undefined || cutoff > decodeAuditFloor(resume))
|
|
739
|
+
writes.push([AUDIT_RESUME_FLOOR_KEY, encodeAuditFloor(cutoff)]);
|
|
740
|
+
}
|
|
741
|
+
return writes;
|
|
742
|
+
},
|
|
743
|
+
'audit retention floor'
|
|
744
|
+
);
|
|
688
745
|
}
|
|
689
746
|
|
|
690
747
|
/**
|
|
@@ -715,8 +772,9 @@ export function raiseAuditFloor(auditStore: any, cutoff: number): void {
|
|
|
715
772
|
* unverified pre-tracking window for as long as it exists, however far the floor has since moved: a
|
|
716
773
|
* prune raising the floor above the epoch certifies only what that prune removed, and says nothing
|
|
717
774
|
* about history removed before tracking began — which may sit above the epoch, since that is exactly
|
|
718
|
-
* the case the guess cannot see.
|
|
719
|
-
*
|
|
775
|
+
* the case the guess cannot see. A floor that has climbed past it does not retire it. Resumable
|
|
776
|
+
* positions never depend on it: each is bound to a database generation minted after tracking began,
|
|
777
|
+
* so the pre-tracking window lies below every one of them (`getDatabaseGeneration`).
|
|
720
778
|
*
|
|
721
779
|
* Ordering. The provenance record is written **first**, so a crash between the two writes leaves a
|
|
722
780
|
* record with no floor — which the next open retries, since the early return above tests the floor.
|
|
@@ -787,10 +845,10 @@ export function establishAuditFloor(auditStore: any): void {
|
|
|
787
845
|
|
|
788
846
|
/**
|
|
789
847
|
* The floor of this database's retained audit history: every audit entry at or after the returned
|
|
790
|
-
* time is still retained,
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
*
|
|
848
|
+
* time is still retained, and below it the log may have lost anything. Returns `Infinity` when the
|
|
849
|
+
* floor is unknown, which fails closed — no cursor compares as safe. A resume is decided by the
|
|
850
|
+
* database generation and its resume floor (`getDatabaseGeneration`) instead: this floor survives a
|
|
851
|
+
* restore, and an unknown one is absorbing.
|
|
794
852
|
*
|
|
795
853
|
* **One exception, and it is the only one: history removed before the floor existed.** Every prune
|
|
796
854
|
* that runs with a floor recorded is covered — it raises the floor first, so it cannot remove an entry
|
|
@@ -798,7 +856,9 @@ export function establishAuditFloor(auditStore: any): void {
|
|
|
798
856
|
* (`establishAuditFloor`), and a legacy `Table.deleteHistory` that removed a table's newest entries,
|
|
799
857
|
* followed by a clock rollback, leaves that stamp below history that is gone; a cursor in the window
|
|
800
858
|
* is then certified over the gap. So the guarantee is one-directional for tracked prunes and silent
|
|
801
|
-
* about untracked ones.
|
|
859
|
+
* about untracked ones. Resumable positions do not face it: they are checked against the database
|
|
860
|
+
* generation and its resume floor (`getDatabaseGeneration`), not against this floor, which also steers
|
|
861
|
+
* `Table.commit`'s reconciliation and so cannot move in the direction a cursor check would want.
|
|
802
862
|
*
|
|
803
863
|
* The time domain is the audit-log key: what `subscribe`'s events carry as `localTime` and what MQTT
|
|
804
864
|
* durable sessions persist as `startTime`, so those compare against the floor directly.
|
|
@@ -817,8 +877,9 @@ export function establishAuditFloor(auditStore: any): void {
|
|
|
817
877
|
*
|
|
818
878
|
* **A moment-in-time observation.** Retention can advance between this call and whatever the caller
|
|
819
879
|
* does with the answer, so a check-then-resume sequence has a window where the floor moves under it.
|
|
820
|
-
* Closing that requires validating the
|
|
821
|
-
* lost race degrades to the truncation that happens
|
|
880
|
+
* Closing that requires validating the position inside the resume itself (harper#2448), against the
|
|
881
|
+
* generation and the resume floor; until then a lost race degrades to the truncation that happens
|
|
882
|
+
* today, never to anything worse.
|
|
822
883
|
*
|
|
823
884
|
* **On RocksDB the floor tracks the configured horizon, not retained reality.** That branch purges at
|
|
824
885
|
* whole-log-file granularity, so it cannot know before the fact which entries a purge will drop, and
|
|
@@ -828,18 +889,179 @@ export function establishAuditFloor(auditStore: any): void {
|
|
|
828
889
|
* resync. Conservative in the one safe direction, and the reason the LMDB branch (which can see a
|
|
829
890
|
* single eligible entry) instead raises off the first one it finds.
|
|
830
891
|
*
|
|
831
|
-
* **
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
* record versions and per-node replication sequence state, so making this one field honest while
|
|
837
|
-
* those stay stale would not give a consumer a coherent answer. It needs a database-level epoch —
|
|
838
|
-
* harper#2451.
|
|
892
|
+
* **Copies of a database's state.** `restore_backup` copies this record with everything else, and it
|
|
893
|
+
* stays accurate for the log the restore carried; a branch checkpoint copies it with no log at all.
|
|
894
|
+
* What a copy changes is which history the database is, and that is the database generation's to
|
|
895
|
+
* answer: every copy path stamps a new one (`stampDatabaseGeneration`), and a copy that carried no log
|
|
896
|
+
* raises this floor to the generation's epoch.
|
|
839
897
|
*/
|
|
840
898
|
export function getAuditFloor(auditStore: any): number {
|
|
841
899
|
return decodeAuditFloor(auditStore.getBinary(AUDIT_FLOOR_KEY));
|
|
842
900
|
}
|
|
901
|
+
|
|
902
|
+
export interface DatabaseGeneration {
|
|
903
|
+
/** Thirty-two lowercase hex characters. */
|
|
904
|
+
id: string;
|
|
905
|
+
/** When the generation began, in the audit-log key domain; 0 for a genesis generation. */
|
|
906
|
+
epoch: number;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* The generation this handle's open established, or undefined when it could not establish one, in
|
|
911
|
+
* which case nothing may resume against it. Per database per node, never cluster-wide.
|
|
912
|
+
*/
|
|
913
|
+
export function getDatabaseGeneration(auditStore: any): DatabaseGeneration | undefined {
|
|
914
|
+
return auditStore?.databaseGeneration;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* The bound a resumed position is checked against, or `Infinity` when unknown: the resume floor, but
|
|
919
|
+
* never below a finite audit floor, since a binary that predates the resume floor raised only the
|
|
920
|
+
* audit floor when it pruned. Such a binary pruning while the audit floor was unknown records nothing
|
|
921
|
+
* either record can show.
|
|
922
|
+
*/
|
|
923
|
+
export function getAuditResumeFloor(auditStore: any): number {
|
|
924
|
+
// The audit floor first: both records only rise, and an unknown audit floor stays unknown, so a prune
|
|
925
|
+
// committing between the two reads is still seen through the resume floor read second.
|
|
926
|
+
const auditFloor = getAuditFloor(auditStore);
|
|
927
|
+
const resumeFloor = decodeAuditFloor(auditStore.getBinary(AUDIT_RESUME_FLOOR_KEY));
|
|
928
|
+
return Number.isFinite(auditFloor) && auditFloor > resumeFloor ? auditFloor : resumeFloor;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Whether a position may resume here: it names this handle's generation and carries a finite cursor
|
|
933
|
+
* at or above the resume floor. The cursor must be progress-based — no lower than the log position
|
|
934
|
+
* observed when the position was established — or a quiet scope's snapshot key falls below a floor
|
|
935
|
+
* that retention keeps advancing and is refused on every resume. The answer holds as of the read: a
|
|
936
|
+
* prune that commits after it returns is the caller's to order against its replay. Always false on
|
|
937
|
+
* LMDB, which has no generation: a caller applies the check to RocksDB databases only.
|
|
938
|
+
*/
|
|
939
|
+
export function isResumablePosition(auditStore: any, generationId: string | undefined, cursor: number): boolean {
|
|
940
|
+
const generation = getDatabaseGeneration(auditStore);
|
|
941
|
+
return (
|
|
942
|
+
generation !== undefined &&
|
|
943
|
+
generationId === generation.id &&
|
|
944
|
+
typeof cursor === 'number' &&
|
|
945
|
+
Number.isFinite(cursor) &&
|
|
946
|
+
cursor >= getAuditResumeFloor(auditStore)
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function newGeneration(epoch: number): DatabaseGeneration {
|
|
951
|
+
return { id: randomBytes(GENERATION_ID_BYTES).toString('hex'), epoch };
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function encodeGeneration(generation: DatabaseGeneration): Uint8Array {
|
|
955
|
+
const bytes = new Uint8Array(GENERATION_RECORD_BYTES);
|
|
956
|
+
bytes.set(Buffer.from(generation.id, 'hex'));
|
|
957
|
+
new DataView(bytes.buffer).setFloat64(GENERATION_ID_BYTES, generation.epoch, true);
|
|
958
|
+
return bytes;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
function decodeGeneration(stored: any): DatabaseGeneration | undefined {
|
|
962
|
+
if (stored?.byteLength !== GENERATION_RECORD_BYTES) return undefined;
|
|
963
|
+
const epoch = new DataView(stored.buffer, stored.byteOffset, GENERATION_RECORD_BYTES).getFloat64(
|
|
964
|
+
GENERATION_ID_BYTES,
|
|
965
|
+
true
|
|
966
|
+
);
|
|
967
|
+
if (!Number.isFinite(epoch) || epoch < 0 || Object.is(epoch, -0)) return undefined;
|
|
968
|
+
return { id: Buffer.from(stored.buffer, stored.byteOffset, GENERATION_ID_BYTES).toString('hex'), epoch };
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Establish this handle's generation at open: adopt the recorded one, or mint genesis for a RocksDB
|
|
973
|
+
* store that has none (an LMDB store gets none). An undecodable record is never replaced here — an
|
|
974
|
+
* ordinary open cannot know whether another worker already serves it; only a copy's stamp replaces a
|
|
975
|
+
* generation. Never fails the open.
|
|
976
|
+
*/
|
|
977
|
+
export function establishDatabaseGeneration(auditStore: any): void {
|
|
978
|
+
auditStore.databaseGeneration = undefined;
|
|
979
|
+
if (!isRocksStore(auditStore)) return;
|
|
980
|
+
try {
|
|
981
|
+
let stored = auditStore.getBinary(DATABASE_GENERATION_KEY);
|
|
982
|
+
if (stored === undefined) {
|
|
983
|
+
if (isReadOnlyMode()) return;
|
|
984
|
+
const genesis = encodeGeneration(newGeneration(0));
|
|
985
|
+
commitAuditMetadata(
|
|
986
|
+
auditStore,
|
|
987
|
+
(read) => {
|
|
988
|
+
// compare-and-set on absence, so racing workers converge on the first one's id
|
|
989
|
+
if (read(DATABASE_GENERATION_KEY) !== undefined) return undefined;
|
|
990
|
+
const writes: Array<[symbol, Uint8Array]> = [[DATABASE_GENERATION_KEY, genesis]];
|
|
991
|
+
if (read(AUDIT_RESUME_FLOOR_KEY) === undefined) {
|
|
992
|
+
// starting at the audit floor rather than 0 keeps the next prune's lock-free skip effective
|
|
993
|
+
const auditFloor = decodeAuditFloor(read(AUDIT_FLOOR_KEY));
|
|
994
|
+
writes.push([AUDIT_RESUME_FLOOR_KEY, encodeAuditFloor(Number.isFinite(auditFloor) ? auditFloor : 0)]);
|
|
995
|
+
}
|
|
996
|
+
return writes;
|
|
997
|
+
},
|
|
998
|
+
'database generation'
|
|
999
|
+
);
|
|
1000
|
+
stored = auditStore.getBinary(DATABASE_GENERATION_KEY);
|
|
1001
|
+
}
|
|
1002
|
+
auditStore.databaseGeneration = decodeGeneration(stored);
|
|
1003
|
+
if (!auditStore.databaseGeneration)
|
|
1004
|
+
warnContained(
|
|
1005
|
+
'The database generation record is unreadable, so no subscription can resume against this database',
|
|
1006
|
+
new Error(`database generation record of ${stored?.byteLength} bytes`)
|
|
1007
|
+
);
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
warnContained('Error establishing the database generation', error);
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Give a copy of a database a generation of its own, before anything can read it; the caller holds
|
|
1015
|
+
* the copy exclusively and makes the stamp durable before publishing. A copy that kept no
|
|
1016
|
+
* transaction log has nothing below its epoch, so a finite floor is raised to it — raising it over
|
|
1017
|
+
* history a copy carried would change `Table.commit`'s reconciliation, and an unknown floor stays
|
|
1018
|
+
* unknown. `generation` lets a caller replay a value it recorded durably first.
|
|
1019
|
+
*/
|
|
1020
|
+
export function stampDatabaseGeneration(
|
|
1021
|
+
store: any,
|
|
1022
|
+
{ carriesLog, generation = newGeneration(Date.now()) }: { carriesLog: boolean; generation?: DatabaseGeneration }
|
|
1023
|
+
): DatabaseGeneration {
|
|
1024
|
+
if (
|
|
1025
|
+
!/^[0-9a-f]{32}$/.test(generation?.id) ||
|
|
1026
|
+
decodeGeneration(encodeGeneration(generation))?.epoch !== generation.epoch
|
|
1027
|
+
)
|
|
1028
|
+
throw new Error(`Invalid database generation: ${JSON.stringify(generation)}`);
|
|
1029
|
+
const stamp: Array<[symbol, Uint8Array]> = [
|
|
1030
|
+
[DATABASE_GENERATION_KEY, encodeGeneration(generation)],
|
|
1031
|
+
[AUDIT_RESUME_FLOOR_KEY, encodeAuditFloor(0)],
|
|
1032
|
+
];
|
|
1033
|
+
commitAuditMetadata(
|
|
1034
|
+
store,
|
|
1035
|
+
(read) => {
|
|
1036
|
+
if (carriesLog) return stamp;
|
|
1037
|
+
const stored = read(AUDIT_FLOOR_KEY);
|
|
1038
|
+
const floor = decodeAuditFloor(stored);
|
|
1039
|
+
return stored !== undefined && Number.isFinite(floor) && floor < generation.epoch
|
|
1040
|
+
? [...stamp, [AUDIT_FLOOR_KEY, encodeAuditFloor(generation.epoch)]]
|
|
1041
|
+
: stamp;
|
|
1042
|
+
},
|
|
1043
|
+
'database generation'
|
|
1044
|
+
);
|
|
1045
|
+
return generation;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* Stamp the RocksDB database at `path`, which nothing else may have open, and flush it: a root-store
|
|
1050
|
+
* write alone is not power-loss durable, and directory fsync is best-effort where unsupported.
|
|
1051
|
+
*/
|
|
1052
|
+
export async function stampDatabaseDirectory(
|
|
1053
|
+
path: string,
|
|
1054
|
+
options: { carriesLog: boolean; generation?: DatabaseGeneration }
|
|
1055
|
+
): Promise<DatabaseGeneration> {
|
|
1056
|
+
const database = openRocksDatabase(path, { disableWAL: false });
|
|
1057
|
+
try {
|
|
1058
|
+
const generation = stampDatabaseGeneration(database, options);
|
|
1059
|
+
await database.flush({ allowWriteStall: true });
|
|
1060
|
+
return generation;
|
|
1061
|
+
} finally {
|
|
1062
|
+
database.close();
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
843
1065
|
export function setAuditRetention(retentionTime, defaultDelay = DEFAULT_AUDIT_CLEANUP_DELAY) {
|
|
844
1066
|
auditRetention = retentionTime;
|
|
845
1067
|
DEFAULT_AUDIT_CLEANUP_DELAY = defaultDelay;
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
retakeBranchIdentity,
|
|
27
27
|
} from './databases.ts';
|
|
28
28
|
import { replayLogs, replayTimeBudgetMs } from './replayLogs.ts';
|
|
29
|
+
import { stampDatabaseDirectory } from './auditStore.ts';
|
|
29
30
|
|
|
30
31
|
/**
|
|
31
32
|
* Private per-application forks of a database, for running several variants of an application against
|
|
@@ -345,6 +346,7 @@ async function materializeBranch(
|
|
|
345
346
|
await base.createCheckpoint(staging);
|
|
346
347
|
report.progress();
|
|
347
348
|
await cloneBlobRoots(baseName, baseRoots, blobRoots, report.progress);
|
|
349
|
+
await stampDatabaseDirectory(staging, { carriesLog: false });
|
|
348
350
|
await writeFile(join(staging, COMPLETION_MARKER), JSON.stringify({ blobRoots } satisfies BranchCompletion));
|
|
349
351
|
await rename(staging, branchPath);
|
|
350
352
|
return blobRoots;
|
package/resources/databases.ts
CHANGED
|
@@ -1459,6 +1459,9 @@ function initStores(
|
|
|
1459
1459
|
const fullTextIndexRetirements = persistedFullTextIndexNames(primaryAttribute.fullTextIndexRetirements);
|
|
1460
1460
|
if (table && !recreateTable) {
|
|
1461
1461
|
if (primaryAttribute.audit === true && table.audit !== true) table.enableAuditing();
|
|
1462
|
+
// Absent means unchanged, not `replicates`: NON_REPLICATING_SYSTEM_TABLES sets `replicate = false`
|
|
1463
|
+
// on the live class and stores nothing, so an unconditional refresh would clear that override.
|
|
1464
|
+
if (typeof primaryAttribute.replicate === 'boolean') table.replicate = primaryAttribute.replicate;
|
|
1462
1465
|
table.fullTextIndexes = fullTextIndexes;
|
|
1463
1466
|
table.fullTextFields = fullTextFields;
|
|
1464
1467
|
table.fullTextIndexGenerations = fullTextIndexGenerations;
|
|
@@ -4304,8 +4307,8 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
4304
4307
|
origin !== 'cluster' &&
|
|
4305
4308
|
(schemaDefinedMismatch ||
|
|
4306
4309
|
(typeof audit === 'boolean' && audit !== attributeDescriptor.audit) ||
|
|
4307
|
-
(sealed !== undefined && sealed !==
|
|
4308
|
-
(replicate !== undefined && replicate !==
|
|
4310
|
+
(sealed !== undefined && sealed !== attributeDescriptor.sealed) ||
|
|
4311
|
+
(replicate !== undefined && replicate !== attributeDescriptor.replicate) ||
|
|
4309
4312
|
(+expiration || undefined) !== (+attributeDescriptor.expiration || undefined) ||
|
|
4310
4313
|
(+eviction || undefined) !== (+attributeDescriptor.eviction || undefined) ||
|
|
4311
4314
|
attribute.type !== attributeDescriptor.type)
|
|
@@ -4695,6 +4698,7 @@ function declareTable<TableResourceType>(target: TableTarget, tableDefinition: T
|
|
|
4695
4698
|
);
|
|
4696
4699
|
refreshDerivedIndexes(Table);
|
|
4697
4700
|
|
|
4701
|
+
if (typeof replicate === 'boolean' && origin !== 'cluster') Table.replicate = replicate;
|
|
4698
4702
|
Table.origin = origin;
|
|
4699
4703
|
// scope-private: replication and other global subscribers must not learn of a branch class
|
|
4700
4704
|
if ((hasChanges || refreshRelationshipAttributes) && !target.branch) {
|