@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.
Files changed (78) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/api-extractor/api-extractor-lint-legacyAlpha.cjs.json +5 -0
  3. package/api-extractor/api-extractor-lint-legacyAlpha.esm.json +5 -0
  4. package/api-extractor/api-extractor.legacy.json +5 -1
  5. package/api-report/container-runtime.legacy.alpha.api.md +453 -0
  6. package/container-runtime.test-files.tar +0 -0
  7. package/dist/containerRuntime.d.ts +7 -0
  8. package/dist/containerRuntime.d.ts.map +1 -1
  9. package/dist/containerRuntime.js +100 -43
  10. package/dist/containerRuntime.js.map +1 -1
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/legacy.d.ts +1 -1
  15. package/dist/legacyAlpha.d.ts +76 -0
  16. package/dist/packageVersion.d.ts +1 -1
  17. package/dist/packageVersion.js +1 -1
  18. package/dist/packageVersion.js.map +1 -1
  19. package/dist/pendingStateManager.d.ts +7 -0
  20. package/dist/pendingStateManager.d.ts.map +1 -1
  21. package/dist/pendingStateManager.js +15 -0
  22. package/dist/pendingStateManager.js.map +1 -1
  23. package/dist/public.d.ts +1 -1
  24. package/dist/runtimeLayerCompatState.d.ts +2 -2
  25. package/dist/versionMarks/inboundBatch.d.ts +36 -0
  26. package/dist/versionMarks/inboundBatch.d.ts.map +1 -0
  27. package/dist/versionMarks/inboundBatch.js +50 -0
  28. package/dist/versionMarks/inboundBatch.js.map +1 -0
  29. package/dist/versionMarks/index.d.ts +7 -0
  30. package/dist/versionMarks/index.d.ts.map +1 -0
  31. package/dist/versionMarks/index.js +12 -0
  32. package/dist/versionMarks/index.js.map +1 -0
  33. package/dist/versionMarks/versionMarkResolver.d.ts +162 -0
  34. package/dist/versionMarks/versionMarkResolver.d.ts.map +1 -0
  35. package/dist/versionMarks/versionMarkResolver.js +198 -0
  36. package/dist/versionMarks/versionMarkResolver.js.map +1 -0
  37. package/internal.d.ts +1 -1
  38. package/legacy/alpha.d.ts +11 -0
  39. package/legacy.d.ts +1 -1
  40. package/lib/containerRuntime.d.ts +7 -0
  41. package/lib/containerRuntime.d.ts.map +1 -1
  42. package/lib/containerRuntime.js +57 -0
  43. package/lib/containerRuntime.js.map +1 -1
  44. package/lib/index.d.ts +1 -0
  45. package/lib/index.d.ts.map +1 -1
  46. package/lib/index.js.map +1 -1
  47. package/lib/legacy.d.ts +1 -1
  48. package/lib/legacyAlpha.d.ts +76 -0
  49. package/lib/packageVersion.d.ts +1 -1
  50. package/lib/packageVersion.js +1 -1
  51. package/lib/packageVersion.js.map +1 -1
  52. package/lib/pendingStateManager.d.ts +7 -0
  53. package/lib/pendingStateManager.d.ts.map +1 -1
  54. package/lib/pendingStateManager.js +15 -0
  55. package/lib/pendingStateManager.js.map +1 -1
  56. package/lib/public.d.ts +1 -1
  57. package/lib/runtimeLayerCompatState.d.ts +2 -2
  58. package/lib/versionMarks/inboundBatch.d.ts +36 -0
  59. package/lib/versionMarks/inboundBatch.d.ts.map +1 -0
  60. package/lib/versionMarks/inboundBatch.js +46 -0
  61. package/lib/versionMarks/inboundBatch.js.map +1 -0
  62. package/lib/versionMarks/index.d.ts +7 -0
  63. package/lib/versionMarks/index.d.ts.map +1 -0
  64. package/lib/versionMarks/index.js +7 -0
  65. package/lib/versionMarks/index.js.map +1 -0
  66. package/lib/versionMarks/versionMarkResolver.d.ts +162 -0
  67. package/lib/versionMarks/versionMarkResolver.d.ts.map +1 -0
  68. package/lib/versionMarks/versionMarkResolver.js +194 -0
  69. package/lib/versionMarks/versionMarkResolver.js.map +1 -0
  70. package/package.json +32 -20
  71. package/src/containerRuntime.ts +87 -0
  72. package/src/index.ts +5 -0
  73. package/src/packageVersion.ts +1 -1
  74. package/src/pendingStateManager.ts +18 -0
  75. package/src/versionMarks/DEV.md +329 -0
  76. package/src/versionMarks/inboundBatch.ts +70 -0
  77. package/src/versionMarks/index.ts +13 -0
  78. package/src/versionMarks/versionMarkResolver.ts +341 -0
@@ -0,0 +1,46 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ import { getEffectiveBatchId } from "../opLifecycle/index.js";
6
+ /**
7
+ * Maps an inbound batch result to the version-mark resolver update. A batch's resolved point is its last
8
+ * op's sequence number, so this reports `(batchId, sequenceNumber)` only once a batch completes. It handles
9
+ * both a batch delivered whole (`fullBatch`, incl. an empty grouped batch via the batch-start key message)
10
+ * and one delivered piecemeal (`batchStartingMessage` then `nextBatchMessage` with `batchEnd`), carrying the
11
+ * batch id across the piecemeal messages via `carriedBatchId`.
12
+ *
13
+ * @internal
14
+ */
15
+ export function inboundVersionMarkUpdate(inboundResult, carriedBatchId) {
16
+ if ("batchStart" in inboundResult) {
17
+ const batchId = getEffectiveBatchId(inboundResult.batchStart);
18
+ if (inboundResult.type === "fullBatch") {
19
+ // The last op is the batch's resolved point; an empty batch falls back to the batch-start key message.
20
+ const lastMessage =
21
+ // eslint-disable-next-line unicorn/prefer-at -- Array.prototype.at is not in this package's lib target
22
+ inboundResult.messages[inboundResult.messages.length - 1] ??
23
+ inboundResult.batchStart.keyMessage;
24
+ return {
25
+ sequenced: { batchId, sequenceNumber: lastMessage.sequenceNumber },
26
+ carriedBatchId: undefined,
27
+ };
28
+ }
29
+ // batchStartingMessage: carry the id until the batch's end message arrives.
30
+ return { carriedBatchId: batchId };
31
+ }
32
+ if (inboundResult.type === "nextBatchMessage" &&
33
+ inboundResult.batchEnd === true &&
34
+ carriedBatchId !== undefined) {
35
+ return {
36
+ sequenced: {
37
+ batchId: carriedBatchId,
38
+ sequenceNumber: inboundResult.nextMessage.sequenceNumber,
39
+ },
40
+ carriedBatchId: undefined,
41
+ };
42
+ }
43
+ // A mid-batch message, or a batch end with no carried id: no change.
44
+ return { carriedBatchId };
45
+ }
46
+ //# sourceMappingURL=inboundBatch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"inboundBatch.js","sourceRoot":"","sources":["../../src/versionMarks/inboundBatch.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,mBAAmB,EAA6B,MAAM,yBAAyB,CAAC;AAoBzF;;;;;;;;GAQG;AACH,MAAM,UAAU,wBAAwB,CACvC,aAAmC,EACnC,cAAkC;IAElC,IAAI,YAAY,IAAI,aAAa,EAAE,CAAC;QACnC,MAAM,OAAO,GAAG,mBAAmB,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;QAC9D,IAAI,aAAa,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YACxC,uGAAuG;YACvG,MAAM,WAAW;YAChB,uGAAuG;YACvG,aAAa,CAAC,QAAQ,CAAC,aAAa,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;gBACzD,aAAa,CAAC,UAAU,CAAC,UAAU,CAAC;YACrC,OAAO;gBACN,SAAS,EAAE,EAAE,OAAO,EAAE,cAAc,EAAE,WAAW,CAAC,cAAc,EAAE;gBAClE,cAAc,EAAE,SAAS;aACzB,CAAC;QACH,CAAC;QACD,4EAA4E;QAC5E,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,CAAC;IACpC,CAAC;IACD,IACC,aAAa,CAAC,IAAI,KAAK,kBAAkB;QACzC,aAAa,CAAC,QAAQ,KAAK,IAAI;QAC/B,cAAc,KAAK,SAAS,EAC3B,CAAC;QACF,OAAO;YACN,SAAS,EAAE;gBACV,OAAO,EAAE,cAAc;gBACvB,cAAc,EAAE,aAAa,CAAC,WAAW,CAAC,cAAc;aACxD;YACD,cAAc,EAAE,SAAS;SACzB,CAAC;IACH,CAAC;IACD,qEAAqE;IACrE,OAAO,EAAE,cAAc,EAAE,CAAC;AAC3B,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { getEffectiveBatchId, type InboundMessageResult } from \"../opLifecycle/index.js\";\n\n/**\n * The version-mark resolver update derived from an inbound batch result.\n *\n * @internal\n */\nexport interface InboundVersionMarkUpdate {\n\t/**\n\t * A completed batch to record via `processInboundBatch`: its id and its last op's sequence number.\n\t * Undefined when no batch completed on this message (a piecemeal batch still in progress).\n\t */\n\treadonly sequenced?: { readonly batchId: string; readonly sequenceNumber: number };\n\t/**\n\t * The batch id to carry for a piecemeal batch still in progress, or undefined when none is pending.\n\t * The caller stores this and passes it back on the next inbound result.\n\t */\n\treadonly carriedBatchId: string | undefined;\n}\n\n/**\n * Maps an inbound batch result to the version-mark resolver update. A batch's resolved point is its last\n * op's sequence number, so this reports `(batchId, sequenceNumber)` only once a batch completes. It handles\n * both a batch delivered whole (`fullBatch`, incl. an empty grouped batch via the batch-start key message)\n * and one delivered piecemeal (`batchStartingMessage` then `nextBatchMessage` with `batchEnd`), carrying the\n * batch id across the piecemeal messages via `carriedBatchId`.\n *\n * @internal\n */\nexport function inboundVersionMarkUpdate(\n\tinboundResult: InboundMessageResult,\n\tcarriedBatchId: string | undefined,\n): InboundVersionMarkUpdate {\n\tif (\"batchStart\" in inboundResult) {\n\t\tconst batchId = getEffectiveBatchId(inboundResult.batchStart);\n\t\tif (inboundResult.type === \"fullBatch\") {\n\t\t\t// The last op is the batch's resolved point; an empty batch falls back to the batch-start key message.\n\t\t\tconst lastMessage =\n\t\t\t\t// eslint-disable-next-line unicorn/prefer-at -- Array.prototype.at is not in this package's lib target\n\t\t\t\tinboundResult.messages[inboundResult.messages.length - 1] ??\n\t\t\t\tinboundResult.batchStart.keyMessage;\n\t\t\treturn {\n\t\t\t\tsequenced: { batchId, sequenceNumber: lastMessage.sequenceNumber },\n\t\t\t\tcarriedBatchId: undefined,\n\t\t\t};\n\t\t}\n\t\t// batchStartingMessage: carry the id until the batch's end message arrives.\n\t\treturn { carriedBatchId: batchId };\n\t}\n\tif (\n\t\tinboundResult.type === \"nextBatchMessage\" &&\n\t\tinboundResult.batchEnd === true &&\n\t\tcarriedBatchId !== undefined\n\t) {\n\t\treturn {\n\t\t\tsequenced: {\n\t\t\t\tbatchId: carriedBatchId,\n\t\t\t\tsequenceNumber: inboundResult.nextMessage.sequenceNumber,\n\t\t\t},\n\t\t\tcarriedBatchId: undefined,\n\t\t};\n\t}\n\t// A mid-batch message, or a batch end with no carried id: no change.\n\treturn { carriedBatchId };\n}\n"]}
@@ -0,0 +1,7 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ export { VersionMarkResolver, type IVersionMarkResolver, type ResolveResult, type VersionMarkCapture, type VersionMarkResolverRuntimeHooks, } from "./versionMarkResolver.js";
6
+ export { inboundVersionMarkUpdate, type InboundVersionMarkUpdate } from "./inboundBatch.js";
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/versionMarks/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACN,mBAAmB,EACnB,KAAK,oBAAoB,EACzB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,+BAA+B,GACpC,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,wBAAwB,EAAE,KAAK,wBAAwB,EAAE,MAAM,mBAAmB,CAAC"}
@@ -0,0 +1,7 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ export { VersionMarkResolver, } from "./versionMarkResolver.js";
6
+ export { inboundVersionMarkUpdate } from "./inboundBatch.js";
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/versionMarks/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EACN,mBAAmB,GAKnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EAAE,wBAAwB,EAAiC,MAAM,mBAAmB,CAAC","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nexport {\n\tVersionMarkResolver,\n\ttype IVersionMarkResolver,\n\ttype ResolveResult,\n\ttype VersionMarkCapture,\n\ttype VersionMarkResolverRuntimeHooks,\n} from \"./versionMarkResolver.js\";\nexport { inboundVersionMarkUpdate, type InboundVersionMarkUpdate } from \"./inboundBatch.js\";\n"]}
@@ -0,0 +1,162 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ import type { ISequencedDocumentMessage, IStream } from "@fluidframework/driver-definitions/internal";
6
+ import type { TelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal";
7
+ import type { InboundMessageResult } from "../opLifecycle/index.js";
8
+ /**
9
+ * Reads sequenced ops in `[from, to)` from delta storage, injected by the container so the runtime stays
10
+ * driver-agnostic. `abortSignal` cancels an in-flight fetch.
11
+ *
12
+ * @internal
13
+ */
14
+ export interface IHistoricalOpReader {
15
+ fetchMessages(from: number, to?: number, abortSignal?: AbortSignal): Promise<IStream<ISequencedDocumentMessage[]>>;
16
+ }
17
+ /**
18
+ * Result of resolving a pending batchId.
19
+ *
20
+ * @legacy @alpha
21
+ */
22
+ export type ResolveResult = {
23
+ readonly kind: "resolved";
24
+ readonly sequenceNumber: number;
25
+ } | {
26
+ readonly kind: "pending";
27
+ } | {
28
+ readonly kind: "unresolvable";
29
+ };
30
+ /**
31
+ * The data captured for a version mark. `pending` when the captured edit is local and not yet sequenced
32
+ * (resolve it later via {@link IVersionMarkResolver.resolve}); `resolved` when there is no in-flight local
33
+ * work, so the mark already points at a durable sequence number. The app packs its own stored record from
34
+ * this — the runtime does not define the stored locator shape.
35
+ *
36
+ * @legacy @alpha
37
+ */
38
+ export type VersionMarkCapture = {
39
+ readonly kind: "pending";
40
+ readonly batchId: string;
41
+ readonly sequenceNumberLowerBound: number;
42
+ } | {
43
+ readonly kind: "resolved";
44
+ readonly sequenceNumber: number;
45
+ };
46
+ /**
47
+ * Runtime-owned resolver for app-stored version mark locators.
48
+ *
49
+ * @legacy @alpha
50
+ */
51
+ export interface IVersionMarkResolver {
52
+ /**
53
+ * Captures a version mark at the current point. Seals the current outbound batch first (so a just-made
54
+ * local edit has a stable `batchId`, which is only assigned when a batch is flushed), then returns the
55
+ * mark data atomically: a `pending` capture (`batchId` + `sequenceNumberLowerBound`) when there is an
56
+ * unacked local batch, or a `resolved` capture (`sequenceNumber`) when there is no in-flight local work.
57
+ *
58
+ * @remarks Sealing the batch is a side effect (it submits the current batch), so capture at savepoint
59
+ * boundaries, not per keystroke.
60
+ *
61
+ * @returns The pending batch identity and exclusive sequence number lower bound, or the current sequence number
62
+ * when there is no pending local batch.
63
+ */
64
+ sealAndCaptureVersionMark(): VersionMarkCapture;
65
+ /**
66
+ * Resolves a pending mark's batchId to a global sequence number (`sequenceNumberLowerBound` is the
67
+ * exclusive lower bound for a history read). A `resolved` sequence number feeds the loader's
68
+ * `loadContainerToSequenceNumber`.
69
+ *
70
+ * @param batchId - The stable identity of the pending batch.
71
+ * @param sequenceNumberLowerBound - The exclusive lower bound for the historical op search.
72
+ * @returns The resolved sequence number, or a result indicating that the batch remains pending or can
73
+ * no longer be resolved.
74
+ */
75
+ resolve(batchId: string, sequenceNumberLowerBound: number): Promise<ResolveResult>;
76
+ /**
77
+ * Subscribes to inbound batch sequencing: fires `(batchId, sequenceNumber)` per batch so any connected
78
+ * client can promote a matching pending mark. Returns an unsubscribe function.
79
+ *
80
+ * @param listener - Called with the stable batch identity and its final sequence number.
81
+ * @returns A function that unsubscribes the listener.
82
+ */
83
+ onBatchSequenced(listener: (batchId: string, sequenceNumber: number) => void): () => void;
84
+ }
85
+ /**
86
+ * @internal
87
+ */
88
+ export interface VersionMarkResolverRuntimeHooks {
89
+ readonly getCurrentSequenceNumber: () => number;
90
+ readonly getCurrentMinimumSequenceNumber: () => number;
91
+ readonly getCurrentPendingBatchId: () => string | undefined;
92
+ /** Logs faults from app-supplied `onBatchSequenced` listeners (see {@link VersionMarkResolver.processInboundBatch}). */
93
+ readonly logger: TelemetryLoggerExt;
94
+ /**
95
+ * Seals the current outbound batch (flushes the runtime), so a just-submitted local edit is moved into
96
+ * the pending state with a stable `batchId` before
97
+ * {@link IVersionMarkResolver.sealAndCaptureVersionMark} reads it.
98
+ */
99
+ readonly flushPendingBatch: () => void;
100
+ /**
101
+ * Injected by the container, backed by delta storage. When absent, unknown batchIds resolve as `pending`.
102
+ */
103
+ readonly getHistoricalOpReader?: () => IHistoricalOpReader | undefined;
104
+ /**
105
+ * Creates a per-scan op unpacker that mirrors the live inbound path (chunk reassembly, ungroup,
106
+ * decompress), so the history scan observes the same restored `batchId` — in particular for chunked
107
+ * batches, whose id is stripped from the final chunk's wire metadata and restored only after
108
+ * reassembly. A fresh unpacker is created per scan since it holds reassembly state. When absent, the
109
+ * history scan cannot identify batches and reports `pending`/`unresolvable`.
110
+ */
111
+ readonly createHistoricalOpUnpacker?: () => (op: ISequencedDocumentMessage) => InboundMessageResult | undefined;
112
+ }
113
+ /**
114
+ * Resolves a mark's batchId to a global sequence number. The app owns mark storage and promotion; the
115
+ * runtime exposes the capture point and an ephemeral in-session `batchId -> sequenceNumber` map.
116
+ *
117
+ * @internal
118
+ */
119
+ export declare class VersionMarkResolver implements IVersionMarkResolver {
120
+ private readonly hooks;
121
+ private readonly sequenceNumberByBatchId;
122
+ private readonly batchListeners;
123
+ /**
124
+ * Sticky flag: false until the feature is first used this session (a pending capture or a listener).
125
+ * While false the runtime skips all per-inbound-batch tracking, so a container that never uses version
126
+ * marks pays nothing on the hot path (mirrors #22497 gating `DuplicateBatchDetector` on offline load).
127
+ */
128
+ private tracking;
129
+ constructor(hooks: VersionMarkResolverRuntimeHooks);
130
+ /** Whether inbound batches need tracking (see {@link tracking}). Read by the runtime to gate the hot path. */
131
+ get isTracking(): boolean;
132
+ sealAndCaptureVersionMark(): VersionMarkCapture;
133
+ resolve(batchId: string, sequenceNumberLowerBound: number): Promise<ResolveResult>;
134
+ /**
135
+ * Scans sequenced ops after `sequenceNumberLowerBound` for `batchId`, returning its last op's
136
+ * sequence number when found and aborting the fetch once found or exhausted. Each op is routed through
137
+ * the same unpack pipeline the live inbound path uses (chunk reassembly, ungroup, decompress) and
138
+ * {@link inboundVersionMarkUpdate} derives its batch identity, so chunked batches resolve here too. The
139
+ * scan anchor is not guaranteed to be a batch boundary, so a leading batch clipped by the anchor is
140
+ * tolerated (its orphan end marker is dropped rather than fed to the processor). On a miss,
141
+ * {@link classifyMiss} distinguishes `pending` (not sequenced yet) from `unresolvable` (ops trimmed).
142
+ * See DEV.md.
143
+ */
144
+ private resolveFromHistory;
145
+ /**
146
+ * Classifies a history-scan miss as `pending` or `unresolvable`. See {@link resolveFromHistory}.
147
+ */
148
+ private classifyMiss;
149
+ onBatchSequenced(listener: (batchId: string, sequenceNumber: number) => void): () => void;
150
+ /** The sequence number for a batch seen live this session, or undefined. */
151
+ private sessionSequenceFor;
152
+ /** Records an inbound batch and notifies listeners so any client can promote matching pending marks. */
153
+ processInboundBatch(batchId: string, sequenceNumber: number): void;
154
+ /**
155
+ * Bounds the map to the collaboration window by dropping entries below the MSN (mirrors
156
+ * `DuplicateBatchDetector`). Safe because below the MSN every client has processed the batch, so its
157
+ * `onBatchSequenced` already fired; a dropped entry falls back to the history scan. Entries insert in
158
+ * sequence order, so iteration stops at the first retained one.
159
+ */
160
+ private evictBelowMinimumSequenceNumber;
161
+ }
162
+ //# sourceMappingURL=versionMarkResolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"versionMarkResolver.d.ts","sourceRoot":"","sources":["../../src/versionMarks/versionMarkResolver.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,194 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ import { assert } from "@fluidframework/core-utils/internal";
6
+ import { asBatchMetadata } from "../metadata.js";
7
+ import { inboundVersionMarkUpdate } from "./inboundBatch.js";
8
+ /**
9
+ * Resolves a mark's batchId to a global sequence number. The app owns mark storage and promotion; the
10
+ * runtime exposes the capture point and an ephemeral in-session `batchId -> sequenceNumber` map.
11
+ *
12
+ * @internal
13
+ */
14
+ export class VersionMarkResolver {
15
+ constructor(hooks) {
16
+ this.hooks = hooks;
17
+ this.sequenceNumberByBatchId = new Map();
18
+ this.batchListeners = new Set();
19
+ /**
20
+ * Sticky flag: false until the feature is first used this session (a pending capture or a listener).
21
+ * While false the runtime skips all per-inbound-batch tracking, so a container that never uses version
22
+ * marks pays nothing on the hot path (mirrors #22497 gating `DuplicateBatchDetector` on offline load).
23
+ */
24
+ this.tracking = false;
25
+ }
26
+ /** Whether inbound batches need tracking (see {@link tracking}). Read by the runtime to gate the hot path. */
27
+ get isTracking() {
28
+ return this.tracking;
29
+ }
30
+ sealAndCaptureVersionMark() {
31
+ // Seal the current batch so a just-submitted local edit is flushed into the pending state with a
32
+ // stable batchId (which is only assigned at flush) before we read it.
33
+ this.hooks.flushPendingBatch();
34
+ const sequenceNumberLowerBound = this.hooks.getCurrentSequenceNumber();
35
+ const batchId = this.hooks.getCurrentPendingBatchId();
36
+ if (batchId === undefined) {
37
+ // No unacked local batch: the mark already points at a durable sequence number.
38
+ return { kind: "resolved", sequenceNumber: sequenceNumberLowerBound };
39
+ }
40
+ // A pending mark needs its batch tracked so resolve() can promote it from the live map.
41
+ this.tracking = true;
42
+ return { kind: "pending", batchId, sequenceNumberLowerBound };
43
+ }
44
+ async resolve(batchId, sequenceNumberLowerBound) {
45
+ // Fast path: batch sequenced live this session.
46
+ const sequenceNumber = this.sessionSequenceFor(batchId);
47
+ if (sequenceNumber !== undefined) {
48
+ return { kind: "resolved", sequenceNumber };
49
+ }
50
+ // Otherwise scan history from the mark's reference point.
51
+ const reader = this.hooks.getHistoricalOpReader?.();
52
+ if (reader === undefined) {
53
+ // No reader: the batch may still sequence live, so report pending.
54
+ return { kind: "pending" };
55
+ }
56
+ return this.resolveFromHistory(reader, batchId, sequenceNumberLowerBound);
57
+ }
58
+ /**
59
+ * Scans sequenced ops after `sequenceNumberLowerBound` for `batchId`, returning its last op's
60
+ * sequence number when found and aborting the fetch once found or exhausted. Each op is routed through
61
+ * the same unpack pipeline the live inbound path uses (chunk reassembly, ungroup, decompress) and
62
+ * {@link inboundVersionMarkUpdate} derives its batch identity, so chunked batches resolve here too. The
63
+ * scan anchor is not guaranteed to be a batch boundary, so a leading batch clipped by the anchor is
64
+ * tolerated (its orphan end marker is dropped rather than fed to the processor). On a miss,
65
+ * {@link classifyMiss} distinguishes `pending` (not sequenced yet) from `unresolvable` (ops trimmed).
66
+ * See DEV.md.
67
+ */
68
+ async resolveFromHistory(reader, batchId, sequenceNumberLowerBound) {
69
+ const from = sequenceNumberLowerBound + 1;
70
+ const unpack = this.hooks.createHistoricalOpUnpacker?.();
71
+ const abortController = new AbortController();
72
+ try {
73
+ const stream = await reader.fetchMessages(from, undefined, abortController.signal);
74
+ let firstScannedSequenceNumber;
75
+ let carriedBatchId;
76
+ // Mirrors the processor's batch-in-progress state so we can drop an orphan end marker (below).
77
+ let inBatch = false;
78
+ while (true) {
79
+ const result = await stream.read();
80
+ if (result.done) {
81
+ break;
82
+ }
83
+ for (const op of result.value) {
84
+ firstScannedSequenceNumber ??= op.sequenceNumber;
85
+ if (!inBatch && asBatchMetadata(op.metadata)?.batch === false) {
86
+ // The anchor (`sequenceNumberLowerBound + 1`) may land inside a batch whose start
87
+ // precedes the window; its orphan end marker would trip the processor's 0x9d5 assert.
88
+ // Drop the clipped tail. The target batch is fully in-window, so this can't skip it.
89
+ continue;
90
+ }
91
+ // undefined = a system op, or an incomplete chunk awaiting later fragments.
92
+ const inboundResult = unpack?.(op);
93
+ if (inboundResult === undefined) {
94
+ continue;
95
+ }
96
+ // Track batch progress with the processor so the guard above skips only orphan ends.
97
+ if (inboundResult.type === "batchStartingMessage") {
98
+ inBatch = true;
99
+ }
100
+ else if (inboundResult.type === "nextBatchMessage" &&
101
+ inboundResult.batchEnd === true) {
102
+ inBatch = false;
103
+ }
104
+ // Derive batch identity exactly as the live inbound path does, carrying the id across a
105
+ // piecemeal batch's messages.
106
+ const update = inboundVersionMarkUpdate(inboundResult, carriedBatchId);
107
+ carriedBatchId = update.carriedBatchId;
108
+ if (update.sequenced?.batchId === batchId) {
109
+ return { kind: "resolved", sequenceNumber: update.sequenced.sequenceNumber };
110
+ }
111
+ }
112
+ }
113
+ // Not found: distinguish "not sequenced yet" from "trimmed" via the tip and where the scan landed.
114
+ return this.classifyMiss(from, firstScannedSequenceNumber);
115
+ }
116
+ finally {
117
+ // Cancel any in-flight fetch once we stop reading (found or exhausted).
118
+ abortController.abort();
119
+ }
120
+ }
121
+ /**
122
+ * Classifies a history-scan miss as `pending` or `unresolvable`. See {@link resolveFromHistory}.
123
+ */
124
+ classifyMiss(from, firstScannedSequenceNumber) {
125
+ // Invariant: a reader must never return an op before the requested range start. If it does, the
126
+ // trim inference below is meaningless, so fail loudly rather than misclassify.
127
+ assert(firstScannedSequenceNumber === undefined || firstScannedSequenceNumber >= from, 0xd1c /* historical op reader returned an op before the requested range start */);
128
+ const tip = this.hooks.getCurrentSequenceNumber();
129
+ if (from > tip) {
130
+ // Nothing is sequenced at/after the mark's reference point yet, so the batch cannot have landed.
131
+ return { kind: "pending" };
132
+ }
133
+ if (firstScannedSequenceNumber === undefined) {
134
+ // Empty read though ops should exist in `[from, tip]` → trimmed. ODSP-specific: a strict driver
135
+ // empties a `from`-misaligned trimmed range (validateMessages); a return-from-earliest driver
136
+ // would instead surface the trim via the `firstScannedSequenceNumber > from` branch below.
137
+ return { kind: "unresolvable" };
138
+ }
139
+ if (firstScannedSequenceNumber > from) {
140
+ // A trim gap at the anchor: the mark's ops (just after the reference point) are gone.
141
+ return { kind: "unresolvable" };
142
+ }
143
+ // Ops are present from the reference point and the batch is not among them: not yet sequenced.
144
+ return { kind: "pending" };
145
+ }
146
+ onBatchSequenced(listener) {
147
+ // A subscriber wants live promotions, so start tracking inbound batches from here on.
148
+ this.tracking = true;
149
+ this.batchListeners.add(listener);
150
+ return () => {
151
+ this.batchListeners.delete(listener);
152
+ };
153
+ }
154
+ /** The sequence number for a batch seen live this session, or undefined. */
155
+ sessionSequenceFor(batchId) {
156
+ return this.sequenceNumberByBatchId.get(batchId);
157
+ }
158
+ /** Records an inbound batch and notifies listeners so any client can promote matching pending marks. */
159
+ processInboundBatch(batchId, sequenceNumber) {
160
+ const existingSequenceNumber = this.sequenceNumberByBatchId.get(batchId);
161
+ if (existingSequenceNumber === sequenceNumber) {
162
+ return;
163
+ }
164
+ this.sequenceNumberByBatchId.set(batchId, sequenceNumber);
165
+ this.evictBelowMinimumSequenceNumber();
166
+ for (const listener of this.batchListeners) {
167
+ try {
168
+ listener(batchId, sequenceNumber);
169
+ }
170
+ catch (error) {
171
+ // Isolate each app listener (like the container's EventEmitterWithErrorHandling): one throw
172
+ // must not abort op processing or starve the rest. Log and continue rather than fault the
173
+ // container — a missed promotion is recoverable via resolve()'s history scan.
174
+ this.hooks.logger.sendErrorEvent({ eventName: "VersionMarkListenerException" }, error);
175
+ }
176
+ }
177
+ }
178
+ /**
179
+ * Bounds the map to the collaboration window by dropping entries below the MSN (mirrors
180
+ * `DuplicateBatchDetector`). Safe because below the MSN every client has processed the batch, so its
181
+ * `onBatchSequenced` already fired; a dropped entry falls back to the history scan. Entries insert in
182
+ * sequence order, so iteration stops at the first retained one.
183
+ */
184
+ evictBelowMinimumSequenceNumber() {
185
+ const minimumSequenceNumber = this.hooks.getCurrentMinimumSequenceNumber();
186
+ for (const [id, sequenceNumber] of this.sequenceNumberByBatchId) {
187
+ if (sequenceNumber >= minimumSequenceNumber) {
188
+ break;
189
+ }
190
+ this.sequenceNumberByBatchId.delete(id);
191
+ }
192
+ }
193
+ }
194
+ //# sourceMappingURL=versionMarkResolver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"versionMarkResolver.js","sourceRoot":"","sources":["../../src/versionMarks/versionMarkResolver.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport type {\n\tISequencedDocumentMessage,\n\tIStream,\n} from \"@fluidframework/driver-definitions/internal\";\n\nimport { assert } from \"@fluidframework/core-utils/internal\";\nimport type { TelemetryLoggerExt } from \"@fluidframework/telemetry-utils/internal\";\n\nimport type { InboundMessageResult } from \"../opLifecycle/index.js\";\nimport { asBatchMetadata } from \"../metadata.js\";\n\nimport { inboundVersionMarkUpdate } from \"./inboundBatch.js\";\n\n/**\n * Reads sequenced ops in `[from, to)` from delta storage, injected by the container so the runtime stays\n * driver-agnostic. `abortSignal` cancels an in-flight fetch.\n *\n * @internal\n */\nexport interface IHistoricalOpReader {\n\tfetchMessages(\n\t\tfrom: number,\n\t\tto?: number,\n\t\tabortSignal?: AbortSignal,\n\t): Promise<IStream<ISequencedDocumentMessage[]>>;\n}\n\n/**\n * Result of resolving a pending batchId.\n *\n * @legacy @alpha\n */\nexport type ResolveResult =\n\t| { readonly kind: \"resolved\"; readonly sequenceNumber: number }\n\t| { readonly kind: \"pending\" }\n\t| { readonly kind: \"unresolvable\" };\n\n/**\n * The data captured for a version mark. `pending` when the captured edit is local and not yet sequenced\n * (resolve it later via {@link IVersionMarkResolver.resolve}); `resolved` when there is no in-flight local\n * work, so the mark already points at a durable sequence number. The app packs its own stored record from\n * this — the runtime does not define the stored locator shape.\n *\n * @legacy @alpha\n */\nexport type VersionMarkCapture =\n\t| {\n\t\t\treadonly kind: \"pending\";\n\t\t\treadonly batchId: string;\n\t\t\treadonly sequenceNumberLowerBound: number;\n\t }\n\t| { readonly kind: \"resolved\"; readonly sequenceNumber: number };\n\n/**\n * Runtime-owned resolver for app-stored version mark locators.\n *\n * @legacy @alpha\n */\nexport interface IVersionMarkResolver {\n\t/**\n\t * Captures a version mark at the current point. Seals the current outbound batch first (so a just-made\n\t * local edit has a stable `batchId`, which is only assigned when a batch is flushed), then returns the\n\t * mark data atomically: a `pending` capture (`batchId` + `sequenceNumberLowerBound`) when there is an\n\t * unacked local batch, or a `resolved` capture (`sequenceNumber`) when there is no in-flight local work.\n\t *\n\t * @remarks Sealing the batch is a side effect (it submits the current batch), so capture at savepoint\n\t * boundaries, not per keystroke.\n\t *\n\t * @returns The pending batch identity and exclusive sequence number lower bound, or the current sequence number\n\t * when there is no pending local batch.\n\t */\n\tsealAndCaptureVersionMark(): VersionMarkCapture;\n\t/**\n\t * Resolves a pending mark's batchId to a global sequence number (`sequenceNumberLowerBound` is the\n\t * exclusive lower bound for a history read). A `resolved` sequence number feeds the loader's\n\t * `loadContainerToSequenceNumber`.\n\t *\n\t * @param batchId - The stable identity of the pending batch.\n\t * @param sequenceNumberLowerBound - The exclusive lower bound for the historical op search.\n\t * @returns The resolved sequence number, or a result indicating that the batch remains pending or can\n\t * no longer be resolved.\n\t */\n\tresolve(batchId: string, sequenceNumberLowerBound: number): Promise<ResolveResult>;\n\t/**\n\t * Subscribes to inbound batch sequencing: fires `(batchId, sequenceNumber)` per batch so any connected\n\t * client can promote a matching pending mark. Returns an unsubscribe function.\n\t *\n\t * @param listener - Called with the stable batch identity and its final sequence number.\n\t * @returns A function that unsubscribes the listener.\n\t */\n\tonBatchSequenced(listener: (batchId: string, sequenceNumber: number) => void): () => void;\n}\n\n/**\n * @internal\n */\nexport interface VersionMarkResolverRuntimeHooks {\n\treadonly getCurrentSequenceNumber: () => number;\n\treadonly getCurrentMinimumSequenceNumber: () => number;\n\treadonly getCurrentPendingBatchId: () => string | undefined;\n\t/** Logs faults from app-supplied `onBatchSequenced` listeners (see {@link VersionMarkResolver.processInboundBatch}). */\n\treadonly logger: TelemetryLoggerExt;\n\t/**\n\t * Seals the current outbound batch (flushes the runtime), so a just-submitted local edit is moved into\n\t * the pending state with a stable `batchId` before\n\t * {@link IVersionMarkResolver.sealAndCaptureVersionMark} reads it.\n\t */\n\treadonly flushPendingBatch: () => void;\n\t/**\n\t * Injected by the container, backed by delta storage. When absent, unknown batchIds resolve as `pending`.\n\t */\n\treadonly getHistoricalOpReader?: () => IHistoricalOpReader | undefined;\n\t/**\n\t * Creates a per-scan op unpacker that mirrors the live inbound path (chunk reassembly, ungroup,\n\t * decompress), so the history scan observes the same restored `batchId` — in particular for chunked\n\t * batches, whose id is stripped from the final chunk's wire metadata and restored only after\n\t * reassembly. A fresh unpacker is created per scan since it holds reassembly state. When absent, the\n\t * history scan cannot identify batches and reports `pending`/`unresolvable`.\n\t */\n\treadonly createHistoricalOpUnpacker?: () => (\n\t\top: ISequencedDocumentMessage,\n\t) => InboundMessageResult | undefined;\n}\n\n/**\n * Resolves a mark's batchId to a global sequence number. The app owns mark storage and promotion; the\n * runtime exposes the capture point and an ephemeral in-session `batchId -> sequenceNumber` map.\n *\n * @internal\n */\nexport class VersionMarkResolver implements IVersionMarkResolver {\n\tprivate readonly sequenceNumberByBatchId = new Map<string, number>();\n\tprivate readonly batchListeners = new Set<\n\t\t(batchId: string, sequenceNumber: number) => void\n\t>();\n\t/**\n\t * Sticky flag: false until the feature is first used this session (a pending capture or a listener).\n\t * While false the runtime skips all per-inbound-batch tracking, so a container that never uses version\n\t * marks pays nothing on the hot path (mirrors #22497 gating `DuplicateBatchDetector` on offline load).\n\t */\n\tprivate tracking = false;\n\n\tpublic constructor(private readonly hooks: VersionMarkResolverRuntimeHooks) {}\n\n\t/** Whether inbound batches need tracking (see {@link tracking}). Read by the runtime to gate the hot path. */\n\tpublic get isTracking(): boolean {\n\t\treturn this.tracking;\n\t}\n\n\tpublic sealAndCaptureVersionMark(): VersionMarkCapture {\n\t\t// Seal the current batch so a just-submitted local edit is flushed into the pending state with a\n\t\t// stable batchId (which is only assigned at flush) before we read it.\n\t\tthis.hooks.flushPendingBatch();\n\t\tconst sequenceNumberLowerBound = this.hooks.getCurrentSequenceNumber();\n\t\tconst batchId = this.hooks.getCurrentPendingBatchId();\n\t\tif (batchId === undefined) {\n\t\t\t// No unacked local batch: the mark already points at a durable sequence number.\n\t\t\treturn { kind: \"resolved\", sequenceNumber: sequenceNumberLowerBound };\n\t\t}\n\t\t// A pending mark needs its batch tracked so resolve() can promote it from the live map.\n\t\tthis.tracking = true;\n\t\treturn { kind: \"pending\", batchId, sequenceNumberLowerBound };\n\t}\n\n\tpublic async resolve(\n\t\tbatchId: string,\n\t\tsequenceNumberLowerBound: number,\n\t): Promise<ResolveResult> {\n\t\t// Fast path: batch sequenced live this session.\n\t\tconst sequenceNumber = this.sessionSequenceFor(batchId);\n\t\tif (sequenceNumber !== undefined) {\n\t\t\treturn { kind: \"resolved\", sequenceNumber };\n\t\t}\n\n\t\t// Otherwise scan history from the mark's reference point.\n\t\tconst reader = this.hooks.getHistoricalOpReader?.();\n\t\tif (reader === undefined) {\n\t\t\t// No reader: the batch may still sequence live, so report pending.\n\t\t\treturn { kind: \"pending\" };\n\t\t}\n\t\treturn this.resolveFromHistory(reader, batchId, sequenceNumberLowerBound);\n\t}\n\n\t/**\n\t * Scans sequenced ops after `sequenceNumberLowerBound` for `batchId`, returning its last op's\n\t * sequence number when found and aborting the fetch once found or exhausted. Each op is routed through\n\t * the same unpack pipeline the live inbound path uses (chunk reassembly, ungroup, decompress) and\n\t * {@link inboundVersionMarkUpdate} derives its batch identity, so chunked batches resolve here too. The\n\t * scan anchor is not guaranteed to be a batch boundary, so a leading batch clipped by the anchor is\n\t * tolerated (its orphan end marker is dropped rather than fed to the processor). On a miss,\n\t * {@link classifyMiss} distinguishes `pending` (not sequenced yet) from `unresolvable` (ops trimmed).\n\t * See DEV.md.\n\t */\n\tprivate async resolveFromHistory(\n\t\treader: IHistoricalOpReader,\n\t\tbatchId: string,\n\t\tsequenceNumberLowerBound: number,\n\t): Promise<ResolveResult> {\n\t\tconst from = sequenceNumberLowerBound + 1;\n\t\tconst unpack = this.hooks.createHistoricalOpUnpacker?.();\n\t\tconst abortController = new AbortController();\n\t\ttry {\n\t\t\tconst stream = await reader.fetchMessages(from, undefined, abortController.signal);\n\t\t\tlet firstScannedSequenceNumber: number | undefined;\n\t\t\tlet carriedBatchId: string | undefined;\n\t\t\t// Mirrors the processor's batch-in-progress state so we can drop an orphan end marker (below).\n\t\t\tlet inBatch = false;\n\t\t\twhile (true) {\n\t\t\t\tconst result = await stream.read();\n\t\t\t\tif (result.done) {\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tfor (const op of result.value) {\n\t\t\t\t\tfirstScannedSequenceNumber ??= op.sequenceNumber;\n\t\t\t\t\tif (!inBatch && asBatchMetadata(op.metadata)?.batch === false) {\n\t\t\t\t\t\t// The anchor (`sequenceNumberLowerBound + 1`) may land inside a batch whose start\n\t\t\t\t\t\t// precedes the window; its orphan end marker would trip the processor's 0x9d5 assert.\n\t\t\t\t\t\t// Drop the clipped tail. The target batch is fully in-window, so this can't skip it.\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t// undefined = a system op, or an incomplete chunk awaiting later fragments.\n\t\t\t\t\tconst inboundResult = unpack?.(op);\n\t\t\t\t\tif (inboundResult === undefined) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\t\t\t\t\t// Track batch progress with the processor so the guard above skips only orphan ends.\n\t\t\t\t\tif (inboundResult.type === \"batchStartingMessage\") {\n\t\t\t\t\t\tinBatch = true;\n\t\t\t\t\t} else if (\n\t\t\t\t\t\tinboundResult.type === \"nextBatchMessage\" &&\n\t\t\t\t\t\tinboundResult.batchEnd === true\n\t\t\t\t\t) {\n\t\t\t\t\t\tinBatch = false;\n\t\t\t\t\t}\n\t\t\t\t\t// Derive batch identity exactly as the live inbound path does, carrying the id across a\n\t\t\t\t\t// piecemeal batch's messages.\n\t\t\t\t\tconst update = inboundVersionMarkUpdate(inboundResult, carriedBatchId);\n\t\t\t\t\tcarriedBatchId = update.carriedBatchId;\n\t\t\t\t\tif (update.sequenced?.batchId === batchId) {\n\t\t\t\t\t\treturn { kind: \"resolved\", sequenceNumber: update.sequenced.sequenceNumber };\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Not found: distinguish \"not sequenced yet\" from \"trimmed\" via the tip and where the scan landed.\n\t\t\treturn this.classifyMiss(from, firstScannedSequenceNumber);\n\t\t} finally {\n\t\t\t// Cancel any in-flight fetch once we stop reading (found or exhausted).\n\t\t\tabortController.abort();\n\t\t}\n\t}\n\n\t/**\n\t * Classifies a history-scan miss as `pending` or `unresolvable`. See {@link resolveFromHistory}.\n\t */\n\tprivate classifyMiss(\n\t\tfrom: number,\n\t\tfirstScannedSequenceNumber: number | undefined,\n\t): ResolveResult {\n\t\t// Invariant: a reader must never return an op before the requested range start. If it does, the\n\t\t// trim inference below is meaningless, so fail loudly rather than misclassify.\n\t\tassert(\n\t\t\tfirstScannedSequenceNumber === undefined || firstScannedSequenceNumber >= from,\n\t\t\t0xd1c /* historical op reader returned an op before the requested range start */,\n\t\t);\n\t\tconst tip = this.hooks.getCurrentSequenceNumber();\n\t\tif (from > tip) {\n\t\t\t// Nothing is sequenced at/after the mark's reference point yet, so the batch cannot have landed.\n\t\t\treturn { kind: \"pending\" };\n\t\t}\n\t\tif (firstScannedSequenceNumber === undefined) {\n\t\t\t// Empty read though ops should exist in `[from, tip]` → trimmed. ODSP-specific: a strict driver\n\t\t\t// empties a `from`-misaligned trimmed range (validateMessages); a return-from-earliest driver\n\t\t\t// would instead surface the trim via the `firstScannedSequenceNumber > from` branch below.\n\t\t\treturn { kind: \"unresolvable\" };\n\t\t}\n\t\tif (firstScannedSequenceNumber > from) {\n\t\t\t// A trim gap at the anchor: the mark's ops (just after the reference point) are gone.\n\t\t\treturn { kind: \"unresolvable\" };\n\t\t}\n\t\t// Ops are present from the reference point and the batch is not among them: not yet sequenced.\n\t\treturn { kind: \"pending\" };\n\t}\n\n\tpublic onBatchSequenced(\n\t\tlistener: (batchId: string, sequenceNumber: number) => void,\n\t): () => void {\n\t\t// A subscriber wants live promotions, so start tracking inbound batches from here on.\n\t\tthis.tracking = true;\n\t\tthis.batchListeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.batchListeners.delete(listener);\n\t\t};\n\t}\n\n\t/** The sequence number for a batch seen live this session, or undefined. */\n\tprivate sessionSequenceFor(batchId: string): number | undefined {\n\t\treturn this.sequenceNumberByBatchId.get(batchId);\n\t}\n\n\t/** Records an inbound batch and notifies listeners so any client can promote matching pending marks. */\n\tpublic processInboundBatch(batchId: string, sequenceNumber: number): void {\n\t\tconst existingSequenceNumber = this.sequenceNumberByBatchId.get(batchId);\n\t\tif (existingSequenceNumber === sequenceNumber) {\n\t\t\treturn;\n\t\t}\n\n\t\tthis.sequenceNumberByBatchId.set(batchId, sequenceNumber);\n\t\tthis.evictBelowMinimumSequenceNumber();\n\t\tfor (const listener of this.batchListeners) {\n\t\t\ttry {\n\t\t\t\tlistener(batchId, sequenceNumber);\n\t\t\t} catch (error) {\n\t\t\t\t// Isolate each app listener (like the container's EventEmitterWithErrorHandling): one throw\n\t\t\t\t// must not abort op processing or starve the rest. Log and continue rather than fault the\n\t\t\t\t// container — a missed promotion is recoverable via resolve()'s history scan.\n\t\t\t\tthis.hooks.logger.sendErrorEvent({ eventName: \"VersionMarkListenerException\" }, error);\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Bounds the map to the collaboration window by dropping entries below the MSN (mirrors\n\t * `DuplicateBatchDetector`). Safe because below the MSN every client has processed the batch, so its\n\t * `onBatchSequenced` already fired; a dropped entry falls back to the history scan. Entries insert in\n\t * sequence order, so iteration stops at the first retained one.\n\t */\n\tprivate evictBelowMinimumSequenceNumber(): void {\n\t\tconst minimumSequenceNumber = this.hooks.getCurrentMinimumSequenceNumber();\n\t\tfor (const [id, sequenceNumber] of this.sequenceNumberByBatchId) {\n\t\t\tif (sequenceNumber >= minimumSequenceNumber) {\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tthis.sequenceNumberByBatchId.delete(id);\n\t\t}\n\t}\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fluidframework/container-runtime",
3
- "version": "2.114.0",
3
+ "version": "2.115.0",
4
4
  "description": "Fluid container runtime",
5
5
  "homepage": "https://fluidframework.com",
6
6
  "repository": {
@@ -33,6 +33,16 @@
33
33
  "default": "./dist/index.js"
34
34
  }
35
35
  },
36
+ "./legacy/alpha": {
37
+ "import": {
38
+ "types": "./lib/legacyAlpha.d.ts",
39
+ "default": "./lib/index.js"
40
+ },
41
+ "require": {
42
+ "types": "./dist/legacyAlpha.d.ts",
43
+ "default": "./dist/index.js"
44
+ }
45
+ },
36
46
  "./internal": {
37
47
  "import": {
38
48
  "types": "./lib/index.d.ts",
@@ -119,18 +129,18 @@
119
129
  "temp-directory": "nyc/.nyc_output"
120
130
  },
121
131
  "dependencies": {
122
- "@fluid-internal/client-utils": "~2.114.0",
123
- "@fluidframework/container-definitions": "~2.114.0",
124
- "@fluidframework/container-runtime-definitions": "~2.114.0",
125
- "@fluidframework/core-interfaces": "~2.114.0",
126
- "@fluidframework/core-utils": "~2.114.0",
127
- "@fluidframework/datastore": "~2.114.0",
128
- "@fluidframework/driver-definitions": "~2.114.0",
129
- "@fluidframework/driver-utils": "~2.114.0",
130
- "@fluidframework/id-compressor": "~2.114.0",
131
- "@fluidframework/runtime-definitions": "~2.114.0",
132
- "@fluidframework/runtime-utils": "~2.114.0",
133
- "@fluidframework/telemetry-utils": "~2.114.0",
132
+ "@fluid-internal/client-utils": "~2.115.0",
133
+ "@fluidframework/container-definitions": "~2.115.0",
134
+ "@fluidframework/container-runtime-definitions": "~2.115.0",
135
+ "@fluidframework/core-interfaces": "~2.115.0",
136
+ "@fluidframework/core-utils": "~2.115.0",
137
+ "@fluidframework/datastore": "~2.115.0",
138
+ "@fluidframework/driver-definitions": "~2.115.0",
139
+ "@fluidframework/driver-utils": "~2.115.0",
140
+ "@fluidframework/id-compressor": "~2.115.0",
141
+ "@fluidframework/runtime-definitions": "~2.115.0",
142
+ "@fluidframework/runtime-utils": "~2.115.0",
143
+ "@fluidframework/telemetry-utils": "~2.115.0",
134
144
  "@tylerbu/sorted-btree-es6": "^2.1.1",
135
145
  "double-ended-queue": "^2.1.0-0",
136
146
  "lz4js": "^0.2.0",
@@ -140,16 +150,16 @@
140
150
  "devDependencies": {
141
151
  "@arethetypeswrong/cli": "^0.18.2",
142
152
  "@biomejs/biome": "~2.4.5",
143
- "@fluid-internal/mocha-test-setup": "~2.114.0",
144
- "@fluid-private/stochastic-test-utils": "~2.114.0",
145
- "@fluid-private/test-pairwise-generator": "~2.114.0",
153
+ "@fluid-internal/mocha-test-setup": "~2.115.0",
154
+ "@fluid-private/stochastic-test-utils": "~2.115.0",
155
+ "@fluid-private/test-pairwise-generator": "~2.115.0",
146
156
  "@fluid-tools/benchmark": "^0.59.0",
147
157
  "@fluid-tools/build-cli": "^0.65.0",
148
158
  "@fluidframework/build-common": "^2.0.3",
149
159
  "@fluidframework/build-tools": "^0.65.0",
150
160
  "@fluidframework/container-runtime-previous": "npm:@fluidframework/container-runtime@2.112.0",
151
161
  "@fluidframework/eslint-config-fluid": "^13.0.0",
152
- "@fluidframework/test-runtime-utils": "~2.114.0",
162
+ "@fluidframework/test-runtime-utils": "~2.115.0",
153
163
  "@microsoft/api-extractor": "7.58.1",
154
164
  "@types/double-ended-queue": "^2.1.0",
155
165
  "@types/lz4js": "^0.2.0",
@@ -183,8 +193,8 @@
183
193
  "build:compile": "fluid-build . --task compile",
184
194
  "build:docs": "api-extractor run --local",
185
195
  "build:entrypoints": "fluid-build . --task build:entrypoints",
186
- "build:entrypoints:cjs": "flub generate entrypoints --resolutionConditions require --outFileLegacyBeta legacy --outDir ./dist",
187
- "build:entrypoints:esm": "flub generate entrypoints --outFileLegacyBeta legacy --outDir ./lib --node10TypeCompat",
196
+ "build:entrypoints:cjs": "flub generate entrypoints --resolutionConditions require --outFileLegacyBeta legacy --outFileLegacyAlpha legacyAlpha --outDir ./dist",
197
+ "build:entrypoints:esm": "flub generate entrypoints --outFileLegacyBeta legacy --outFileLegacyAlpha legacyAlpha --outDir ./lib --node10TypeCompat",
188
198
  "build:esnext": "tsc --project ./tsconfig.json",
189
199
  "build:genver": "gen-version",
190
200
  "build:test": "concurrently npm:build:test:esm npm:build:test:cjs",
@@ -195,15 +205,17 @@
195
205
  "check:exports": "concurrently \"npm:check:exports:*\"",
196
206
  "check:exports:bundle-release-tags": "api-extractor run --config api-extractor/api-extractor-lint-bundle.json",
197
207
  "check:exports:cjs:legacy": "api-extractor run --config api-extractor/api-extractor-lint-legacy.cjs.json",
208
+ "check:exports:cjs:legacyAlpha": "api-extractor run --config api-extractor/api-extractor-lint-legacyAlpha.cjs.json",
198
209
  "check:exports:cjs:public": "api-extractor run --config api-extractor/api-extractor-lint-public.cjs.json",
199
210
  "check:exports:esm:legacy": "api-extractor run --config api-extractor/api-extractor-lint-legacy.esm.json",
211
+ "check:exports:esm:legacyAlpha": "api-extractor run --config api-extractor/api-extractor-lint-legacyAlpha.esm.json",
200
212
  "check:exports:esm:public": "api-extractor run --config api-extractor/api-extractor-lint-public.esm.json",
201
213
  "check:format": "npm run check:biome",
202
214
  "ci:build:api-reports": "concurrently \"npm:ci:build:api-reports:*\"",
203
215
  "ci:build:api-reports:current": "api-extractor run --config api-extractor/api-extractor.current.json",
204
216
  "ci:build:api-reports:legacy": "api-extractor run --config api-extractor/api-extractor.legacy.json",
205
217
  "ci:build:docs": "api-extractor run",
206
- "clean": "rimraf --glob dist lib {alpha,beta,internal,legacy}.d.ts \"**/*.tsbuildinfo\" \"**/*.build.log\" _api-extractor-temp nyc",
218
+ "clean": "rimraf --glob dist lib {alpha,beta,internal,legacy}.d.ts legacyAlpha.d.ts \"**/*.tsbuildinfo\" \"**/*.build.log\" _api-extractor-temp nyc",
207
219
  "depcruise": "depcruise src/",
208
220
  "eslint": "eslint --quiet --format stylish src",
209
221
  "eslint:fix": "eslint --quiet --format stylish src --fix --fix-type problem,suggestion,layout",