@fluidframework/container-loader 1.3.0 → 2.0.0-dev.1.4.5.105745

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 (148) hide show
  1. package/.eslintrc.js +8 -21
  2. package/.mocharc.js +12 -0
  3. package/dist/audience.d.ts +2 -2
  4. package/dist/audience.d.ts.map +1 -1
  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 +13 -18
  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 +30 -17
  27. package/dist/container.d.ts.map +1 -1
  28. package/dist/container.js +173 -165
  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 +33 -6
  43. package/dist/deltaManager.js.map +1 -1
  44. package/dist/deltaQueue.js +3 -3
  45. package/dist/deltaQueue.js.map +1 -1
  46. package/dist/index.d.ts +1 -0
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js.map +1 -1
  49. package/dist/loader.d.ts +8 -1
  50. package/dist/loader.d.ts.map +1 -1
  51. package/dist/loader.js +4 -3
  52. package/dist/loader.js.map +1 -1
  53. package/dist/packageVersion.d.ts +1 -1
  54. package/dist/packageVersion.d.ts.map +1 -1
  55. package/dist/packageVersion.js +1 -1
  56. package/dist/packageVersion.js.map +1 -1
  57. package/dist/protocol.d.ts +22 -0
  58. package/dist/protocol.d.ts.map +1 -0
  59. package/dist/protocol.js +53 -0
  60. package/dist/protocol.js.map +1 -0
  61. package/dist/protocolTreeDocumentStorageService.d.ts +1 -1
  62. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  63. package/dist/retriableDocumentStorageService.d.ts +2 -2
  64. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  65. package/dist/retriableDocumentStorageService.js +2 -2
  66. package/dist/retriableDocumentStorageService.js.map +1 -1
  67. package/lib/audience.d.ts +2 -2
  68. package/lib/audience.d.ts.map +1 -1
  69. package/lib/audience.js.map +1 -1
  70. package/lib/catchUpMonitor.d.ts +29 -0
  71. package/lib/catchUpMonitor.d.ts.map +1 -0
  72. package/lib/catchUpMonitor.js +39 -0
  73. package/lib/catchUpMonitor.js.map +1 -0
  74. package/lib/collabWindowTracker.d.ts +1 -1
  75. package/lib/collabWindowTracker.d.ts.map +1 -1
  76. package/lib/collabWindowTracker.js +13 -5
  77. package/lib/collabWindowTracker.js.map +1 -1
  78. package/lib/connectionManager.d.ts +5 -5
  79. package/lib/connectionManager.d.ts.map +1 -1
  80. package/lib/connectionManager.js +14 -21
  81. package/lib/connectionManager.js.map +1 -1
  82. package/lib/connectionState.d.ts +0 -5
  83. package/lib/connectionState.d.ts.map +1 -1
  84. package/lib/connectionState.js +0 -5
  85. package/lib/connectionState.js.map +1 -1
  86. package/lib/connectionStateHandler.d.ts +84 -22
  87. package/lib/connectionStateHandler.d.ts.map +1 -1
  88. package/lib/connectionStateHandler.js +171 -59
  89. package/lib/connectionStateHandler.js.map +1 -1
  90. package/lib/container.d.ts +30 -17
  91. package/lib/container.d.ts.map +1 -1
  92. package/lib/container.js +176 -168
  93. package/lib/container.js.map +1 -1
  94. package/lib/containerContext.d.ts +18 -7
  95. package/lib/containerContext.d.ts.map +1 -1
  96. package/lib/containerContext.js +19 -9
  97. package/lib/containerContext.js.map +1 -1
  98. package/lib/containerStorageAdapter.d.ts +11 -25
  99. package/lib/containerStorageAdapter.d.ts.map +1 -1
  100. package/lib/containerStorageAdapter.js +51 -16
  101. package/lib/containerStorageAdapter.js.map +1 -1
  102. package/lib/contracts.d.ts +5 -5
  103. package/lib/contracts.js.map +1 -1
  104. package/lib/deltaManager.d.ts +4 -1
  105. package/lib/deltaManager.d.ts.map +1 -1
  106. package/lib/deltaManager.js +35 -8
  107. package/lib/deltaManager.js.map +1 -1
  108. package/lib/deltaQueue.js +3 -3
  109. package/lib/deltaQueue.js.map +1 -1
  110. package/lib/index.d.ts +1 -0
  111. package/lib/index.d.ts.map +1 -1
  112. package/lib/index.js.map +1 -1
  113. package/lib/loader.d.ts +8 -1
  114. package/lib/loader.d.ts.map +1 -1
  115. package/lib/loader.js +4 -3
  116. package/lib/loader.js.map +1 -1
  117. package/lib/packageVersion.d.ts +1 -1
  118. package/lib/packageVersion.d.ts.map +1 -1
  119. package/lib/packageVersion.js +1 -1
  120. package/lib/packageVersion.js.map +1 -1
  121. package/lib/protocol.d.ts +22 -0
  122. package/lib/protocol.d.ts.map +1 -0
  123. package/lib/protocol.js +49 -0
  124. package/lib/protocol.js.map +1 -0
  125. package/lib/protocolTreeDocumentStorageService.d.ts +1 -1
  126. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  127. package/lib/retriableDocumentStorageService.d.ts +2 -2
  128. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  129. package/lib/retriableDocumentStorageService.js +2 -2
  130. package/lib/retriableDocumentStorageService.js.map +1 -1
  131. package/package.json +26 -20
  132. package/src/audience.ts +2 -2
  133. package/src/catchUpMonitor.ts +59 -0
  134. package/src/collabWindowTracker.ts +15 -6
  135. package/src/connectionManager.ts +23 -27
  136. package/src/connectionState.ts +0 -6
  137. package/src/connectionStateHandler.ts +235 -70
  138. package/src/container.ts +223 -209
  139. package/src/containerContext.ts +22 -8
  140. package/src/containerStorageAdapter.ts +71 -16
  141. package/src/contracts.ts +7 -7
  142. package/src/deltaManager.ts +42 -11
  143. package/src/deltaQueue.ts +3 -3
  144. package/src/index.ts +4 -0
  145. package/src/loader.ts +14 -3
  146. package/src/packageVersion.ts +1 -1
  147. package/src/protocol.ts +97 -0
  148. 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);
@@ -762,6 +791,8 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
762
791
 
763
792
  private processInboundMessage(message: ISequencedDocumentMessage): void {
764
793
  const startTime = Date.now();
794
+ assert(!this.currentlyProcessingOps, 0x3af /* Already processing ops. */);
795
+ this.currentlyProcessingOps = true;
765
796
  this.lastProcessedMessage = message;
766
797
 
767
798
  // All non-system messages are coming from some client, and should have clientId
@@ -816,7 +847,7 @@ export class DeltaManager<TConnectionManager extends IConnectionManager>
816
847
  throw new Error("Attempted to process an inbound message without a handler attached");
817
848
  }
818
849
  this.handler.process(message);
819
-
850
+ this.currentlyProcessingOps = false;
820
851
  const endTime = Date.now();
821
852
 
822
853
  // Should be last, after changing this.lastProcessedSequenceNumber above, as many callers
package/src/deltaQueue.ts CHANGED
@@ -114,9 +114,9 @@ export class DeltaQueue<T>
114
114
  if (this.anythingToProcess() && this.processingPromise === undefined) {
115
115
  // Use a resolved promise to start the processing on a separate stack.
116
116
  this.processingPromise = Promise.resolve().then(() => {
117
- assert(this.processingPromise !== undefined, "reentrancy?");
117
+ assert(this.processingPromise !== undefined, 0x37f /* reentrancy? */);
118
118
  const result = this.processDeltas();
119
- assert(this.processingPromise !== undefined, "reentrancy?");
119
+ assert(this.processingPromise !== undefined, 0x380 /* reentrancy? */);
120
120
  // WARNING: Do not move next line to .finally() clause!
121
121
  // It runs async and creates a race condition where incoming ensureProcessing() call observes
122
122
  // from previous run while previous run is over (but finally clause was not scheduled yet)
@@ -128,7 +128,7 @@ export class DeltaQueue<T>
128
128
  this.emit("error", error);
129
129
  return { count: 0, duration: 0 };
130
130
  });
131
- assert(this.processingPromise !== undefined, "processDeltas() should run async");
131
+ assert(this.processingPromise !== undefined, 0x381 /* processDeltas() should run async */);
132
132
  }
133
133
  }
134
134
 
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";
package/src/loader.ts CHANGED
@@ -48,6 +48,7 @@ import {
48
48
  import { Container, IPendingContainerState } from "./container";
49
49
  import { IParsedUrl, parseUrl } from "./utils";
50
50
  import { pkgVersion } from "./packageVersion";
51
+ import { ProtocolHandlerBuilder } from "./protocol";
51
52
 
52
53
  function canUseCache(request: IRequest): boolean {
53
54
  if (request.headers === undefined) {
@@ -132,7 +133,7 @@ export interface ILoaderOptions extends ILoaderOptions1 {
132
133
 
133
134
  /**
134
135
  * @deprecated IFluidModuleWithDetails interface is moved to
135
- * {@link @fluidframework/container-definition#IFluidModuleWithDetails}
136
+ * {@link @fluidframework/container-definitions#IFluidModuleWithDetails}
136
137
  * to have all the code loading modules in one package. #8193
137
138
  * Encapsulates a module entry point with corresponding code details.
138
139
  */
@@ -211,7 +212,13 @@ export interface ILoaderProps {
211
212
  /**
212
213
  * The configuration provider which may be used to control features.
213
214
  */
214
- readonly configProvider?: IConfigProviderBase;
215
+ readonly configProvider?: IConfigProviderBase;
216
+
217
+ /**
218
+ * Optional property for allowing the container to use a custom
219
+ * protocol implementation for handling the quorum and/or the audience.
220
+ */
221
+ readonly protocolHandlerBuilder?: ProtocolHandlerBuilder;
215
222
  }
216
223
 
217
224
  /**
@@ -278,6 +285,7 @@ export class Loader implements IHostLoader {
278
285
  private readonly containers = new Map<string, Promise<Container>>();
279
286
  public readonly services: ILoaderServices;
280
287
  private readonly mc: MonitoringContext;
288
+ private readonly protocolHandlerBuilder: ProtocolHandlerBuilder | undefined;
281
289
 
282
290
  constructor(loaderProps: ILoaderProps) {
283
291
  const scope: FluidObject<ILoader> = { ...loaderProps.scope };
@@ -306,6 +314,7 @@ export class Loader implements IHostLoader {
306
314
  };
307
315
  this.mc = loggerToMonitoringContext(
308
316
  ChildLogger.create(this.services.subLogger, "Loader"));
317
+ this.protocolHandlerBuilder = loaderProps.protocolHandlerBuilder;
309
318
  }
310
319
 
311
320
  public get IFluidRouter(): IFluidRouter { return this; }
@@ -314,6 +323,7 @@ export class Loader implements IHostLoader {
314
323
  const container = await Container.createDetached(
315
324
  this,
316
325
  codeDetails,
326
+ this.protocolHandlerBuilder,
317
327
  );
318
328
 
319
329
  if (this.cachingEnabled) {
@@ -330,7 +340,7 @@ export class Loader implements IHostLoader {
330
340
  }
331
341
 
332
342
  public async rehydrateDetachedContainerFromSnapshot(snapshot: string): Promise<IContainer> {
333
- return Container.rehydrateDetachedFromSnapshot(this, snapshot);
343
+ return Container.rehydrateDetachedFromSnapshot(this, snapshot, this.protocolHandlerBuilder);
334
344
  }
335
345
 
336
346
  public async resolve(request: IRequest, pendingLocalState?: string): Promise<IContainer> {
@@ -482,6 +492,7 @@ export class Loader implements IHostLoader {
482
492
  loadMode: request.headers?.[LoaderHeader.loadMode],
483
493
  },
484
494
  pendingLocalState,
495
+ this.protocolHandlerBuilder,
485
496
  );
486
497
  }
487
498
  }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-loader";
9
- export const pkgVersion = "1.3.0";
9
+ export const pkgVersion = "2.0.0-dev.1.4.5.105745";
@@ -0,0 +1,97 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { IAudienceOwner } from "@fluidframework/container-definitions";
7
+ import {
8
+ ILocalSequencedClient,
9
+ IProtocolHandler as IBaseProtocolHandler,
10
+ IQuorumSnapshot,
11
+ ProtocolOpHandler,
12
+ } from "@fluidframework/protocol-base";
13
+ import {
14
+ IDocumentAttributes,
15
+ IProcessMessageResult,
16
+ ISequencedDocumentMessage,
17
+ ISignalClient,
18
+ ISignalMessage,
19
+ MessageType,
20
+ } from "@fluidframework/protocol-definitions";
21
+ import { canBeCoalescedByService } from "@fluidframework/driver-utils";
22
+
23
+ /**
24
+ * Function to be used for creating a protocol handler.
25
+ */
26
+ export type ProtocolHandlerBuilder = (
27
+ attributes: IDocumentAttributes,
28
+ snapshot: IQuorumSnapshot,
29
+ sendProposal: (key: string, value: any) => number,
30
+ initialClients: ISignalClient[],
31
+ ) => IProtocolHandler;
32
+
33
+ export interface IProtocolHandler extends IBaseProtocolHandler {
34
+ readonly audience: IAudienceOwner;
35
+ processSignal(message: ISignalMessage);
36
+ }
37
+
38
+ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandler {
39
+ constructor(
40
+ attributes: IDocumentAttributes,
41
+ quorumSnapshot: IQuorumSnapshot,
42
+ sendProposal: (key: string, value: any) => number,
43
+ initialClients: ISignalClient[],
44
+ readonly audience: IAudienceOwner,
45
+ ) {
46
+ super(
47
+ attributes.minimumSequenceNumber,
48
+ attributes.sequenceNumber,
49
+ attributes.term,
50
+ quorumSnapshot.members,
51
+ quorumSnapshot.proposals,
52
+ quorumSnapshot.values,
53
+ sendProposal,
54
+ );
55
+
56
+ for (const initialClient of initialClients) {
57
+ this.audience.addMember(initialClient.clientId, initialClient.client);
58
+ }
59
+ }
60
+
61
+ public processMessage(message: ISequencedDocumentMessage, local: boolean): IProcessMessageResult {
62
+ const client: ILocalSequencedClient | undefined = this.quorum.getMember(message.clientId);
63
+
64
+ // Check and report if we're getting messages from a clientId that we previously
65
+ // flagged as shouldHaveLeft, or from a client that's not in the quorum but should be
66
+ if (message.clientId != null) {
67
+ if (client === undefined && message.type !== MessageType.ClientJoin) {
68
+ // pre-0.58 error message: messageClientIdMissingFromQuorum
69
+ throw new Error("Remote message's clientId is missing from the quorum");
70
+ }
71
+
72
+ if (client?.shouldHaveLeft === true && !canBeCoalescedByService(message)) {
73
+ // pre-0.58 error message: messageClientIdShouldHaveLeft
74
+ throw new Error("Remote message's clientId already should have left");
75
+ }
76
+ }
77
+
78
+ return super.processMessage(message, local);
79
+ }
80
+
81
+ public processSignal(message: ISignalMessage) {
82
+ const innerContent = message.content as { content: any; type: string; };
83
+ switch (innerContent.type) {
84
+ case MessageType.ClientJoin: {
85
+ const newClient = innerContent.content as ISignalClient;
86
+ this.audience.addMember(newClient.clientId, newClient.client);
87
+ break;
88
+ }
89
+ case MessageType.ClientLeave: {
90
+ const leftClientId = innerContent.content as string;
91
+ this.audience.removeMember(leftClientId);
92
+ break;
93
+ }
94
+ default: break;
95
+ }
96
+ }
97
+ }