@fluidframework/container-loader 2.0.0-dev.5.2.0.169897 → 2.0.0-dev.6.4.0.191258

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 (204) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/README.md +10 -6
  3. package/dist/audience.d.ts +1 -0
  4. package/dist/audience.d.ts.map +1 -1
  5. package/dist/audience.js +5 -3
  6. package/dist/audience.js.map +1 -1
  7. package/dist/catchUpMonitor.d.ts +1 -1
  8. package/dist/catchUpMonitor.d.ts.map +1 -1
  9. package/dist/catchUpMonitor.js +2 -2
  10. package/dist/catchUpMonitor.js.map +1 -1
  11. package/dist/connectionManager.d.ts +6 -6
  12. package/dist/connectionManager.d.ts.map +1 -1
  13. package/dist/connectionManager.js +97 -93
  14. package/dist/connectionManager.js.map +1 -1
  15. package/dist/connectionStateHandler.d.ts +19 -15
  16. package/dist/connectionStateHandler.d.ts.map +1 -1
  17. package/dist/connectionStateHandler.js +59 -59
  18. package/dist/connectionStateHandler.js.map +1 -1
  19. package/dist/container.d.ts +48 -38
  20. package/dist/container.d.ts.map +1 -1
  21. package/dist/container.js +447 -325
  22. package/dist/container.js.map +1 -1
  23. package/dist/containerContext.d.ts +22 -70
  24. package/dist/containerContext.d.ts.map +1 -1
  25. package/dist/containerContext.js +24 -221
  26. package/dist/containerContext.js.map +1 -1
  27. package/dist/containerStorageAdapter.d.ts +1 -1
  28. package/dist/containerStorageAdapter.d.ts.map +1 -1
  29. package/dist/containerStorageAdapter.js +47 -16
  30. package/dist/containerStorageAdapter.js.map +1 -1
  31. package/dist/contracts.d.ts +21 -10
  32. package/dist/contracts.d.ts.map +1 -1
  33. package/dist/contracts.js +3 -3
  34. package/dist/contracts.js.map +1 -1
  35. package/dist/debugLogger.d.ts +30 -0
  36. package/dist/debugLogger.d.ts.map +1 -0
  37. package/dist/debugLogger.js +95 -0
  38. package/dist/debugLogger.js.map +1 -0
  39. package/dist/deltaManager.d.ts +21 -9
  40. package/dist/deltaManager.d.ts.map +1 -1
  41. package/dist/deltaManager.js +114 -66
  42. package/dist/deltaManager.js.map +1 -1
  43. package/dist/deltaQueue.d.ts +1 -1
  44. package/dist/deltaQueue.d.ts.map +1 -1
  45. package/dist/deltaQueue.js +10 -10
  46. package/dist/deltaQueue.js.map +1 -1
  47. package/dist/disposal.d.ts +13 -0
  48. package/dist/disposal.d.ts.map +1 -0
  49. package/dist/disposal.js +25 -0
  50. package/dist/disposal.js.map +1 -0
  51. package/dist/error.d.ts +23 -0
  52. package/dist/error.d.ts.map +1 -0
  53. package/dist/error.js +32 -0
  54. package/dist/error.js.map +1 -0
  55. package/dist/loader.d.ts +23 -5
  56. package/dist/loader.d.ts.map +1 -1
  57. package/dist/loader.js +82 -51
  58. package/dist/loader.js.map +1 -1
  59. package/dist/noopHeuristic.d.ts +23 -0
  60. package/dist/noopHeuristic.d.ts.map +1 -0
  61. package/dist/noopHeuristic.js +90 -0
  62. package/dist/noopHeuristic.js.map +1 -0
  63. package/dist/packageVersion.d.ts +1 -1
  64. package/dist/packageVersion.js +1 -1
  65. package/dist/packageVersion.js.map +1 -1
  66. package/dist/protocol.d.ts +9 -12
  67. package/dist/protocol.d.ts.map +1 -1
  68. package/dist/protocol.js +26 -7
  69. package/dist/protocol.js.map +1 -1
  70. package/dist/protocolTreeDocumentStorageService.d.ts +1 -1
  71. package/dist/protocolTreeDocumentStorageService.d.ts.map +1 -1
  72. package/dist/protocolTreeDocumentStorageService.js.map +1 -1
  73. package/dist/quorum.d.ts +1 -14
  74. package/dist/quorum.d.ts.map +1 -1
  75. package/dist/quorum.js +1 -29
  76. package/dist/quorum.js.map +1 -1
  77. package/dist/retriableDocumentStorageService.d.ts +1 -1
  78. package/dist/retriableDocumentStorageService.d.ts.map +1 -1
  79. package/dist/retriableDocumentStorageService.js +4 -4
  80. package/dist/retriableDocumentStorageService.js.map +1 -1
  81. package/dist/utils.d.ts +8 -1
  82. package/dist/utils.d.ts.map +1 -1
  83. package/dist/utils.js +30 -11
  84. package/dist/utils.js.map +1 -1
  85. package/lib/audience.d.ts +1 -0
  86. package/lib/audience.d.ts.map +1 -1
  87. package/lib/audience.js +4 -2
  88. package/lib/audience.js.map +1 -1
  89. package/lib/catchUpMonitor.d.ts +1 -1
  90. package/lib/catchUpMonitor.d.ts.map +1 -1
  91. package/lib/catchUpMonitor.js +1 -1
  92. package/lib/catchUpMonitor.js.map +1 -1
  93. package/lib/connectionManager.d.ts +6 -6
  94. package/lib/connectionManager.d.ts.map +1 -1
  95. package/lib/connectionManager.js +74 -67
  96. package/lib/connectionManager.js.map +1 -1
  97. package/lib/connectionStateHandler.d.ts +19 -15
  98. package/lib/connectionStateHandler.d.ts.map +1 -1
  99. package/lib/connectionStateHandler.js +36 -36
  100. package/lib/connectionStateHandler.js.map +1 -1
  101. package/lib/container.d.ts +48 -38
  102. package/lib/container.d.ts.map +1 -1
  103. package/lib/container.js +414 -292
  104. package/lib/container.js.map +1 -1
  105. package/lib/containerContext.d.ts +22 -70
  106. package/lib/containerContext.d.ts.map +1 -1
  107. package/lib/containerContext.js +24 -221
  108. package/lib/containerContext.js.map +1 -1
  109. package/lib/containerStorageAdapter.d.ts +1 -1
  110. package/lib/containerStorageAdapter.d.ts.map +1 -1
  111. package/lib/containerStorageAdapter.js +43 -12
  112. package/lib/containerStorageAdapter.js.map +1 -1
  113. package/lib/contracts.d.ts +21 -10
  114. package/lib/contracts.d.ts.map +1 -1
  115. package/lib/contracts.js +3 -3
  116. package/lib/contracts.js.map +1 -1
  117. package/lib/debugLogger.d.ts +30 -0
  118. package/lib/debugLogger.d.ts.map +1 -0
  119. package/lib/debugLogger.js +91 -0
  120. package/lib/debugLogger.js.map +1 -0
  121. package/lib/deltaManager.d.ts +21 -9
  122. package/lib/deltaManager.d.ts.map +1 -1
  123. package/lib/deltaManager.js +88 -37
  124. package/lib/deltaManager.js.map +1 -1
  125. package/lib/deltaQueue.d.ts +1 -1
  126. package/lib/deltaQueue.d.ts.map +1 -1
  127. package/lib/deltaQueue.js +3 -3
  128. package/lib/deltaQueue.js.map +1 -1
  129. package/lib/disposal.d.ts +13 -0
  130. package/lib/disposal.d.ts.map +1 -0
  131. package/lib/disposal.js +21 -0
  132. package/lib/disposal.js.map +1 -0
  133. package/lib/error.d.ts +23 -0
  134. package/lib/error.d.ts.map +1 -0
  135. package/lib/error.js +28 -0
  136. package/lib/error.js.map +1 -0
  137. package/lib/loader.d.ts +23 -5
  138. package/lib/loader.d.ts.map +1 -1
  139. package/lib/loader.js +82 -51
  140. package/lib/loader.js.map +1 -1
  141. package/lib/noopHeuristic.d.ts +23 -0
  142. package/lib/noopHeuristic.d.ts.map +1 -0
  143. package/lib/{collabWindowTracker.js → noopHeuristic.js} +31 -42
  144. package/lib/noopHeuristic.js.map +1 -0
  145. package/lib/packageVersion.d.ts +1 -1
  146. package/lib/packageVersion.js +1 -1
  147. package/lib/packageVersion.js.map +1 -1
  148. package/lib/protocol.d.ts +9 -12
  149. package/lib/protocol.d.ts.map +1 -1
  150. package/lib/protocol.js +24 -6
  151. package/lib/protocol.js.map +1 -1
  152. package/lib/protocolTreeDocumentStorageService.d.ts +1 -1
  153. package/lib/protocolTreeDocumentStorageService.d.ts.map +1 -1
  154. package/lib/protocolTreeDocumentStorageService.js.map +1 -1
  155. package/lib/quorum.d.ts +1 -14
  156. package/lib/quorum.d.ts.map +1 -1
  157. package/lib/quorum.js +0 -26
  158. package/lib/quorum.js.map +1 -1
  159. package/lib/retriableDocumentStorageService.d.ts +1 -1
  160. package/lib/retriableDocumentStorageService.d.ts.map +1 -1
  161. package/lib/retriableDocumentStorageService.js +2 -2
  162. package/lib/retriableDocumentStorageService.js.map +1 -1
  163. package/lib/utils.d.ts +8 -1
  164. package/lib/utils.d.ts.map +1 -1
  165. package/lib/utils.js +25 -7
  166. package/lib/utils.js.map +1 -1
  167. package/package.json +26 -28
  168. package/src/audience.ts +7 -1
  169. package/src/catchUpMonitor.ts +2 -2
  170. package/src/connectionManager.ts +76 -52
  171. package/src/connectionStateHandler.ts +46 -48
  172. package/src/container.ts +561 -326
  173. package/src/containerContext.ts +31 -349
  174. package/src/containerStorageAdapter.ts +49 -6
  175. package/src/contracts.ts +27 -13
  176. package/src/debugLogger.ts +113 -0
  177. package/src/deltaManager.ts +93 -36
  178. package/src/deltaQueue.ts +2 -1
  179. package/src/disposal.ts +25 -0
  180. package/src/error.ts +44 -0
  181. package/src/loader.ts +84 -36
  182. package/src/{collabWindowTracker.ts → noopHeuristic.ts} +38 -47
  183. package/src/packageVersion.ts +1 -1
  184. package/src/protocol.ts +26 -16
  185. package/src/protocolTreeDocumentStorageService.ts +1 -1
  186. package/src/quorum.ts +1 -40
  187. package/src/retriableDocumentStorageService.ts +3 -4
  188. package/src/utils.ts +33 -8
  189. package/dist/collabWindowTracker.d.ts +0 -19
  190. package/dist/collabWindowTracker.d.ts.map +0 -1
  191. package/dist/collabWindowTracker.js +0 -101
  192. package/dist/collabWindowTracker.js.map +0 -1
  193. package/dist/deltaManagerProxy.d.ts +0 -42
  194. package/dist/deltaManagerProxy.d.ts.map +0 -1
  195. package/dist/deltaManagerProxy.js +0 -79
  196. package/dist/deltaManagerProxy.js.map +0 -1
  197. package/lib/collabWindowTracker.d.ts +0 -19
  198. package/lib/collabWindowTracker.d.ts.map +0 -1
  199. package/lib/collabWindowTracker.js.map +0 -1
  200. package/lib/deltaManagerProxy.d.ts +0 -42
  201. package/lib/deltaManagerProxy.d.ts.map +0 -1
  202. package/lib/deltaManagerProxy.js +0 -74
  203. package/lib/deltaManagerProxy.js.map +0 -1
  204. package/src/deltaManagerProxy.ts +0 -109
package/src/loader.ts CHANGED
@@ -4,19 +4,18 @@
4
4
  */
5
5
 
6
6
  import { v4 as uuid } from "uuid";
7
- import { ITelemetryBaseLogger } from "@fluidframework/common-definitions";
8
7
  import {
9
8
  ITelemetryLoggerExt,
10
- ChildLogger,
11
- DebugLogger,
12
9
  IConfigProviderBase,
13
- loggerToMonitoringContext,
14
10
  mixinMonitoringContext,
15
11
  MonitoringContext,
16
12
  PerformanceEvent,
17
13
  sessionStorageConfigProvider,
14
+ createChildMonitoringContext,
15
+ UsageError,
18
16
  } from "@fluidframework/telemetry-utils";
19
17
  import {
18
+ ITelemetryBaseLogger,
20
19
  FluidObject,
21
20
  IFluidRouter,
22
21
  IRequest,
@@ -39,18 +38,15 @@ import {
39
38
  IResolvedUrl,
40
39
  IUrlResolver,
41
40
  } from "@fluidframework/driver-definitions";
42
- import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
41
+ import { IClientDetails } from "@fluidframework/protocol-definitions";
43
42
  import { Container, IPendingContainerState } from "./container";
44
43
  import { IParsedUrl, parseUrl } from "./utils";
45
44
  import { pkgVersion } from "./packageVersion";
46
45
  import { ProtocolHandlerBuilder } from "./protocol";
46
+ import { DebugLogger } from "./debugLogger";
47
47
 
48
48
  function canUseCache(request: IRequest): boolean {
49
- if (request.headers === undefined) {
50
- return true;
51
- }
52
-
53
- return request.headers[LoaderHeader.cache] !== false;
49
+ return request.headers?.[LoaderHeader.cache] === true;
54
50
  }
55
51
 
56
52
  function ensureResolvedUrlDefined(
@@ -69,6 +65,9 @@ export class RelativeLoader implements ILoader {
69
65
  private readonly loader: ILoader | undefined,
70
66
  ) {}
71
67
 
68
+ /**
69
+ * @deprecated - Will be removed in future major release. Migrate all usage of IFluidRouter to the Container's IFluidRouter/request.
70
+ */
72
71
  public get IFluidRouter(): IFluidRouter {
73
72
  return this;
74
73
  }
@@ -100,6 +99,9 @@ export class RelativeLoader implements ILoader {
100
99
  return this.loader.resolve(request);
101
100
  }
102
101
 
102
+ /**
103
+ * @deprecated - Will be removed in future major release. Migrate all usage of IFluidRouter to the Container's IFluidRouter/request.
104
+ */
103
105
  public async request(request: IRequest): Promise<IResponse> {
104
106
  if (request.url.startsWith("/")) {
105
107
  const container = await this.resolve(request);
@@ -149,7 +151,7 @@ export interface ICodeDetailsLoader extends Partial<IProvideFluidCodeDetailsComp
149
151
  * Load the code module (package) that is capable to interact with the document.
150
152
  *
151
153
  * @param source - Code proposal that articulates the current schema the document is written in.
152
- * @returns - Code module entry point along with the code details associated with it.
154
+ * @returns Code module entry point along with the code details associated with it.
153
155
  */
154
156
  load(source: IFluidCodeDetails): Promise<IFluidModuleWithDetails>;
155
157
  }
@@ -343,15 +345,33 @@ export class Loader implements IHostLoader {
343
345
  protocolHandlerBuilder,
344
346
  subLogger: subMc.logger,
345
347
  };
346
- this.mc = loggerToMonitoringContext(ChildLogger.create(this.services.subLogger, "Loader"));
348
+ this.mc = createChildMonitoringContext({
349
+ logger: this.services.subLogger,
350
+ namespace: "Loader",
351
+ });
347
352
  }
348
353
 
354
+ /**
355
+ * @deprecated - Will be removed in future major release. Migrate all usage of IFluidRouter to the Container's IFluidRouter/request.
356
+ */
349
357
  public get IFluidRouter(): IFluidRouter {
350
358
  return this;
351
359
  }
352
360
 
353
- public async createDetachedContainer(codeDetails: IFluidCodeDetails): Promise<IContainer> {
354
- const container = await Container.createDetached(this.services, codeDetails);
361
+ public async createDetachedContainer(
362
+ codeDetails: IFluidCodeDetails,
363
+ createDetachedProps?: {
364
+ canReconnect?: boolean;
365
+ clientDetailsOverride?: IClientDetails;
366
+ },
367
+ ): Promise<IContainer> {
368
+ const container = await Container.createDetached(
369
+ {
370
+ ...createDetachedProps,
371
+ ...this.services,
372
+ },
373
+ codeDetails,
374
+ );
355
375
 
356
376
  if (this.cachingEnabled) {
357
377
  container.once("attached", () => {
@@ -366,8 +386,20 @@ export class Loader implements IHostLoader {
366
386
  return container;
367
387
  }
368
388
 
369
- public async rehydrateDetachedContainerFromSnapshot(snapshot: string): Promise<IContainer> {
370
- return Container.rehydrateDetachedFromSnapshot(this.services, snapshot);
389
+ public async rehydrateDetachedContainerFromSnapshot(
390
+ snapshot: string,
391
+ createDetachedProps?: {
392
+ canReconnect?: boolean;
393
+ clientDetailsOverride?: IClientDetails;
394
+ },
395
+ ): Promise<IContainer> {
396
+ return Container.rehydrateDetachedFromSnapshot(
397
+ {
398
+ ...createDetachedProps,
399
+ ...this.services,
400
+ },
401
+ snapshot,
402
+ );
371
403
  }
372
404
 
373
405
  public async resolve(request: IRequest, pendingLocalState?: string): Promise<IContainer> {
@@ -381,6 +413,9 @@ export class Loader implements IHostLoader {
381
413
  });
382
414
  }
383
415
 
416
+ /**
417
+ * @deprecated - Will be removed in future major release. Migrate all usage of IFluidRouter to the Container's IFluidRouter/request.
418
+ */
384
419
  public async request(request: IRequest): Promise<IResponse> {
385
420
  return PerformanceEvent.timedExecAsync(
386
421
  this.mc.logger,
@@ -407,16 +442,23 @@ export class Loader implements IHostLoader {
407
442
  this.containers.set(key, containerP);
408
443
  containerP
409
444
  .then((container) => {
410
- // If the container is closed or becomes closed after we resolve it, remove it from the cache.
411
- if (container.closed) {
445
+ // If the container is closed/disposed or becomes closed/disposed after we resolve it,
446
+ // remove it from the cache.
447
+ if (container.closed || container.disposed) {
412
448
  this.containers.delete(key);
413
449
  } else {
414
450
  container.once("closed", () => {
415
451
  this.containers.delete(key);
416
452
  });
453
+ container.once("disposed", () => {
454
+ this.containers.delete(key);
455
+ });
417
456
  }
418
457
  })
419
- .catch((error) => {});
458
+ .catch((error) => {
459
+ // If an error occured while resolving the container request, then remove it from the cache.
460
+ this.containers.delete(key);
461
+ });
420
462
  }
421
463
 
422
464
  private async resolveCore(
@@ -447,11 +489,29 @@ export class Loader implements IHostLoader {
447
489
  // If set in both query string and headers, use query string. Also write the value from the query string into the header either way.
448
490
  request.headers[LoaderHeader.version] =
449
491
  parsed.version ?? request.headers[LoaderHeader.version];
492
+ const cacheHeader = request.headers[LoaderHeader.cache];
450
493
  const canCache =
451
- this.cachingEnabled &&
452
- request.headers[LoaderHeader.cache] !== false &&
494
+ // Take header value if present, else use ILoaderOptions.cache value
495
+ (cacheHeader !== undefined ? cacheHeader === true : this.cachingEnabled) &&
453
496
  pendingLocalState === undefined;
454
- const fromSequenceNumber = request.headers[LoaderHeader.sequenceNumber] ?? -1;
497
+ const fromSequenceNumber = request.headers[LoaderHeader.sequenceNumber] as
498
+ | number
499
+ | undefined;
500
+ const opsBeforeReturn = request.headers[LoaderHeader.loadMode]?.opsBeforeReturn as
501
+ | string
502
+ | undefined;
503
+
504
+ if (
505
+ opsBeforeReturn === "sequenceNumber" &&
506
+ (fromSequenceNumber === undefined || fromSequenceNumber < 0)
507
+ ) {
508
+ // If opsBeforeReturn is set to "sequenceNumber", then fromSequenceNumber should be set to a non-negative integer.
509
+ throw new UsageError("sequenceNumber must be set to a non-negative integer");
510
+ } else if (opsBeforeReturn !== "sequenceNumber" && fromSequenceNumber !== undefined) {
511
+ // If opsBeforeReturn is not set to "sequenceNumber", then fromSequenceNumber should be undefined (default value).
512
+ // In this case, we should throw an error since opsBeforeReturn is not explicitly set to "sequenceNumber".
513
+ throw new UsageError('opsBeforeReturn must be set to "sequenceNumber"');
514
+ }
455
515
 
456
516
  let container: Container;
457
517
  if (canCache) {
@@ -468,24 +528,11 @@ export class Loader implements IHostLoader {
468
528
  container = await this.loadContainer(request, resolvedAsFluid, pendingLocalState);
469
529
  }
470
530
 
471
- if (container.deltaManager.lastSequenceNumber <= fromSequenceNumber) {
472
- await new Promise<void>((resolve, reject) => {
473
- function opHandler(message: ISequencedDocumentMessage) {
474
- if (message.sequenceNumber > fromSequenceNumber) {
475
- resolve();
476
- container.removeListener("op", opHandler);
477
- }
478
- }
479
-
480
- container.on("op", opHandler);
481
- });
482
- }
483
-
484
531
  return { container, parsed };
485
532
  }
486
533
 
487
534
  private get cachingEnabled() {
488
- return this.services.options.cache !== false;
535
+ return this.services.options.cache === true;
489
536
  }
490
537
 
491
538
  private async loadContainer(
@@ -499,6 +546,7 @@ export class Loader implements IHostLoader {
499
546
  version: request.headers?.[LoaderHeader.version] ?? undefined,
500
547
  loadMode: request.headers?.[LoaderHeader.loadMode],
501
548
  pendingLocalState,
549
+ loadToSequenceNumber: request.headers?.[LoaderHeader.sequenceNumber],
502
550
  },
503
551
  {
504
552
  canReconnect: request.headers?.[LoaderHeader.reconnect],
@@ -3,13 +3,19 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { assert, Timer } from "@fluidframework/common-utils";
7
- import { ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
8
- import { isRuntimeMessage, MessageType2 } from "@fluidframework/driver-utils";
6
+ import { TypedEventEmitter } from "@fluid-internal/client-utils";
7
+ import { assert, Timer } from "@fluidframework/core-utils";
8
+ import { ISequencedDocumentMessage } from "@fluidframework/protocol-definitions";
9
+ import { isRuntimeMessage } from "@fluidframework/driver-utils";
10
+ import { IEvent } from "@fluidframework/core-interfaces";
9
11
 
10
12
  const defaultNoopTimeFrequency = 2000;
11
13
  const defaultNoopCountFrequency = 50;
12
14
 
15
+ export interface INoopSenderEvents extends IEvent {
16
+ (event: "wantsNoop", listener: () => void);
17
+ }
18
+
13
19
  // Here are key considerations when deciding conditions for when to send non-immediate noops:
14
20
  // 1. Sending them too often results in increase in file size and bandwidth, as well as catch up performance
15
21
  // 2. Sending too infrequently ensures that collab window is large, and as result Sequence DDS would have
@@ -29,21 +35,21 @@ const defaultNoopCountFrequency = 50;
29
35
  // server timeout of 2000ms should be reconsidered to be increased.
30
36
  // 2. If there are more than 50 ops received without sending any ops, send noop to keep collab window small.
31
37
  // Note that system ops (including noops themselves) are excluded, so it's 1 noop per 50 real ops.
32
- export class CollabWindowTracker {
33
- private opsCountSinceNoop = 0;
38
+ export class NoopHeuristic extends TypedEventEmitter<INoopSenderEvents> {
39
+ private opsProcessedSinceOpSent = 0;
34
40
  private readonly timer: Timer | undefined;
35
41
 
36
42
  constructor(
37
- private readonly submit: (type: MessageType) => void,
38
43
  NoopTimeFrequency: number = defaultNoopTimeFrequency,
39
44
  private readonly NoopCountFrequency: number = defaultNoopCountFrequency,
40
45
  ) {
46
+ super();
41
47
  if (NoopTimeFrequency !== Infinity) {
42
48
  this.timer = new Timer(NoopTimeFrequency, () => {
43
- // Can get here due to this.stopSequenceNumberUpdate() not resetting timer.
44
- // Also timer callback can fire even after timer cancellation if it was queued before cancellation.
45
- if (this.opsCountSinceNoop !== 0) {
46
- this.submitNoop(false /* immediate */);
49
+ // We allow the timer to expire even if an op is sent or we disconnect.
50
+ // This condition is to guard against trying to send a noop anyway in that case.
51
+ if (this.opsProcessedSinceOpSent !== 0) {
52
+ this.emit("wantsNoop");
47
53
  }
48
54
  });
49
55
  }
@@ -52,17 +58,7 @@ export class CollabWindowTracker {
52
58
  /**
53
59
  * Schedules as ack to the server to update the reference sequence number
54
60
  */
55
- public scheduleSequenceNumberUpdate(
56
- message: ISequencedDocumentMessage,
57
- immediateNoOp: boolean,
58
- ): void {
59
- // While processing a message, an immediate no-op can be requested.
60
- // i.e. to expedite approve or commit phase of quorum.
61
- if (immediateNoOp) {
62
- this.submitNoop(true /* immediate */);
63
- return;
64
- }
65
-
61
+ public notifyMessageProcessed(message: ISequencedDocumentMessage): void {
66
62
  // We don't acknowledge no-ops to avoid acknowledgement cycles (i.e. ack the MSN
67
63
  // update, which updates the MSN, then ack the update, etc...).
68
64
  // Intent here is for runtime (and DDSes) not to keep too much tracking state / memory
@@ -71,22 +67,27 @@ export class CollabWindowTracker {
71
67
  return;
72
68
  }
73
69
 
74
- this.opsCountSinceNoop++;
75
- if (this.opsCountSinceNoop === this.NoopCountFrequency) {
76
- // Ensure we only send noop after a batch of many ops is processed
77
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
78
- Promise.resolve().then(() => {
79
- if (this.opsCountSinceNoop >= this.NoopCountFrequency) {
80
- this.submitNoop(false /* immediate */);
81
- // reset count now that all ops are processed
82
- this.opsCountSinceNoop = 0;
70
+ this.opsProcessedSinceOpSent++;
71
+ if (this.opsProcessedSinceOpSent === this.NoopCountFrequency) {
72
+ // Wait to send a noop if we are still synchronously processing ops. This guards against two things:
73
+ // 1. If we're processing many ops, we may pass the frequency threshold many times. We only need to send one noop at the very end in this case.
74
+ // 2. We may send another (non-noop) op in response to processing those ops, e.g. an Accept op.
75
+ queueMicrotask(() => {
76
+ if (this.opsProcessedSinceOpSent >= this.NoopCountFrequency) {
77
+ this.emit("wantsNoop");
78
+ assert(
79
+ this.opsProcessedSinceOpSent === 0,
80
+ 0x243 /* "Expected a noop to be synchronously sent" */,
81
+ );
83
82
  }
84
83
  return;
85
84
  });
86
85
  }
87
86
 
88
87
  if (this.timer !== undefined) {
89
- if (this.opsCountSinceNoop === 1) {
88
+ // Start the timer if we newly have ops that want a noop.
89
+ // If the timer was already running (e.g. we surpassed the op count and sent a noop) this will reset it to its full duration.
90
+ if (this.opsProcessedSinceOpSent === 1) {
90
91
  this.timer.restart();
91
92
  }
92
93
 
@@ -94,23 +95,13 @@ export class CollabWindowTracker {
94
95
  }
95
96
  }
96
97
 
97
- private submitNoop(immediate: boolean) {
98
- // Anything other than null is immediate noop
99
- // ADO:1385: Remove cast and use MessageType once definition changes propagate
100
- this.submit(immediate ? (MessageType2.Accept as unknown as MessageType) : MessageType.NoOp);
101
- assert(
102
- this.opsCountSinceNoop === 0,
103
- 0x243 /* "stopSequenceNumberUpdate should be called as result of sending any op!" */,
104
- );
98
+ public notifyDisconnect(): void {
99
+ // No need to noop for any ops processed prior to disconnect - we are already removed from MSN calculation.
100
+ this.opsProcessedSinceOpSent = 0;
105
101
  }
106
102
 
107
- public stopSequenceNumberUpdate(): void {
108
- this.opsCountSinceNoop = 0;
109
- // Ideally, we cancel timer here. But that will result in too often set/reset cycle if this client
110
- // keeps sending ops. In most cases it's actually better to let it expire (at most - 4 times per second)
111
- // for nothing, then have a ton of set/reset cycles.
112
- // Note that Timer.restart() is smart and will not change timer expiration if we keep extending timer
113
- // expiration - it will restart the timer instead when it fires with adjusted expiration.
114
- // this.timer.clear();
103
+ public notifyMessageSent(): void {
104
+ // Sending any message is as good as a noop for updating MSN.
105
+ this.opsProcessedSinceOpSent = 0;
115
106
  }
116
107
  }
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-loader";
9
- export const pkgVersion = "2.0.0-dev.5.2.0.169897";
9
+ export const pkgVersion = "2.0.0-dev.6.4.0.191258";
package/src/protocol.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { IAudienceOwner } from "@fluidframework/container-definitions";
7
+ import { canBeCoalescedByService } from "@fluidframework/driver-utils";
7
8
  import {
8
9
  IProtocolHandler as IBaseProtocolHandler,
9
10
  IQuorumSnapshot,
@@ -18,7 +19,6 @@ import {
18
19
  ISignalMessage,
19
20
  MessageType,
20
21
  } from "@fluidframework/protocol-definitions";
21
- import { canBeCoalescedByService } from "@fluidframework/driver-utils";
22
22
 
23
23
  // "term" was an experimental feature that is being removed. The only safe value to use is 1.
24
24
  export const OnlyValidTermValue = 1 as const;
@@ -30,17 +30,6 @@ export enum SignalType {
30
30
  Clear = "clear", // used only by client for synthetic signals
31
31
  }
32
32
 
33
- /**
34
- * ADO: #4277: ConnectionStateHandler can mutate Quorum members, but shouldn't
35
- * This interface might go away after the above ADO item is done
36
- */
37
- export interface ILocalSequencedClient extends ISequencedClient {
38
- /**
39
- * True if the client should have left the quorum, false otherwise
40
- */
41
- shouldHaveLeft?: boolean;
42
- }
43
-
44
33
  /**
45
34
  * Function to be used for creating a protocol handler.
46
35
  */
@@ -60,12 +49,12 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
60
49
  attributes: IDocumentAttributes,
61
50
  quorumSnapshot: IQuorumSnapshot,
62
51
  sendProposal: (key: string, value: any) => number,
63
- readonly audience: IAudienceOwner,
52
+ public readonly audience: IAudienceOwner,
53
+ private readonly shouldClientHaveLeft: (clientId: string) => boolean,
64
54
  ) {
65
55
  super(
66
56
  attributes.minimumSequenceNumber,
67
57
  attributes.sequenceNumber,
68
- OnlyValidTermValue,
69
58
  quorumSnapshot.members,
70
59
  quorumSnapshot.proposals,
71
60
  quorumSnapshot.values,
@@ -86,7 +75,7 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
86
75
  message: ISequencedDocumentMessage,
87
76
  local: boolean,
88
77
  ): IProcessMessageResult {
89
- const client: ILocalSequencedClient | undefined = this.quorum.getMember(message.clientId);
78
+ const client: ISequencedClient | undefined = this.quorum.getMember(message.clientId);
90
79
 
91
80
  // Check and report if we're getting messages from a clientId that we previously
92
81
  // flagged as shouldHaveLeft, or from a client that's not in the quorum but should be
@@ -96,7 +85,10 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
96
85
  throw new Error("Remote message's clientId is missing from the quorum");
97
86
  }
98
87
 
99
- if (client?.shouldHaveLeft === true && !canBeCoalescedByService(message)) {
88
+ // Here checking canBeCoalescedByService is used as an approximation of "is benign to process despite being unexpected".
89
+ // It's still not good to see these messages from unexpected clientIds, but since they don't harm the integrity of the
90
+ // document we don't need to blow up aggressively.
91
+ if (this.shouldClientHaveLeft(message.clientId) && !canBeCoalescedByService(message)) {
100
92
  // pre-0.58 error message: messageClientIdShouldHaveLeft
101
93
  throw new Error("Remote message's clientId already should have left");
102
94
  }
@@ -138,3 +130,21 @@ export class ProtocolHandler extends ProtocolOpHandler implements IProtocolHandl
138
130
  }
139
131
  }
140
132
  }
133
+
134
+ /**
135
+ * Function to check whether the protocol handler should process the Signal.
136
+ * The protocol handler should strictly handle only ClientJoin, ClientLeave
137
+ * and Clear signal types.
138
+ */
139
+ export function protocolHandlerShouldProcessSignal(message: ISignalMessage) {
140
+ // Signal originates from server
141
+ if (message.clientId === null) {
142
+ const innerContent = message.content as { content: unknown; type: string };
143
+ return (
144
+ innerContent.type === SignalType.Clear ||
145
+ innerContent.type === SignalType.ClientJoin ||
146
+ innerContent.type === SignalType.ClientLeave
147
+ );
148
+ }
149
+ return false;
150
+ }
@@ -3,7 +3,7 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { IDisposable } from "@fluidframework/common-definitions";
6
+ import { IDisposable } from "@fluidframework/core-interfaces";
7
7
  import { IDocumentStorageService, ISummaryContext } from "@fluidframework/driver-definitions";
8
8
  import { ISummaryTree } from "@fluidframework/protocol-definitions";
9
9
 
package/src/quorum.ts CHANGED
@@ -2,47 +2,8 @@
2
2
  * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
3
  * Licensed under the MIT License.
4
4
  */
5
- import { assert, EventForwarder, doIfNotDisposed } from "@fluidframework/common-utils";
6
5
  import { IFluidCodeDetails } from "@fluidframework/core-interfaces";
7
- import {
8
- ICommittedProposal,
9
- IQuorum,
10
- IQuorumEvents,
11
- ISequencedClient,
12
- } from "@fluidframework/protocol-definitions";
13
-
14
- /**
15
- * Proxies Quorum events.
16
- */
17
- export class QuorumProxy extends EventForwarder<IQuorumEvents> implements IQuorum {
18
- public readonly propose: (key: string, value: any) => Promise<void>;
19
- public readonly has: (key: string) => boolean;
20
- public readonly get: (key: string) => any;
21
- public readonly getMembers: () => Map<string, ISequencedClient>;
22
- public readonly getMember: (clientId: string) => ISequencedClient | undefined;
23
-
24
- constructor(quorum: IQuorum) {
25
- super(quorum);
26
-
27
- // This is heavily used object, increase limit at which Node prints warnings.
28
- super.setMaxListeners(50);
29
-
30
- this.propose = doIfNotDisposed(this, quorum.propose.bind(quorum));
31
- this.has = doIfNotDisposed(this, quorum.has.bind(quorum));
32
- this.get = doIfNotDisposed(this, quorum.get.bind(quorum));
33
- this.getMembers = doIfNotDisposed(this, quorum.getMembers.bind(quorum));
34
- this.getMember = doIfNotDisposed(this, quorum.getMember.bind(quorum));
35
- }
36
- }
37
-
38
- export function getCodeDetailsFromQuorumValues(
39
- quorumValues: [string, ICommittedProposal][],
40
- ): IFluidCodeDetails {
41
- const qValuesMap = new Map(quorumValues);
42
- const proposal = qValuesMap.get("code");
43
- assert(proposal !== undefined, 0x2dc /* "Cannot find code proposal" */);
44
- return proposal?.value as IFluidCodeDetails;
45
- }
6
+ import { ICommittedProposal } from "@fluidframework/protocol-definitions";
46
7
 
47
8
  export function initQuorumValuesFromCodeDetails(
48
9
  source: IFluidCodeDetails,
@@ -3,8 +3,7 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { assert } from "@fluidframework/common-utils";
7
- import { GenericError } from "@fluidframework/container-utils";
6
+ import { assert } from "@fluidframework/core-utils";
8
7
  import {
9
8
  FetchSource,
10
9
  IDocumentStorageService,
@@ -18,8 +17,8 @@ import {
18
17
  ISummaryTree,
19
18
  IVersion,
20
19
  } from "@fluidframework/protocol-definitions";
21
- import { IDisposable } from "@fluidframework/common-definitions";
22
- import { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils";
20
+ import { IDisposable } from "@fluidframework/core-interfaces";
21
+ import { GenericError, ITelemetryLoggerExt } from "@fluidframework/telemetry-utils";
23
22
  import { runWithRetry } from "@fluidframework/driver-utils";
24
23
 
25
24
  export class RetriableDocumentStorageService implements IDocumentStorageService, IDisposable {
package/src/utils.ts CHANGED
@@ -5,19 +5,16 @@
5
5
 
6
6
  import { parse } from "url";
7
7
  import { v4 as uuid } from "uuid";
8
- import {
9
- assert,
10
- stringToBuffer,
11
- Uint8ArrayToArrayBuffer,
12
- unreachableCase,
13
- } from "@fluidframework/common-utils";
8
+ import { stringToBuffer, Uint8ArrayToArrayBuffer } from "@fluid-internal/client-utils";
9
+ import { assert, unreachableCase } from "@fluidframework/core-utils";
14
10
  import { ISummaryTree, ISnapshotTree, SummaryType } from "@fluidframework/protocol-definitions";
15
11
  import { LoggingError } from "@fluidframework/telemetry-utils";
16
12
  import {
13
+ CombinedAppAndProtocolSummary,
17
14
  DeltaStreamConnectionForbiddenError,
18
15
  isCombinedAppAndProtocolSummary,
19
16
  } from "@fluidframework/driver-utils";
20
- import { DriverErrorType } from "@fluidframework/driver-definitions";
17
+ import { DriverErrorTypes } from "@fluidframework/driver-definitions";
21
18
 
22
19
  // This is used when we rehydrate a container from the snapshot. Here we put the blob contents
23
20
  // in separate property: blobContents.
@@ -51,6 +48,34 @@ export function parseUrl(url: string): IParsedUrl | undefined {
51
48
  : undefined;
52
49
  }
53
50
 
51
+ /**
52
+ * Combine the app summary and protocol summary in 1 tree.
53
+ * @param appSummary - Summary of the app.
54
+ * @param protocolSummary - Summary of the protocol.
55
+ * @internal
56
+ */
57
+ export function combineAppAndProtocolSummary(
58
+ appSummary: ISummaryTree,
59
+ protocolSummary: ISummaryTree,
60
+ ): CombinedAppAndProtocolSummary {
61
+ assert(
62
+ !isCombinedAppAndProtocolSummary(appSummary),
63
+ 0x5a8 /* app summary is already a combined tree! */,
64
+ );
65
+ assert(
66
+ !isCombinedAppAndProtocolSummary(protocolSummary),
67
+ 0x5a9 /* protocol summary is already a combined tree! */,
68
+ );
69
+ const createNewSummary: CombinedAppAndProtocolSummary = {
70
+ type: SummaryType.Tree,
71
+ tree: {
72
+ ".protocol": protocolSummary,
73
+ ".app": appSummary,
74
+ },
75
+ };
76
+ return createNewSummary;
77
+ }
78
+
54
79
  /**
55
80
  * Converts summary tree (for upload) to snapshot tree (for download).
56
81
  * Summary tree blobs contain contents, but snapshot tree blobs normally
@@ -156,6 +181,6 @@ export function isDeltaStreamConnectionForbiddenError(
156
181
  return (
157
182
  typeof error === "object" &&
158
183
  error !== null &&
159
- error?.errorType === DriverErrorType.deltaStreamConnectionForbidden
184
+ error?.errorType === DriverErrorTypes.deltaStreamConnectionForbidden
160
185
  );
161
186
  }
@@ -1,19 +0,0 @@
1
- /*!
2
- * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
- * Licensed under the MIT License.
4
- */
5
- import { ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
6
- export declare class CollabWindowTracker {
7
- private readonly submit;
8
- private readonly NoopCountFrequency;
9
- private opsCountSinceNoop;
10
- private readonly timer;
11
- constructor(submit: (type: MessageType) => void, NoopTimeFrequency?: number, NoopCountFrequency?: number);
12
- /**
13
- * Schedules as ack to the server to update the reference sequence number
14
- */
15
- scheduleSequenceNumberUpdate(message: ISequencedDocumentMessage, immediateNoOp: boolean): void;
16
- private submitNoop;
17
- stopSequenceNumberUpdate(): void;
18
- }
19
- //# sourceMappingURL=collabWindowTracker.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"collabWindowTracker.d.ts","sourceRoot":"","sources":["../src/collabWindowTracker.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,yBAAyB,EAAE,WAAW,EAAE,MAAM,sCAAsC,CAAC;AAyB9F,qBAAa,mBAAmB;IAK9B,OAAO,CAAC,QAAQ,CAAC,MAAM;IAEvB,OAAO,CAAC,QAAQ,CAAC,kBAAkB;IANpC,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAoB;gBAGxB,MAAM,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,EACpD,iBAAiB,GAAE,MAAiC,EACnC,kBAAkB,GAAE,MAAkC;IAaxE;;OAEG;IACI,4BAA4B,CAClC,OAAO,EAAE,yBAAyB,EAClC,aAAa,EAAE,OAAO,GACpB,IAAI;IAuCP,OAAO,CAAC,UAAU;IAUX,wBAAwB,IAAI,IAAI;CASvC"}