@fluid-experimental/pact-map 2.0.0-internal.3.0.1 → 2.0.0-internal.3.1.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 +18 -19
- package/.mocharc.js +2 -2
- package/api-extractor.json +2 -2
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.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/pactMap.d.ts.map +1 -1
- package/dist/pactMap.js +14 -15
- package/dist/pactMap.js.map +1 -1
- package/dist/pactMapFactory.d.ts.map +1 -1
- package/dist/pactMapFactory.js.map +1 -1
- package/lib/interfaces.d.ts.map +1 -1
- package/lib/interfaces.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/pactMap.d.ts.map +1 -1
- package/lib/pactMap.js +15 -16
- package/lib/pactMap.js.map +1 -1
- package/lib/pactMapFactory.d.ts.map +1 -1
- package/lib/pactMapFactory.js.map +1 -1
- package/package.json +96 -96
- package/prettier.config.cjs +1 -1
- package/src/interfaces.ts +33 -33
- package/src/packageVersion.ts +1 -1
- package/src/pactMap.ts +403 -385
- package/src/pactMapFactory.ts +34 -33
- package/tsconfig.esnext.json +5 -5
- package/tsconfig.json +8 -12
package/src/pactMap.ts
CHANGED
|
@@ -10,14 +10,18 @@ import { EventEmitter } from "events";
|
|
|
10
10
|
import { assert } from "@fluidframework/common-utils";
|
|
11
11
|
import { ISequencedDocumentMessage, MessageType } from "@fluidframework/protocol-definitions";
|
|
12
12
|
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
IChannelAttributes,
|
|
14
|
+
IFluidDataStoreRuntime,
|
|
15
|
+
IChannelStorageService,
|
|
16
|
+
IChannelFactory,
|
|
17
17
|
} from "@fluidframework/datastore-definitions";
|
|
18
18
|
import { ISummaryTreeWithStats } from "@fluidframework/runtime-definitions";
|
|
19
19
|
import { readAndParse } from "@fluidframework/driver-utils";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
createSingleBlobSummary,
|
|
22
|
+
IFluidSerializer,
|
|
23
|
+
SharedObject,
|
|
24
|
+
} from "@fluidframework/shared-object-base";
|
|
21
25
|
import { PactMapFactory } from "./pactMapFactory";
|
|
22
26
|
import { IPactMap, IPactMapEvents } from "./interfaces";
|
|
23
27
|
|
|
@@ -25,69 +29,69 @@ import { IPactMap, IPactMapEvents } from "./interfaces";
|
|
|
25
29
|
* The accepted pact information, if any.
|
|
26
30
|
*/
|
|
27
31
|
interface IAcceptedPact<T> {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The accepted value of the given type or undefined (typically in case of delete).
|
|
34
|
+
*/
|
|
35
|
+
value: T | undefined;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The sequence number when the value was accepted, which will normally coincide with one of three possibilities:
|
|
39
|
+
* - The sequence number of the "accept" op from the final client we expected signoff from
|
|
40
|
+
* - The sequence number of the ClientLeave of the final client we expected signoff from
|
|
41
|
+
* - The sequence number of the "set" op, if there were no expected signoffs (i.e. only the submitting client
|
|
42
|
+
* was connected when the op was sequenced)
|
|
43
|
+
*
|
|
44
|
+
* For values set in detached state, it will be 0.
|
|
45
|
+
*/
|
|
46
|
+
sequenceNumber: number;
|
|
43
47
|
}
|
|
44
48
|
|
|
45
49
|
/**
|
|
46
50
|
* The pending pact information, if any.
|
|
47
51
|
*/
|
|
48
52
|
interface IPendingPact<T> {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
/**
|
|
54
|
+
* The pending value of the given type or undefined (typically in case of delete).
|
|
55
|
+
*/
|
|
56
|
+
value: T | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* The list of clientIds that we expect "accept" ops from. Clients are also removed from this list if they
|
|
59
|
+
* disconnect without accepting. When this list empties, the pending value transitions to accepted.
|
|
60
|
+
*/
|
|
61
|
+
expectedSignoffs: string[];
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
/**
|
|
61
65
|
* Internal format of the values stored in the PactMap.
|
|
62
66
|
*/
|
|
63
67
|
type Pact<T> =
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
| { accepted: IAcceptedPact<T>; pending: undefined }
|
|
69
|
+
| { accepted: undefined; pending: IPendingPact<T> }
|
|
70
|
+
| { accepted: IAcceptedPact<T>; pending: IPendingPact<T> };
|
|
67
71
|
|
|
68
72
|
/**
|
|
69
73
|
* PactMap operation formats
|
|
70
74
|
*/
|
|
71
75
|
interface IPactMapSetOperation<T> {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
76
|
+
type: "set";
|
|
77
|
+
key: string;
|
|
78
|
+
value: T | undefined;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A "set" is only valid if it is made with knowledge of the most-recent accepted proposal - its reference
|
|
82
|
+
* sequence number is greater than or equal to the sequence number when that prior value was accepted.
|
|
83
|
+
*
|
|
84
|
+
* However, we can't trust the built-in referenceSequenceNumber of the op because of resubmit on reconnect,
|
|
85
|
+
* which will update the referenceSequenceNumber on our behalf.
|
|
86
|
+
*
|
|
87
|
+
* Instead we need to separately stamp the real reference sequence number on the op itself.
|
|
88
|
+
*/
|
|
89
|
+
refSeq: number;
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
interface IPactMapAcceptOperation {
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
type: "accept";
|
|
94
|
+
key: string;
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
type IPactMapOperation<T> = IPactMapSetOperation<T> | IPactMapAcceptOperation;
|
|
@@ -153,341 +157,355 @@ const snapshotFileName = "header";
|
|
|
153
157
|
* ```
|
|
154
158
|
*/
|
|
155
159
|
export class PactMap<T = unknown> extends SharedObject<IPactMapEvents> implements IPactMap<T> {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Create a new PactMap
|
|
162
|
+
*
|
|
163
|
+
* @param runtime - data store runtime the new PactMap belongs to
|
|
164
|
+
* @param id - optional name of the PactMap
|
|
165
|
+
* @returns newly created PactMap (but not attached yet)
|
|
166
|
+
*/
|
|
167
|
+
public static create(runtime: IFluidDataStoreRuntime, id?: string): PactMap {
|
|
168
|
+
return runtime.createChannel(id, PactMapFactory.Type) as PactMap;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Get a factory for PactMap to register with the data store.
|
|
173
|
+
*
|
|
174
|
+
* @returns a factory that creates and loads PactMaps
|
|
175
|
+
*/
|
|
176
|
+
public static getFactory(): IChannelFactory {
|
|
177
|
+
return new PactMapFactory();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private readonly values: Map<string, Pact<T>> = new Map();
|
|
181
|
+
|
|
182
|
+
private readonly incomingOp: EventEmitter = new EventEmitter();
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Constructs a new PactMap. If the object is non-local an id and service interfaces will
|
|
186
|
+
* be provided
|
|
187
|
+
*
|
|
188
|
+
* @param runtime - data store runtime the PactMap belongs to
|
|
189
|
+
* @param id - optional name of the PactMap
|
|
190
|
+
*/
|
|
191
|
+
public constructor(
|
|
192
|
+
id: string,
|
|
193
|
+
runtime: IFluidDataStoreRuntime,
|
|
194
|
+
attributes: IChannelAttributes,
|
|
195
|
+
) {
|
|
196
|
+
super(id, runtime, attributes, "fluid_pactMap_");
|
|
197
|
+
|
|
198
|
+
this.incomingOp.on("set", this.handleIncomingSet);
|
|
199
|
+
this.incomingOp.on("accept", this.handleIncomingAccept);
|
|
200
|
+
|
|
201
|
+
this.runtime.getQuorum().on("removeMember", this.handleQuorumRemoveMember);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* {@inheritDoc IPactMap.get}
|
|
206
|
+
*/
|
|
207
|
+
public get(key: string): T | undefined {
|
|
208
|
+
return this.values.get(key)?.accepted?.value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* {@inheritDoc IPactMap.isPending}
|
|
213
|
+
*/
|
|
214
|
+
public isPending(key: string): boolean {
|
|
215
|
+
return this.values.get(key)?.pending !== undefined;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* {@inheritDoc IPactMap.getPending}
|
|
220
|
+
*/
|
|
221
|
+
public getPending(key: string): T | undefined {
|
|
222
|
+
return this.values.get(key)?.pending?.value;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* {@inheritDoc IPactMap.set}
|
|
227
|
+
*/
|
|
228
|
+
public set(key: string, value: T | undefined): void {
|
|
229
|
+
const currentValue = this.values.get(key);
|
|
230
|
+
// Early-exit if we can't submit a valid proposal (there's already a pending proposal)
|
|
231
|
+
if (currentValue?.pending !== undefined) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// If not attached, we basically pretend we got an ack immediately.
|
|
236
|
+
if (!this.isAttached()) {
|
|
237
|
+
// Queueing as a microtask to permit callers to complete their callstacks before the result of the set
|
|
238
|
+
// takes effect. This more closely resembles the pattern in the attached state, where the ack will not
|
|
239
|
+
// be received synchronously.
|
|
240
|
+
queueMicrotask(() => {
|
|
241
|
+
this.handleIncomingSet(key, value, 0 /* refSeq */, 0 /* setSequenceNumber */);
|
|
242
|
+
});
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const setOp: IPactMapSetOperation<T> = {
|
|
247
|
+
type: "set",
|
|
248
|
+
key,
|
|
249
|
+
value,
|
|
250
|
+
refSeq: this.runtime.deltaManager.lastSequenceNumber,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
this.submitLocalMessage(setOp);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* {@inheritDoc IPactMap.delete}
|
|
258
|
+
*/
|
|
259
|
+
public delete(key: string): void {
|
|
260
|
+
const currentValue = this.values.get(key);
|
|
261
|
+
// Early-exit if:
|
|
262
|
+
if (
|
|
263
|
+
// there's nothing to delete
|
|
264
|
+
currentValue === undefined ||
|
|
265
|
+
// if something is pending (and so our proposal won't be valid)
|
|
266
|
+
currentValue.pending !== undefined ||
|
|
267
|
+
// or if the accepted value is undefined which is equivalent to already being deleted
|
|
268
|
+
currentValue.accepted.value === undefined
|
|
269
|
+
) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
this.set(key, undefined);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Get a point-in-time list of clients who must sign off on values coming in for them to move from "pending" to
|
|
278
|
+
* "accepted" state. This list is finalized for a value at the moment it goes pending (i.e. if more clients
|
|
279
|
+
* join later, they are not added to the list of signoffs).
|
|
280
|
+
* @returns The list of clientIds for clients who must sign off to accept the incoming pending value
|
|
281
|
+
*/
|
|
282
|
+
private getSignoffClients(): string[] {
|
|
283
|
+
// If detached, we don't need anyone to sign off. Otherwise, we need all currently connected clients.
|
|
284
|
+
return this.isAttached() ? [...this.runtime.getQuorum().getMembers().keys()] : [];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private readonly handleIncomingSet = (
|
|
288
|
+
key: string,
|
|
289
|
+
value: T | undefined,
|
|
290
|
+
refSeq: number,
|
|
291
|
+
setSequenceNumber: number,
|
|
292
|
+
): void => {
|
|
293
|
+
const currentValue = this.values.get(key);
|
|
294
|
+
// We use a consensus-like approach here, so a proposal is valid if the value is unset or if there is no
|
|
295
|
+
// pending change and it was made with knowledge of the most recently accepted value. We'll drop invalid
|
|
296
|
+
// proposals on the ground.
|
|
297
|
+
const proposalValid =
|
|
298
|
+
currentValue === undefined ||
|
|
299
|
+
(currentValue.pending === undefined && currentValue.accepted.sequenceNumber <= refSeq);
|
|
300
|
+
if (!proposalValid) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const accepted = currentValue?.accepted;
|
|
305
|
+
|
|
306
|
+
// We expect signoffs from all connected clients at the time the set was sequenced (including the client who
|
|
307
|
+
// sent the set).
|
|
308
|
+
const expectedSignoffs = this.getSignoffClients();
|
|
309
|
+
|
|
310
|
+
const newPact: Pact<T> = {
|
|
311
|
+
accepted,
|
|
312
|
+
pending: {
|
|
313
|
+
value,
|
|
314
|
+
expectedSignoffs,
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
this.values.set(key, newPact);
|
|
319
|
+
|
|
320
|
+
this.emit("pending", key);
|
|
321
|
+
|
|
322
|
+
if (expectedSignoffs.length === 0) {
|
|
323
|
+
// At least the submitting client should be amongst the expectedSignoffs, but keeping this check around
|
|
324
|
+
// as extra protection and in case we bring back the "submitting client implicitly accepts" optimization.
|
|
325
|
+
this.values.set(key, {
|
|
326
|
+
accepted: { value, sequenceNumber: setSequenceNumber },
|
|
327
|
+
pending: undefined,
|
|
328
|
+
});
|
|
329
|
+
this.emit("accepted", key);
|
|
330
|
+
} else if (
|
|
331
|
+
this.runtime.clientId !== undefined &&
|
|
332
|
+
expectedSignoffs.includes(this.runtime.clientId)
|
|
333
|
+
) {
|
|
334
|
+
// Emit an accept upon a new key entering pending state if our accept is expected.
|
|
335
|
+
const acceptOp: IPactMapAcceptOperation = {
|
|
336
|
+
type: "accept",
|
|
337
|
+
key,
|
|
338
|
+
};
|
|
339
|
+
this.submitLocalMessage(acceptOp);
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
private readonly handleIncomingAccept = (
|
|
344
|
+
key: string,
|
|
345
|
+
clientId: string,
|
|
346
|
+
sequenceNumber: number,
|
|
347
|
+
): void => {
|
|
348
|
+
const pending = this.values.get(key)?.pending;
|
|
349
|
+
// We don't resubmit accepts on reconnect so this should only run for expected accepts.
|
|
350
|
+
assert(pending !== undefined, 0x2f8 /* Unexpected accept op, nothing pending */);
|
|
351
|
+
assert(
|
|
352
|
+
pending.expectedSignoffs.includes(clientId),
|
|
353
|
+
0x2f9 /* Unexpected accept op, client not in expectedSignoffs */,
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
// Remove the client from the expected signoffs
|
|
357
|
+
pending.expectedSignoffs = pending.expectedSignoffs.filter(
|
|
358
|
+
(expectedClientId) => expectedClientId !== clientId,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
if (pending.expectedSignoffs.length === 0) {
|
|
362
|
+
// The pending value has settled
|
|
363
|
+
this.values.set(key, {
|
|
364
|
+
accepted: { value: pending.value, sequenceNumber },
|
|
365
|
+
pending: undefined,
|
|
366
|
+
});
|
|
367
|
+
this.emit("accepted", key);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
private readonly handleQuorumRemoveMember = (clientId: string): void => {
|
|
372
|
+
for (const [key, { pending }] of this.values) {
|
|
373
|
+
if (pending !== undefined) {
|
|
374
|
+
pending.expectedSignoffs = pending.expectedSignoffs.filter(
|
|
375
|
+
(expectedClientId) => expectedClientId !== clientId,
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
if (pending.expectedSignoffs.length === 0) {
|
|
379
|
+
// The pending value has settled
|
|
380
|
+
this.values.set(key, {
|
|
381
|
+
accepted: {
|
|
382
|
+
value: pending.value,
|
|
383
|
+
// The sequence number of the ClientLeave message.
|
|
384
|
+
sequenceNumber: this.runtime.deltaManager.lastSequenceNumber,
|
|
385
|
+
},
|
|
386
|
+
pending: undefined,
|
|
387
|
+
});
|
|
388
|
+
this.emit("accepted", key);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Create a summary for the PactMap
|
|
396
|
+
*
|
|
397
|
+
* @returns the summary of the current state of the PactMap
|
|
398
|
+
* @internal
|
|
399
|
+
*/
|
|
400
|
+
protected summarizeCore(serializer: IFluidSerializer): ISummaryTreeWithStats {
|
|
401
|
+
const allEntries = [...this.values.entries()];
|
|
402
|
+
// Filter out items that are ineffectual
|
|
403
|
+
const summaryEntries = allEntries.filter(([, pact]) => {
|
|
404
|
+
return (
|
|
405
|
+
// Items have an effect if they are still pending, have a real value, or some client may try to
|
|
406
|
+
// reference state before the value was accepted. Otherwise they can be dropped.
|
|
407
|
+
pact.pending !== undefined ||
|
|
408
|
+
pact.accepted.value !== undefined ||
|
|
409
|
+
pact.accepted.sequenceNumber > this.runtime.deltaManager.minimumSequenceNumber
|
|
410
|
+
);
|
|
411
|
+
});
|
|
412
|
+
return createSingleBlobSummary(snapshotFileName, JSON.stringify(summaryEntries));
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* {@inheritDoc @fluidframework/shared-object-base#SharedObject.loadCore}
|
|
417
|
+
* @internal
|
|
418
|
+
*/
|
|
419
|
+
protected async loadCore(storage: IChannelStorageService): Promise<void> {
|
|
420
|
+
const content = await readAndParse<[string, Pact<T>][]>(storage, snapshotFileName);
|
|
421
|
+
for (const [key, value] of content) {
|
|
422
|
+
this.values.set(key, value);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.initializeLocalCore}
|
|
428
|
+
* @internal
|
|
429
|
+
*/
|
|
430
|
+
protected initializeLocalCore(): void {}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.onDisconnect}
|
|
434
|
+
* @internal
|
|
435
|
+
*/
|
|
436
|
+
protected onDisconnect(): void {}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* {@inheritDoc @fluidframework/shared-object-base#SharedObjectCore.reSubmitCore}
|
|
440
|
+
* @internal
|
|
441
|
+
*/
|
|
442
|
+
protected reSubmitCore(content: unknown, localOpMetadata: unknown): void {
|
|
443
|
+
const pactMapOp = content as IPactMapOperation<T>;
|
|
444
|
+
// Filter out accept messages - if we're coming back from a disconnect, our acceptance is never required
|
|
445
|
+
// because we're implicitly removed from the list of expected accepts.
|
|
446
|
+
if (pactMapOp.type === "accept") {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Filter out set messages that have no chance of being accepted because there's another value pending
|
|
451
|
+
// or another value was accepted while we were disconnected.
|
|
452
|
+
const currentValue = this.values.get(pactMapOp.key);
|
|
453
|
+
if (
|
|
454
|
+
currentValue !== undefined &&
|
|
455
|
+
(currentValue.pending !== undefined ||
|
|
456
|
+
pactMapOp.refSeq < currentValue.accepted?.sequenceNumber)
|
|
457
|
+
) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// Otherwise we can resubmit
|
|
462
|
+
this.submitLocalMessage(pactMapOp, localOpMetadata);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Process a PactMap operation
|
|
467
|
+
*
|
|
468
|
+
* @param message - the message to prepare
|
|
469
|
+
* @param local - whether the message was sent by the local client
|
|
470
|
+
* @param localOpMetadata - For local client messages, this is the metadata that was submitted with the message.
|
|
471
|
+
* For messages from a remote client, this will be undefined.
|
|
472
|
+
* @internal
|
|
473
|
+
*/
|
|
474
|
+
protected processCore(
|
|
475
|
+
message: ISequencedDocumentMessage,
|
|
476
|
+
local: boolean,
|
|
477
|
+
localOpMetadata: unknown,
|
|
478
|
+
): void {
|
|
479
|
+
if (message.type === MessageType.Operation) {
|
|
480
|
+
const op = message.contents as IPactMapOperation<T>;
|
|
481
|
+
|
|
482
|
+
switch (op.type) {
|
|
483
|
+
case "set":
|
|
484
|
+
this.incomingOp.emit(
|
|
485
|
+
"set",
|
|
486
|
+
op.key,
|
|
487
|
+
op.value,
|
|
488
|
+
op.refSeq,
|
|
489
|
+
message.sequenceNumber,
|
|
490
|
+
);
|
|
491
|
+
break;
|
|
492
|
+
|
|
493
|
+
case "accept":
|
|
494
|
+
this.incomingOp.emit(
|
|
495
|
+
"accept",
|
|
496
|
+
op.key,
|
|
497
|
+
message.clientId,
|
|
498
|
+
message.sequenceNumber,
|
|
499
|
+
);
|
|
500
|
+
break;
|
|
501
|
+
|
|
502
|
+
default:
|
|
503
|
+
throw new Error("Unknown operation");
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
public applyStashedOp(): void {
|
|
509
|
+
throw new Error("not implemented");
|
|
510
|
+
}
|
|
493
511
|
}
|