@fluid-tools/fetch-tool 1.4.0-115997 → 2.0.0-dev-rc.1.0.0.224419
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 +6 -8
- package/CHANGELOG.md +117 -0
- package/README.md +38 -7
- package/bin/fluid-fetch +0 -0
- package/dist/fluidAnalyzeMessages.d.ts.map +1 -1
- package/dist/fluidAnalyzeMessages.js +106 -116
- package/dist/fluidAnalyzeMessages.js.map +1 -1
- package/dist/fluidFetch.js +5 -3
- package/dist/fluidFetch.js.map +1 -1
- package/dist/fluidFetchArgs.d.ts +0 -3
- package/dist/fluidFetchArgs.d.ts.map +1 -1
- package/dist/fluidFetchArgs.js +10 -14
- package/dist/fluidFetchArgs.js.map +1 -1
- package/dist/fluidFetchInit.d.ts +0 -1
- package/dist/fluidFetchInit.d.ts.map +1 -1
- package/dist/fluidFetchInit.js +41 -34
- package/dist/fluidFetchInit.js.map +1 -1
- package/dist/fluidFetchMessages.d.ts.map +1 -1
- package/dist/fluidFetchMessages.js +168 -200
- package/dist/fluidFetchMessages.js.map +1 -1
- package/dist/fluidFetchSharePoint.d.ts +0 -1
- package/dist/fluidFetchSharePoint.d.ts.map +1 -1
- package/dist/fluidFetchSharePoint.js +20 -6
- package/dist/fluidFetchSharePoint.js.map +1 -1
- package/dist/fluidFetchSnapshot.d.ts.map +1 -1
- package/dist/fluidFetchSnapshot.js +18 -20
- package/dist/fluidFetchSnapshot.js.map +1 -1
- package/package.json +47 -42
- package/prettier.config.cjs +8 -0
- package/src/fluidAnalyzeMessages.ts +701 -630
- package/src/fluidFetch.ts +93 -88
- package/src/fluidFetchArgs.ts +167 -168
- package/src/fluidFetchInit.ts +133 -104
- package/src/fluidFetchMessages.ts +253 -232
- package/src/fluidFetchSharePoint.ts +130 -112
- package/src/fluidFetchSnapshot.ts +313 -295
- package/tsconfig.json +8 -15
|
@@ -4,277 +4,298 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import fs from "fs";
|
|
7
|
-
import { assert } from "@fluidframework/
|
|
7
|
+
import { assert } from "@fluidframework/core-utils";
|
|
8
|
+
import { IDocumentService } from "@fluidframework/driver-definitions";
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
ISequencedDocumentMessage,
|
|
14
|
-
MessageType,
|
|
15
|
-
ScopeType,
|
|
10
|
+
IClient,
|
|
11
|
+
ISequencedDocumentMessage,
|
|
12
|
+
MessageType,
|
|
13
|
+
ScopeType,
|
|
16
14
|
} from "@fluidframework/protocol-definitions";
|
|
17
15
|
import { printMessageStats } from "./fluidAnalyzeMessages";
|
|
18
16
|
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
connectToWebSocket,
|
|
18
|
+
dumpMessages,
|
|
19
|
+
dumpMessageStats,
|
|
20
|
+
overWrite,
|
|
21
|
+
paramActualFormatting,
|
|
22
|
+
messageTypeFilter,
|
|
25
23
|
} from "./fluidFetchArgs";
|
|
26
24
|
|
|
27
25
|
function filenameFromIndex(index: number): string {
|
|
28
|
-
|
|
26
|
+
return index === 0 ? "" : index.toString(); // support old tools...
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
let firstAvailableDelta = 1;
|
|
32
30
|
async function* loadAllSequencedMessages(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
documentService?: IDocumentService,
|
|
32
|
+
dir?: string,
|
|
33
|
+
files?: string[],
|
|
34
|
+
) {
|
|
35
|
+
let lastSeq = 0;
|
|
36
|
+
// flag for mismatch between last sequence number read and new one to be read
|
|
37
|
+
let seqNumMismatch = false;
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
39
|
+
// If we have local save, read ops from there first
|
|
40
|
+
if (files !== undefined) {
|
|
41
|
+
for (let i = 0; i < files.length; i++) {
|
|
42
|
+
const file = filenameFromIndex(i);
|
|
43
|
+
try {
|
|
44
|
+
console.log(`reading messages${file}.json`);
|
|
45
|
+
const fileContent = fs.readFileSync(`${dir}/messages${file}.json`, {
|
|
46
|
+
encoding: "utf-8",
|
|
47
|
+
});
|
|
48
|
+
const messages: ISequencedDocumentMessage[] = JSON.parse(fileContent);
|
|
49
|
+
// check if there is mismatch
|
|
50
|
+
seqNumMismatch = messages[0].sequenceNumber !== lastSeq + 1;
|
|
51
|
+
assert(
|
|
52
|
+
!seqNumMismatch,
|
|
53
|
+
0x1b9 /* "Unexpected value for sequence number of first message in file" */,
|
|
54
|
+
);
|
|
55
|
+
lastSeq = messages[messages.length - 1].sequenceNumber;
|
|
56
|
+
yield messages;
|
|
57
|
+
} catch (e) {
|
|
58
|
+
if (seqNumMismatch) {
|
|
59
|
+
if (overWrite) {
|
|
60
|
+
// with overWrite option on, we will delete all exisintg message.json files
|
|
61
|
+
for (let index = 0; index < files.length; index++) {
|
|
62
|
+
const name = filenameFromIndex(index);
|
|
63
|
+
fs.unlinkSync(`${dir}/messages${name}.json`);
|
|
64
|
+
}
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
// prompt user to back up and delete existing files
|
|
68
|
+
console.error(
|
|
69
|
+
"There are deleted ops in the document being requested," +
|
|
70
|
+
" please back up the existing messages.json file and delete it from its directory." +
|
|
71
|
+
" Then try fetch tool again.",
|
|
72
|
+
);
|
|
73
|
+
console.error(e);
|
|
74
|
+
return;
|
|
75
|
+
} else {
|
|
76
|
+
console.error(`Error reading / parsing messages from ${files}`);
|
|
77
|
+
console.error(e);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (lastSeq !== 0) {
|
|
83
|
+
console.log(`Read ${lastSeq} ops from local cache`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
80
86
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
if (!documentService) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
84
90
|
|
|
85
|
-
|
|
91
|
+
const deltaStorage = await documentService.connectToDeltaStorage();
|
|
86
92
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
let timeStart = Date.now();
|
|
94
|
+
let requests = 0;
|
|
95
|
+
let opsStorage = 0;
|
|
90
96
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
lastSeq + 1,
|
|
94
|
-
lastSeq + 2);
|
|
97
|
+
// reading only 1 op to test if there is mismatch
|
|
98
|
+
const teststream = deltaStorage.fetchMessages(lastSeq + 1, lastSeq + 2);
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
let statusCode;
|
|
101
|
+
let innerMostErrorCode;
|
|
102
|
+
let response;
|
|
99
103
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
104
|
+
try {
|
|
105
|
+
await teststream.read();
|
|
106
|
+
} catch (error: any) {
|
|
107
|
+
statusCode = error.getTelemetryProperties().statusCode;
|
|
108
|
+
innerMostErrorCode = error.getTelemetryProperties().innerMostErrorCode;
|
|
109
|
+
// if there is gap between ops, catch the error and check it is the error we need
|
|
110
|
+
if (statusCode !== 410 || innerMostErrorCode !== "fluidDeltaDataNotAvailable") {
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
// get firstAvailableDelta from the error response, and set current sequence number to that
|
|
114
|
+
response = JSON.parse(error.getTelemetryProperties().response);
|
|
115
|
+
firstAvailableDelta = response.error.firstAvailableDelta;
|
|
116
|
+
lastSeq = firstAvailableDelta - 1;
|
|
117
|
+
}
|
|
114
118
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
// continue reading rest of the ops
|
|
120
|
+
const stream = deltaStorage.fetchMessages(
|
|
121
|
+
lastSeq + 1, // inclusive left
|
|
122
|
+
undefined, // to
|
|
123
|
+
);
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
while (true) {
|
|
126
|
+
const result = await stream.read();
|
|
127
|
+
if (result.done) {
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
130
|
+
requests++;
|
|
131
|
+
const messages = result.value;
|
|
128
132
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
133
|
+
// Empty buckets should never be returned
|
|
134
|
+
assert(messages.length !== 0, 0x1ba /* "should not return empty buckets" */);
|
|
135
|
+
// console.log(`Loaded ops at ${messages[0].sequenceNumber}`);
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
137
|
+
// This parsing of message contents happens in delta manager. But when we analyze messages
|
|
138
|
+
// for message stats, we skip that path. So parsing of json contents needs to happen here.
|
|
139
|
+
for (const message of messages) {
|
|
140
|
+
if (
|
|
141
|
+
typeof message.contents === "string" &&
|
|
142
|
+
message.contents !== "" &&
|
|
143
|
+
message.type !== MessageType.ClientLeave
|
|
144
|
+
) {
|
|
145
|
+
message.contents = JSON.parse(message.contents);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
143
148
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
149
|
+
opsStorage += messages.length;
|
|
150
|
+
lastSeq = messages[messages.length - 1].sequenceNumber;
|
|
151
|
+
yield messages;
|
|
152
|
+
}
|
|
148
153
|
|
|
149
|
-
|
|
150
|
-
|
|
154
|
+
console.log(
|
|
155
|
+
`\n${Math.floor(
|
|
156
|
+
(Date.now() - timeStart) / 1000,
|
|
157
|
+
)} seconds to retrieve ${opsStorage} ops in ${requests} requests`,
|
|
158
|
+
);
|
|
151
159
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
160
|
+
if (connectToWebSocket) {
|
|
161
|
+
let logMsg = "";
|
|
162
|
+
const client: IClient = {
|
|
163
|
+
mode: "write",
|
|
164
|
+
permission: [],
|
|
165
|
+
scopes: [ScopeType.DocRead, ScopeType.DocWrite, ScopeType.SummaryWrite],
|
|
166
|
+
details: {
|
|
167
|
+
capabilities: { interactive: true },
|
|
168
|
+
},
|
|
169
|
+
user: { id: "blah" },
|
|
170
|
+
};
|
|
171
|
+
console.log("Retrieving messages from web socket");
|
|
172
|
+
timeStart = Date.now();
|
|
173
|
+
const deltaStream = await documentService.connectToDeltaStream(client);
|
|
174
|
+
const initialMessages = deltaStream.initialMessages;
|
|
175
|
+
deltaStream.dispose();
|
|
176
|
+
console.log(
|
|
177
|
+
`${Math.floor((Date.now() - timeStart) / 1000)} seconds to connect to web socket`,
|
|
178
|
+
);
|
|
169
179
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
180
|
+
if (initialMessages !== undefined) {
|
|
181
|
+
const lastSequenceNumber = lastSeq;
|
|
182
|
+
const filtered = initialMessages.filter((a) => a.sequenceNumber > lastSequenceNumber);
|
|
183
|
+
const sorted = filtered.sort((a, b) => a.sequenceNumber - b.sequenceNumber);
|
|
184
|
+
lastSeq = sorted[sorted.length - 1].sequenceNumber;
|
|
185
|
+
logMsg = ` (${opsStorage} delta storage, ${
|
|
186
|
+
initialMessages.length
|
|
187
|
+
} initial ws messages, ${initialMessages.length - sorted.length} dup)`;
|
|
188
|
+
yield sorted;
|
|
189
|
+
}
|
|
190
|
+
console.log(`${lastSeq} total messages${logMsg}`);
|
|
191
|
+
}
|
|
181
192
|
}
|
|
182
193
|
|
|
183
194
|
async function* saveOps(
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
195
|
+
gen, // AsyncGenerator<ISequencedDocumentMessage[]>,
|
|
196
|
+
dir: string,
|
|
197
|
+
files: string[],
|
|
198
|
+
) {
|
|
199
|
+
// Split into 100K ops
|
|
200
|
+
const chunk = 100 * 1000;
|
|
189
201
|
|
|
190
|
-
|
|
202
|
+
let sequencedMessages: ISequencedDocumentMessage[] = [];
|
|
191
203
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
204
|
+
// Figure out first file we want to write to
|
|
205
|
+
let index = 0;
|
|
206
|
+
let curr: number = 1;
|
|
207
|
+
if (files.length !== 0) {
|
|
208
|
+
index = files.length - 1;
|
|
209
|
+
const name = filenameFromIndex(index);
|
|
210
|
+
const fileContent = fs.readFileSync(`${dir}/messages${name}.json`, { encoding: "utf-8" });
|
|
211
|
+
const messages: ISequencedDocumentMessage[] = JSON.parse(fileContent);
|
|
212
|
+
curr = messages[0].sequenceNumber;
|
|
213
|
+
}
|
|
202
214
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
215
|
+
while (true) {
|
|
216
|
+
const result: IteratorResult<ISequencedDocumentMessage[]> = await gen.next();
|
|
217
|
+
if (files.length === 0) {
|
|
218
|
+
curr = firstAvailableDelta;
|
|
219
|
+
}
|
|
220
|
+
if (result.done !== true) {
|
|
221
|
+
let messages = result.value;
|
|
222
|
+
yield messages;
|
|
223
|
+
if (messages[messages.length - 1].sequenceNumber < curr) {
|
|
224
|
+
// Nothing interesting.
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (messages[0].sequenceNumber < curr) {
|
|
228
|
+
messages = messages.filter((msg) => msg.sequenceNumber >= curr);
|
|
229
|
+
}
|
|
230
|
+
sequencedMessages = sequencedMessages.concat(messages);
|
|
231
|
+
assert(
|
|
232
|
+
sequencedMessages[0].sequenceNumber === curr,
|
|
233
|
+
0x1bb /* "Unexpected sequence number on first of messages to save" */,
|
|
234
|
+
);
|
|
235
|
+
assert(
|
|
236
|
+
sequencedMessages[sequencedMessages.length - 1].sequenceNumber ===
|
|
237
|
+
curr + sequencedMessages.length - 1,
|
|
238
|
+
0x1bc /* "Unexpected sequence number on last of messages to save" */,
|
|
239
|
+
);
|
|
240
|
+
}
|
|
225
241
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
242
|
+
// Time to write it out?
|
|
243
|
+
while (
|
|
244
|
+
sequencedMessages.length >= chunk ||
|
|
245
|
+
(result.done === true && sequencedMessages.length !== 0)
|
|
246
|
+
) {
|
|
247
|
+
const name = filenameFromIndex(index);
|
|
248
|
+
const write = sequencedMessages.splice(0, chunk);
|
|
249
|
+
console.log(`writing messages${name}.json`);
|
|
250
|
+
fs.writeFileSync(
|
|
251
|
+
`${dir}/messages${name}.json`,
|
|
252
|
+
JSON.stringify(write, undefined, paramActualFormatting ? 0 : 2),
|
|
253
|
+
);
|
|
254
|
+
// increment curr by chunk
|
|
255
|
+
curr += chunk;
|
|
256
|
+
assert(
|
|
257
|
+
sequencedMessages.length === 0 || sequencedMessages[0].sequenceNumber === curr,
|
|
258
|
+
0x1bd /* "Stopped writing at unexpected sequence number" */,
|
|
259
|
+
);
|
|
260
|
+
index++;
|
|
261
|
+
}
|
|
240
262
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
263
|
+
if (result.done === true) {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
245
267
|
}
|
|
246
268
|
|
|
247
269
|
export async function fluidFetchMessages(documentService?: IDocumentService, saveDir?: string) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
270
|
+
const messageStats = dumpMessageStats || dumpMessages;
|
|
271
|
+
if (!messageStats && (saveDir === undefined || documentService === undefined)) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
252
274
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
275
|
+
const files =
|
|
276
|
+
saveDir === undefined
|
|
277
|
+
? undefined
|
|
278
|
+
: fs
|
|
279
|
+
.readdirSync(saveDir)
|
|
280
|
+
.filter((file) => {
|
|
281
|
+
if (!file.startsWith("messages")) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
return true;
|
|
285
|
+
})
|
|
286
|
+
.sort((a, b) => a.localeCompare(b));
|
|
263
287
|
|
|
264
|
-
|
|
288
|
+
let generator = loadAllSequencedMessages(documentService, saveDir, files);
|
|
265
289
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
290
|
+
if (saveDir !== undefined && files !== undefined && documentService) {
|
|
291
|
+
generator = saveOps(generator, saveDir, files);
|
|
292
|
+
}
|
|
269
293
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
let item;
|
|
278
|
-
for await (item of generator) { }
|
|
279
|
-
}
|
|
294
|
+
if (messageStats) {
|
|
295
|
+
return printMessageStats(generator, dumpMessageStats, dumpMessages, messageTypeFilter);
|
|
296
|
+
} else {
|
|
297
|
+
let item;
|
|
298
|
+
for await (item of generator) {
|
|
299
|
+
}
|
|
300
|
+
}
|
|
280
301
|
}
|