@fluidframework/container-loader 2.0.0-dev-rc.3.0.0.254866 → 2.0.0-dev-rc.5.0.0.263932
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/CHANGELOG.md +50 -0
- package/api-report/container-loader.api.md +7 -3
- package/dist/attachment.d.ts +3 -2
- package/dist/attachment.d.ts.map +1 -1
- package/dist/attachment.js +5 -5
- package/dist/attachment.js.map +1 -1
- package/dist/audience.d.ts +6 -4
- package/dist/audience.d.ts.map +1 -1
- package/dist/audience.js +18 -3
- package/dist/audience.js.map +1 -1
- package/dist/catchUpMonitor.d.ts +1 -1
- package/dist/catchUpMonitor.d.ts.map +1 -1
- package/dist/catchUpMonitor.js.map +1 -1
- package/dist/connectionManager.d.ts +7 -3
- package/dist/connectionManager.d.ts.map +1 -1
- package/dist/connectionManager.js +63 -39
- package/dist/connectionManager.js.map +1 -1
- package/dist/connectionStateHandler.d.ts +31 -10
- package/dist/connectionStateHandler.d.ts.map +1 -1
- package/dist/connectionStateHandler.js +49 -36
- package/dist/connectionStateHandler.js.map +1 -1
- package/dist/container.d.ts +22 -13
- package/dist/container.d.ts.map +1 -1
- package/dist/container.js +145 -117
- package/dist/container.js.map +1 -1
- package/dist/containerContext.d.ts +3 -3
- package/dist/containerContext.d.ts.map +1 -1
- package/dist/containerContext.js.map +1 -1
- package/dist/containerStorageAdapter.d.ts +12 -3
- package/dist/containerStorageAdapter.d.ts.map +1 -1
- package/dist/containerStorageAdapter.js +42 -4
- package/dist/containerStorageAdapter.js.map +1 -1
- package/dist/contracts.d.ts +2 -2
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js.map +1 -1
- package/dist/debugLogger.d.ts +1 -2
- package/dist/debugLogger.d.ts.map +1 -1
- package/dist/debugLogger.js.map +1 -1
- package/dist/deltaManager.d.ts +5 -6
- package/dist/deltaManager.d.ts.map +1 -1
- package/dist/deltaManager.js +29 -24
- package/dist/deltaManager.js.map +1 -1
- package/dist/deltaQueue.d.ts +1 -1
- package/dist/deltaQueue.d.ts.map +1 -1
- package/dist/deltaQueue.js.map +1 -1
- package/dist/error.d.ts +1 -2
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/{alpha.d.ts → legacy.d.ts} +5 -2
- package/dist/loadPaused.d.ts +35 -0
- package/dist/loadPaused.d.ts.map +1 -0
- package/dist/loadPaused.js +115 -0
- package/dist/loadPaused.js.map +1 -0
- package/dist/loader.d.ts +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +1 -14
- package/dist/loader.js.map +1 -1
- package/dist/location-redirection-utilities/resolveWithLocationRedirection.d.ts.map +1 -1
- package/dist/location-redirection-utilities/resolveWithLocationRedirection.js +4 -4
- package/dist/location-redirection-utilities/resolveWithLocationRedirection.js.map +1 -1
- package/dist/packageVersion.d.ts +1 -1
- package/dist/packageVersion.js +1 -1
- package/dist/packageVersion.js.map +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +3 -0
- package/dist/protocol.js.map +1 -1
- package/dist/public.d.ts +2 -1
- package/dist/retriableDocumentStorageService.d.ts +1 -1
- package/dist/retriableDocumentStorageService.d.ts.map +1 -1
- package/dist/retriableDocumentStorageService.js.map +1 -1
- package/dist/serializedStateManager.d.ts +89 -9
- package/dist/serializedStateManager.d.ts.map +1 -1
- package/dist/serializedStateManager.js +153 -37
- package/dist/serializedStateManager.js.map +1 -1
- package/dist/utils.d.ts +13 -3
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +31 -17
- package/dist/utils.js.map +1 -1
- package/{dist/beta.d.ts → internal.d.ts} +2 -4
- package/{lib/beta.d.ts → legacy.d.ts} +2 -4
- package/lib/attachment.d.ts +3 -2
- package/lib/attachment.d.ts.map +1 -1
- package/lib/attachment.js +5 -5
- package/lib/attachment.js.map +1 -1
- package/lib/audience.d.ts +6 -4
- package/lib/audience.d.ts.map +1 -1
- package/lib/audience.js +19 -4
- package/lib/audience.js.map +1 -1
- package/lib/catchUpMonitor.d.ts +1 -1
- package/lib/catchUpMonitor.d.ts.map +1 -1
- package/lib/catchUpMonitor.js.map +1 -1
- package/lib/connectionManager.d.ts +7 -3
- package/lib/connectionManager.d.ts.map +1 -1
- package/lib/connectionManager.js +42 -18
- package/lib/connectionManager.js.map +1 -1
- package/lib/connectionStateHandler.d.ts +31 -10
- package/lib/connectionStateHandler.d.ts.map +1 -1
- package/lib/connectionStateHandler.js +49 -36
- package/lib/connectionStateHandler.js.map +1 -1
- package/lib/container.d.ts +22 -13
- package/lib/container.d.ts.map +1 -1
- package/lib/container.js +146 -118
- package/lib/container.js.map +1 -1
- package/lib/containerContext.d.ts +3 -3
- package/lib/containerContext.d.ts.map +1 -1
- package/lib/containerContext.js.map +1 -1
- package/lib/containerStorageAdapter.d.ts +12 -3
- package/lib/containerStorageAdapter.d.ts.map +1 -1
- package/lib/containerStorageAdapter.js +42 -4
- package/lib/containerStorageAdapter.js.map +1 -1
- package/lib/contracts.d.ts +2 -2
- package/lib/contracts.d.ts.map +1 -1
- package/lib/contracts.js +1 -1
- package/lib/contracts.js.map +1 -1
- package/lib/debugLogger.d.ts +1 -2
- package/lib/debugLogger.d.ts.map +1 -1
- package/lib/debugLogger.js.map +1 -1
- package/lib/deltaManager.d.ts +5 -6
- package/lib/deltaManager.d.ts.map +1 -1
- package/lib/deltaManager.js +10 -5
- package/lib/deltaManager.js.map +1 -1
- package/lib/deltaQueue.d.ts +1 -1
- package/lib/deltaQueue.d.ts.map +1 -1
- package/lib/deltaQueue.js.map +1 -1
- package/lib/error.d.ts +1 -2
- package/lib/error.d.ts.map +1 -1
- package/lib/error.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/{alpha.d.ts → legacy.d.ts} +5 -2
- package/lib/loadPaused.d.ts +35 -0
- package/lib/loadPaused.d.ts.map +1 -0
- package/lib/loadPaused.js +111 -0
- package/lib/loadPaused.js.map +1 -0
- package/lib/loader.d.ts +1 -1
- package/lib/loader.d.ts.map +1 -1
- package/lib/loader.js +3 -16
- package/lib/loader.js.map +1 -1
- package/lib/location-redirection-utilities/resolveWithLocationRedirection.d.ts.map +1 -1
- package/lib/location-redirection-utilities/resolveWithLocationRedirection.js +1 -1
- package/lib/location-redirection-utilities/resolveWithLocationRedirection.js.map +1 -1
- package/lib/packageVersion.d.ts +1 -1
- package/lib/packageVersion.js +1 -1
- package/lib/packageVersion.js.map +1 -1
- package/lib/protocol.d.ts.map +1 -1
- package/lib/protocol.js +3 -0
- package/lib/protocol.js.map +1 -1
- package/lib/public.d.ts +2 -1
- package/lib/retriableDocumentStorageService.d.ts +1 -1
- package/lib/retriableDocumentStorageService.d.ts.map +1 -1
- package/lib/retriableDocumentStorageService.js +1 -1
- package/lib/retriableDocumentStorageService.js.map +1 -1
- package/lib/serializedStateManager.d.ts +89 -9
- package/lib/serializedStateManager.d.ts.map +1 -1
- package/lib/serializedStateManager.js +149 -33
- package/lib/serializedStateManager.js.map +1 -1
- package/lib/tsdoc-metadata.json +1 -1
- package/lib/utils.d.ts +13 -3
- package/lib/utils.d.ts.map +1 -1
- package/lib/utils.js +17 -4
- package/lib/utils.js.map +1 -1
- package/package.json +33 -30
- package/src/attachment.ts +12 -13
- package/src/audience.ts +30 -9
- package/src/catchUpMonitor.ts +1 -1
- package/src/connectionManager.ts +53 -23
- package/src/connectionStateHandler.ts +78 -45
- package/src/container.ts +181 -160
- package/src/containerContext.ts +2 -2
- package/src/containerStorageAdapter.ts +61 -6
- package/src/contracts.ts +5 -4
- package/src/debugLogger.ts +1 -1
- package/src/deltaManager.ts +15 -8
- package/src/deltaQueue.ts +1 -1
- package/src/error.ts +1 -1
- package/src/index.ts +1 -0
- package/src/loadPaused.ts +140 -0
- package/src/loader.ts +6 -23
- package/src/location-redirection-utilities/resolveWithLocationRedirection.ts +1 -1
- package/src/packageVersion.ts +1 -1
- package/src/protocol.ts +4 -0
- package/src/retriableDocumentStorageService.ts +5 -2
- package/src/serializedStateManager.ts +224 -51
- package/src/utils.ts +22 -5
|
@@ -7,30 +7,36 @@ import {
|
|
|
7
7
|
IGetPendingLocalStateProps,
|
|
8
8
|
IRuntime,
|
|
9
9
|
} from "@fluidframework/container-definitions/internal";
|
|
10
|
+
import { stringToBuffer } from "@fluid-internal/client-utils";
|
|
10
11
|
import { assert } from "@fluidframework/core-utils/internal";
|
|
11
12
|
import {
|
|
13
|
+
FetchSource,
|
|
12
14
|
IDocumentStorageService,
|
|
13
15
|
IResolvedUrl,
|
|
14
16
|
ISnapshot,
|
|
15
17
|
} from "@fluidframework/driver-definitions/internal";
|
|
16
|
-
import {
|
|
18
|
+
import { getSnapshotTree } from "@fluidframework/driver-utils/internal";
|
|
17
19
|
import {
|
|
18
20
|
type IDocumentAttributes,
|
|
19
21
|
ISequencedDocumentMessage,
|
|
20
22
|
ISnapshotTree,
|
|
21
23
|
IVersion,
|
|
22
24
|
} from "@fluidframework/protocol-definitions";
|
|
23
|
-
import { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils";
|
|
24
25
|
import {
|
|
25
26
|
MonitoringContext,
|
|
26
27
|
PerformanceEvent,
|
|
27
28
|
UsageError,
|
|
28
29
|
createChildMonitoringContext,
|
|
29
30
|
} from "@fluidframework/telemetry-utils/internal";
|
|
30
|
-
|
|
31
|
+
import type { ITelemetryBaseLogger, IEventProvider, IEvent } from "@fluidframework/core-interfaces";
|
|
31
32
|
import { ISerializableBlobContents, getBlobContentsFromTree } from "./containerStorageAdapter.js";
|
|
32
|
-
import { getDocumentAttributes } from "./utils.js";
|
|
33
|
+
import { convertSnapshotToSnapshotInfo, getDocumentAttributes } from "./utils.js";
|
|
33
34
|
|
|
35
|
+
/**
|
|
36
|
+
* This is very similar to {@link @fluidframework/protocol-definitions/internal#ISnapshot}, but the difference is
|
|
37
|
+
* that the blobs of ISnapshot are of type ArrayBufferLike, while the blobs of this interface are serializable because
|
|
38
|
+
* they are already converted to string.
|
|
39
|
+
*/
|
|
34
40
|
export interface SnapshotWithBlobs {
|
|
35
41
|
/**
|
|
36
42
|
* Snapshot from which container initially loaded.
|
|
@@ -42,21 +48,38 @@ export interface SnapshotWithBlobs {
|
|
|
42
48
|
*/
|
|
43
49
|
snapshotBlobs: ISerializableBlobContents;
|
|
44
50
|
}
|
|
51
|
+
|
|
45
52
|
/**
|
|
46
53
|
* State saved by a container at close time, to be used to load a new instance
|
|
47
54
|
* of the container to the same state
|
|
55
|
+
*
|
|
56
|
+
* This is very similar to {@link @fluidframework/protocol-definitions/internal#ISnapshot}, but the difference is
|
|
57
|
+
* that the blobs of ISnapshot are of type ArrayBufferLike, while the blobs of this interface are serializable because
|
|
58
|
+
* they are already converted to string.
|
|
59
|
+
*
|
|
48
60
|
* @internal
|
|
49
61
|
*/
|
|
50
62
|
export interface IPendingContainerState extends SnapshotWithBlobs {
|
|
63
|
+
/** This container was attached (as opposed to IPendingDetachedContainerState.attached which is false) */
|
|
51
64
|
attached: true;
|
|
65
|
+
/**
|
|
66
|
+
* Runtime-specific state that will be needed to properly rehydrate
|
|
67
|
+
* (it's included in ContainerContext passed to instantiateRuntime)
|
|
68
|
+
*/
|
|
52
69
|
pendingRuntimeState: unknown;
|
|
70
|
+
/**
|
|
71
|
+
* Any group snapshots (aka delay-loaded) we've downloaded from the service for this container
|
|
72
|
+
*/
|
|
73
|
+
loadedGroupIdSnapshots?: Record<string, ISnapshotInfo>;
|
|
53
74
|
/**
|
|
54
75
|
* All ops since base snapshot sequence number up to the latest op
|
|
55
76
|
* seen when the container was closed. Used to apply stashed (saved pending)
|
|
56
77
|
* ops at the same sequence number at which they were made.
|
|
57
78
|
*/
|
|
58
79
|
savedOps: ISequencedDocumentMessage[];
|
|
80
|
+
/** The Container's URL in the service, needed to hook up the driver during rehydration */
|
|
59
81
|
url: string;
|
|
82
|
+
/** If the Container was connected when serialized, its clientId. Used as the initial clientId upon rehydration, until reconnected. */
|
|
60
83
|
clientId?: string;
|
|
61
84
|
}
|
|
62
85
|
|
|
@@ -66,42 +89,91 @@ export interface IPendingContainerState extends SnapshotWithBlobs {
|
|
|
66
89
|
* @internal
|
|
67
90
|
*/
|
|
68
91
|
export interface IPendingDetachedContainerState extends SnapshotWithBlobs {
|
|
92
|
+
/** This container was not attached (as opposed to IPendingContainerState.attached which is true) */
|
|
69
93
|
attached: false;
|
|
94
|
+
/** Indicates whether we expect the rehydrated container to have non-empty Detached Blob Storage */
|
|
70
95
|
hasAttachmentBlobs: boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Runtime-specific state that will be needed to properly rehydrate
|
|
98
|
+
* (it's included in ContainerContext passed to instantiateRuntime)
|
|
99
|
+
*/
|
|
71
100
|
pendingRuntimeState?: unknown;
|
|
72
101
|
}
|
|
73
102
|
|
|
74
103
|
export interface ISnapshotInfo extends SnapshotWithBlobs {
|
|
75
104
|
snapshotSequenceNumber: number;
|
|
105
|
+
snapshotFetchedTime?: number | undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type ISerializedStateManagerDocumentStorageService = Pick<
|
|
109
|
+
IDocumentStorageService,
|
|
110
|
+
"getSnapshot" | "getSnapshotTree" | "getVersions" | "readBlob"
|
|
111
|
+
> & {
|
|
112
|
+
loadedGroupIdSnapshots: Record<string, ISnapshot>;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
interface ISerializerEvent extends IEvent {
|
|
116
|
+
(event: "saved", listener: (dirty: boolean) => void): void;
|
|
76
117
|
}
|
|
77
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Helper class to manage the state of the container needed for proper serialization.
|
|
121
|
+
*
|
|
122
|
+
* It holds the pendingLocalState the container was rehydrated from (if any),
|
|
123
|
+
* as well as the snapshot to be used for serialization.
|
|
124
|
+
* It also keeps track of container dirty state and which local ops have been processed
|
|
125
|
+
*/
|
|
78
126
|
export class SerializedStateManager {
|
|
79
127
|
private readonly processedOps: ISequencedDocumentMessage[] = [];
|
|
80
|
-
private snapshot: ISnapshotInfo | undefined;
|
|
81
128
|
private readonly mc: MonitoringContext;
|
|
129
|
+
private snapshot: ISnapshotInfo | undefined;
|
|
82
130
|
private latestSnapshot: ISnapshotInfo | undefined;
|
|
83
|
-
private
|
|
131
|
+
private refreshSnapshotP: Promise<void> | undefined;
|
|
132
|
+
private readonly lastSavedOpSequenceNumber: number = 0;
|
|
84
133
|
|
|
134
|
+
/**
|
|
135
|
+
* @param pendingLocalState - The pendingLocalState being rehydrated, if any (undefined when loading directly from storage)
|
|
136
|
+
* @param subLogger - Container's logger to use as parent for our logger
|
|
137
|
+
* @param storageAdapter - Storage adapter for fetching snapshots
|
|
138
|
+
* @param _offlineLoadEnabled - Is serializing/rehydrating containers allowed?
|
|
139
|
+
* @param containerEvent - Source of the "saved" event when the container has all its pending state uploaded
|
|
140
|
+
* @param containerDirty - Is the container "dirty"? That's the opposite of "saved" - there is pending state that may not have been received yet by the service.
|
|
141
|
+
*/
|
|
85
142
|
constructor(
|
|
86
143
|
private readonly pendingLocalState: IPendingContainerState | undefined,
|
|
87
|
-
subLogger:
|
|
88
|
-
private readonly storageAdapter:
|
|
89
|
-
IDocumentStorageService,
|
|
90
|
-
"readBlob" | "getSnapshotTree" | "getSnapshot" | "getVersions"
|
|
91
|
-
>,
|
|
144
|
+
subLogger: ITelemetryBaseLogger,
|
|
145
|
+
private readonly storageAdapter: ISerializedStateManagerDocumentStorageService,
|
|
92
146
|
private readonly _offlineLoadEnabled: boolean,
|
|
93
|
-
|
|
147
|
+
containerEvent: IEventProvider<ISerializerEvent>,
|
|
148
|
+
private readonly containerDirty: () => boolean,
|
|
94
149
|
) {
|
|
95
150
|
this.mc = createChildMonitoringContext({
|
|
96
151
|
logger: subLogger,
|
|
97
152
|
namespace: "serializedStateManager",
|
|
98
153
|
});
|
|
154
|
+
|
|
155
|
+
if (pendingLocalState && pendingLocalState.savedOps.length > 0) {
|
|
156
|
+
const savedOpsSize = pendingLocalState.savedOps.length;
|
|
157
|
+
this.lastSavedOpSequenceNumber =
|
|
158
|
+
pendingLocalState.savedOps[savedOpsSize - 1].sequenceNumber;
|
|
159
|
+
}
|
|
160
|
+
containerEvent.once("saved", () => this.updateSnapshotAndProcessedOpsMaybe());
|
|
99
161
|
}
|
|
100
162
|
|
|
101
163
|
public get offlineLoadEnabled(): boolean {
|
|
102
164
|
return this._offlineLoadEnabled;
|
|
103
165
|
}
|
|
104
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Promise that will resolve (or reject) once we've tried to download the latest snapshot(s) from storage
|
|
169
|
+
*/
|
|
170
|
+
public get waitForInitialRefresh(): Promise<void> | undefined {
|
|
171
|
+
return this.refreshSnapshotP;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Called whenever an incoming op is processed by the Container
|
|
176
|
+
*/
|
|
105
177
|
public addProcessedOp(message: ISequencedDocumentMessage) {
|
|
106
178
|
if (this.offlineLoadEnabled) {
|
|
107
179
|
this.processedOps.push(message);
|
|
@@ -109,26 +181,40 @@ export class SerializedStateManager {
|
|
|
109
181
|
}
|
|
110
182
|
}
|
|
111
183
|
|
|
184
|
+
/**
|
|
185
|
+
* This wraps the basic functionality of fetching the snapshot for this container during Container load.
|
|
186
|
+
*
|
|
187
|
+
* If we have pendingLocalState, we get the snapshot from there.
|
|
188
|
+
* Otherwise, fetch it from storage (according to specifiedVersion if provided)
|
|
189
|
+
*
|
|
190
|
+
* @param specifiedVersion - If a version is specified and we don't have pendingLocalState, fetch this version from storage
|
|
191
|
+
* @param supportGetSnapshotApi - a boolean indicating whether to use the fetchISnapshot or fetchISnapshotTree.
|
|
192
|
+
* @returns The snapshot to boot the container from
|
|
193
|
+
*/
|
|
112
194
|
public async fetchSnapshot(
|
|
113
195
|
specifiedVersion: string | undefined,
|
|
114
196
|
supportGetSnapshotApi: boolean,
|
|
115
197
|
) {
|
|
116
198
|
if (this.pendingLocalState === undefined) {
|
|
117
|
-
const { baseSnapshot, version } = await
|
|
199
|
+
const { baseSnapshot, version } = await getSnapshot(
|
|
118
200
|
this.mc,
|
|
119
201
|
this.storageAdapter,
|
|
120
202
|
supportGetSnapshotApi,
|
|
121
203
|
specifiedVersion,
|
|
122
204
|
);
|
|
205
|
+
const baseSnapshotTree: ISnapshotTree | undefined = getSnapshotTree(baseSnapshot);
|
|
123
206
|
// non-interactive clients will not have any pending state we want to save
|
|
124
207
|
if (this.offlineLoadEnabled) {
|
|
125
208
|
const snapshotBlobs = await getBlobContentsFromTree(
|
|
126
|
-
|
|
209
|
+
baseSnapshotTree,
|
|
127
210
|
this.storageAdapter,
|
|
128
211
|
);
|
|
129
|
-
const attributes = await getDocumentAttributes(
|
|
212
|
+
const attributes = await getDocumentAttributes(
|
|
213
|
+
this.storageAdapter,
|
|
214
|
+
baseSnapshotTree,
|
|
215
|
+
);
|
|
130
216
|
this.snapshot = {
|
|
131
|
-
baseSnapshot,
|
|
217
|
+
baseSnapshot: baseSnapshotTree,
|
|
132
218
|
snapshotBlobs,
|
|
133
219
|
snapshotSequenceNumber: attributes.sequenceNumber,
|
|
134
220
|
};
|
|
@@ -142,25 +228,83 @@ export class SerializedStateManager {
|
|
|
142
228
|
snapshotBlobs,
|
|
143
229
|
snapshotSequenceNumber: attributes.sequenceNumber,
|
|
144
230
|
};
|
|
145
|
-
this.refreshSnapshot ??= (async () => {
|
|
146
|
-
this.latestSnapshot = await getLatestSnapshotInfo(
|
|
147
|
-
this.mc,
|
|
148
|
-
this.storageAdapter,
|
|
149
|
-
supportGetSnapshotApi,
|
|
150
|
-
);
|
|
151
|
-
this.newSnapshotFetched?.();
|
|
152
|
-
this.updateSnapshotAndProcessedOpsMaybe();
|
|
153
|
-
})();
|
|
154
231
|
|
|
155
|
-
|
|
232
|
+
if (
|
|
233
|
+
this.refreshSnapshotP === undefined &&
|
|
234
|
+
this.mc.config.getBoolean("Fluid.Container.enableOfflineSnapshotRefresh") === true
|
|
235
|
+
) {
|
|
236
|
+
// Don't block on the refresh snapshot call - it is for the next time we serialize, not booting this incarnation
|
|
237
|
+
this.refreshSnapshotP = this.refreshLatestSnapshot(supportGetSnapshotApi);
|
|
238
|
+
this.refreshSnapshotP.catch((e) => {
|
|
239
|
+
this.mc.logger.sendErrorEvent({
|
|
240
|
+
eventName: "RefreshLatestSnapshotFailed",
|
|
241
|
+
error: e,
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const blobContents = new Map<string, ArrayBuffer>();
|
|
247
|
+
for (const [id, value] of Object.entries(snapshotBlobs)) {
|
|
248
|
+
blobContents.set(id, stringToBuffer(value, "utf8"));
|
|
249
|
+
}
|
|
250
|
+
const iSnapshot: ISnapshot = {
|
|
251
|
+
sequenceNumber: this.snapshot.snapshotSequenceNumber,
|
|
252
|
+
snapshotTree: baseSnapshot,
|
|
253
|
+
blobContents,
|
|
254
|
+
latestSequenceNumber: undefined,
|
|
255
|
+
ops: [],
|
|
256
|
+
snapshotFormatV: 1,
|
|
257
|
+
};
|
|
258
|
+
return { baseSnapshot: iSnapshot, version: undefined };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Fetch the latest snapshot for the container, including delay-loaded groupIds if pendingLocalState was provided and contained any groupIds.
|
|
264
|
+
* Note that this will update the StorageAdapter's cached snapshots for the groupIds (if present)
|
|
265
|
+
*
|
|
266
|
+
* @param supportGetSnapshotApi - a boolean indicating whether to use the fetchISnapshot or fetchISnapshotTree (must be true to fetch by groupIds)
|
|
267
|
+
*/
|
|
268
|
+
private async refreshLatestSnapshot(supportGetSnapshotApi: boolean): Promise<void> {
|
|
269
|
+
this.latestSnapshot = await getLatestSnapshotInfo(
|
|
270
|
+
this.mc,
|
|
271
|
+
this.storageAdapter,
|
|
272
|
+
supportGetSnapshotApi,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// These are loading groupIds that the containerRuntime has requested over its lifetime.
|
|
276
|
+
// We will fetch the latest snapshot for the groupIds, which will update storageAdapter.loadedGroupIdSnapshots's cache
|
|
277
|
+
const downloadedGroupIds = Object.keys(this.storageAdapter.loadedGroupIdSnapshots);
|
|
278
|
+
if (supportGetSnapshotApi && downloadedGroupIds.length > 0) {
|
|
279
|
+
assert(
|
|
280
|
+
this.storageAdapter.getSnapshot !== undefined,
|
|
281
|
+
0x972 /* getSnapshot should exist */,
|
|
282
|
+
);
|
|
283
|
+
// (This is a separate network call from above because it requires work for storage to add a special base groupId)
|
|
284
|
+
const snapshot = await this.storageAdapter.getSnapshot({
|
|
285
|
+
versionId: undefined,
|
|
286
|
+
scenarioName: "getLatestSnapshotInfo",
|
|
287
|
+
cacheSnapshot: false,
|
|
288
|
+
loadingGroupIds: downloadedGroupIds,
|
|
289
|
+
fetchSource: FetchSource.noCache,
|
|
290
|
+
});
|
|
291
|
+
assert(snapshot !== undefined, 0x973 /* Snapshot should exist */);
|
|
156
292
|
}
|
|
293
|
+
|
|
294
|
+
this.updateSnapshotAndProcessedOpsMaybe();
|
|
157
295
|
}
|
|
158
296
|
|
|
159
297
|
/**
|
|
160
298
|
* Updates class snapshot and processedOps if we have a new snapshot and it's among processedOps range.
|
|
161
299
|
*/
|
|
162
300
|
private updateSnapshotAndProcessedOpsMaybe() {
|
|
163
|
-
if (
|
|
301
|
+
if (
|
|
302
|
+
this.latestSnapshot === undefined ||
|
|
303
|
+
this.processedOps.length === 0 ||
|
|
304
|
+
this.processedOps[this.processedOps.length - 1].sequenceNumber <
|
|
305
|
+
this.lastSavedOpSequenceNumber ||
|
|
306
|
+
this.containerDirty()
|
|
307
|
+
) {
|
|
164
308
|
// can't refresh latest snapshot until we have processed the ops up to it.
|
|
165
309
|
// Pending state would be behind the latest snapshot.
|
|
166
310
|
return;
|
|
@@ -203,31 +347,42 @@ export class SerializedStateManager {
|
|
|
203
347
|
}
|
|
204
348
|
|
|
205
349
|
/**
|
|
350
|
+
* When the Container attaches, we need to stash the initial snapshot (a form of the attach summary).
|
|
206
351
|
* This method is only meant to be used by Container.attach() to set the initial
|
|
207
352
|
* base snapshot when attaching.
|
|
208
|
-
* @param snapshot - snapshot and blobs collected while attaching
|
|
353
|
+
* @param snapshot - snapshot and blobs collected while attaching (a form of the attach summary)
|
|
209
354
|
*/
|
|
210
355
|
public setInitialSnapshot(snapshot: SnapshotWithBlobs | undefined) {
|
|
211
356
|
if (this.offlineLoadEnabled) {
|
|
212
|
-
assert(
|
|
213
|
-
|
|
357
|
+
assert(
|
|
358
|
+
this.snapshot === undefined,
|
|
359
|
+
0x937 /* inital snapshot should only be defined once */,
|
|
360
|
+
);
|
|
361
|
+
assert(snapshot !== undefined, 0x938 /* attachment snapshot should be defined */);
|
|
214
362
|
const { baseSnapshot, snapshotBlobs } = snapshot;
|
|
215
363
|
const attributesHash =
|
|
216
364
|
".protocol" in baseSnapshot.trees
|
|
217
365
|
? baseSnapshot.trees[".protocol"].blobs.attributes
|
|
218
366
|
: baseSnapshot.blobs[".attributes"];
|
|
219
367
|
const attributes = JSON.parse(snapshotBlobs[attributesHash]);
|
|
220
|
-
assert(
|
|
368
|
+
assert(
|
|
369
|
+
attributes.sequenceNumber === 0,
|
|
370
|
+
0x939 /* trying to set a non attachment snapshot */,
|
|
371
|
+
);
|
|
221
372
|
this.snapshot = { ...snapshot, snapshotSequenceNumber: attributes.sequenceNumber };
|
|
222
373
|
}
|
|
223
374
|
}
|
|
224
375
|
|
|
225
|
-
|
|
376
|
+
/**
|
|
377
|
+
* Assembles and serializes the {@link IPendingContainerState} for the container,
|
|
378
|
+
* to be stored and used to rehydrate the container at a later time.
|
|
379
|
+
*/
|
|
380
|
+
public async getPendingLocalState(
|
|
226
381
|
props: IGetPendingLocalStateProps,
|
|
227
382
|
clientId: string | undefined,
|
|
228
383
|
runtime: Pick<IRuntime, "getPendingLocalState">,
|
|
229
384
|
resolvedUrl: IResolvedUrl,
|
|
230
|
-
) {
|
|
385
|
+
): Promise<string> {
|
|
231
386
|
return PerformanceEvent.timedExecAsync(
|
|
232
387
|
this.mc.logger,
|
|
233
388
|
{
|
|
@@ -243,16 +398,32 @@ export class SerializedStateManager {
|
|
|
243
398
|
);
|
|
244
399
|
}
|
|
245
400
|
assert(this.snapshot !== undefined, 0x8e5 /* no base data */);
|
|
246
|
-
const pendingRuntimeState = await runtime.getPendingLocalState(
|
|
401
|
+
const pendingRuntimeState = await runtime.getPendingLocalState({
|
|
402
|
+
...props,
|
|
403
|
+
snapshotSequenceNumber: this.snapshot.snapshotSequenceNumber,
|
|
404
|
+
sessionExpiryTimerStarted: this.snapshot.snapshotFetchedTime,
|
|
405
|
+
});
|
|
406
|
+
// This conversion is required because ArrayBufferLike doesn't survive JSON.stringify
|
|
407
|
+
const loadedGroupIdSnapshots = {};
|
|
408
|
+
let hasGroupIdSnapshots = false;
|
|
409
|
+
const groupIdSnapshots = Object.entries(this.storageAdapter.loadedGroupIdSnapshots);
|
|
410
|
+
if (groupIdSnapshots.length > 0) {
|
|
411
|
+
for (const [groupId, snapshot] of groupIdSnapshots) {
|
|
412
|
+
hasGroupIdSnapshots = true;
|
|
413
|
+
loadedGroupIdSnapshots[groupId] = convertSnapshotToSnapshotInfo(snapshot);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
247
416
|
const pendingState: IPendingContainerState = {
|
|
248
417
|
attached: true,
|
|
249
418
|
pendingRuntimeState,
|
|
250
419
|
baseSnapshot: this.snapshot.baseSnapshot,
|
|
251
420
|
snapshotBlobs: this.snapshot.snapshotBlobs,
|
|
421
|
+
loadedGroupIdSnapshots: hasGroupIdSnapshots
|
|
422
|
+
? loadedGroupIdSnapshots
|
|
423
|
+
: undefined,
|
|
252
424
|
savedOps: this.processedOps,
|
|
253
425
|
url: resolvedUrl.url,
|
|
254
|
-
|
|
255
|
-
clientId: pendingRuntimeState !== undefined ? clientId : undefined,
|
|
426
|
+
clientId,
|
|
256
427
|
};
|
|
257
428
|
|
|
258
429
|
return JSON.stringify(pendingState);
|
|
@@ -271,29 +442,34 @@ export class SerializedStateManager {
|
|
|
271
442
|
*/
|
|
272
443
|
export async function getLatestSnapshotInfo(
|
|
273
444
|
mc: MonitoringContext,
|
|
274
|
-
storageAdapter:
|
|
275
|
-
IDocumentStorageService,
|
|
276
|
-
"getSnapshot" | "getSnapshotTree" | "getVersions" | "readBlob"
|
|
277
|
-
>,
|
|
445
|
+
storageAdapter: ISerializedStateManagerDocumentStorageService,
|
|
278
446
|
supportGetSnapshotApi: boolean,
|
|
279
447
|
): Promise<ISnapshotInfo | undefined> {
|
|
280
448
|
return PerformanceEvent.timedExecAsync(
|
|
281
449
|
mc.logger,
|
|
282
450
|
{ eventName: "GetLatestSnapshotInfo" },
|
|
283
451
|
async () => {
|
|
284
|
-
const { baseSnapshot } = await
|
|
452
|
+
const { baseSnapshot } = await getSnapshot(
|
|
285
453
|
mc,
|
|
286
454
|
storageAdapter,
|
|
287
455
|
supportGetSnapshotApi,
|
|
288
456
|
undefined,
|
|
289
457
|
);
|
|
290
|
-
|
|
458
|
+
|
|
459
|
+
const baseSnapshotTree: ISnapshotTree | undefined = getSnapshotTree(baseSnapshot);
|
|
460
|
+
const snapshotFetchedTime = Date.now();
|
|
461
|
+
const snapshotBlobs = await getBlobContentsFromTree(baseSnapshotTree, storageAdapter);
|
|
291
462
|
const attributes: IDocumentAttributes = await getDocumentAttributes(
|
|
292
463
|
storageAdapter,
|
|
293
|
-
|
|
464
|
+
baseSnapshotTree,
|
|
294
465
|
);
|
|
295
466
|
const snapshotSequenceNumber = attributes.sequenceNumber;
|
|
296
|
-
return {
|
|
467
|
+
return {
|
|
468
|
+
baseSnapshot: baseSnapshotTree,
|
|
469
|
+
snapshotBlobs,
|
|
470
|
+
snapshotSequenceNumber,
|
|
471
|
+
snapshotFetchedTime,
|
|
472
|
+
};
|
|
297
473
|
},
|
|
298
474
|
).catch(() => undefined);
|
|
299
475
|
}
|
|
@@ -307,7 +483,7 @@ export async function getLatestSnapshotInfo(
|
|
|
307
483
|
* @param specifiedVersion - An optional version string specifying the version of the snapshot tree to fetch.
|
|
308
484
|
* @returns - An ISnapshotTree and its version.
|
|
309
485
|
*/
|
|
310
|
-
async function
|
|
486
|
+
async function getSnapshot(
|
|
311
487
|
mc: MonitoringContext,
|
|
312
488
|
storageAdapter: Pick<
|
|
313
489
|
IDocumentStorageService,
|
|
@@ -315,15 +491,12 @@ async function getSnapshotTree(
|
|
|
315
491
|
>,
|
|
316
492
|
supportGetSnapshotApi: boolean,
|
|
317
493
|
specifiedVersion: string | undefined,
|
|
318
|
-
): Promise<{ baseSnapshot: ISnapshotTree; version?: IVersion }> {
|
|
494
|
+
): Promise<{ baseSnapshot: ISnapshot | ISnapshotTree; version?: IVersion }> {
|
|
319
495
|
const { snapshot, version } = supportGetSnapshotApi
|
|
320
496
|
? await fetchISnapshot(mc, storageAdapter, specifiedVersion)
|
|
321
497
|
: await fetchISnapshotTree(mc, storageAdapter, specifiedVersion);
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
: snapshot;
|
|
325
|
-
assert(baseSnapshot !== undefined, 0x8e4 /* Snapshot should exist */);
|
|
326
|
-
return { baseSnapshot, version };
|
|
498
|
+
assert(snapshot !== undefined, 0x8e4 /* Snapshot should exist */);
|
|
499
|
+
return { baseSnapshot: snapshot, version };
|
|
327
500
|
}
|
|
328
501
|
|
|
329
502
|
/**
|
package/src/utils.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { Uint8ArrayToString, bufferToString, stringToBuffer } from "@fluid-internal/client-utils";
|
|
7
7
|
import { assert, compareArrays, unreachableCase } from "@fluidframework/core-utils/internal";
|
|
8
|
-
import { DriverErrorTypes } from "@fluidframework/driver-definitions";
|
|
8
|
+
import { DriverErrorTypes } from "@fluidframework/driver-definitions/internal";
|
|
9
9
|
import {
|
|
10
10
|
IDocumentStorageService,
|
|
11
11
|
type ISnapshot,
|
|
@@ -27,6 +27,7 @@ import { v4 as uuid } from "uuid";
|
|
|
27
27
|
|
|
28
28
|
import { ISerializableBlobContents } from "./containerStorageAdapter.js";
|
|
29
29
|
import type {
|
|
30
|
+
IPendingContainerState,
|
|
30
31
|
IPendingDetachedContainerState,
|
|
31
32
|
ISnapshotInfo,
|
|
32
33
|
SnapshotWithBlobs,
|
|
@@ -43,7 +44,7 @@ export interface ISnapshotTreeWithBlobContents extends ISnapshotTree {
|
|
|
43
44
|
* Interface to represent the parsed parts of IResolvedUrl.url to help
|
|
44
45
|
* in getting info about different parts of the url.
|
|
45
46
|
* May not be compatible or relevant for any Url Resolver
|
|
46
|
-
* @
|
|
47
|
+
* @alpha
|
|
47
48
|
*/
|
|
48
49
|
export interface IParsedUrl {
|
|
49
50
|
/**
|
|
@@ -72,7 +73,7 @@ export interface IParsedUrl {
|
|
|
72
73
|
* with urls of type: protocol://<string>/.../..?<querystring>
|
|
73
74
|
* @param url - This is the IResolvedUrl.url part of the resolved url.
|
|
74
75
|
* @returns The IParsedUrl representing the input URL, or undefined if the format was not supported
|
|
75
|
-
* @
|
|
76
|
+
* @alpha
|
|
76
77
|
*/
|
|
77
78
|
export function tryParseCompatibleResolvedUrl(url: string): IParsedUrl | undefined {
|
|
78
79
|
const parsed = new URL(url);
|
|
@@ -163,7 +164,6 @@ function convertSummaryToSnapshotAndBlobs(summary: ISummaryTree): SnapshotWithBl
|
|
|
163
164
|
throw new LoggingError(
|
|
164
165
|
"No handles should be there in summary in detached container!!",
|
|
165
166
|
);
|
|
166
|
-
break;
|
|
167
167
|
default: {
|
|
168
168
|
unreachableCase(summaryObject, `Unknown tree type ${(summaryObject as any).type}`);
|
|
169
169
|
}
|
|
@@ -181,7 +181,7 @@ function convertSummaryToSnapshotAndBlobs(summary: ISummaryTree): SnapshotWithBl
|
|
|
181
181
|
* @param snapshot - ISnapshot
|
|
182
182
|
*/
|
|
183
183
|
export function convertSnapshotToSnapshotInfo(snapshot: ISnapshot): ISnapshotInfo {
|
|
184
|
-
assert(snapshot.sequenceNumber !== undefined,
|
|
184
|
+
assert(snapshot.sequenceNumber !== undefined, 0x93a /* Snapshot sequence number is missing */);
|
|
185
185
|
const snapshotBlobs: ISerializableBlobContents = {};
|
|
186
186
|
for (const [blobId, arrayBufferLike] of snapshot.blobContents.entries()) {
|
|
187
187
|
snapshotBlobs[blobId] = bufferToString(arrayBufferLike, "utf8");
|
|
@@ -314,6 +314,11 @@ function isPendingDetachedContainerState(
|
|
|
314
314
|
return true;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Parses the given string into {@link IPendingDetachedContainerState} format,
|
|
319
|
+
* with validation (if invalid, throws a UsageError).
|
|
320
|
+
* This is the inverse of the JSON.stringify call in {@link Container.serialize}
|
|
321
|
+
*/
|
|
317
322
|
export function getDetachedContainerStateFromSerializedContainer(
|
|
318
323
|
serializedContainer: string,
|
|
319
324
|
): IPendingDetachedContainerState {
|
|
@@ -336,6 +341,18 @@ export function getDetachedContainerStateFromSerializedContainer(
|
|
|
336
341
|
}
|
|
337
342
|
}
|
|
338
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Blindly parses the given string into {@link IPendingContainerState} format.
|
|
346
|
+
* This is the inverse of the JSON.stringify call in {@link SerializedStateManager.getPendingLocalState}
|
|
347
|
+
*/
|
|
348
|
+
export function getAttachedContainerStateFromSerializedContainer(
|
|
349
|
+
serializedContainer: string | undefined,
|
|
350
|
+
): IPendingContainerState | undefined {
|
|
351
|
+
return serializedContainer !== undefined
|
|
352
|
+
? (JSON.parse(serializedContainer) as IPendingContainerState)
|
|
353
|
+
: undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
339
356
|
/**
|
|
340
357
|
* Ensures only a single instance of the provided async function is running.
|
|
341
358
|
* If there are multiple calls they will all get the same promise to wait on.
|