@fluidframework/container-runtime 2.114.0 → 2.115.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/CHANGELOG.md +4 -0
- package/api-extractor/api-extractor-lint-legacyAlpha.cjs.json +5 -0
- package/api-extractor/api-extractor-lint-legacyAlpha.esm.json +5 -0
- package/api-extractor/api-extractor.legacy.json +5 -1
- package/api-report/container-runtime.legacy.alpha.api.md +453 -0
- package/container-runtime.test-files.tar +0 -0
- package/dist/containerRuntime.d.ts +7 -0
- package/dist/containerRuntime.d.ts.map +1 -1
- package/dist/containerRuntime.js +100 -43
- package/dist/containerRuntime.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/legacy.d.ts +1 -1
- package/dist/legacyAlpha.d.ts +76 -0
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/pendingStateManager.d.ts +7 -0
- package/dist/pendingStateManager.d.ts.map +1 -1
- package/dist/pendingStateManager.js +15 -0
- package/dist/pendingStateManager.js.map +1 -1
- package/dist/public.d.ts +1 -1
- package/dist/runtimeLayerCompatState.d.ts +2 -2
- package/dist/versionMarks/inboundBatch.d.ts +36 -0
- package/dist/versionMarks/inboundBatch.d.ts.map +1 -0
- package/dist/versionMarks/inboundBatch.js +50 -0
- package/dist/versionMarks/inboundBatch.js.map +1 -0
- package/dist/versionMarks/index.d.ts +7 -0
- package/dist/versionMarks/index.d.ts.map +1 -0
- package/dist/versionMarks/index.js +12 -0
- package/dist/versionMarks/index.js.map +1 -0
- package/dist/versionMarks/versionMarkResolver.d.ts +162 -0
- package/dist/versionMarks/versionMarkResolver.d.ts.map +1 -0
- package/dist/versionMarks/versionMarkResolver.js +198 -0
- package/dist/versionMarks/versionMarkResolver.js.map +1 -0
- package/internal.d.ts +1 -1
- package/legacy/alpha.d.ts +11 -0
- package/legacy.d.ts +1 -1
- package/lib/containerRuntime.d.ts +7 -0
- package/lib/containerRuntime.d.ts.map +1 -1
- package/lib/containerRuntime.js +57 -0
- package/lib/containerRuntime.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/legacy.d.ts +1 -1
- package/lib/legacyAlpha.d.ts +76 -0
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/pendingStateManager.d.ts +7 -0
- package/lib/pendingStateManager.d.ts.map +1 -1
- package/lib/pendingStateManager.js +15 -0
- package/lib/pendingStateManager.js.map +1 -1
- package/lib/public.d.ts +1 -1
- package/lib/runtimeLayerCompatState.d.ts +2 -2
- package/lib/versionMarks/inboundBatch.d.ts +36 -0
- package/lib/versionMarks/inboundBatch.d.ts.map +1 -0
- package/lib/versionMarks/inboundBatch.js +46 -0
- package/lib/versionMarks/inboundBatch.js.map +1 -0
- package/lib/versionMarks/index.d.ts +7 -0
- package/lib/versionMarks/index.d.ts.map +1 -0
- package/lib/versionMarks/index.js +7 -0
- package/lib/versionMarks/index.js.map +1 -0
- package/lib/versionMarks/versionMarkResolver.d.ts +162 -0
- package/lib/versionMarks/versionMarkResolver.d.ts.map +1 -0
- package/lib/versionMarks/versionMarkResolver.js +194 -0
- package/lib/versionMarks/versionMarkResolver.js.map +1 -0
- package/package.json +32 -20
- package/src/containerRuntime.ts +87 -0
- package/src/index.ts +5 -0
- package/src/packageVersion.ts +1 -1
- package/src/pendingStateManager.ts +18 -0
- package/src/versionMarks/DEV.md +329 -0
- package/src/versionMarks/inboundBatch.ts +70 -0
- package/src/versionMarks/index.ts +13 -0
- package/src/versionMarks/versionMarkResolver.ts +341 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type {
|
|
7
|
+
ISequencedDocumentMessage,
|
|
8
|
+
IStream,
|
|
9
|
+
} from "@fluidframework/driver-definitions/internal";
|
|
10
|
+
|
|
11
|
+
import { assert } from "@fluidframework/core-utils/internal";
|
|
12
|
+
import type { TelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal";
|
|
13
|
+
|
|
14
|
+
import type { InboundMessageResult } from "../opLifecycle/index.js";
|
|
15
|
+
import { asBatchMetadata } from "../metadata.js";
|
|
16
|
+
|
|
17
|
+
import { inboundVersionMarkUpdate } from "./inboundBatch.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Reads sequenced ops in `[from, to)` from delta storage, injected by the container so the runtime stays
|
|
21
|
+
* driver-agnostic. `abortSignal` cancels an in-flight fetch.
|
|
22
|
+
*
|
|
23
|
+
* @internal
|
|
24
|
+
*/
|
|
25
|
+
export interface IHistoricalOpReader {
|
|
26
|
+
fetchMessages(
|
|
27
|
+
from: number,
|
|
28
|
+
to?: number,
|
|
29
|
+
abortSignal?: AbortSignal,
|
|
30
|
+
): Promise<IStream<ISequencedDocumentMessage[]>>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Result of resolving a pending batchId.
|
|
35
|
+
*
|
|
36
|
+
* @legacy @alpha
|
|
37
|
+
*/
|
|
38
|
+
export type ResolveResult =
|
|
39
|
+
| { readonly kind: "resolved"; readonly sequenceNumber: number }
|
|
40
|
+
| { readonly kind: "pending" }
|
|
41
|
+
| { readonly kind: "unresolvable" };
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The data captured for a version mark. `pending` when the captured edit is local and not yet sequenced
|
|
45
|
+
* (resolve it later via {@link IVersionMarkResolver.resolve}); `resolved` when there is no in-flight local
|
|
46
|
+
* work, so the mark already points at a durable sequence number. The app packs its own stored record from
|
|
47
|
+
* this — the runtime does not define the stored locator shape.
|
|
48
|
+
*
|
|
49
|
+
* @legacy @alpha
|
|
50
|
+
*/
|
|
51
|
+
export type VersionMarkCapture =
|
|
52
|
+
| {
|
|
53
|
+
readonly kind: "pending";
|
|
54
|
+
readonly batchId: string;
|
|
55
|
+
readonly sequenceNumberLowerBound: number;
|
|
56
|
+
}
|
|
57
|
+
| { readonly kind: "resolved"; readonly sequenceNumber: number };
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Runtime-owned resolver for app-stored version mark locators.
|
|
61
|
+
*
|
|
62
|
+
* @legacy @alpha
|
|
63
|
+
*/
|
|
64
|
+
export interface IVersionMarkResolver {
|
|
65
|
+
/**
|
|
66
|
+
* Captures a version mark at the current point. Seals the current outbound batch first (so a just-made
|
|
67
|
+
* local edit has a stable `batchId`, which is only assigned when a batch is flushed), then returns the
|
|
68
|
+
* mark data atomically: a `pending` capture (`batchId` + `sequenceNumberLowerBound`) when there is an
|
|
69
|
+
* unacked local batch, or a `resolved` capture (`sequenceNumber`) when there is no in-flight local work.
|
|
70
|
+
*
|
|
71
|
+
* @remarks Sealing the batch is a side effect (it submits the current batch), so capture at savepoint
|
|
72
|
+
* boundaries, not per keystroke.
|
|
73
|
+
*
|
|
74
|
+
* @returns The pending batch identity and exclusive sequence number lower bound, or the current sequence number
|
|
75
|
+
* when there is no pending local batch.
|
|
76
|
+
*/
|
|
77
|
+
sealAndCaptureVersionMark(): VersionMarkCapture;
|
|
78
|
+
/**
|
|
79
|
+
* Resolves a pending mark's batchId to a global sequence number (`sequenceNumberLowerBound` is the
|
|
80
|
+
* exclusive lower bound for a history read). A `resolved` sequence number feeds the loader's
|
|
81
|
+
* `loadContainerToSequenceNumber`.
|
|
82
|
+
*
|
|
83
|
+
* @param batchId - The stable identity of the pending batch.
|
|
84
|
+
* @param sequenceNumberLowerBound - The exclusive lower bound for the historical op search.
|
|
85
|
+
* @returns The resolved sequence number, or a result indicating that the batch remains pending or can
|
|
86
|
+
* no longer be resolved.
|
|
87
|
+
*/
|
|
88
|
+
resolve(batchId: string, sequenceNumberLowerBound: number): Promise<ResolveResult>;
|
|
89
|
+
/**
|
|
90
|
+
* Subscribes to inbound batch sequencing: fires `(batchId, sequenceNumber)` per batch so any connected
|
|
91
|
+
* client can promote a matching pending mark. Returns an unsubscribe function.
|
|
92
|
+
*
|
|
93
|
+
* @param listener - Called with the stable batch identity and its final sequence number.
|
|
94
|
+
* @returns A function that unsubscribes the listener.
|
|
95
|
+
*/
|
|
96
|
+
onBatchSequenced(listener: (batchId: string, sequenceNumber: number) => void): () => void;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
export interface VersionMarkResolverRuntimeHooks {
|
|
103
|
+
readonly getCurrentSequenceNumber: () => number;
|
|
104
|
+
readonly getCurrentMinimumSequenceNumber: () => number;
|
|
105
|
+
readonly getCurrentPendingBatchId: () => string | undefined;
|
|
106
|
+
/** Logs faults from app-supplied `onBatchSequenced` listeners (see {@link VersionMarkResolver.processInboundBatch}). */
|
|
107
|
+
readonly logger: TelemetryLoggerExt;
|
|
108
|
+
/**
|
|
109
|
+
* Seals the current outbound batch (flushes the runtime), so a just-submitted local edit is moved into
|
|
110
|
+
* the pending state with a stable `batchId` before
|
|
111
|
+
* {@link IVersionMarkResolver.sealAndCaptureVersionMark} reads it.
|
|
112
|
+
*/
|
|
113
|
+
readonly flushPendingBatch: () => void;
|
|
114
|
+
/**
|
|
115
|
+
* Injected by the container, backed by delta storage. When absent, unknown batchIds resolve as `pending`.
|
|
116
|
+
*/
|
|
117
|
+
readonly getHistoricalOpReader?: () => IHistoricalOpReader | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* Creates a per-scan op unpacker that mirrors the live inbound path (chunk reassembly, ungroup,
|
|
120
|
+
* decompress), so the history scan observes the same restored `batchId` — in particular for chunked
|
|
121
|
+
* batches, whose id is stripped from the final chunk's wire metadata and restored only after
|
|
122
|
+
* reassembly. A fresh unpacker is created per scan since it holds reassembly state. When absent, the
|
|
123
|
+
* history scan cannot identify batches and reports `pending`/`unresolvable`.
|
|
124
|
+
*/
|
|
125
|
+
readonly createHistoricalOpUnpacker?: () => (
|
|
126
|
+
op: ISequencedDocumentMessage,
|
|
127
|
+
) => InboundMessageResult | undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resolves a mark's batchId to a global sequence number. The app owns mark storage and promotion; the
|
|
132
|
+
* runtime exposes the capture point and an ephemeral in-session `batchId -> sequenceNumber` map.
|
|
133
|
+
*
|
|
134
|
+
* @internal
|
|
135
|
+
*/
|
|
136
|
+
export class VersionMarkResolver implements IVersionMarkResolver {
|
|
137
|
+
private readonly sequenceNumberByBatchId = new Map<string, number>();
|
|
138
|
+
private readonly batchListeners = new Set<
|
|
139
|
+
(batchId: string, sequenceNumber: number) => void
|
|
140
|
+
>();
|
|
141
|
+
/**
|
|
142
|
+
* Sticky flag: false until the feature is first used this session (a pending capture or a listener).
|
|
143
|
+
* While false the runtime skips all per-inbound-batch tracking, so a container that never uses version
|
|
144
|
+
* marks pays nothing on the hot path (mirrors #22497 gating `DuplicateBatchDetector` on offline load).
|
|
145
|
+
*/
|
|
146
|
+
private tracking = false;
|
|
147
|
+
|
|
148
|
+
public constructor(private readonly hooks: VersionMarkResolverRuntimeHooks) {}
|
|
149
|
+
|
|
150
|
+
/** Whether inbound batches need tracking (see {@link tracking}). Read by the runtime to gate the hot path. */
|
|
151
|
+
public get isTracking(): boolean {
|
|
152
|
+
return this.tracking;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public sealAndCaptureVersionMark(): VersionMarkCapture {
|
|
156
|
+
// Seal the current batch so a just-submitted local edit is flushed into the pending state with a
|
|
157
|
+
// stable batchId (which is only assigned at flush) before we read it.
|
|
158
|
+
this.hooks.flushPendingBatch();
|
|
159
|
+
const sequenceNumberLowerBound = this.hooks.getCurrentSequenceNumber();
|
|
160
|
+
const batchId = this.hooks.getCurrentPendingBatchId();
|
|
161
|
+
if (batchId === undefined) {
|
|
162
|
+
// No unacked local batch: the mark already points at a durable sequence number.
|
|
163
|
+
return { kind: "resolved", sequenceNumber: sequenceNumberLowerBound };
|
|
164
|
+
}
|
|
165
|
+
// A pending mark needs its batch tracked so resolve() can promote it from the live map.
|
|
166
|
+
this.tracking = true;
|
|
167
|
+
return { kind: "pending", batchId, sequenceNumberLowerBound };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public async resolve(
|
|
171
|
+
batchId: string,
|
|
172
|
+
sequenceNumberLowerBound: number,
|
|
173
|
+
): Promise<ResolveResult> {
|
|
174
|
+
// Fast path: batch sequenced live this session.
|
|
175
|
+
const sequenceNumber = this.sessionSequenceFor(batchId);
|
|
176
|
+
if (sequenceNumber !== undefined) {
|
|
177
|
+
return { kind: "resolved", sequenceNumber };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Otherwise scan history from the mark's reference point.
|
|
181
|
+
const reader = this.hooks.getHistoricalOpReader?.();
|
|
182
|
+
if (reader === undefined) {
|
|
183
|
+
// No reader: the batch may still sequence live, so report pending.
|
|
184
|
+
return { kind: "pending" };
|
|
185
|
+
}
|
|
186
|
+
return this.resolveFromHistory(reader, batchId, sequenceNumberLowerBound);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Scans sequenced ops after `sequenceNumberLowerBound` for `batchId`, returning its last op's
|
|
191
|
+
* sequence number when found and aborting the fetch once found or exhausted. Each op is routed through
|
|
192
|
+
* the same unpack pipeline the live inbound path uses (chunk reassembly, ungroup, decompress) and
|
|
193
|
+
* {@link inboundVersionMarkUpdate} derives its batch identity, so chunked batches resolve here too. The
|
|
194
|
+
* scan anchor is not guaranteed to be a batch boundary, so a leading batch clipped by the anchor is
|
|
195
|
+
* tolerated (its orphan end marker is dropped rather than fed to the processor). On a miss,
|
|
196
|
+
* {@link classifyMiss} distinguishes `pending` (not sequenced yet) from `unresolvable` (ops trimmed).
|
|
197
|
+
* See DEV.md.
|
|
198
|
+
*/
|
|
199
|
+
private async resolveFromHistory(
|
|
200
|
+
reader: IHistoricalOpReader,
|
|
201
|
+
batchId: string,
|
|
202
|
+
sequenceNumberLowerBound: number,
|
|
203
|
+
): Promise<ResolveResult> {
|
|
204
|
+
const from = sequenceNumberLowerBound + 1;
|
|
205
|
+
const unpack = this.hooks.createHistoricalOpUnpacker?.();
|
|
206
|
+
const abortController = new AbortController();
|
|
207
|
+
try {
|
|
208
|
+
const stream = await reader.fetchMessages(from, undefined, abortController.signal);
|
|
209
|
+
let firstScannedSequenceNumber: number | undefined;
|
|
210
|
+
let carriedBatchId: string | undefined;
|
|
211
|
+
// Mirrors the processor's batch-in-progress state so we can drop an orphan end marker (below).
|
|
212
|
+
let inBatch = false;
|
|
213
|
+
while (true) {
|
|
214
|
+
const result = await stream.read();
|
|
215
|
+
if (result.done) {
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
for (const op of result.value) {
|
|
219
|
+
firstScannedSequenceNumber ??= op.sequenceNumber;
|
|
220
|
+
if (!inBatch && asBatchMetadata(op.metadata)?.batch === false) {
|
|
221
|
+
// The anchor (`sequenceNumberLowerBound + 1`) may land inside a batch whose start
|
|
222
|
+
// precedes the window; its orphan end marker would trip the processor's 0x9d5 assert.
|
|
223
|
+
// Drop the clipped tail. The target batch is fully in-window, so this can't skip it.
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
// undefined = a system op, or an incomplete chunk awaiting later fragments.
|
|
227
|
+
const inboundResult = unpack?.(op);
|
|
228
|
+
if (inboundResult === undefined) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
// Track batch progress with the processor so the guard above skips only orphan ends.
|
|
232
|
+
if (inboundResult.type === "batchStartingMessage") {
|
|
233
|
+
inBatch = true;
|
|
234
|
+
} else if (
|
|
235
|
+
inboundResult.type === "nextBatchMessage" &&
|
|
236
|
+
inboundResult.batchEnd === true
|
|
237
|
+
) {
|
|
238
|
+
inBatch = false;
|
|
239
|
+
}
|
|
240
|
+
// Derive batch identity exactly as the live inbound path does, carrying the id across a
|
|
241
|
+
// piecemeal batch's messages.
|
|
242
|
+
const update = inboundVersionMarkUpdate(inboundResult, carriedBatchId);
|
|
243
|
+
carriedBatchId = update.carriedBatchId;
|
|
244
|
+
if (update.sequenced?.batchId === batchId) {
|
|
245
|
+
return { kind: "resolved", sequenceNumber: update.sequenced.sequenceNumber };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
// Not found: distinguish "not sequenced yet" from "trimmed" via the tip and where the scan landed.
|
|
250
|
+
return this.classifyMiss(from, firstScannedSequenceNumber);
|
|
251
|
+
} finally {
|
|
252
|
+
// Cancel any in-flight fetch once we stop reading (found or exhausted).
|
|
253
|
+
abortController.abort();
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Classifies a history-scan miss as `pending` or `unresolvable`. See {@link resolveFromHistory}.
|
|
259
|
+
*/
|
|
260
|
+
private classifyMiss(
|
|
261
|
+
from: number,
|
|
262
|
+
firstScannedSequenceNumber: number | undefined,
|
|
263
|
+
): ResolveResult {
|
|
264
|
+
// Invariant: a reader must never return an op before the requested range start. If it does, the
|
|
265
|
+
// trim inference below is meaningless, so fail loudly rather than misclassify.
|
|
266
|
+
assert(
|
|
267
|
+
firstScannedSequenceNumber === undefined || firstScannedSequenceNumber >= from,
|
|
268
|
+
0xd1c /* historical op reader returned an op before the requested range start */,
|
|
269
|
+
);
|
|
270
|
+
const tip = this.hooks.getCurrentSequenceNumber();
|
|
271
|
+
if (from > tip) {
|
|
272
|
+
// Nothing is sequenced at/after the mark's reference point yet, so the batch cannot have landed.
|
|
273
|
+
return { kind: "pending" };
|
|
274
|
+
}
|
|
275
|
+
if (firstScannedSequenceNumber === undefined) {
|
|
276
|
+
// Empty read though ops should exist in `[from, tip]` → trimmed. ODSP-specific: a strict driver
|
|
277
|
+
// empties a `from`-misaligned trimmed range (validateMessages); a return-from-earliest driver
|
|
278
|
+
// would instead surface the trim via the `firstScannedSequenceNumber > from` branch below.
|
|
279
|
+
return { kind: "unresolvable" };
|
|
280
|
+
}
|
|
281
|
+
if (firstScannedSequenceNumber > from) {
|
|
282
|
+
// A trim gap at the anchor: the mark's ops (just after the reference point) are gone.
|
|
283
|
+
return { kind: "unresolvable" };
|
|
284
|
+
}
|
|
285
|
+
// Ops are present from the reference point and the batch is not among them: not yet sequenced.
|
|
286
|
+
return { kind: "pending" };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
public onBatchSequenced(
|
|
290
|
+
listener: (batchId: string, sequenceNumber: number) => void,
|
|
291
|
+
): () => void {
|
|
292
|
+
// A subscriber wants live promotions, so start tracking inbound batches from here on.
|
|
293
|
+
this.tracking = true;
|
|
294
|
+
this.batchListeners.add(listener);
|
|
295
|
+
return () => {
|
|
296
|
+
this.batchListeners.delete(listener);
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/** The sequence number for a batch seen live this session, or undefined. */
|
|
301
|
+
private sessionSequenceFor(batchId: string): number | undefined {
|
|
302
|
+
return this.sequenceNumberByBatchId.get(batchId);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** Records an inbound batch and notifies listeners so any client can promote matching pending marks. */
|
|
306
|
+
public processInboundBatch(batchId: string, sequenceNumber: number): void {
|
|
307
|
+
const existingSequenceNumber = this.sequenceNumberByBatchId.get(batchId);
|
|
308
|
+
if (existingSequenceNumber === sequenceNumber) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
this.sequenceNumberByBatchId.set(batchId, sequenceNumber);
|
|
313
|
+
this.evictBelowMinimumSequenceNumber();
|
|
314
|
+
for (const listener of this.batchListeners) {
|
|
315
|
+
try {
|
|
316
|
+
listener(batchId, sequenceNumber);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
// Isolate each app listener (like the container's EventEmitterWithErrorHandling): one throw
|
|
319
|
+
// must not abort op processing or starve the rest. Log and continue rather than fault the
|
|
320
|
+
// container — a missed promotion is recoverable via resolve()'s history scan.
|
|
321
|
+
this.hooks.logger.sendErrorEvent({ eventName: "VersionMarkListenerException" }, error);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Bounds the map to the collaboration window by dropping entries below the MSN (mirrors
|
|
328
|
+
* `DuplicateBatchDetector`). Safe because below the MSN every client has processed the batch, so its
|
|
329
|
+
* `onBatchSequenced` already fired; a dropped entry falls back to the history scan. Entries insert in
|
|
330
|
+
* sequence order, so iteration stops at the first retained one.
|
|
331
|
+
*/
|
|
332
|
+
private evictBelowMinimumSequenceNumber(): void {
|
|
333
|
+
const minimumSequenceNumber = this.hooks.getCurrentMinimumSequenceNumber();
|
|
334
|
+
for (const [id, sequenceNumber] of this.sequenceNumberByBatchId) {
|
|
335
|
+
if (sequenceNumber >= minimumSequenceNumber) {
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
this.sequenceNumberByBatchId.delete(id);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|