@abloatai/humans 0.38.0 → 0.39.0
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/dist/core.d.ts +1 -1
- package/dist/core.js +1 -1
- package/dist/local/BaseSyncedStore.js +7 -1
- package/dist/local/Database.d.ts +20 -0
- package/dist/local/Database.js +83 -49
- package/dist/local/InstanceCache.d.ts +18 -8
- package/dist/local/InstanceCache.js +74 -74
- package/dist/local/Model.d.ts +18 -0
- package/dist/local/Model.js +83 -32
- package/dist/local/SyncClient.d.ts +1 -4
- package/dist/local/SyncClient.js +55 -60
- package/dist/local/client/reactiveEngine.js +18 -0
- package/dist/local/sync/SyncWebSocket.js +3 -6
- package/dist/local/sync/deltaPipeline.js +11 -1
- package/dist/local/sync/drainProfile.d.ts +45 -0
- package/dist/local/sync/drainProfile.js +55 -0
- package/package.json +2 -2
- package/src/core.ts +3 -0
- package/src/local/BaseSyncedStore.ts +7 -1
- package/src/local/Database.ts +87 -50
- package/src/local/InstanceCache.ts +77 -71
- package/src/local/Model.ts +98 -37
- package/src/local/SyncClient.ts +57 -61
- package/src/local/client/reactiveEngine.ts +18 -0
- package/src/local/sync/SyncWebSocket.ts +3 -6
- package/src/local/sync/deltaPipeline.ts +21 -1
- package/src/local/sync/drainProfile.ts +93 -0
package/src/local/SyncClient.ts
CHANGED
|
@@ -1290,34 +1290,25 @@ export class SyncClient extends EventEmitter {
|
|
|
1290
1290
|
* local model has unsynced changes, so the two sides stay consistent.
|
|
1291
1291
|
*/
|
|
1292
1292
|
resolveConflicts(localModel: Model, serverData: Record<string, unknown>): Model {
|
|
1293
|
-
|
|
1294
|
-
//
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
serverUpdatedAt: serverData.updatedAt,
|
|
1308
|
-
localChanges: localModel.getChanges(),
|
|
1309
|
-
serverState: this.extractCriticalState(serverData),
|
|
1310
|
-
});
|
|
1311
|
-
|
|
1312
|
-
// PRIORITY 1: Check for critical server states that must be respected
|
|
1313
|
-
// These states override any local changes to maintain data consistency
|
|
1314
|
-
const criticalServerStates = this.extractCriticalState(serverData);
|
|
1315
|
-
const shouldForceAcceptServer = this.hasCriticalStateChange(criticalServerStates);
|
|
1293
|
+
// No entry-point debug here: this runs once per incoming update delta,
|
|
1294
|
+
// and a debug call's payload is BUILT even when the logger discards it —
|
|
1295
|
+
// a per-delta model scan on the apply hot path. The outcome branches
|
|
1296
|
+
// below log the cases worth reading.
|
|
1297
|
+
|
|
1298
|
+
// PRIORITY 1: Check for critical server states that must be respected.
|
|
1299
|
+
// These states override any local changes to maintain data consistency.
|
|
1300
|
+
// Checked inline — the collected-object form (`extractCriticalState`)
|
|
1301
|
+
// only materializes on the rare force-accept branch, for its log line.
|
|
1302
|
+
const shouldForceAcceptServer =
|
|
1303
|
+
(serverData.deletedAt !== undefined && serverData.deletedAt !== null) ||
|
|
1304
|
+
(serverData.archivedAt !== undefined && serverData.archivedAt !== null) ||
|
|
1305
|
+
serverData.isActive === false ||
|
|
1306
|
+
(serverData.unassignedAt !== undefined && serverData.unassignedAt !== null);
|
|
1316
1307
|
|
|
1317
1308
|
if (shouldForceAcceptServer) {
|
|
1318
1309
|
this.runtime.logger.debug('Accepting server update - critical state change detected', {
|
|
1319
1310
|
modelId: localModel.id,
|
|
1320
|
-
criticalStates:
|
|
1311
|
+
criticalStates: this.extractCriticalState(serverData),
|
|
1321
1312
|
});
|
|
1322
1313
|
|
|
1323
1314
|
// Force accept server state for critical changes
|
|
@@ -1329,7 +1320,7 @@ export class SyncClient extends EventEmitter {
|
|
|
1329
1320
|
|
|
1330
1321
|
// Local-first: if we have local dirty fields, merge by field.
|
|
1331
1322
|
// Keep locally changed fields; apply server for the rest.
|
|
1332
|
-
if (
|
|
1323
|
+
if (localModel.hasChanges) {
|
|
1333
1324
|
const localChanges = localModel.getChanges();
|
|
1334
1325
|
this.runtime.logger.debug('Merging server update with local dirty fields', {
|
|
1335
1326
|
modelId: localModel.id,
|
|
@@ -1341,6 +1332,13 @@ export class SyncClient extends EventEmitter {
|
|
|
1341
1332
|
|
|
1342
1333
|
// Preserve the most recent updatedAt without clearing dirty flags
|
|
1343
1334
|
if (serverData.updatedAt || localModel.updatedAt) {
|
|
1335
|
+
// Safely get timestamp, handling both Date objects and strings
|
|
1336
|
+
const localUpdatedAt = localModel.updatedAt
|
|
1337
|
+
? localModel.updatedAt instanceof Date
|
|
1338
|
+
? localModel.updatedAt.getTime()
|
|
1339
|
+
: new Date(localModel.updatedAt).getTime()
|
|
1340
|
+
: 0;
|
|
1341
|
+
const serverUpdatedAt = toEpochMs(serverData.updatedAt);
|
|
1344
1342
|
const mergedUpdatedAt = new Date(Math.max(localUpdatedAt, serverUpdatedAt));
|
|
1345
1343
|
// updateFromData accepts Date or ISO string for dates
|
|
1346
1344
|
merged.updatedAt = mergedUpdatedAt;
|
|
@@ -1351,10 +1349,9 @@ export class SyncClient extends EventEmitter {
|
|
|
1351
1349
|
return localModel;
|
|
1352
1350
|
}
|
|
1353
1351
|
|
|
1354
|
-
// No local changes: fall back to LWW to converge
|
|
1355
|
-
//
|
|
1356
|
-
|
|
1357
|
-
this.runtime.logger.debug(`Accepting server update - ${acceptReason}`);
|
|
1352
|
+
// No local changes: fall back to LWW to converge. Accept server
|
|
1353
|
+
// regardless of timestamp equality to stay in sync. Not logged — this is
|
|
1354
|
+
// the common path for every collaborator update a client receives.
|
|
1358
1355
|
localModel.updateFromData(serverData);
|
|
1359
1356
|
localModel.clearChanges();
|
|
1360
1357
|
localModel.markAsSynced();
|
|
@@ -1392,17 +1389,6 @@ export class SyncClient extends EventEmitter {
|
|
|
1392
1389
|
return critical;
|
|
1393
1390
|
}
|
|
1394
1391
|
|
|
1395
|
-
/**
|
|
1396
|
-
* Check if critical state changes exist that require forcing server state
|
|
1397
|
-
*/
|
|
1398
|
-
private hasCriticalStateChange(criticalStates: Record<string, unknown>): boolean {
|
|
1399
|
-
// Any critical state present means we should force accept server
|
|
1400
|
-
return (
|
|
1401
|
-
Object.keys(criticalStates).length > 0 &&
|
|
1402
|
-
Object.values(criticalStates).some((v) => v !== null && v !== undefined)
|
|
1403
|
-
);
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
1392
|
/**
|
|
1407
1393
|
* Handle network reconnection
|
|
1408
1394
|
*/
|
|
@@ -1901,6 +1887,22 @@ export class SyncClient extends EventEmitter {
|
|
|
1901
1887
|
applyDeltaBatchToPool(
|
|
1902
1888
|
dbResults: readonly AppliedChange[],
|
|
1903
1889
|
enrichRelations: (modelName: string, data: Record<string, unknown>) => Record<string, unknown>,
|
|
1890
|
+
): void {
|
|
1891
|
+
// The WHOLE batch — conflict resolution and model mutation included, not
|
|
1892
|
+
// just the pool bookkeeping at the end — runs in one MobX action.
|
|
1893
|
+
// `resolveConflicts` writes model fields via `updateFromData`; when those
|
|
1894
|
+
// writes ran before the action, every delta opened its own top-level
|
|
1895
|
+
// action and flushed reactions at its boundary, so a large frame paid one
|
|
1896
|
+
// reaction pass per delta and an observer could see a partially applied
|
|
1897
|
+
// frame between them.
|
|
1898
|
+
runInAction(() => {
|
|
1899
|
+
this.applyDeltaBatchToPoolInAction(dbResults, enrichRelations);
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
private applyDeltaBatchToPoolInAction(
|
|
1904
|
+
dbResults: readonly AppliedChange[],
|
|
1905
|
+
enrichRelations: (modelName: string, data: Record<string, unknown>) => Record<string, unknown>,
|
|
1904
1906
|
): void {
|
|
1905
1907
|
const modelsToAdd: Model[] = [];
|
|
1906
1908
|
const modelsToUpsert: Model[] = [];
|
|
@@ -2002,27 +2004,21 @@ export class SyncClient extends EventEmitter {
|
|
|
2002
2004
|
}
|
|
2003
2005
|
}
|
|
2004
2006
|
|
|
2005
|
-
// Reveal the whole frame
|
|
2006
|
-
//
|
|
2007
|
-
//
|
|
2008
|
-
//
|
|
2009
|
-
//
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
// Emit changed model types so QueryProcessor can auto-invalidate.
|
|
2021
|
-
// Kept inside the action so any observable query-cache state it
|
|
2022
|
-
// flips is part of the same atomic reveal.
|
|
2023
|
-
const changedTypes = new Set(dbResults.map(r => r.modelName));
|
|
2024
|
-
if (changedTypes.size > 0) this.emit('models:changed', changedTypes);
|
|
2025
|
-
});
|
|
2007
|
+
// Reveal the whole frame at one reaction boundary: the caller's single
|
|
2008
|
+
// action covers the collection loop above and these batch pool writes, so
|
|
2009
|
+
// dependents recompute exactly once regardless of how many models or
|
|
2010
|
+
// operation kinds the frame touched, and the app never observes a
|
|
2011
|
+
// partially applied frame.
|
|
2012
|
+
if (modelsToAdd.length > 0) this.objectPool.addBatch(modelsToAdd, ModelScope.live);
|
|
2013
|
+
if (modelsToUpsert.length > 0) this.objectPool.upsertBatch(modelsToUpsert, ModelScope.live);
|
|
2014
|
+
if (idsToRemove.length > 0) this.objectPool.removeBatch(idsToRemove);
|
|
2015
|
+
for (const id of idsToArchive) this.objectPool.updateScope(id, ModelScope.archived);
|
|
2016
|
+
|
|
2017
|
+
// Emit changed model types so QueryProcessor can auto-invalidate.
|
|
2018
|
+
// Kept inside the action so any observable query-cache state it
|
|
2019
|
+
// flips is part of the same atomic reveal.
|
|
2020
|
+
const changedTypes = new Set(dbResults.map(r => r.modelName));
|
|
2021
|
+
if (changedTypes.size > 0) this.emit('models:changed', changedTypes);
|
|
2026
2022
|
}
|
|
2027
2023
|
|
|
2028
2024
|
/**
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { Schema, SchemaRecord } from '@abloatai/transaction/schema/schema';
|
|
16
|
+
import { omittedModelError } from '@abloatai/transaction/schema/select';
|
|
16
17
|
import {
|
|
17
18
|
durableCommitOperationSchema,
|
|
18
19
|
type DurableCommitOperation,
|
|
@@ -934,5 +935,22 @@ export function buildReactiveEngine<const S extends SchemaRecord>(
|
|
|
934
935
|
},
|
|
935
936
|
} as Ablo<S>;
|
|
936
937
|
|
|
938
|
+
// A model the schema projection left out answers with an error naming the
|
|
939
|
+
// model and the fix, not `undefined`. An app can compile against the full
|
|
940
|
+
// source schema while running a projection, so the type system never sees
|
|
941
|
+
// this gap; without the stub the caller crashes one property later with a
|
|
942
|
+
// bare TypeError ("reading 'local'") that names neither. Non-enumerable so
|
|
943
|
+
// spread, Object.keys, and JSON.stringify walk past the stubs untriggered.
|
|
944
|
+
for (const name of schema.omittedModels ?? []) {
|
|
945
|
+
if (name in engine) continue;
|
|
946
|
+
Object.defineProperty(engine, name, {
|
|
947
|
+
get() {
|
|
948
|
+
throw omittedModelError(name);
|
|
949
|
+
},
|
|
950
|
+
enumerable: false,
|
|
951
|
+
configurable: true,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
|
|
937
955
|
return engine;
|
|
938
956
|
}
|
|
@@ -272,12 +272,9 @@ export class SyncWebSocket<
|
|
|
272
272
|
protected override handleDelta(rawDelta: unknown): void {
|
|
273
273
|
const delta = this.normalizeWireDelta(rawDelta);
|
|
274
274
|
if (!delta) return;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
id: delta.modelId,
|
|
279
|
-
syncId: delta.id,
|
|
280
|
-
});
|
|
275
|
+
// No per-delta debug here: the payload object is built even when the
|
|
276
|
+
// logger discards it, and this runs at the full live wire rate. Dropped
|
|
277
|
+
// malformed deltas are still logged by `normalizeWireDelta`.
|
|
281
278
|
|
|
282
279
|
// Do not advance `this.cursor.lastSyncId` on receipt. The runtime cursor
|
|
283
280
|
// must stay consistent with what has been persisted locally; otherwise the
|
|
@@ -24,7 +24,14 @@ import {
|
|
|
24
24
|
type AbloPlugin,
|
|
25
25
|
type AppliedChange,
|
|
26
26
|
} from '../../plugin.js';
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
observeDrainBatch,
|
|
29
|
+
observeDrainAcknowledge,
|
|
30
|
+
timeDrainStage,
|
|
31
|
+
timeDrainStageAsync,
|
|
32
|
+
openDrainBatchRow,
|
|
33
|
+
closeDrainBatchRow,
|
|
34
|
+
} from './drainProfile.js';
|
|
28
35
|
|
|
29
36
|
/**
|
|
30
37
|
* What the pipeline needs back from the surrounding store: the shared mutable
|
|
@@ -408,6 +415,18 @@ function yieldToHost(): Promise<void> {
|
|
|
408
415
|
async function flushDeltaBatch(
|
|
409
416
|
ctx: DeltaPipelineContext,
|
|
410
417
|
queuedDeltas: SyncDelta[],
|
|
418
|
+
): Promise<void> {
|
|
419
|
+
openDrainBatchRow(queuedDeltas.length);
|
|
420
|
+
try {
|
|
421
|
+
await flushDeltaBatchInner(ctx, queuedDeltas);
|
|
422
|
+
} finally {
|
|
423
|
+
closeDrainBatchRow();
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function flushDeltaBatchInner(
|
|
428
|
+
ctx: DeltaPipelineContext,
|
|
429
|
+
queuedDeltas: SyncDelta[],
|
|
411
430
|
): Promise<void> {
|
|
412
431
|
const stagePlugins = ctx.stagePlugins ?? [];
|
|
413
432
|
const deduplicatedDeltas = timeDrainStage('dedupe', () => ctx.deduplicateDeltas(queuedDeltas));
|
|
@@ -489,6 +508,7 @@ async function flushDeltaBatch(
|
|
|
489
508
|
timeDrainStage('acknowledge', () => {
|
|
490
509
|
ctx.acknowledge(persistedSyncId);
|
|
491
510
|
ctx.advancePersisted(persistedSyncId);
|
|
511
|
+
observeDrainAcknowledge(persistedSyncId);
|
|
492
512
|
runStage(stagePlugins, 'acknowledge', { syncId: persistedSyncId });
|
|
493
513
|
});
|
|
494
514
|
}
|
|
@@ -32,6 +32,19 @@ export interface DrainStageTotals {
|
|
|
32
32
|
readonly calls: number;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* One flush batch on the wall clock. Wall time (`Date.now`) rather than
|
|
37
|
+
* `performance.now`, because rows cross the worker boundary and each thread
|
|
38
|
+
* has its own `performance` origin — the drain-tail stamps learned the same
|
|
39
|
+
* lesson. Stage entries are the batch's own share of each pipeline stage.
|
|
40
|
+
*/
|
|
41
|
+
export interface DrainBatchRow {
|
|
42
|
+
readonly startedAtWallMs: number;
|
|
43
|
+
readonly endedAtWallMs: number;
|
|
44
|
+
readonly deltas: number;
|
|
45
|
+
readonly stages: Readonly<Partial<Record<DrainStage, number>>>;
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
export interface DrainProfile {
|
|
36
49
|
/** Flush batches drained. The per-batch fixed cost multiplies by this. */
|
|
37
50
|
readonly batches: number;
|
|
@@ -42,6 +55,12 @@ export interface DrainProfile {
|
|
|
42
55
|
/** Wall time from the first observed stage to the last. */
|
|
43
56
|
readonly spanMs: number;
|
|
44
57
|
readonly stages: Readonly<Record<DrainStage, DrainStageTotals>>;
|
|
58
|
+
/**
|
|
59
|
+
* The most recent flush batches, oldest first, capped — enough to cover a
|
|
60
|
+
* drain tail. Optional because derived profiles (window subtraction, fleet
|
|
61
|
+
* merges) drop it; only a worker's own snapshot carries rows.
|
|
62
|
+
*/
|
|
63
|
+
readonly recentBatches?: readonly DrainBatchRow[];
|
|
45
64
|
}
|
|
46
65
|
|
|
47
66
|
const DRAIN_STAGES: readonly DrainStage[] = [
|
|
@@ -72,6 +91,73 @@ let deduplicated = 0;
|
|
|
72
91
|
let firstMark: number | undefined;
|
|
73
92
|
let lastMark = 0;
|
|
74
93
|
|
|
94
|
+
/** Ring of recent batch rows. ~50 batches/sec at benchmark rates, so this covers seconds of tail. */
|
|
95
|
+
const BATCH_ROW_CAP = 128;
|
|
96
|
+
let batchRows: DrainBatchRow[] = [];
|
|
97
|
+
interface OpenBatchRow {
|
|
98
|
+
startedAtWallMs: number;
|
|
99
|
+
deltas: number;
|
|
100
|
+
stages: Partial<Record<DrainStage, number>>;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The batch currently being flushed. Module-global like the totals above, so
|
|
104
|
+
* an isolate hosting several stores attributes interleaved awaits to whichever
|
|
105
|
+
* batch is open — the same per-isolate approximation the totals already make.
|
|
106
|
+
*/
|
|
107
|
+
let currentRow: OpenBatchRow | null = null;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Wall-stamped persisted-cursor advances, oldest first. The benchmark's drain
|
|
111
|
+
* gate reads THESE rather than observing the cursor from a timer or a
|
|
112
|
+
* cross-thread poll: any observation that has to be scheduled onto the
|
|
113
|
+
* worker's event loop queues behind the very drain burst it is measuring and
|
|
114
|
+
* reports the queue's latency as drain. A stamp taken synchronously inside
|
|
115
|
+
* the acknowledge stage cannot be deferred by anything.
|
|
116
|
+
*/
|
|
117
|
+
export interface AcknowledgeStamp {
|
|
118
|
+
readonly syncId: number;
|
|
119
|
+
readonly atWallMs: number;
|
|
120
|
+
}
|
|
121
|
+
const ACK_STAMP_CAP = 512;
|
|
122
|
+
let ackStamps: AcknowledgeStamp[] = [];
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Record a persisted-cursor advance. Called by the pipeline's acknowledge
|
|
126
|
+
* stage. Unlike every stage timer here, this is NOT gated on the profiler
|
|
127
|
+
* flag: it is one wall-clock read and one bounded push per flush batch —
|
|
128
|
+
* nothing against the batch's own work — and the certification benchmark
|
|
129
|
+
* runs unprofiled (the profiler costs ~15%), so the honest drain stamp must
|
|
130
|
+
* exist without it.
|
|
131
|
+
*/
|
|
132
|
+
export function observeDrainAcknowledge(syncId: number): void {
|
|
133
|
+
ackStamps.push({ syncId, atWallMs: Date.now() });
|
|
134
|
+
if (ackStamps.length > ACK_STAMP_CAP) ackStamps.shift();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The recorded persisted-advance stamps, oldest first. */
|
|
138
|
+
export function drainAcknowledgeStamps(): readonly AcknowledgeStamp[] {
|
|
139
|
+
return [...ackStamps];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Begin a batch row. Called by the pipeline at flush entry when profiling. */
|
|
143
|
+
export function openDrainBatchRow(deltaCount: number): void {
|
|
144
|
+
if (!enabled) return;
|
|
145
|
+
currentRow = { startedAtWallMs: Date.now(), deltas: deltaCount, stages: {} };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Close the open batch row and commit it to the ring. */
|
|
149
|
+
export function closeDrainBatchRow(): void {
|
|
150
|
+
if (!enabled || currentRow === null) return;
|
|
151
|
+
batchRows.push({
|
|
152
|
+
startedAtWallMs: currentRow.startedAtWallMs,
|
|
153
|
+
endedAtWallMs: Date.now(),
|
|
154
|
+
deltas: currentRow.deltas,
|
|
155
|
+
stages: currentRow.stages,
|
|
156
|
+
});
|
|
157
|
+
if (batchRows.length > BATCH_ROW_CAP) batchRows.shift();
|
|
158
|
+
currentRow = null;
|
|
159
|
+
}
|
|
160
|
+
|
|
75
161
|
/**
|
|
76
162
|
* Read once. A profiler that consults the environment on every delta would
|
|
77
163
|
* itself become a per-delta cost in the path it is measuring.
|
|
@@ -98,6 +184,9 @@ export function observeDrainStage(stage: DrainStage, elapsedMs: number): void {
|
|
|
98
184
|
const entry = totals[stage];
|
|
99
185
|
entry.totalMs += elapsedMs;
|
|
100
186
|
entry.calls += 1;
|
|
187
|
+
if (currentRow !== null) {
|
|
188
|
+
currentRow.stages[stage] = (currentRow.stages[stage] ?? 0) + elapsedMs;
|
|
189
|
+
}
|
|
101
190
|
mark(elapsedMs);
|
|
102
191
|
}
|
|
103
192
|
|
|
@@ -150,6 +239,7 @@ export function drainProfileSnapshot(): DrainProfile {
|
|
|
150
239
|
deduplicated,
|
|
151
240
|
spanMs: firstMark === undefined ? 0 : lastMark - firstMark,
|
|
152
241
|
stages,
|
|
242
|
+
recentBatches: [...batchRows],
|
|
153
243
|
};
|
|
154
244
|
}
|
|
155
245
|
|
|
@@ -161,4 +251,7 @@ export function resetDrainProfile(): void {
|
|
|
161
251
|
deduplicated = 0;
|
|
162
252
|
firstMark = undefined;
|
|
163
253
|
lastMark = 0;
|
|
254
|
+
batchRows = [];
|
|
255
|
+
currentRow = null;
|
|
256
|
+
ackStamps = [];
|
|
164
257
|
}
|