@fluidframework/container-loader 1.2.6 → 2.0.0-dev.1.3.0.96595

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 (157) hide show
  1. package/.mocharc.js +12 -0
  2. package/dist/audience.d.ts +2 -6
  3. package/dist/audience.d.ts.map +1 -1
  4. package/dist/audience.js +6 -11
  5. package/dist/audience.js.map +1 -1
  6. package/dist/catchUpMonitor.d.ts +29 -0
  7. package/dist/catchUpMonitor.d.ts.map +1 -0
  8. package/dist/catchUpMonitor.js +43 -0
  9. package/dist/catchUpMonitor.js.map +1 -0
  10. package/dist/collabWindowTracker.d.ts +1 -1
  11. package/dist/collabWindowTracker.d.ts.map +1 -1
  12. package/dist/collabWindowTracker.js +12 -4
  13. package/dist/collabWindowTracker.js.map +1 -1
  14. package/dist/connectionManager.d.ts +5 -5
  15. package/dist/connectionManager.d.ts.map +1 -1
  16. package/dist/connectionManager.js +43 -22
  17. package/dist/connectionManager.js.map +1 -1
  18. package/dist/connectionState.d.ts +0 -5
  19. package/dist/connectionState.d.ts.map +1 -1
  20. package/dist/connectionState.js +0 -5
  21. package/dist/connectionState.js.map +1 -1
  22. package/dist/connectionStateHandler.d.ts +84 -22
  23. package/dist/connectionStateHandler.d.ts.map +1 -1
  24. package/dist/connectionStateHandler.js +172 -59
  25. package/dist/connectionStateHandler.js.map +1 -1
  26. package/dist/container.d.ts +29 -17
  27. package/dist/container.d.ts.map +1 -1
  28. package/dist/container.js +181 -171
  29. package/dist/container.js.map +1 -1
  30. package/dist/containerContext.d.ts +18 -7
  31. package/dist/containerContext.d.ts.map +1 -1
  32. package/dist/containerContext.js +18 -8
  33. package/dist/containerContext.js.map +1 -1
  34. package/dist/containerStorageAdapter.d.ts +11 -25
  35. package/dist/containerStorageAdapter.d.ts.map +1 -1
  36. package/dist/containerStorageAdapter.js +51 -17
  37. package/dist/containerStorageAdapter.js.map +1 -1
  38. package/dist/contracts.d.ts +5 -5
  39. package/dist/contracts.js.map +1 -1
  40. package/dist/deltaManager.d.ts +4 -1
  41. package/dist/deltaManager.d.ts.map +1 -1
  42. package/dist/deltaManager.js +39 -12
  43. package/dist/deltaManager.js.map +1 -1
  44. package/dist/deltaManagerProxy.d.ts +4 -1
  45. package/dist/deltaManagerProxy.d.ts.map +1 -1
  46. package/dist/deltaQueue.d.ts +9 -2
  47. package/dist/deltaQueue.d.ts.map +1 -1
  48. package/dist/deltaQueue.js +31 -26
  49. package/dist/deltaQueue.js.map +1 -1
  50. package/dist/index.d.ts +1 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/loader.d.ts +8 -1
  54. package/dist/loader.d.ts.map +1 -1
  55. package/dist/loader.js +4 -3
  56. package/dist/loader.js.map +1 -1
  57. package/dist/packageVersion.d.ts +1 -1
  58. package/dist/packageVersion.d.ts.map +1 -1
  59. package/dist/packageVersion.js +1 -1
  60. package/dist/packageVersion.js.map +1 -1
  61. package/dist/protocol.d.ts +27 -0
  62. package/dist/protocol.d.ts.map +1 -0
  63. package/dist/protocol.js +79 -0
  64. package/dist/protocol.js.map +1 -0
  65. package/dist/protocolTreeDocumentStorageService.d.ts +1 -1
  66. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  67. package/dist/retriableDocumentStorageService.d.ts +2 -2
  68. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  69. package/dist/retriableDocumentStorageService.js +2 -2
  70. package/dist/retriableDocumentStorageService.js.map +1 -1
  71. package/lib/audience.d.ts +2 -6
  72. package/lib/audience.d.ts.map +1 -1
  73. package/lib/audience.js +6 -11
  74. package/lib/audience.js.map +1 -1
  75. package/lib/catchUpMonitor.d.ts +29 -0
  76. package/lib/catchUpMonitor.d.ts.map +1 -0
  77. package/lib/catchUpMonitor.js +39 -0
  78. package/lib/catchUpMonitor.js.map +1 -0
  79. package/lib/collabWindowTracker.d.ts +1 -1
  80. package/lib/collabWindowTracker.d.ts.map +1 -1
  81. package/lib/collabWindowTracker.js +13 -5
  82. package/lib/collabWindowTracker.js.map +1 -1
  83. package/lib/connectionManager.d.ts +5 -5
  84. package/lib/connectionManager.d.ts.map +1 -1
  85. package/lib/connectionManager.js +44 -25
  86. package/lib/connectionManager.js.map +1 -1
  87. package/lib/connectionState.d.ts +0 -5
  88. package/lib/connectionState.d.ts.map +1 -1
  89. package/lib/connectionState.js +0 -5
  90. package/lib/connectionState.js.map +1 -1
  91. package/lib/connectionStateHandler.d.ts +84 -22
  92. package/lib/connectionStateHandler.d.ts.map +1 -1
  93. package/lib/connectionStateHandler.js +171 -59
  94. package/lib/connectionStateHandler.js.map +1 -1
  95. package/lib/container.d.ts +29 -17
  96. package/lib/container.d.ts.map +1 -1
  97. package/lib/container.js +184 -174
  98. package/lib/container.js.map +1 -1
  99. package/lib/containerContext.d.ts +18 -7
  100. package/lib/containerContext.d.ts.map +1 -1
  101. package/lib/containerContext.js +19 -9
  102. package/lib/containerContext.js.map +1 -1
  103. package/lib/containerStorageAdapter.d.ts +11 -25
  104. package/lib/containerStorageAdapter.d.ts.map +1 -1
  105. package/lib/containerStorageAdapter.js +51 -16
  106. package/lib/containerStorageAdapter.js.map +1 -1
  107. package/lib/contracts.d.ts +5 -5
  108. package/lib/contracts.js.map +1 -1
  109. package/lib/deltaManager.d.ts +4 -1
  110. package/lib/deltaManager.d.ts.map +1 -1
  111. package/lib/deltaManager.js +41 -14
  112. package/lib/deltaManager.js.map +1 -1
  113. package/lib/deltaManagerProxy.d.ts +4 -1
  114. package/lib/deltaManagerProxy.d.ts.map +1 -1
  115. package/lib/deltaQueue.d.ts +9 -2
  116. package/lib/deltaQueue.d.ts.map +1 -1
  117. package/lib/deltaQueue.js +32 -27
  118. package/lib/deltaQueue.js.map +1 -1
  119. package/lib/index.d.ts +1 -0
  120. package/lib/index.d.ts.map +1 -1
  121. package/lib/index.js.map +1 -1
  122. package/lib/loader.d.ts +8 -1
  123. package/lib/loader.d.ts.map +1 -1
  124. package/lib/loader.js +4 -3
  125. package/lib/loader.js.map +1 -1
  126. package/lib/packageVersion.d.ts +1 -1
  127. package/lib/packageVersion.d.ts.map +1 -1
  128. package/lib/packageVersion.js +1 -1
  129. package/lib/packageVersion.js.map +1 -1
  130. package/lib/protocol.d.ts +27 -0
  131. package/lib/protocol.d.ts.map +1 -0
  132. package/lib/protocol.js +75 -0
  133. package/lib/protocol.js.map +1 -0
  134. package/lib/protocolTreeDocumentStorageService.d.ts +1 -1
  135. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  136. package/lib/retriableDocumentStorageService.d.ts +2 -2
  137. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  138. package/lib/retriableDocumentStorageService.js +2 -2
  139. package/lib/retriableDocumentStorageService.js.map +1 -1
  140. package/package.json +27 -19
  141. package/src/audience.ts +8 -14
  142. package/src/catchUpMonitor.ts +59 -0
  143. package/src/collabWindowTracker.ts +15 -6
  144. package/src/connectionManager.ts +56 -33
  145. package/src/connectionState.ts +0 -6
  146. package/src/connectionStateHandler.ts +235 -70
  147. package/src/container.ts +241 -218
  148. package/src/containerContext.ts +22 -8
  149. package/src/containerStorageAdapter.ts +71 -16
  150. package/src/contracts.ts +7 -7
  151. package/src/deltaManager.ts +48 -15
  152. package/src/deltaQueue.ts +34 -28
  153. package/src/index.ts +4 -0
  154. package/src/loader.ts +14 -3
  155. package/src/packageVersion.ts +1 -1
  156. package/src/protocol.ts +120 -0
  157. package/src/retriableDocumentStorageService.ts +8 -2
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { ITelemetryLogger } from "@fluidframework/common-definitions";
7
- import { assert, LazyPromise } from "@fluidframework/common-utils";
7
+ import { LazyPromise } from "@fluidframework/common-utils";
8
8
  import {
9
9
  IAudience,
10
10
  IContainerContext,
@@ -22,6 +22,7 @@ import {
22
22
  ICodeDetailsLoader,
23
23
  IFluidModuleWithDetails,
24
24
  ISnapshotTreeWithBlobContents,
25
+ IBatchMessage,
25
26
  } from "@fluidframework/container-definitions";
26
27
  import {
27
28
  IRequest,
@@ -42,6 +43,7 @@ import {
42
43
  ISummaryTree,
43
44
  IVersion,
44
45
  MessageType,
46
+ ISummaryContent,
45
47
  } from "@fluidframework/protocol-definitions";
46
48
  import { PerformanceEvent } from "@fluidframework/telemetry-utils";
47
49
  import { Container } from "./container";
@@ -59,6 +61,8 @@ export class ContainerContext implements IContainerContext {
59
61
  quorum: IQuorum,
60
62
  loader: ILoader,
61
63
  submitFn: (type: MessageType, contents: any, batch: boolean, appData: any) => number,
64
+ submitSummaryFn: (summaryOp: ISummaryContent) => number,
65
+ submitBatchFn: (batch: IBatchMessage[]) => number,
62
66
  submitSignalFn: (contents: any) => void,
63
67
  closeFn: (error?: ICriticalContainerError) => void,
64
68
  version: string,
@@ -76,6 +80,8 @@ export class ContainerContext implements IContainerContext {
76
80
  quorum,
77
81
  loader,
78
82
  submitFn,
83
+ submitSummaryFn,
84
+ submitBatchFn,
79
85
  submitSignalFn,
80
86
  closeFn,
81
87
  version,
@@ -107,8 +113,13 @@ export class ContainerContext implements IContainerContext {
107
113
  return this.container.clientDetails;
108
114
  }
109
115
 
116
+ private _connected: boolean;
117
+ /**
118
+ * When true, ops are free to flow
119
+ * When false, ops should be kept as pending or rejected
120
+ */
110
121
  public get connected(): boolean {
111
- return this.container.connected;
122
+ return this._connected;
112
123
  }
113
124
 
114
125
  public get canSummarize(): boolean {
@@ -166,6 +177,9 @@ export class ContainerContext implements IContainerContext {
166
177
  quorum: IQuorum,
167
178
  public readonly loader: ILoader,
168
179
  public readonly submitFn: (type: MessageType, contents: any, batch: boolean, appData: any) => number,
180
+ public readonly submitSummaryFn: (summaryOp: ISummaryContent) => number,
181
+ /** @returns clientSequenceNumber of last message in a batch */
182
+ public readonly submitBatchFn: (batch: IBatchMessage[]) => number,
169
183
  public readonly submitSignalFn: (contents: any) => void,
170
184
  public readonly closeFn: (error?: ICriticalContainerError) => void,
171
185
  public readonly version: string,
@@ -174,6 +188,7 @@ export class ContainerContext implements IContainerContext {
174
188
  public readonly pendingLocalState?: unknown,
175
189
 
176
190
  ) {
191
+ this._connected = this.container.connected;
177
192
  this._quorum = quorum;
178
193
  this.taggedLogger = container.subLogger;
179
194
  this._fluidModuleP = new LazyPromise<IFluidModuleWithDetails>(
@@ -183,9 +198,10 @@ export class ContainerContext implements IContainerContext {
183
198
  }
184
199
 
185
200
  /**
186
- * @deprecated - Temporary migratory API, to be removed when customers no longer need it. When removed,
187
- * ContainerContext should only take an IQuorumClients rather than an IQuorum. See IContainerContext for more
188
- * details.
201
+ * @deprecated Temporary migratory API, to be removed when customers no longer need it.
202
+ * When removed, `ContainerContext` should only take an {@link @fluidframework/container-definitions#IQuorumClients}
203
+ * rather than an {@link @fluidframework/protocol-definitions#IQuorum}.
204
+ * See {@link @fluidframework/container-definitions#IContainerContext} for more details.
189
205
  */
190
206
  public getSpecifiedCodeDetails(): IFluidCodeDetails | undefined {
191
207
  return (this._quorum.get("code") ?? this._quorum.get("code2")) as IFluidCodeDetails | undefined;
@@ -223,9 +239,7 @@ export class ContainerContext implements IContainerContext {
223
239
 
224
240
  public setConnectionState(connected: boolean, clientId?: string) {
225
241
  const runtime = this.runtime;
226
-
227
- assert(connected === this.connected, 0x0de /* "Mismatch in connection state while setting" */);
228
-
242
+ this._connected = connected;
229
243
  runtime.setConnectionState(connected, clientId);
230
244
  }
231
245
 
@@ -3,13 +3,17 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { ITelemetryLogger } from "@fluidframework/common-definitions";
6
+ import { IDisposable, ITelemetryLogger } from "@fluidframework/common-definitions";
7
+ import { assert } from "@fluidframework/common-utils";
7
8
  import { ISnapshotTreeWithBlobContents } from "@fluidframework/container-definitions";
8
9
  import {
10
+ FetchSource,
11
+ IDocumentService,
9
12
  IDocumentStorageService,
10
13
  IDocumentStorageServicePolicies,
11
14
  ISummaryContext,
12
15
  } from "@fluidframework/driver-definitions";
16
+ import { UsageError } from "@fluidframework/driver-utils";
13
17
  import {
14
18
  ICreateBlobResponse,
15
19
  ISnapshotTree,
@@ -18,14 +22,52 @@ import {
18
22
  IVersion,
19
23
  } from "@fluidframework/protocol-definitions";
20
24
  import { IDetachedBlobStorage } from "./loader";
25
+ import { ProtocolTreeStorageService } from "./protocolTreeDocumentStorageService";
26
+ import { RetriableDocumentStorageService } from "./retriableDocumentStorageService";
21
27
 
22
28
  /**
23
29
  * This class wraps the actual storage and make sure no wrong apis are called according to
24
30
  * container attach state.
25
31
  */
26
- export class ContainerStorageAdapter implements IDocumentStorageService {
32
+ export class ContainerStorageAdapter implements IDocumentStorageService, IDisposable {
27
33
  private readonly blobContents: { [id: string]: ArrayBufferLike; } = {};
28
- constructor(private readonly storageGetter: () => IDocumentStorageService) {}
34
+ private _storageService: IDocumentStorageService & Partial<IDisposable>;
35
+
36
+ constructor(
37
+ detachedBlobStorage: IDetachedBlobStorage | undefined,
38
+ private readonly logger: ITelemetryLogger,
39
+ private readonly captureProtocolSummary?: () => ISummaryTree,
40
+ ) {
41
+ this._storageService = new BlobOnlyStorage(detachedBlobStorage, logger);
42
+ }
43
+
44
+ disposed: boolean = false;
45
+ dispose(error?: Error): void {
46
+ this._storageService?.dispose?.(error);
47
+ this.disposed = true;
48
+ }
49
+
50
+ public async connectToService(service: IDocumentService): Promise<void> {
51
+ if (!(this._storageService instanceof BlobOnlyStorage)) {
52
+ return;
53
+ }
54
+
55
+ const storageService = await service.connectToStorage();
56
+ const retriableStorage = this._storageService =
57
+ new RetriableDocumentStorageService(
58
+ storageService,
59
+ this.logger);
60
+
61
+ if (this.captureProtocolSummary !== undefined) {
62
+ this.logger.sendTelemetryEvent({ eventName: "summarizeProtocolTreeEnabled" });
63
+ this._storageService =
64
+ new ProtocolTreeStorageService(retriableStorage, this.captureProtocolSummary);
65
+ }
66
+
67
+ // ensure we did not lose that policy in the process of wrapping
68
+ assert(storageService.policies?.minBlobSize === this._storageService.policies?.minBlobSize,
69
+ 0x0e0 /* "lost minBlobSize policy" */);
70
+ }
29
71
 
30
72
  public loadSnapshotForRehydratingContainer(snapshotTree: ISnapshotTreeWithBlobContents) {
31
73
  this.getBlobContents(snapshotTree);
@@ -44,17 +86,17 @@ export class ContainerStorageAdapter implements IDocumentStorageService {
44
86
  // back-compat 0.40 containerRuntime requests policies even in detached container if storage is present
45
87
  // and storage is always present in >=0.41.
46
88
  try {
47
- return this.storageGetter().policies;
89
+ return this._storageService.policies;
48
90
  } catch (e) {}
49
91
  return undefined;
50
92
  }
51
93
 
52
94
  public get repositoryUrl(): string {
53
- return this.storageGetter().repositoryUrl;
95
+ return this._storageService.repositoryUrl;
54
96
  }
55
97
 
56
98
  public async getSnapshotTree(version?: IVersion, scenarioName?: string): Promise<ISnapshotTree | null> {
57
- return this.storageGetter().getSnapshotTree(version, scenarioName);
99
+ return this._storageService.getSnapshotTree(version, scenarioName);
58
100
  }
59
101
 
60
102
  public async readBlob(id: string): Promise<ArrayBufferLike> {
@@ -62,23 +104,28 @@ export class ContainerStorageAdapter implements IDocumentStorageService {
62
104
  if (blob !== undefined) {
63
105
  return blob;
64
106
  }
65
- return this.storageGetter().readBlob(id);
107
+ return this._storageService.readBlob(id);
66
108
  }
67
109
 
68
- public async getVersions(versionId: string | null, count: number, scenarioName?: string): Promise<IVersion[]> {
69
- return this.storageGetter().getVersions(versionId, count, scenarioName);
110
+ public async getVersions(
111
+ versionId: string | null,
112
+ count: number,
113
+ scenarioName?: string,
114
+ fetchSource?: FetchSource,
115
+ ): Promise<IVersion[]> {
116
+ return this._storageService.getVersions(versionId, count, scenarioName, fetchSource);
70
117
  }
71
118
 
72
119
  public async uploadSummaryWithContext(summary: ISummaryTree, context: ISummaryContext): Promise<string> {
73
- return this.storageGetter().uploadSummaryWithContext(summary, context);
120
+ return this._storageService.uploadSummaryWithContext(summary, context);
74
121
  }
75
122
 
76
123
  public async downloadSummary(handle: ISummaryHandle): Promise<ISummaryTree> {
77
- return this.storageGetter().downloadSummary(handle);
124
+ return this._storageService.downloadSummary(handle);
78
125
  }
79
126
 
80
127
  public async createBlob(file: ArrayBufferLike): Promise<ICreateBlobResponse> {
81
- return this.storageGetter().createBlob(file);
128
+ return this._storageService.createBlob(file);
82
129
  }
83
130
  }
84
131
 
@@ -86,18 +133,25 @@ export class ContainerStorageAdapter implements IDocumentStorageService {
86
133
  * Storage which only supports createBlob() and readBlob(). This is used with IDetachedBlobStorage to support
87
134
  * blobs in detached containers.
88
135
  */
89
- export class BlobOnlyStorage implements IDocumentStorageService {
136
+ class BlobOnlyStorage implements IDocumentStorageService {
90
137
  constructor(
91
- private readonly blobStorage: IDetachedBlobStorage,
138
+ private readonly detachedStorage: IDetachedBlobStorage | undefined,
92
139
  private readonly logger: ITelemetryLogger,
93
140
  ) { }
94
141
 
95
142
  public async createBlob(content: ArrayBufferLike): Promise<ICreateBlobResponse> {
96
- return this.blobStorage.createBlob(content);
143
+ return this.verifyStorage().createBlob(content);
97
144
  }
98
145
 
99
146
  public async readBlob(blobId: string): Promise<ArrayBufferLike> {
100
- return this.blobStorage.readBlob(blobId);
147
+ return this.verifyStorage().readBlob(blobId);
148
+ }
149
+
150
+ private verifyStorage(): IDetachedBlobStorage {
151
+ if (this.detachedStorage === undefined) {
152
+ throw new UsageError("Real storage calls not allowed in Unattached container");
153
+ }
154
+ return this.detachedStorage;
101
155
  }
102
156
 
103
157
  public get policies(): IDocumentStorageServicePolicies | undefined {
@@ -117,6 +171,7 @@ export class BlobOnlyStorage implements IDocumentStorageService {
117
171
  /* eslint-enable @typescript-eslint/unbound-method */
118
172
 
119
173
  private notCalled(): never {
174
+ this.verifyStorage();
120
175
  try {
121
176
  // some browsers may not populate stack unless exception is thrown
122
177
  throw new Error("BlobOnlyStorage not implemented method used");
package/src/contracts.ts CHANGED
@@ -56,13 +56,13 @@ export interface IConnectionManager {
56
56
 
57
57
  readonly readOnlyInfo: ReadOnlyInfo;
58
58
 
59
- // Various connectivity propetries for telemetry describing type of current connection
59
+ // Various connectivity properties for telemetry describing type of current connection
60
60
  // Things like connection mode, service info, etc.
61
61
  // Called when connection state changes (connect / disconnect)
62
62
  readonly connectionProps: ITelemetryProperties;
63
63
 
64
64
  // Verbose information about connection logged to telemetry in case of issues with
65
- // maintaining healphy connection, including op gaps, not receiving join op in time, etc.
65
+ // maintaining healthy connection, including op gaps, not receiving join op in time, etc.
66
66
  // Contains details information, like sequence numbers at connection time, initial ops info, etc.
67
67
  readonly connectionVerboseProps: ITelemetryProperties;
68
68
 
@@ -73,7 +73,7 @@ export interface IConnectionManager {
73
73
  prepareMessageToSend(message: Omit<IDocumentMessage, "clientSequenceNumber">): IDocumentMessage | undefined;
74
74
 
75
75
  /**
76
- * Called before incomming message is processed. Incomming messages can be comming from connection,
76
+ * Called before incoming message is processed. Incoming messages can be combing from connection,
77
77
  * but also could come from storage.
78
78
  * This call allows connection manager to adjust knowledge about acked ops sent on previous connection.
79
79
  * Can be called at any time, including when there is no active connection.
@@ -107,11 +107,11 @@ export interface IConnectionManager {
107
107
 
108
108
  /**
109
109
  * This interface represents a set of callbacks provided by DeltaManager to IConnectionManager on its creation
110
- * IConnectionManager instance will use them to communicate to DeltaManager abour various events.
110
+ * IConnectionManager instance will use them to communicate to DeltaManager about various events.
111
111
  */
112
112
  export interface IConnectionManagerFactoryArgs {
113
113
  /**
114
- * Called by connection manager for each incomming op. Some ops maybe delivered before
114
+ * Called by connection manager for each incoming op. Some ops maybe delivered before
115
115
  * connectHandler is called (initial ops on socket connection)
116
116
  */
117
117
  readonly incomingOpHandler: (messages: ISequencedDocumentMessage[], reason: string) => void;
@@ -131,8 +131,8 @@ export interface IConnectionManagerFactoryArgs {
131
131
  readonly reconnectionDelayHandler: (delayMs: number, error: unknown) => void;
132
132
 
133
133
  /**
134
- * Called by connection manager whwnever critical error happens and container should be closed.
135
- * Expects dispose() call in respose to this call.
134
+ * Called by connection manager whenever critical error happens and container should be closed.
135
+ * Expects dispose() call in response to this call.
136
136
  */
137
137
  readonly closeHandler: (error?: any) => void;
138
138
 
@@ -25,6 +25,8 @@ import {
25
25
  normalizeError,
26
26
  logIfFalse,
27
27
  safeRaiseEvent,
28
+ MonitoringContext,
29
+ loggerToMonitoringContext,
28
30
  } from "@fluidframework/telemetry-utils";
29
31
  import {
30
32
  IDocumentDeltaStorageService,
@@ -47,6 +49,7 @@ import {
47
49
  DataCorruptionError,
48
50
  extractSafePropertiesFromMessage,
49
51
  DataProcessingError,
52
+ UsageError,
50
53
  } from "@fluidframework/container-utils";
51
54
  import { DeltaQueue } from "./deltaQueue";
52
55
  import {
@@ -89,6 +92,15 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
89
92
  private pending: ISequencedDocumentMessage[] = [];
90
93
  private fetchReason: string | undefined;
91
94
 
95
+ private readonly mc: MonitoringContext;
96
+
97
+ // A boolean used to assert that ops are not being sent while processing another op.
98
+ private currentlyProcessingOps: boolean = false;
99
+
100
+ // Feature gate that closes a container when sending an op if the container is
101
+ // concurrently processing another op
102
+ private readonly preventConcurrentOpSend: boolean = true;
103
+
92
104
  // The minimum sequence number and last sequence number received from the server
93
105
  private minSequenceNumber: number = 0;
94
106
 
@@ -113,6 +125,8 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
113
125
  private previouslyProcessedMessage: ISequencedDocumentMessage | undefined;
114
126
 
115
127
  // The sequence number we initially loaded from
128
+ // In case of reading from a snapshot or pending state, its value will be equal to
129
+ // the last message that got serialized.
116
130
  private initSequenceNumber: number = 0;
117
131
 
118
132
  private readonly _inbound: DeltaQueue<ISequencedDocumentMessage>;
@@ -185,9 +199,12 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
185
199
  public get readOnlyInfo() { return this.connectionManager.readOnlyInfo; }
186
200
  public get clientDetails() { return this.connectionManager.clientDetails; }
187
201
 
188
- public submit(type: MessageType, contents: any, batch = false, metadata?: any) {
202
+ public submit(type: MessageType, contents?: string, batch = false, metadata?: any) {
203
+ if (this.currentlyProcessingOps && this.preventConcurrentOpSend) {
204
+ this.close(new UsageError("Making changes to data model is disallowed while processing ops."));
205
+ }
189
206
  const messagePartial: Omit<IDocumentMessage, "clientSequenceNumber"> = {
190
- contents: JSON.stringify(contents),
207
+ contents,
191
208
  metadata,
192
209
  referenceSequenceNumber: this.lastProcessedSequenceNumber,
193
210
  type,
@@ -196,13 +213,14 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
196
213
  if (!batch) {
197
214
  this.flush();
198
215
  }
199
-
200
216
  const message = this.connectionManager.prepareMessageToSend(messagePartial);
201
217
  if (message === undefined) {
202
218
  return -1;
203
219
  }
204
220
 
205
- this.opsSize += message.contents.length;
221
+ if (contents !== undefined) {
222
+ this.opsSize += contents.length;
223
+ }
206
224
 
207
225
  this.messageBuffer.push(message);
208
226
 
@@ -211,22 +229,32 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
211
229
  if (!batch) {
212
230
  this.flush();
213
231
  }
214
-
215
232
  return message.clientSequenceNumber;
216
233
  }
217
234
 
218
235
  public submitSignal(content: any) { return this.connectionManager.submitSignal(content); }
219
236
 
220
237
  public flush() {
221
- if (this.messageBuffer.length === 0) {
238
+ const batch = this.messageBuffer;
239
+ if (batch.length === 0) {
222
240
  return;
223
241
  }
224
242
 
243
+ this.messageBuffer = [];
244
+
225
245
  // The prepareFlush event allows listeners to append metadata to the batch prior to submission.
226
- this.emit("prepareSend", this.messageBuffer);
246
+ this.emit("prepareSend", batch);
227
247
 
228
- this.connectionManager.sendMessages(this.messageBuffer);
229
- this.messageBuffer = [];
248
+ if (batch.length === 1) {
249
+ assert(batch[0].metadata?.batch === undefined, 0x3c9 /* no batch markup on single message */);
250
+ } else {
251
+ assert(batch[0].metadata?.batch === true, 0x3ca /* no start batch markup */);
252
+ assert(batch[batch.length - 1].metadata?.batch === false, 0x3cb /* no end batch markup */);
253
+ }
254
+
255
+ this.connectionManager.sendMessages(batch);
256
+
257
+ assert(this.messageBuffer.length === 0, 0x3cc /* reentrancy */);
230
258
  }
231
259
 
232
260
  public get connectionProps(): ITelemetryProperties {
@@ -293,7 +321,8 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
293
321
  };
294
322
 
295
323
  this.connectionManager = createConnectionManager(props);
296
-
324
+ this.mc = loggerToMonitoringContext(logger);
325
+ this.preventConcurrentOpSend = this.mc.config.getBoolean("Fluid.Container.ConcurrentOpSend") === true;
297
326
  this._inbound = new DeltaQueue<ISequencedDocumentMessage>(
298
327
  (op) => {
299
328
  this.processInboundMessage(op);
@@ -410,7 +439,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
410
439
 
411
440
  if (prefetchType !== "none") {
412
441
  const cacheOnly = prefetchType === "cached";
413
- await this.fetchMissingDeltasCore("DocumentOpen", cacheOnly, this.lastQueuedSequenceNumber);
442
+ await this.fetchMissingDeltasCore(`DocumentOpen_${prefetchType}`, cacheOnly);
414
443
 
415
444
  // Keep going with fetching ops from storage once we have all cached ops in.
416
445
  // But do not block load and make this request async / not blocking this api.
@@ -418,7 +447,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
418
447
  // (which in most cases will happen when we are done processing cached ops)
419
448
  if (cacheOnly) {
420
449
  // fire and forget
421
- this.fetchMissingDeltas("DocumentOpen");
450
+ this.fetchMissingDeltas("PostDocumentOpen");
422
451
  }
423
452
  }
424
453
 
@@ -453,6 +482,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
453
482
  private async getDeltas(
454
483
  from: number, // inclusive
455
484
  to: number | undefined, // exclusive
485
+ fetchReason: string,
456
486
  callback: (messages: ISequencedDocumentMessage[]) => void,
457
487
  cacheOnly: boolean) {
458
488
  const docService = this.serviceProvider();
@@ -473,7 +503,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
473
503
  // received through delta stream. Validate that before moving forward.
474
504
  if (this.lastQueuedSequenceNumber >= lastExpectedOp) {
475
505
  this.logger.sendPerformanceEvent({
476
- reason: this.fetchReason,
506
+ reason: fetchReason,
477
507
  eventName: "ExtraStorageCall",
478
508
  early: true,
479
509
  from,
@@ -521,7 +551,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
521
551
  to, // exclusive
522
552
  controller.signal,
523
553
  cacheOnly,
524
- this.fetchReason);
554
+ fetchReason);
525
555
 
526
556
  // eslint-disable-next-line no-constant-condition
527
557
  while (true) {
@@ -761,6 +791,8 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
761
791
 
762
792
  private processInboundMessage(message: ISequencedDocumentMessage): void {
763
793
  const startTime = Date.now();
794
+ assert(!this.currentlyProcessingOps, 0x3af /* Already processing ops. */);
795
+ this.currentlyProcessingOps = true;
764
796
  this.lastProcessedMessage = message;
765
797
 
766
798
  // All non-system messages are coming from some client, and should have clientId
@@ -815,7 +847,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
815
847
  throw new Error("Attempted to process an inbound message without a handler attached");
816
848
  }
817
849
  this.handler.process(message);
818
-
850
+ this.currentlyProcessingOps = false;
819
851
  const endTime = Date.now();
820
852
 
821
853
  // Should be last, after changing this.lastProcessedSequenceNumber above, as many callers
@@ -876,6 +908,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
876
908
  await this.getDeltas(
877
909
  from,
878
910
  to,
911
+ fetchReason,
879
912
  (messages) => {
880
913
  this.refreshDelayInfo(this.deltaStorageDelayId);
881
914
  this.enqueueMessages(messages, fetchReason);
package/src/deltaQueue.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { IDeltaQueue, IDeltaQueueEvents } from "@fluidframework/container-definitions";
7
- import { assert, performance, Deferred, TypedEventEmitter } from "@fluidframework/common-utils";
7
+ import { assert, performance, TypedEventEmitter } from "@fluidframework/common-utils";
8
8
  import Deque from "double-ended-queue";
9
9
 
10
10
  export interface IDeltaQueueWriter<T> {
@@ -30,7 +30,7 @@ export class DeltaQueue<T>
30
30
  * When processing is ongoing, holds a deferred that will resolve once processing stops.
31
31
  * Undefined when not processing.
32
32
  */
33
- private processingDeferred: Deferred<void> | undefined;
33
+ private processingPromise: Promise<{ count: number; duration: number; }> | undefined;
34
34
 
35
35
  public get disposed(): boolean {
36
36
  return this.isDisposed;
@@ -48,13 +48,11 @@ export class DeltaQueue<T>
48
48
  }
49
49
 
50
50
  public get idle(): boolean {
51
- return this.processingDeferred === undefined && this.q.length === 0;
51
+ return this.processingPromise === undefined && this.q.length === 0;
52
52
  }
53
53
 
54
- public async waitTillProcessingDone(): Promise<void> {
55
- if (this.processingDeferred !== undefined) {
56
- return this.processingDeferred.promise;
57
- }
54
+ public async waitTillProcessingDone() {
55
+ return this.processingPromise ?? { count: 0, duration: 0 };
58
56
  }
59
57
 
60
58
  /**
@@ -98,7 +96,7 @@ export class DeltaQueue<T>
98
96
  this.pauseCount++;
99
97
  // If called from within the processing loop, we are in the middle of processing an op. Return a promise
100
98
  // that will resolve when processing has actually stopped.
101
- return this.waitTillProcessingDone();
99
+ await this.waitTillProcessingDone();
102
100
  }
103
101
 
104
102
  public resume(): void {
@@ -113,20 +111,31 @@ export class DeltaQueue<T>
113
111
  * not already started.
114
112
  */
115
113
  private ensureProcessing() {
116
- if (!this.paused && this.processingDeferred === undefined) {
117
- this.processingDeferred = new Deferred<void>();
114
+ if (this.anythingToProcess() && this.processingPromise === undefined) {
118
115
  // Use a resolved promise to start the processing on a separate stack.
119
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
120
- Promise.resolve().then(() => {
121
- this.processDeltas();
122
- if (this.processingDeferred !== undefined) {
123
- this.processingDeferred.resolve();
124
- this.processingDeferred = undefined;
125
- }
116
+ this.processingPromise = Promise.resolve().then(() => {
117
+ assert(this.processingPromise !== undefined, 0x37f /* reentrancy? */);
118
+ const result = this.processDeltas();
119
+ assert(this.processingPromise !== undefined, 0x380 /* reentrancy? */);
120
+ // WARNING: Do not move next line to .finally() clause!
121
+ // It runs async and creates a race condition where incoming ensureProcessing() call observes
122
+ // from previous run while previous run is over (but finally clause was not scheduled yet)
123
+ this.processingPromise = undefined;
124
+ return result;
125
+ }).catch((error) => {
126
+ this.error = error;
127
+ this.processingPromise = undefined;
128
+ this.emit("error", error);
129
+ return { count: 0, duration: 0 };
126
130
  });
131
+ assert(this.processingPromise !== undefined, 0x381 /* processDeltas() should run async */);
127
132
  }
128
133
  }
129
134
 
135
+ private anythingToProcess() {
136
+ return this.q.length !== 0 && !this.paused && this.error === undefined;
137
+ }
138
+
130
139
  /**
131
140
  * Executes the delta processing loop until a stop condition is reached.
132
141
  */
@@ -136,24 +145,21 @@ export class DeltaQueue<T>
136
145
 
137
146
  // For grouping to work we must process all local messages immediately and in the single turn.
138
147
  // So loop over them until no messages to process, we have become paused, or hit an error.
139
- while (!(this.q.length === 0 || this.paused || this.error !== undefined)) {
148
+ while (this.anythingToProcess()) {
140
149
  // Get the next message in the queue
141
150
  const next = this.q.shift();
142
151
  count++;
143
152
  // Process the message.
144
- try {
145
- // We know next is defined since we did a length check just prior to shifting.
146
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
147
- this.worker(next!);
148
- this.emit("op", next);
149
- } catch (error) {
150
- this.error = error;
151
- this.emit("error", error);
152
- }
153
+ // We know next is defined since we did a length check just prior to shifting.
154
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
155
+ this.worker(next!);
156
+ this.emit("op", next);
153
157
  }
154
158
 
159
+ const duration = performance.now() - start;
155
160
  if (this.q.length === 0) {
156
- this.emit("idle", count, performance.now() - start);
161
+ this.emit("idle", count, duration);
157
162
  }
163
+ return { count, duration };
158
164
  }
159
165
  }
package/src/index.ts CHANGED
@@ -21,3 +21,7 @@ export {
21
21
  Loader,
22
22
  RelativeLoader,
23
23
  } from "./loader";
24
+ export {
25
+ IProtocolHandler,
26
+ ProtocolHandlerBuilder,
27
+ } from "./protocol";