@fluidframework/tool-utils 2.0.0-internal.3.0.2 → 2.0.0-internal.3.2.0
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/.eslintrc.js +19 -23
- package/.mocharc.js +2 -2
- package/api-extractor.json +2 -2
- package/dist/fluidToolRC.d.ts.map +1 -1
- package/dist/fluidToolRC.js.map +1 -1
- package/dist/httpHelpers.d.ts.map +1 -1
- package/dist/httpHelpers.js +6 -2
- package/dist/httpHelpers.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/odspTokenManager.d.ts.map +1 -1
- package/dist/odspTokenManager.js +5 -2
- package/dist/odspTokenManager.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/snapshotNormalizer.d.ts.map +1 -1
- package/dist/snapshotNormalizer.js +13 -10
- package/dist/snapshotNormalizer.js.map +1 -1
- package/lib/fluidToolRC.d.ts.map +1 -1
- package/lib/fluidToolRC.js.map +1 -1
- package/lib/httpHelpers.d.ts.map +1 -1
- package/lib/httpHelpers.js +6 -2
- package/lib/httpHelpers.js.map +1 -1
- package/lib/index.d.ts +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/odspTokenManager.d.ts.map +1 -1
- package/lib/odspTokenManager.js +5 -2
- package/lib/odspTokenManager.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/snapshotNormalizer.d.ts.map +1 -1
- package/lib/snapshotNormalizer.js +14 -11
- package/lib/snapshotNormalizer.js.map +1 -1
- package/package.json +40 -39
- package/prettier.config.cjs +1 -1
- package/src/fluidToolRC.ts +36 -36
- package/src/httpHelpers.ts +58 -44
- package/src/index.ts +5 -1
- package/src/odspTokenManager.ts +301 -309
- package/src/packageVersion.ts +1 -1
- package/src/snapshotNormalizer.ts +178 -171
- package/tsconfig.esnext.json +6 -6
- package/tsconfig.json +10 -16
package/src/packageVersion.ts
CHANGED
|
@@ -4,11 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { AttachmentTreeEntry, BlobTreeEntry, TreeTreeEntry } from "@fluidframework/protocol-base";
|
|
7
|
-
import {
|
|
8
|
-
ITree,
|
|
9
|
-
TreeEntry,
|
|
10
|
-
ITreeEntry,
|
|
11
|
-
} from "@fluidframework/protocol-definitions";
|
|
7
|
+
import { ITree, TreeEntry, ITreeEntry } from "@fluidframework/protocol-definitions";
|
|
12
8
|
|
|
13
9
|
/** The name of the metadata blob added to the root of the container runtime. */
|
|
14
10
|
const metadataBlobName = ".metadata";
|
|
@@ -16,14 +12,14 @@ const metadataBlobName = ".metadata";
|
|
|
16
12
|
export const gcBlobPrefix = "__gc";
|
|
17
13
|
|
|
18
14
|
export interface ISnapshotNormalizerConfig {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
// The paths of blobs whose contents should be normalized.
|
|
16
|
+
blobsToNormalize?: string[];
|
|
17
|
+
/**
|
|
18
|
+
* channel types who's content (non-attribute) blobs will be excluded.
|
|
19
|
+
* this is used to exclude the content of channels who's content cannot be compared
|
|
20
|
+
* as the content is non-deterministic between snapshot at the same sequence number.
|
|
21
|
+
*/
|
|
22
|
+
excludedChannelContentTypes?: string[];
|
|
27
23
|
}
|
|
28
24
|
|
|
29
25
|
/**
|
|
@@ -31,27 +27,27 @@ export interface ISnapshotNormalizerConfig {
|
|
|
31
27
|
* @returns the sorted array.
|
|
32
28
|
*/
|
|
33
29
|
function getDeepSortedArray(array: any[]): any[] {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
30
|
+
const sortedArray: any[] = [];
|
|
31
|
+
// Sort arrays and objects, if any, in the array.
|
|
32
|
+
for (const element of array) {
|
|
33
|
+
if (Array.isArray(element)) {
|
|
34
|
+
sortedArray.push(getDeepSortedArray(element));
|
|
35
|
+
} else if (element instanceof Object) {
|
|
36
|
+
sortedArray.push(getDeepSortedObject(element));
|
|
37
|
+
} else {
|
|
38
|
+
sortedArray.push(element);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Now that all the arrays and objects in this array's elements have been sorted, sort it by comparing each
|
|
43
|
+
// element's stringified version.
|
|
44
|
+
const sortFn = (elem1: any, elem2: any) => {
|
|
45
|
+
const serializedElem1 = JSON.stringify(elem1);
|
|
46
|
+
const serializedElem2 = JSON.stringify(elem2);
|
|
47
|
+
return serializedElem1.localeCompare(serializedElem2);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return sortedArray.sort(sortFn);
|
|
55
51
|
}
|
|
56
52
|
|
|
57
53
|
/**
|
|
@@ -59,21 +55,21 @@ function getDeepSortedArray(array: any[]): any[] {
|
|
|
59
55
|
* @returns the sorted object.
|
|
60
56
|
*/
|
|
61
57
|
function getDeepSortedObject(obj: any): any {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
58
|
+
const sortedObj: any = {};
|
|
59
|
+
// Sort the object keys first. Then sort arrays and objects, if any, in the object.
|
|
60
|
+
const keys = Object.keys(obj).sort();
|
|
61
|
+
for (const key of keys) {
|
|
62
|
+
const value = obj[key];
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
sortedObj[key] = getDeepSortedArray(value);
|
|
65
|
+
} else if (value instanceof Object) {
|
|
66
|
+
sortedObj[key] = getDeepSortedObject(value);
|
|
67
|
+
} else {
|
|
68
|
+
sortedObj[key] = value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return sortedObj;
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
/**
|
|
@@ -82,46 +78,46 @@ function getDeepSortedObject(obj: any): any {
|
|
|
82
78
|
* @returns the normalized blob content.
|
|
83
79
|
*/
|
|
84
80
|
function getNormalizedBlobContent(blobContent: string, blobName: string): string {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
81
|
+
let content = blobContent;
|
|
82
|
+
if (blobName.startsWith(gcBlobPrefix)) {
|
|
83
|
+
// GC blobs may contain `unreferencedTimestampMs` for node that became unreferenced. This is the timestamp
|
|
84
|
+
// of the last op processed or current timestamp and can differ between clients depending on when GC was run.
|
|
85
|
+
// So, remove it for the purposes of comparing snapshots.
|
|
86
|
+
const gcState = JSON.parse(content);
|
|
87
|
+
for (const [, data] of Object.entries(gcState.gcNodes)) {
|
|
88
|
+
delete (data as any).unreferencedTimestampMs;
|
|
89
|
+
}
|
|
90
|
+
content = JSON.stringify(gcState);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The metadata blob has "summaryNumber" or "summaryCount" that tells which summary this is for a container. It can
|
|
95
|
+
* be different in summaries of two clients even if they are generated at the same sequence#. For instance, at seq#
|
|
96
|
+
* 1000, if one client has summarized 10 times and other has summarizer 15 times, summaryNumber will be different
|
|
97
|
+
* for them. So, update "summaryNumber" to 0 for purposes of comparing snapshots.
|
|
98
|
+
*/
|
|
99
|
+
if (blobName === metadataBlobName) {
|
|
100
|
+
const metadata = JSON.parse(content);
|
|
101
|
+
if (metadata.summaryNumber !== undefined) {
|
|
102
|
+
metadata.summaryNumber = 0;
|
|
103
|
+
}
|
|
104
|
+
if (metadata.summaryCount !== undefined) {
|
|
105
|
+
metadata.summaryCount = 0;
|
|
106
|
+
}
|
|
107
|
+
content = JSON.stringify(metadata);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Deep sort the content if it's parseable.
|
|
111
|
+
try {
|
|
112
|
+
let contentObj = JSON.parse(content);
|
|
113
|
+
if (Array.isArray(contentObj)) {
|
|
114
|
+
contentObj = getDeepSortedArray(contentObj);
|
|
115
|
+
} else if (contentObj instanceof Object) {
|
|
116
|
+
contentObj = getDeepSortedObject(contentObj);
|
|
117
|
+
}
|
|
118
|
+
content = JSON.stringify(contentObj);
|
|
119
|
+
} catch {}
|
|
120
|
+
return content;
|
|
125
121
|
}
|
|
126
122
|
|
|
127
123
|
/**
|
|
@@ -134,104 +130,115 @@ function getNormalizedBlobContent(blobContent: string, blobName: string): string
|
|
|
134
130
|
* @returns a copy of the normalized snapshot tree.
|
|
135
131
|
*/
|
|
136
132
|
export function getNormalizedSnapshot(snapshot: ITree, config?: ISnapshotNormalizerConfig): ITree {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
133
|
+
// Merge blobs to normalize in the config with runtime blobs to normalize. The contents of these blobs will be
|
|
134
|
+
// parsed and deep sorted.
|
|
135
|
+
const normalizedEntries: ITreeEntry[] = [];
|
|
136
|
+
|
|
137
|
+
// The metadata blob in the root of the summary tree needs to be normalized.
|
|
138
|
+
const blobsToNormalize = [metadataBlobName, ...(config?.blobsToNormalize ?? [])];
|
|
139
|
+
for (const entry of snapshot.entries) {
|
|
140
|
+
normalizedEntries.push(normalizeEntry(entry, { ...config, blobsToNormalize }));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Sort the tree entries based on their path.
|
|
144
|
+
normalizedEntries.sort((a, b) => a.path.localeCompare(b.path));
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
entries: normalizedEntries,
|
|
148
|
+
id: snapshot.id,
|
|
149
|
+
};
|
|
154
150
|
}
|
|
155
151
|
|
|
156
152
|
function normalizeMatrix(value: ITree): ITree {
|
|
157
|
-
|
|
153
|
+
const rows = value.entries.find((e) => e.path === "rows");
|
|
158
154
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
155
|
+
if (!rows || !("entries" in rows.value)) {
|
|
156
|
+
return value;
|
|
157
|
+
}
|
|
162
158
|
|
|
163
|
-
|
|
159
|
+
const segments = rows.value.entries.find((e) => e.path === "segments");
|
|
164
160
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
161
|
+
if (!segments || !("entries" in segments.value)) {
|
|
162
|
+
return value;
|
|
163
|
+
}
|
|
168
164
|
|
|
169
|
-
|
|
165
|
+
const header = segments.value.entries.find((e) => e.path === "header");
|
|
170
166
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
167
|
+
if (!header || !("contents" in header.value)) {
|
|
168
|
+
return value;
|
|
169
|
+
}
|
|
174
170
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
171
|
+
if (!header?.value.contents.includes("removedClientId")) {
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
178
174
|
|
|
179
|
-
|
|
175
|
+
const contents = JSON.parse(header?.value.contents);
|
|
180
176
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
177
|
+
for (const segment of contents.segments) {
|
|
178
|
+
if ("removedClientId" in segment) {
|
|
179
|
+
segment.removedClientId = undefined;
|
|
180
|
+
}
|
|
185
181
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
182
|
+
if ("removedClientIds" in segment) {
|
|
183
|
+
segment.removedClientIds = undefined;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
190
186
|
|
|
191
|
-
|
|
187
|
+
header.value.contents = JSON.stringify(contents);
|
|
192
188
|
|
|
193
|
-
|
|
189
|
+
return value;
|
|
194
190
|
}
|
|
195
191
|
|
|
196
192
|
function normalizeEntry(
|
|
197
|
-
|
|
198
|
-
|
|
193
|
+
entry: ITreeEntry,
|
|
194
|
+
config: ISnapshotNormalizerConfig | undefined,
|
|
199
195
|
): ITreeEntry {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
196
|
+
switch (entry.type) {
|
|
197
|
+
case TreeEntry.Blob: {
|
|
198
|
+
let contents = entry.value.contents;
|
|
199
|
+
// If this blob has to be normalized or it's a GC blob, parse and sort the blob contents first.
|
|
200
|
+
if (
|
|
201
|
+
config?.blobsToNormalize?.includes(entry.path) ||
|
|
202
|
+
entry.path.startsWith(gcBlobPrefix)
|
|
203
|
+
) {
|
|
204
|
+
contents = getNormalizedBlobContent(contents, entry.path);
|
|
205
|
+
}
|
|
206
|
+
return new BlobTreeEntry(entry.path, contents);
|
|
207
|
+
}
|
|
208
|
+
case TreeEntry.Tree: {
|
|
209
|
+
if (config?.excludedChannelContentTypes !== undefined) {
|
|
210
|
+
for (const maybeAttributes of entry.value.entries) {
|
|
211
|
+
if (
|
|
212
|
+
maybeAttributes.type === TreeEntry.Blob &&
|
|
213
|
+
maybeAttributes.path === ".attributes"
|
|
214
|
+
) {
|
|
215
|
+
const parsed: { type?: string } = JSON.parse(
|
|
216
|
+
maybeAttributes.value.contents,
|
|
217
|
+
);
|
|
218
|
+
if (parsed.type === "https://graph.microsoft.com/types/sharedmatrix") {
|
|
219
|
+
return new TreeTreeEntry(
|
|
220
|
+
entry.path,
|
|
221
|
+
normalizeMatrix(getNormalizedSnapshot(entry.value, config)),
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
if (
|
|
225
|
+
parsed.type !== undefined &&
|
|
226
|
+
config.excludedChannelContentTypes.includes(parsed.type)
|
|
227
|
+
) {
|
|
228
|
+
// remove everything to match the unknown channel
|
|
229
|
+
return new TreeTreeEntry(entry.path, { entries: [maybeAttributes] });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return new TreeTreeEntry(entry.path, getNormalizedSnapshot(entry.value, config));
|
|
236
|
+
}
|
|
237
|
+
case TreeEntry.Attachment: {
|
|
238
|
+
return new AttachmentTreeEntry(entry.path, entry.value.id);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
default:
|
|
242
|
+
throw new Error("Unknown entry type");
|
|
243
|
+
}
|
|
237
244
|
}
|
package/tsconfig.esnext.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./lib",
|
|
5
|
+
"module": "esnext",
|
|
6
|
+
},
|
|
7
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
]
|
|
13
|
-
},
|
|
14
|
-
"include": [
|
|
15
|
-
"src/**/*"
|
|
16
|
-
]
|
|
17
|
-
}
|
|
2
|
+
"extends": "@fluidframework/build-common/ts-common-config.json",
|
|
3
|
+
"exclude": ["src/test/**/*"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"composite": true,
|
|
8
|
+
"types": ["node"],
|
|
9
|
+
},
|
|
10
|
+
"include": ["src/**/*"],
|
|
11
|
+
}
|