@fluidframework/container-loader 0.54.1 → 0.55.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/container.ts CHANGED
@@ -8,7 +8,6 @@ import merge from "lodash/merge";
8
8
  import { v4 as uuid } from "uuid";
9
9
  import {
10
10
  IDisposable,
11
- ITelemetryLogger,
12
11
  } from "@fluidframework/common-definitions";
13
12
  import { assert, performance, unreachableCase } from "@fluidframework/common-utils";
14
13
  import {
@@ -93,6 +92,8 @@ import {
93
92
  connectedEventName,
94
93
  disconnectedEventName,
95
94
  normalizeError,
95
+ MonitoringContext,
96
+ loggerToMonitoringContext,
96
97
  } from "@fluidframework/telemetry-utils";
97
98
  import { Audience } from "./audience";
98
99
  import { ContainerContext } from "./containerContext";
@@ -248,9 +249,9 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
248
249
  });
249
250
 
250
251
  return PerformanceEvent.timedExecAsync(
251
- container.logger,
252
+ container.mc.logger,
252
253
  { eventName: "Load" },
253
- async (event) => new Promise<Container>((res, rej) => {
254
+ async (event) => new Promise<Container>((resolve, reject) => {
254
255
  container._lifecycleState = "loading";
255
256
  const version = loadOptions.version;
256
257
 
@@ -262,7 +263,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
262
263
  const mode: IContainerLoadMode = loadOptions.loadMode ?? defaultMode;
263
264
 
264
265
  const onClosed = (err?: ICriticalContainerError) => {
265
- rej(err ?? new GenericError("containerClosedWithoutErrorDuringLoad"));
266
+ reject(err ?? new GenericError("containerClosedWithoutErrorDuringLoad"));
266
267
  };
267
268
  container.on("closed", onClosed);
268
269
 
@@ -272,7 +273,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
272
273
  })
273
274
  .then((props) => {
274
275
  event.end({ ...props, ...loadOptions.loadMode });
275
- res(container);
276
+ resolve(container);
276
277
  },
277
278
  (error) => {
278
279
  const err = normalizeError(error);
@@ -299,7 +300,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
299
300
  {});
300
301
 
301
302
  return PerformanceEvent.timedExecAsync(
302
- container.logger,
303
+ container.mc.logger,
303
304
  { eventName: "CreateDetached" },
304
305
  async (_event) => {
305
306
  container._lifecycleState = "loading";
@@ -321,7 +322,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
321
322
  loader,
322
323
  {});
323
324
  return PerformanceEvent.timedExecAsync(
324
- container.logger,
325
+ container.mc.logger,
325
326
  { eventName: "RehydrateDetachedFromSnapshot" },
326
327
  async (_event) => {
327
328
  const deserializedSummary = JSON.parse(snapshot) as ISummaryTree;
@@ -338,7 +339,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
338
339
  // If false, container gets closed on loss of connection.
339
340
  private readonly _canReconnect: boolean = true;
340
341
 
341
- private readonly logger: ITelemetryLogger;
342
+ private readonly mc: MonitoringContext;
342
343
 
343
344
  private _lifecycleState: "created" | "loading" | "loaded" | "closing" | "closed" = "created";
344
345
 
@@ -368,7 +369,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
368
369
  return this._storage;
369
370
  }
370
371
 
371
- // Active chaincode and associated runtime
372
372
  private _storageService: IDocumentStorageService & IDisposable | undefined;
373
373
  private get storageService(): IDocumentStorageService {
374
374
  if (this._storageService === undefined) {
@@ -408,6 +408,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
408
408
  private _dirtyContainer = false;
409
409
 
410
410
  private lastVisible: number | undefined;
411
+ private readonly visibilityEventHandler: (() => void) | undefined;
411
412
  private readonly connectionStateHandler: ConnectionStateHandler;
412
413
 
413
414
  private setAutoReconnectTime = performance.now();
@@ -531,7 +532,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
531
532
 
532
533
  private get serviceFactory() {return this.loader.services.documentServiceFactory;}
533
534
  private get urlResolver() {return this.loader.services.urlResolver;}
534
- public get options(): ILoaderOptions { return this.loader.services.options; }
535
+ public readonly options: ILoaderOptions;
535
536
  private get scope() { return this.loader.services.scope;}
536
537
  private get codeLoader() { return this.loader.services.codeLoader;}
537
538
 
@@ -540,7 +541,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
540
541
  config: IContainerConfig,
541
542
  ) {
542
543
  super((name, error) => {
543
- this.logger.sendErrorEvent(
544
+ this.mc.logger.sendErrorEvent(
544
545
  {
545
546
  eventName: "ContainerEventHandlerException",
546
547
  name: typeof name === "string" ? name : undefined,
@@ -574,7 +575,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
574
575
  containerLifecycleState: () => this._lifecycleState,
575
576
  containerConnectionState: () => ConnectionState[this.connectionState],
576
577
  },
577
- // we need to be judicious with our logging here to avoid generting too much data
578
+ // we need to be judicious with our logging here to avoid generating too much data
578
579
  // all data logged here should be broadly applicable, and not specific to a
579
580
  // specific error or class of errors
580
581
  error: {
@@ -587,14 +588,22 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
587
588
  dmLastMsqSeqNumber: () => this.deltaManager?.lastMessage?.sequenceNumber,
588
589
  dmLastMsqSeqTimestamp: () => this.deltaManager?.lastMessage?.timestamp,
589
590
  dmLastMsqSeqClientId: () => this.deltaManager?.lastMessage?.clientId,
590
- connectionState: () => ConnectionState[this.connectionState],
591
591
  connectionStateDuration:
592
592
  () => performance.now() - this.connectionTransitionTimes[this.connectionState],
593
593
  },
594
594
  });
595
595
 
596
596
  // Prefix all events in this file with container-loader
597
- this.logger = ChildLogger.create(this.subLogger, "Container");
597
+ this.mc = loggerToMonitoringContext(ChildLogger.create(this.subLogger, "Container"));
598
+
599
+ const summarizeProtocolTree =
600
+ this.mc.config.getBoolean("Fluid.Container.summarizeProtocolTree")
601
+ ?? this.loader.services.options.summarizeProtocolTree;
602
+
603
+ this.options = {
604
+ ... this.loader.services.options,
605
+ summarizeProtocolTree,
606
+ };
598
607
 
599
608
  this.connectionStateHandler = new ConnectionStateHandler(
600
609
  {
@@ -617,7 +626,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
617
626
  }
618
627
  },
619
628
  },
620
- this.logger,
629
+ this.mc.logger,
621
630
  );
622
631
 
623
632
  this._deltaManager = this.createDeltaManager();
@@ -625,9 +634,9 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
625
634
  () => {
626
635
  if (this.attachState !== AttachState.Attached) {
627
636
  if (this.loader.services.detachedBlobStorage !== undefined) {
628
- return new BlobOnlyStorage(this.loader.services.detachedBlobStorage, this.logger);
637
+ return new BlobOnlyStorage(this.loader.services.detachedBlobStorage, this.mc.logger);
629
638
  }
630
- this.logger.sendErrorEvent({
639
+ this.mc.logger.sendErrorEvent({
631
640
  eventName: "NoRealStorageInDetachedContainer",
632
641
  });
633
642
  throw new Error("Real storage calls not allowed in Unattached container");
@@ -643,14 +652,15 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
643
652
  // keep track of last time page was visible for telemetry
644
653
  if (isDomAvailable) {
645
654
  this.lastVisible = document.hidden ? performance.now() : undefined;
646
- document.addEventListener("visibilitychange", () => {
655
+ this.visibilityEventHandler = () => {
647
656
  if (document.hidden) {
648
657
  this.lastVisible = performance.now();
649
658
  } else {
650
659
  // settimeout so this will hopefully fire after disconnect event if being hidden caused it
651
660
  setTimeout(() => this.lastVisible = undefined, 0);
652
661
  }
653
- });
662
+ };
663
+ document.addEventListener("visibilitychange", this.visibilityEventHandler);
654
664
  }
655
665
 
656
666
  // We observed that most users of platform do not check Container.connected event on load, causing bugs.
@@ -684,7 +694,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
684
694
  default:
685
695
  }
686
696
  }).catch((error) => {
687
- this.logger.sendErrorEvent({ eventName: "RaiseConnectedEventError" }, error);
697
+ this.mc.logger.sendErrorEvent({ eventName: "RaiseConnectedEventError" }, error);
688
698
  });
689
699
  });
690
700
  }
@@ -724,10 +734,10 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
724
734
  // Driver need to ensure all caches are cleared on critical errors
725
735
  this.service?.dispose(error);
726
736
  } catch (exception) {
727
- this.logger.sendErrorEvent({ eventName: "ContainerCloseException"}, exception);
737
+ this.mc.logger.sendErrorEvent({ eventName: "ContainerCloseException"}, exception);
728
738
  }
729
739
 
730
- this.logger.sendTelemetryEvent(
740
+ this.mc.logger.sendTelemetryEvent(
731
741
  {
732
742
  eventName: "ContainerClose",
733
743
  category: error === undefined ? "generic" : "error",
@@ -738,6 +748,9 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
738
748
  this.emit("closed", error);
739
749
 
740
750
  this.removeAllListeners();
751
+ if (this.visibilityEventHandler !== undefined) {
752
+ document.removeEventListener("visibilitychange", this.visibilityEventHandler);
753
+ }
741
754
  } finally {
742
755
  this._lifecycleState = "closed";
743
756
  }
@@ -779,7 +792,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
779
792
  }
780
793
 
781
794
  public async attach(request: IRequest): Promise<void> {
782
- await PerformanceEvent.timedExecAsync(this.logger, { eventName: "Attach" }, async () => {
795
+ await PerformanceEvent.timedExecAsync(this.mc.logger, { eventName: "Attach" }, async () => {
783
796
  if (this._lifecycleState !== "loaded") {
784
797
  throw new UsageError(`containerNotValidForAttach [${this._lifecycleState}]`);
785
798
  }
@@ -824,7 +837,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
824
837
  this.subLogger,
825
838
  ),
826
839
  "containerAttach",
827
- this.logger,
840
+ this.mc.logger,
828
841
  {}, // progress
829
842
  );
830
843
  }
@@ -890,7 +903,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
890
903
 
891
904
  public async request(path: IRequest): Promise<IResponse> {
892
905
  return PerformanceEvent.timedExecAsync(
893
- this.logger,
906
+ this.mc.logger,
894
907
  { eventName: "Request" },
895
908
  async () => this.context.request(path),
896
909
  { end: true, cancel: "error" },
@@ -900,7 +913,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
900
913
  public async snapshot(tagMessage: string, fullTree: boolean = false): Promise<void> {
901
914
  // Only snapshot once a code quorum has been established
902
915
  if (!this.protocolHandler.quorum.has("code") && !this.protocolHandler.quorum.has("code2")) {
903
- this.logger.sendTelemetryEvent({ eventName: "SkipSnapshot" });
916
+ this.mc.logger.sendTelemetryEvent({ eventName: "SkipSnapshot" });
904
917
  return;
905
918
  }
906
919
 
@@ -909,7 +922,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
909
922
  await this.deltaManager.inbound.pause();
910
923
  await this.snapshotCore(tagMessage, fullTree);
911
924
  } catch (ex) {
912
- this.logger.sendErrorEvent({ eventName: "SnapshotExceptionError" }, ex);
925
+ this.mc.logger.sendErrorEvent({ eventName: "SnapshotExceptionError" }, ex);
913
926
  throw ex;
914
927
  } finally {
915
928
  this.deltaManager.inbound.resume();
@@ -931,7 +944,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
931
944
  const duration = now - this.setAutoReconnectTime;
932
945
  this.setAutoReconnectTime = now;
933
946
 
934
- this.logger.sendTelemetryEvent({
947
+ this.mc.logger.sendTelemetryEvent({
935
948
  eventName: reconnect ? "AutoReconnectEnabled" : "AutoReconnectDisabled",
936
949
  connectionMode: this.connectionMode,
937
950
  connectionState: ConnectionState[this.connectionState],
@@ -1007,15 +1020,15 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1007
1020
  }
1008
1021
 
1009
1022
  if (this.codeLoader.IFluidCodeDetailsComparer) {
1010
- const comparision = await this.codeLoader.IFluidCodeDetailsComparer.compare(
1023
+ const comparison = await this.codeLoader.IFluidCodeDetailsComparer.compare(
1011
1024
  codeDetails,
1012
1025
  this.getCodeDetailsFromQuorum());
1013
- if (comparision !== undefined && comparision <= 0) {
1026
+ if (comparison !== undefined && comparison <= 0) {
1014
1027
  throw new Error("Proposed code details should be greater than the current");
1015
1028
  }
1016
1029
  }
1017
1030
 
1018
- return this.getQuorum().propose("code", codeDetails)
1031
+ return this.protocolHandler.quorum.propose("code", codeDetails)
1019
1032
  .then(()=>true)
1020
1033
  .catch(()=>false);
1021
1034
  }
@@ -1341,9 +1354,10 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1341
1354
  const storageService = await this.service.connectToStorage();
1342
1355
 
1343
1356
  this._storageService =
1344
- new RetriableDocumentStorageService(storageService, this.logger);
1357
+ new RetriableDocumentStorageService(storageService, this.mc.logger);
1345
1358
 
1346
1359
  if(this.options.summarizeProtocolTree === true) {
1360
+ this.mc.logger.sendTelemetryEvent({eventName:"summarizeProtocolTreeEnabled"});
1347
1361
  this._storageService =
1348
1362
  new ProtocolTreeStorageService(this._storageService, ()=>this.captureProtocolSummary());
1349
1363
  }
@@ -1449,7 +1463,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1449
1463
  (sequenceNumber, key, value) => {
1450
1464
  if (key === "code" || key === "code2") {
1451
1465
  if (!isFluidCodeDetails(value)) {
1452
- this.logger.sendErrorEvent({
1466
+ this.mc.logger.sendErrorEvent({
1453
1467
  eventName: "CodeProposalNotIFluidCodeDetails",
1454
1468
  });
1455
1469
  }
@@ -1646,7 +1660,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1646
1660
  }
1647
1661
  }
1648
1662
 
1649
- this.logger.sendPerformanceEvent({
1663
+ this.mc.logger.sendPerformanceEvent({
1650
1664
  eventName: `ConnectionStateChange_${ConnectionState[value]}`,
1651
1665
  from: ConnectionState[oldState],
1652
1666
  duration,
@@ -1684,10 +1698,10 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1684
1698
  }
1685
1699
  assert(this.protocolHandler !== undefined, 0x0dc /* "Protocol handler should be set here" */);
1686
1700
  this.protocolHandler.quorum.setConnectionState(state, this.clientId);
1687
- raiseConnectedEvent(this.logger, this, state, this.clientId);
1701
+ raiseConnectedEvent(this.mc.logger, this, state, this.clientId);
1688
1702
 
1689
1703
  if (logOpsOnReconnect) {
1690
- this.logger.sendTelemetryEvent(
1704
+ this.mc.logger.sendTelemetryEvent(
1691
1705
  { eventName: "OpsSentOnReconnect", count: this.messageCountAfterDisconnection });
1692
1706
  }
1693
1707
  }
@@ -1722,7 +1736,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1722
1736
 
1723
1737
  private submitMessage(type: MessageType, contents: any, batch?: boolean, metadata?: any): number {
1724
1738
  if (this.connectionState !== ConnectionState.Connected) {
1725
- this.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
1739
+ this.mc.logger.sendErrorEvent({ eventName: "SubmitMessageWithNoConnection", type });
1726
1740
  return -1;
1727
1741
  }
1728
1742
 
@@ -1800,13 +1814,13 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1800
1814
 
1801
1815
  if (version === undefined && specifiedVersion !== undefined) {
1802
1816
  // We should have a defined version to load from if specified version requested
1803
- this.logger.sendErrorEvent({ eventName: "NoVersionFoundWhenSpecified", id: specifiedVersion });
1817
+ this.mc.logger.sendErrorEvent({ eventName: "NoVersionFoundWhenSpecified", id: specifiedVersion });
1804
1818
  }
1805
1819
  this._loadedFromVersion = version;
1806
1820
  const snapshot = await this.storageService.getSnapshotTree(version) ?? undefined;
1807
1821
 
1808
1822
  if (snapshot === undefined && version !== undefined) {
1809
- this.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
1823
+ this.mc.logger.sendErrorEvent({ eventName: "getSnapshotTreeFailed", id: version.id });
1810
1824
  }
1811
1825
  return { snapshot, versionId: version?.id };
1812
1826
  }
@@ -1838,7 +1852,7 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1838
1852
  assert(this._context?.disposed !== false, 0x0dd /* "Existing context not disposed" */);
1839
1853
  // If this assert fires, our state tracking is likely not synchronized between COntainer & runtime.
1840
1854
  if (this._dirtyContainer) {
1841
- this.logger.sendErrorEvent({ eventName: "DirtyContainerReloadContainer" });
1855
+ this.mc.logger.sendErrorEvent({ eventName: "DirtyContainerReloadContainer" });
1842
1856
  }
1843
1857
 
1844
1858
  // The relative loader will proxy requests to '/' to the loader itself assuming no non-cache flags
@@ -1872,6 +1886,6 @@ export class Container extends EventEmitterWithErrorHandling<IContainerEvents> i
1872
1886
  // Please avoid calling it directly.
1873
1887
  // raiseContainerWarning() is the right flow for most cases
1874
1888
  private logContainerError(warning: ContainerWarning) {
1875
- this.logger.sendErrorEvent({ eventName: "ContainerWarning" }, warning);
1889
+ this.mc.logger.sendErrorEvent({ eventName: "ContainerWarning" }, warning);
1876
1890
  }
1877
1891
  }
@@ -132,6 +132,9 @@ export class ContainerContext implements IContainerContext {
132
132
  return this.container.options;
133
133
  }
134
134
 
135
+ /**
136
+ * @deprecated 0.55 - Configuration is not recommended to be used and will be removed in an upcoming release.
137
+ */
135
138
  public get configuration(): IFluidConfiguration {
136
139
  const config: Partial<IFluidConfiguration> = {
137
140
  scopes: this.container.scopes,
@@ -163,6 +166,10 @@ export class ContainerContext implements IContainerContext {
163
166
 
164
167
  public get codeDetails() { return this._codeDetails; }
165
168
 
169
+ private readonly _quorum: IQuorum;
170
+ // Update to return IQuorumClients after 0.45 container definitions are picked up.
171
+ public get quorum(): IQuorum { return this._quorum; }
172
+
166
173
  private readonly _fluidModuleP: Promise<IFluidModuleWithDetails>;
167
174
 
168
175
  constructor(
@@ -172,7 +179,7 @@ export class ContainerContext implements IContainerContext {
172
179
  private readonly _codeDetails: IFluidCodeDetails,
173
180
  private readonly _baseSnapshot: ISnapshotTree | undefined,
174
181
  public readonly deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
175
- public readonly quorum: IQuorum,
182
+ quorum: IQuorum,
176
183
  public readonly loader: ILoader,
177
184
  public readonly raiseContainerWarning: (warning: ContainerWarning) => void,
178
185
  public readonly submitFn: (type: MessageType, contents: any, batch: boolean, appData: any) => number,
@@ -184,6 +191,7 @@ export class ContainerContext implements IContainerContext {
184
191
  public readonly pendingLocalState?: unknown,
185
192
 
186
193
  ) {
194
+ this._quorum = quorum;
187
195
  this.taggedLogger = container.subLogger;
188
196
  this._fluidModuleP = new LazyPromise<IFluidModuleWithDetails>(
189
197
  async () => this.loadCodeModule(_codeDetails),
@@ -192,7 +200,7 @@ export class ContainerContext implements IContainerContext {
192
200
  }
193
201
 
194
202
  public getSpecifiedCodeDetails(): IFluidCodeDetails | undefined {
195
- return (this.quorum.get("code") ?? this.quorum.get("code2")) as IFluidCodeDetails | undefined;
203
+ return (this._quorum.get("code") ?? this._quorum.get("code2")) as IFluidCodeDetails | undefined;
196
204
  }
197
205
 
198
206
  public dispose(error?: Error): void {
@@ -202,7 +210,7 @@ export class ContainerContext implements IContainerContext {
202
210
  this._disposed = true;
203
211
 
204
212
  this.runtime.dispose(error);
205
- this.quorum.dispose();
213
+ this._quorum.dispose();
206
214
  this.deltaManager.dispose();
207
215
  }
208
216
 
package/src/loader.ts CHANGED
@@ -130,7 +130,7 @@ function createCachedResolver(resolver: IUrlResolver) {
130
130
  }
131
131
 
132
132
  export interface ILoaderOptions extends ILoaderOptions1{
133
- summarizeProtocolTree?: true,
133
+ summarizeProtocolTree?: boolean,
134
134
  }
135
135
 
136
136
  /**
@@ -179,7 +179,7 @@ export interface ILoaderProps {
179
179
  readonly urlResolver: IUrlResolver;
180
180
  /**
181
181
  * The document service factory take the Fluid url provided
182
- * by the resolved url and constucts all the necessary services
182
+ * by the resolved url and constructs all the necessary services
183
183
  * for communication with the container's server.
184
184
  */
185
185
  readonly documentServiceFactory: IDocumentServiceFactory;
@@ -235,7 +235,7 @@ export interface ILoaderServices {
235
235
  readonly urlResolver: IUrlResolver;
236
236
  /**
237
237
  * The document service factory take the Fluid url provided
238
- * by the resolved url and constucts all the necessary services
238
+ * by the resolved url and constructs all the necessary services
239
239
  * for communication with the container's server.
240
240
  */
241
241
  readonly documentServiceFactory: IDocumentServiceFactory;
@@ -6,4 +6,4 @@
6
6
  */
7
7
 
8
8
  export const pkgName = "@fluidframework/container-loader";
9
- export const pkgVersion = "0.54.1";
9
+ export const pkgVersion = "0.55.1";