@harperfast/harper-pro 5.0.17 → 5.0.19
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/core/resources/RecordEncoder.ts +15 -12
- package/core/resources/RocksTransactionLogStore.ts +47 -22
- package/core/resources/Table.ts +98 -32
- package/core/resources/auditStore.ts +87 -6
- package/core/resources/databases.ts +67 -7
- package/dist/cloneNode/cloneNode.js +13 -8
- package/dist/cloneNode/cloneNode.js.map +1 -1
- package/dist/core/resources/RecordEncoder.js +1 -1
- package/dist/core/resources/RecordEncoder.js.map +1 -1
- package/dist/core/resources/RocksTransactionLogStore.js +80 -21
- package/dist/core/resources/RocksTransactionLogStore.js.map +1 -1
- package/dist/core/resources/Table.js +96 -35
- package/dist/core/resources/Table.js.map +1 -1
- package/dist/core/resources/auditStore.js +83 -6
- package/dist/core/resources/auditStore.js.map +1 -1
- package/dist/core/resources/databases.js +68 -5
- package/dist/core/resources/databases.js.map +1 -1
- package/dist/replication/replicationConnection.js +63 -18
- package/dist/replication/replicationConnection.js.map +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/replication/replicationConnection.ts +66 -20
- package/studio/web/assets/{index-DhLu-DHX.js → index-BIjBsaWw.js} +5 -5
- package/studio/web/assets/{index-DhLu-DHX.js.map → index-BIjBsaWw.js.map} +1 -1
- package/studio/web/assets/{index.lazy-DBjOisCz.js → index.lazy-DN6bSQzR.js} +2 -2
- package/studio/web/assets/{index.lazy-DBjOisCz.js.map → index.lazy-DN6bSQzR.js.map} +1 -1
- package/studio/web/assets/{profile-DSL-499E.js → profile-Dyrp-ZIJ.js} +2 -2
- package/studio/web/assets/{profile-DSL-499E.js.map → profile-Dyrp-ZIJ.js.map} +1 -1
- package/studio/web/assets/{status-BRW5QtzY.js → status-BrfTnnpt.js} +2 -2
- package/studio/web/assets/{status-BRW5QtzY.js.map → status-BrfTnnpt.js.map} +1 -1
- package/studio/web/index.html +1 -1
|
@@ -93,6 +93,7 @@ exports.EVICTED = 8; // note that 2 is reserved for timestamps
|
|
|
93
93
|
const TEST_WRITE_KEY_BUFFER = Buffer.allocUnsafeSlow(8192);
|
|
94
94
|
const MAX_KEY_BYTES = 1978;
|
|
95
95
|
const EVENT_HIGH_WATER_MARK = 100;
|
|
96
|
+
const REPLAY_YIELD_INTERVAL = 100; // yield to the event loop every N records during subscription replay
|
|
96
97
|
const FULL_PERMISSIONS = {
|
|
97
98
|
read: true,
|
|
98
99
|
insert: true,
|
|
@@ -732,28 +733,25 @@ function makeTable(options) {
|
|
|
732
733
|
/**
|
|
733
734
|
* Set TTL expiration for records in this table. On retrieval, record timestamps are checked for expiration.
|
|
734
735
|
* This also informs the scheduling for record eviction.
|
|
735
|
-
* @param
|
|
736
|
-
*
|
|
736
|
+
* @param opts Time in seconds until records expire, or an options object with `expiration`, `eviction`,
|
|
737
|
+
* and `scanInterval` (all in seconds, all optional). Number form preserves any previously configured
|
|
738
|
+
* eviction/scanInterval; object form replaces all three.
|
|
737
739
|
*/
|
|
738
|
-
static setTTLExpiration(
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
if (typeof
|
|
742
|
-
expirationMs =
|
|
743
|
-
if (!evictionMs)
|
|
744
|
-
evictionMs = 0; // by default, no extra time for eviction
|
|
740
|
+
static setTTLExpiration(opts) {
|
|
741
|
+
if (opts == null || (typeof opts !== 'number' && typeof opts !== 'object'))
|
|
742
|
+
throw new Error('Invalid expiration value type');
|
|
743
|
+
if (typeof opts === 'number') {
|
|
744
|
+
expirationMs = opts * 1000;
|
|
745
745
|
}
|
|
746
|
-
else
|
|
747
|
-
// an
|
|
748
|
-
expirationMs =
|
|
749
|
-
evictionMs = (
|
|
750
|
-
cleanupInterval =
|
|
746
|
+
else {
|
|
747
|
+
// `??` so an explicit 0 is treated as the user's chosen value, not as "missing"
|
|
748
|
+
expirationMs = (opts.expiration ?? 0) * 1000;
|
|
749
|
+
evictionMs = (opts.eviction ?? 0) * 1000;
|
|
750
|
+
cleanupInterval = (opts.scanInterval ?? 0) * 1000;
|
|
751
751
|
}
|
|
752
|
-
else
|
|
753
|
-
throw new Error('Invalid expiration value type');
|
|
754
752
|
if (expirationMs < 0)
|
|
755
753
|
throw new Error('Expiration can not be negative');
|
|
756
|
-
// default to one quarter of the total eviction
|
|
754
|
+
// default to one quarter of the total expiration+eviction window
|
|
757
755
|
cleanupInterval = cleanupInterval || (expirationMs + evictionMs) / 4;
|
|
758
756
|
scheduleCleanup();
|
|
759
757
|
}
|
|
@@ -2467,9 +2465,19 @@ function makeTable(options) {
|
|
|
2467
2465
|
if (!request)
|
|
2468
2466
|
request = {};
|
|
2469
2467
|
const getFullRecord = !request.rawEvents;
|
|
2470
|
-
|
|
2468
|
+
// While the count, !omitCurrent, and non-collection branches replay older messages, real-time
|
|
2469
|
+
// messages from the listener accumulate here and are drained at the end of the IIFE so they
|
|
2470
|
+
// arrive after the replayed history, in order. The startTime branch sets this to null and
|
|
2471
|
+
// uses dropDuringReplay instead — its snapshot:false cursor picks up the live tail directly.
|
|
2472
|
+
let pendingRealTimeQueue = [];
|
|
2473
|
+
// Set during the startTime audit-log replay. The cursor iterates the audit log forward with
|
|
2474
|
+
// snapshot:false, which catches any commits that land during yield points; dropping in the
|
|
2475
|
+
// listener avoids duplicate delivery.
|
|
2476
|
+
let dropDuringReplay = false;
|
|
2471
2477
|
const thisId = requestTargetToId(request) ?? null; // treat undefined and null as the root
|
|
2472
2478
|
const subscription = (0, transactionBroadcast_ts_1.addSubscription)(TableResource, thisId, function (id, auditRecord, localTime, beginTxn) {
|
|
2479
|
+
if (dropDuringReplay)
|
|
2480
|
+
return;
|
|
2473
2481
|
try {
|
|
2474
2482
|
let type = auditRecord.type;
|
|
2475
2483
|
let value;
|
|
@@ -2514,6 +2522,12 @@ function makeTable(options) {
|
|
|
2514
2522
|
logger_ts_1.logger.error?.(error);
|
|
2515
2523
|
}
|
|
2516
2524
|
}, request.startTime || 0, request);
|
|
2525
|
+
// Attach the request.listener BEFORE invoking the IIFE so that sync sends from the
|
|
2526
|
+
// IIFE's prologue go directly to the listener via emit('data') instead of accumulating
|
|
2527
|
+
// in subscription.queue. Without this, the IIFE can fill the queue past
|
|
2528
|
+
// EVENT_HIGH_WATER_MARK and hit waitForDrain before the consumer's listener exists.
|
|
2529
|
+
if (request.listener)
|
|
2530
|
+
subscription.on('data', request.listener);
|
|
2517
2531
|
const result = (async () => {
|
|
2518
2532
|
const isCollection = request.isCollection ?? thisId == null;
|
|
2519
2533
|
if (isCollection) {
|
|
@@ -2527,17 +2541,25 @@ function makeTable(options) {
|
|
|
2527
2541
|
if (count > 1000)
|
|
2528
2542
|
count = 1000; // don't allow too many, we have to hold these in memory
|
|
2529
2543
|
let startTime = request.startTime;
|
|
2544
|
+
let recordsSinceYield = 0;
|
|
2530
2545
|
if (isCollection) {
|
|
2531
2546
|
// a collection should retrieve all descendant ids
|
|
2532
2547
|
if (startTime) {
|
|
2533
2548
|
if (count)
|
|
2534
2549
|
throw new hdbError_js_1.ClientError('startTime and previousCount can not be combined for a table level subscription');
|
|
2535
|
-
// start time specified, get the audit history for this time range
|
|
2550
|
+
// start time specified, get the audit history for this time range. We drop real-time
|
|
2551
|
+
// messages during this loop because the snapshot:false cursor will pick them up itself.
|
|
2552
|
+
pendingRealTimeQueue = null;
|
|
2553
|
+
dropDuringReplay = true;
|
|
2536
2554
|
for (const auditRecord of auditStore.getRange({
|
|
2537
2555
|
start: startTime,
|
|
2538
2556
|
exclusiveStart: true,
|
|
2539
2557
|
snapshot: false, // no need for a snapshot, audits don't change
|
|
2540
2558
|
})) {
|
|
2559
|
+
if (++recordsSinceYield >= REPLAY_YIELD_INTERVAL) {
|
|
2560
|
+
recordsSinceYield = 0;
|
|
2561
|
+
await rest();
|
|
2562
|
+
}
|
|
2541
2563
|
if (auditRecord.tableId !== tableId)
|
|
2542
2564
|
continue;
|
|
2543
2565
|
const id = auditRecord.recordId;
|
|
@@ -2557,15 +2579,34 @@ function makeTable(options) {
|
|
|
2557
2579
|
return;
|
|
2558
2580
|
}
|
|
2559
2581
|
}
|
|
2560
|
-
|
|
2561
|
-
//await rest(); // yield for fairness
|
|
2562
|
-
subscription.startTime = auditRecord.localTime; // update so we don't double send
|
|
2582
|
+
subscription.startTime = auditRecord.localTime ?? auditRecord.version; // update so we don't double send
|
|
2563
2583
|
}
|
|
2584
|
+
// No catch-up sweep needed. With snapshot:false (lmdb), notifyFromTransactionData
|
|
2585
|
+
// calls resetReadTxn before iterating, which bumps renewId; on the cursor's next
|
|
2586
|
+
// .next() it renews to a fresh txn whose snapshot is at least as recent. With
|
|
2587
|
+
// rocksdb, the audit-log iterator re-reads `_lastCommittedPosition` on each next()
|
|
2588
|
+
// (live tail). Either way, at loop exit subscription.startTime is at or past
|
|
2589
|
+
// lastTxnTime, and the gate in notifyFromTransactionData handles the handoff
|
|
2590
|
+
// once dropDuringReplay flips back.
|
|
2591
|
+
dropDuringReplay = false;
|
|
2564
2592
|
}
|
|
2565
2593
|
else if (count) {
|
|
2594
|
+
// Raise the listener's gate up front so that any in-flight 'committed' callbacks
|
|
2595
|
+
// for records the cursor will capture in `history` get gated out of
|
|
2596
|
+
// pendingRealTimeQueue rather than queued and re-emitted as duplicates after
|
|
2597
|
+
// history is sent. getNextMonotonicTime() returns a strictly-greater value than
|
|
2598
|
+
// any audit record's localTime issued so far — it's the same source Harper uses
|
|
2599
|
+
// to assign localTimes — so this gates exactly the records the cursor's
|
|
2600
|
+
// snapshot:true view can see. Anything committed strictly after this point will
|
|
2601
|
+
// pass the gate and reach the queue.
|
|
2602
|
+
subscription.startTime = (0, commonUtility_js_1.getNextMonotonicTime)();
|
|
2566
2603
|
const history = [];
|
|
2567
2604
|
// we are collecting the history in reverse order to get the right count, then reversing to send
|
|
2568
2605
|
for (const auditRecord of auditStore.getRange({ start: 'z', end: false, reverse: true })) {
|
|
2606
|
+
if (++recordsSinceYield >= REPLAY_YIELD_INTERVAL) {
|
|
2607
|
+
recordsSinceYield = 0;
|
|
2608
|
+
await rest();
|
|
2609
|
+
}
|
|
2569
2610
|
try {
|
|
2570
2611
|
if (auditRecord.tableId !== tableId)
|
|
2571
2612
|
continue;
|
|
@@ -2586,22 +2627,34 @@ function makeTable(options) {
|
|
|
2586
2627
|
catch (error) {
|
|
2587
2628
|
logger_ts_1.logger.error?.('Error getting history entry', auditRecord.localTime, error);
|
|
2588
2629
|
}
|
|
2589
|
-
// TODO: Would like to do this asynchronously, but would need to catch up on anything published during iteration
|
|
2590
|
-
//await rest(); // yield for fairness
|
|
2591
2630
|
}
|
|
2592
2631
|
for (let i = history.length; i > 0;) {
|
|
2593
2632
|
send(history[--i]);
|
|
2594
2633
|
}
|
|
2595
|
-
if (history[0])
|
|
2596
|
-
subscription.startTime = history[0].localTime; // update so don't double send
|
|
2597
2634
|
}
|
|
2598
2635
|
else if (!request.omitCurrent) {
|
|
2636
|
+
// Raise the listener's gate up front so that any in-flight 'committed' callbacks
|
|
2637
|
+
// for pre-subscribe commits (which haven't yet advanced lastTxnTime when subscribe
|
|
2638
|
+
// is called) get gated out of the queue. Otherwise the listener fires for them
|
|
2639
|
+
// during cursor yields and emits stale events the cursor either covered (current
|
|
2640
|
+
// state) or correctly skipped (e.g., deletes via `if (!value) continue`).
|
|
2641
|
+
// getNextMonotonicTime() is the same source Harper uses to assign audit record
|
|
2642
|
+
// localTimes, so the gate cuts at a precise instant in the same time domain.
|
|
2643
|
+
subscription.startTime = (0, commonUtility_js_1.getNextMonotonicTime)();
|
|
2644
|
+
// Retained-message semantics: subscriber may legitimately receive a record twice
|
|
2645
|
+
// if a post-subscribe write hits a key the cursor also visits. This is
|
|
2646
|
+
// idempotent for "current state then live updates" — both deliveries land at
|
|
2647
|
+
// the same final state. We don't dedupe.
|
|
2599
2648
|
for (const { key: id, value, version, localTime, size } of primaryStore.getRange({
|
|
2600
2649
|
start: thisId ?? false,
|
|
2601
2650
|
end: thisId == null ? undefined : [thisId, ordered_binary_1.MAXIMUM_KEY],
|
|
2602
2651
|
versions: true,
|
|
2603
2652
|
snapshot: false, // no need for a snapshot, just want the latest data
|
|
2604
2653
|
})) {
|
|
2654
|
+
if (++recordsSinceYield >= REPLAY_YIELD_INTERVAL) {
|
|
2655
|
+
recordsSinceYield = 0;
|
|
2656
|
+
await rest();
|
|
2657
|
+
}
|
|
2605
2658
|
if (!value)
|
|
2606
2659
|
continue;
|
|
2607
2660
|
send({ id, localTime, value, version, type: 'put', size });
|
|
@@ -2632,13 +2685,19 @@ function makeTable(options) {
|
|
|
2632
2685
|
}
|
|
2633
2686
|
logger_ts_1.logger.trace?.('Subscription from', startTime, 'from', thisId, localTime);
|
|
2634
2687
|
if (startTime < localTime) {
|
|
2635
|
-
// start time specified, get the audit history for this record
|
|
2688
|
+
// start time specified, get the audit history for this record. Set startTime up
|
|
2689
|
+
// front so the listener gate skips any in-flight 'committed' for this version
|
|
2690
|
+
// during the yields below — otherwise that event would be queued and drained as a
|
|
2691
|
+
// duplicate of the entry send.
|
|
2692
|
+
subscription.startTime = localTime ?? entry?.version;
|
|
2636
2693
|
const history = [];
|
|
2637
2694
|
let nextTime = localTime;
|
|
2638
2695
|
let nodeId = entry?.nodeId;
|
|
2639
2696
|
do {
|
|
2640
|
-
|
|
2641
|
-
|
|
2697
|
+
if (++recordsSinceYield >= REPLAY_YIELD_INTERVAL) {
|
|
2698
|
+
recordsSinceYield = 0;
|
|
2699
|
+
await rest();
|
|
2700
|
+
}
|
|
2642
2701
|
const auditRecord = auditStore.getSync(nextTime, tableId, thisId, nodeId);
|
|
2643
2702
|
if (auditRecord) {
|
|
2644
2703
|
if (startTime < nextTime) {
|
|
@@ -2664,7 +2723,6 @@ function makeTable(options) {
|
|
|
2664
2723
|
for (let i = history.length; i > 0;) {
|
|
2665
2724
|
send(history[--i]);
|
|
2666
2725
|
}
|
|
2667
|
-
subscription.startTime = localTime; // make sure we don't re-broadcast the current version that we already sent
|
|
2668
2726
|
}
|
|
2669
2727
|
if (!request.omitCurrent && entry?.value) {
|
|
2670
2728
|
// if retain and it exists, send the current value first
|
|
@@ -2676,10 +2734,12 @@ function makeTable(options) {
|
|
|
2676
2734
|
}
|
|
2677
2735
|
}
|
|
2678
2736
|
// now send any queued messages
|
|
2679
|
-
|
|
2680
|
-
|
|
2737
|
+
if (pendingRealTimeQueue) {
|
|
2738
|
+
for (const event of pendingRealTimeQueue) {
|
|
2739
|
+
send(event);
|
|
2740
|
+
}
|
|
2741
|
+
pendingRealTimeQueue = null;
|
|
2681
2742
|
}
|
|
2682
|
-
pendingRealTimeQueue = null;
|
|
2683
2743
|
})();
|
|
2684
2744
|
result.catch((error) => {
|
|
2685
2745
|
harper_logger_js_1.default.error?.('Error in real-time subscription:', error);
|
|
@@ -2691,8 +2751,6 @@ function makeTable(options) {
|
|
|
2691
2751
|
}
|
|
2692
2752
|
subscription.send(event);
|
|
2693
2753
|
}
|
|
2694
|
-
if (request.listener)
|
|
2695
|
-
subscription.on('data', request.listener);
|
|
2696
2754
|
return subscription;
|
|
2697
2755
|
}
|
|
2698
2756
|
/**
|
|
@@ -4061,6 +4119,9 @@ function makeTable(options) {
|
|
|
4061
4119
|
transaction,
|
|
4062
4120
|
tableToTrack: tableName,
|
|
4063
4121
|
}, 'put', Boolean(invalidated), auditRecord);
|
|
4122
|
+
// arm the eviction scanner, mirroring the .put() path
|
|
4123
|
+
if (sourceContext.expiresAt)
|
|
4124
|
+
scheduleCleanup();
|
|
4064
4125
|
}
|
|
4065
4126
|
else if (existingEntry) {
|
|
4066
4127
|
logger_ts_1.logger.trace?.(`Deleting resolved record from source with id: ${id}, timestamp: ${new Date(txnTime).toISOString()}`);
|