@fluidframework/shared-object-base 2.33.2 → 2.40.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/CHANGELOG.md +14 -0
- package/api-report/shared-object-base.legacy.alpha.api.md +7 -5
- package/dist/gcHandleVisitor.d.ts +1 -1
- package/dist/gcHandleVisitor.d.ts.map +1 -1
- package/dist/gcHandleVisitor.js +1 -1
- package/dist/gcHandleVisitor.js.map +1 -1
- package/dist/handle.d.ts +21 -1
- package/dist/handle.d.ts.map +1 -1
- package/dist/handle.js +10 -1
- package/dist/handle.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.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/serializer.d.ts +3 -2
- package/dist/serializer.d.ts.map +1 -1
- package/dist/serializer.js +4 -2
- package/dist/serializer.js.map +1 -1
- package/dist/sharedObject.d.ts +57 -11
- package/dist/sharedObject.d.ts.map +1 -1
- package/dist/sharedObject.js +72 -16
- package/dist/sharedObject.js.map +1 -1
- package/dist/sharedObjectKernel.d.ts +186 -0
- package/dist/sharedObjectKernel.d.ts.map +1 -0
- package/dist/sharedObjectKernel.js +227 -0
- package/dist/sharedObjectKernel.js.map +1 -0
- package/dist/types.d.ts +13 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/lib/gcHandleVisitor.d.ts +1 -1
- package/lib/gcHandleVisitor.d.ts.map +1 -1
- package/lib/gcHandleVisitor.js +1 -1
- package/lib/gcHandleVisitor.js.map +1 -1
- package/lib/handle.d.ts +21 -1
- package/lib/handle.d.ts.map +1 -1
- package/lib/handle.js +8 -0
- package/lib/handle.js.map +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.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/serializer.d.ts +3 -2
- package/lib/serializer.d.ts.map +1 -1
- package/lib/serializer.js +4 -2
- package/lib/serializer.js.map +1 -1
- package/lib/sharedObject.d.ts +57 -11
- package/lib/sharedObject.d.ts.map +1 -1
- package/lib/sharedObject.js +73 -17
- package/lib/sharedObject.js.map +1 -1
- package/lib/sharedObjectKernel.d.ts +186 -0
- package/lib/sharedObjectKernel.d.ts.map +1 -0
- package/lib/sharedObjectKernel.js +222 -0
- package/lib/sharedObjectKernel.js.map +1 -0
- package/lib/types.d.ts +13 -1
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js.map +1 -1
- package/package.json +15 -14
- package/src/gcHandleVisitor.ts +1 -4
- package/src/handle.ts +29 -1
- package/src/index.ts +11 -0
- package/src/packageVersion.ts +1 -1
- package/src/serializer.ts +7 -4
- package/src/sharedObject.ts +74 -18
- package/src/sharedObjectKernel.ts +467 -0
- package/src/types.ts +13 -1
package/src/sharedObject.ts
CHANGED
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
tagCodeArtifacts,
|
|
52
52
|
type ICustomData,
|
|
53
53
|
type IFluidErrorBase,
|
|
54
|
+
LoggingError,
|
|
54
55
|
} from "@fluidframework/telemetry-utils/internal";
|
|
55
56
|
import { v4 as uuid } from "uuid";
|
|
56
57
|
|
|
@@ -70,7 +71,18 @@ interface ProcessTelemetryProperties {
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
/**
|
|
73
|
-
* Base class from which all shared objects derive.
|
|
74
|
+
* Base class from which all {@link ISharedObject|shared objects} derive.
|
|
75
|
+
* @remarks
|
|
76
|
+
* This class implements common behaviors that implementations of {@link ISharedObject} may want to reuse.
|
|
77
|
+
* Even more such behaviors are implemented in the {@link SharedObject} class.
|
|
78
|
+
* @privateRemarks
|
|
79
|
+
* Currently some documentation (like the above) implies that this is supposed to be the only implementation of ISharedObject, which is both package-exported and not `@sealed`.
|
|
80
|
+
* This situation should be clarified to indicate if other implementations of ISharedObject are allowed and just currently don't exist,
|
|
81
|
+
* or if the intention is that no other implementations should exist and creating some might break things.
|
|
82
|
+
* As part of this, any existing implementations of ISharedObject (via SharedObjectCore or otherwise) in use by legacy API users will need to be considered.
|
|
83
|
+
*
|
|
84
|
+
* TODO:
|
|
85
|
+
* This class should eventually be made internal, as custom subclasses of it outside this repository are intended to be made unsupported in the future.
|
|
74
86
|
* @legacy
|
|
75
87
|
* @alpha
|
|
76
88
|
*/
|
|
@@ -129,14 +141,18 @@ export abstract class SharedObjectCore<
|
|
|
129
141
|
return this._connected;
|
|
130
142
|
}
|
|
131
143
|
|
|
132
|
-
/**
|
|
133
|
-
* @param id - The id of the shared object
|
|
134
|
-
* @param runtime - The IFluidDataStoreRuntime which contains the shared object
|
|
135
|
-
* @param attributes - Attributes of the shared object
|
|
136
|
-
*/
|
|
137
144
|
constructor(
|
|
145
|
+
/**
|
|
146
|
+
* The ID of the shared object.
|
|
147
|
+
*/
|
|
138
148
|
public id: string,
|
|
149
|
+
/**
|
|
150
|
+
* The runtime instance that contains the Shared Object.
|
|
151
|
+
*/
|
|
139
152
|
protected runtime: IFluidDataStoreRuntime,
|
|
153
|
+
/**
|
|
154
|
+
* The attributes of the Shared Object.
|
|
155
|
+
*/
|
|
140
156
|
public readonly attributes: IChannelAttributes,
|
|
141
157
|
) {
|
|
142
158
|
super((event: EventEmitterEventType, e: unknown) =>
|
|
@@ -504,6 +520,27 @@ export abstract class SharedObjectCore<
|
|
|
504
520
|
this.submitLocalMessage(content, localOpMetadata);
|
|
505
521
|
}
|
|
506
522
|
|
|
523
|
+
/**
|
|
524
|
+
* Called when a message has to be resubmitted but its content should be "squashed" if any subsequent pending changes
|
|
525
|
+
* override the content in the fashion described on {@link @fluidframework/datastore-definitions#IDeltaHandler.reSubmit}.
|
|
526
|
+
*
|
|
527
|
+
* @param content - The content of the original message.
|
|
528
|
+
* @param localOpMetadata - The local metadata associated with the original message.
|
|
529
|
+
*/
|
|
530
|
+
protected reSubmitSquashed(content: unknown, localOpMetadata: unknown): void {
|
|
531
|
+
const allowStagingModeWithoutSquashing =
|
|
532
|
+
loggerToMonitoringContext(this.logger).config.getBoolean(
|
|
533
|
+
"Fluid.SharedObject.AllowStagingModeWithoutSquashing",
|
|
534
|
+
) ??
|
|
535
|
+
(this.runtime.options.allowStagingModeWithoutSquashing as boolean | undefined) ??
|
|
536
|
+
true;
|
|
537
|
+
if (allowStagingModeWithoutSquashing) {
|
|
538
|
+
this.reSubmitCore(content, localOpMetadata);
|
|
539
|
+
} else {
|
|
540
|
+
this.throwUnsupported("reSubmitSquashed");
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
507
544
|
/**
|
|
508
545
|
* Promises that are waiting for an ack from the server before resolving should use this instead of new Promise.
|
|
509
546
|
* It ensures that if something changes that will interrupt that ack (e.g. the FluidDataStoreRuntime disposes),
|
|
@@ -550,8 +587,8 @@ export abstract class SharedObjectCore<
|
|
|
550
587
|
setConnectionState: (connected: boolean) => {
|
|
551
588
|
this.setConnectionState(connected);
|
|
552
589
|
},
|
|
553
|
-
reSubmit: (content: unknown, localOpMetadata: unknown) => {
|
|
554
|
-
this.reSubmit(content, localOpMetadata);
|
|
590
|
+
reSubmit: (content: unknown, localOpMetadata: unknown, squash?: boolean) => {
|
|
591
|
+
this.reSubmit(content, localOpMetadata, squash);
|
|
555
592
|
},
|
|
556
593
|
applyStashedOp: (content: unknown): void => {
|
|
557
594
|
this.applyStashedOp(parseHandles(content, this.serializer));
|
|
@@ -668,16 +705,24 @@ export abstract class SharedObjectCore<
|
|
|
668
705
|
* reconnection.
|
|
669
706
|
* @param content - The content of the original message.
|
|
670
707
|
* @param localOpMetadata - The local metadata associated with the original message.
|
|
708
|
+
* @param squash - Optional. If `true`, the message will be resubmitted in a squashed form. If `undefined` or `false`,
|
|
709
|
+
* the legacy behavior (no squashing) will be used. Defaults to `false` for backward compatibility.
|
|
671
710
|
*/
|
|
672
|
-
private reSubmit(content: unknown, localOpMetadata: unknown): void {
|
|
673
|
-
|
|
711
|
+
private reSubmit(content: unknown, localOpMetadata: unknown, squash?: boolean): void {
|
|
712
|
+
// Back-compat: squash argument may not be provided by container-runtime layer.
|
|
713
|
+
// Default to previous behavior (no squash).
|
|
714
|
+
if (squash ?? false) {
|
|
715
|
+
this.reSubmitSquashed(content, localOpMetadata);
|
|
716
|
+
} else {
|
|
717
|
+
this.reSubmitCore(content, localOpMetadata);
|
|
718
|
+
}
|
|
674
719
|
}
|
|
675
720
|
|
|
676
721
|
/**
|
|
677
722
|
* Revert an op
|
|
678
723
|
*/
|
|
679
724
|
protected rollback(content: unknown, localOpMetadata: unknown): void {
|
|
680
|
-
|
|
725
|
+
this.throwUnsupported("rollback");
|
|
681
726
|
}
|
|
682
727
|
|
|
683
728
|
/**
|
|
@@ -726,11 +771,24 @@ export abstract class SharedObjectCore<
|
|
|
726
771
|
private emitInternal(event: EventEmitterEventType, ...args: unknown[]): boolean {
|
|
727
772
|
return super.emit(event, ...args);
|
|
728
773
|
}
|
|
774
|
+
|
|
775
|
+
private throwUnsupported(featureName: string): never {
|
|
776
|
+
throw new LoggingError("Unsupported DDS feature", {
|
|
777
|
+
featureName,
|
|
778
|
+
...tagCodeArtifacts({ ddsType: this.attributes.type }),
|
|
779
|
+
});
|
|
780
|
+
}
|
|
729
781
|
}
|
|
730
782
|
|
|
731
783
|
/**
|
|
732
|
-
*
|
|
733
|
-
*
|
|
784
|
+
* Helper for implementing {@link ISharedObject} with simplified, synchronous summarization and garbage collection.
|
|
785
|
+
* @remarks
|
|
786
|
+
* DDS implementations with async and incremental summarization should extend {@link SharedObjectCore} directly instead.
|
|
787
|
+
* @privateRemarks
|
|
788
|
+
* TODO:
|
|
789
|
+
* This class is badly named.
|
|
790
|
+
* Once it becomes `@internal` "SharedObjectCore" should probably become "SharedObject"
|
|
791
|
+
* and this class should be renamed to something like "SharedObjectSynchronous".
|
|
734
792
|
* @legacy
|
|
735
793
|
* @alpha
|
|
736
794
|
*/
|
|
@@ -762,15 +820,13 @@ export abstract class SharedObject<
|
|
|
762
820
|
return this._serializer;
|
|
763
821
|
}
|
|
764
822
|
|
|
765
|
-
/**
|
|
766
|
-
* @param id - The id of the shared object
|
|
767
|
-
* @param runtime - The IFluidDataStoreRuntime which contains the shared object
|
|
768
|
-
* @param attributes - Attributes of the shared object
|
|
769
|
-
*/
|
|
770
823
|
constructor(
|
|
771
824
|
id: string,
|
|
772
825
|
runtime: IFluidDataStoreRuntime,
|
|
773
826
|
attributes: IChannelAttributes,
|
|
827
|
+
/**
|
|
828
|
+
* The prefix to use for telemetry events emitted by this object.
|
|
829
|
+
*/
|
|
774
830
|
private readonly telemetryContextPrefix: string,
|
|
775
831
|
) {
|
|
776
832
|
super(id, runtime, attributes);
|
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) Microsoft Corporation and contributors. All rights reserved.
|
|
3
|
+
* Licensed under the MIT License.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TypedEventEmitter } from "@fluid-internal/client-utils";
|
|
7
|
+
import type { IFluidLoadable } from "@fluidframework/core-interfaces";
|
|
8
|
+
import { assert, fail } from "@fluidframework/core-utils/internal";
|
|
9
|
+
import {
|
|
10
|
+
IChannelStorageService,
|
|
11
|
+
type IChannel,
|
|
12
|
+
type IChannelAttributes,
|
|
13
|
+
type IChannelFactory,
|
|
14
|
+
type IChannelServices,
|
|
15
|
+
type IFluidDataStoreRuntime,
|
|
16
|
+
} from "@fluidframework/datastore-definitions/internal";
|
|
17
|
+
import type { IIdCompressor } from "@fluidframework/id-compressor/internal";
|
|
18
|
+
import {
|
|
19
|
+
ISummaryTreeWithStats,
|
|
20
|
+
ITelemetryContext,
|
|
21
|
+
type IExperimentalIncrementalSummaryContext,
|
|
22
|
+
type IRuntimeMessageCollection,
|
|
23
|
+
} from "@fluidframework/runtime-definitions/internal";
|
|
24
|
+
import type { ITelemetryLoggerExt } from "@fluidframework/telemetry-utils/internal";
|
|
25
|
+
|
|
26
|
+
import { IFluidSerializer } from "./serializer.js";
|
|
27
|
+
import {
|
|
28
|
+
createSharedObjectKind,
|
|
29
|
+
SharedObject,
|
|
30
|
+
type ISharedObjectKind,
|
|
31
|
+
type SharedObjectKind,
|
|
32
|
+
} from "./sharedObject.js";
|
|
33
|
+
import { ISharedObjectEvents, type ISharedObject } from "./types.js";
|
|
34
|
+
import type { IChannelView } from "./utils.js";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Functionality specific to a particular kind of {@link ISharedObject}.
|
|
38
|
+
* @remarks
|
|
39
|
+
* Shared objects expose APIs for two consumers:
|
|
40
|
+
*
|
|
41
|
+
* 1. The runtime, which uses {@link @fluidframework/datastore-definitions#IChannel} to summarize and apply ops and {@link @fluidframework/datastore-definitions#IChannelFactory} to create the load summaries.
|
|
42
|
+
*
|
|
43
|
+
* 2. The app, which uses shared object kind specific APIs to read and write data.
|
|
44
|
+
*
|
|
45
|
+
* There is some common functionality all shared objects use, provided by {@link SharedObject} and {@link SharedObjectCore}.
|
|
46
|
+
* SharedKernel describes the portion of the behavior required by the runtime which
|
|
47
|
+
* differs between different kinds of shared objects.
|
|
48
|
+
*
|
|
49
|
+
* {@link makeSharedObjectKind} is then used to wrap up the kernel into a full {@link ISharedObject} implementation.
|
|
50
|
+
* The runtime specific APIs are then type erased into a {@link SharedObjectKind}.
|
|
51
|
+
* @privateRemarks
|
|
52
|
+
* Unlike the `SharedObject` class, this interface is internal, and thus can be adjusted more easily.
|
|
53
|
+
* Therefore this interface is not intended to address all needs, and will likely need small changes as it gets more adoption.
|
|
54
|
+
*
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
export interface SharedKernel {
|
|
58
|
+
/**
|
|
59
|
+
* {@inheritDoc SharedObject.summarizeCore}
|
|
60
|
+
*/
|
|
61
|
+
summarizeCore(
|
|
62
|
+
serializer: IFluidSerializer,
|
|
63
|
+
telemetryContext: ITelemetryContext | undefined,
|
|
64
|
+
incrementalSummaryContext: IExperimentalIncrementalSummaryContext | undefined,
|
|
65
|
+
): ISummaryTreeWithStats;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* {@inheritDoc SharedObjectCore.onDisconnect}
|
|
69
|
+
*/
|
|
70
|
+
onDisconnect(): void;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* {@inheritDoc SharedObjectCore.reSubmitCore}
|
|
74
|
+
*/
|
|
75
|
+
reSubmitCore(content: unknown, localOpMetadata: unknown): void;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* {@inheritDoc SharedObjectCore.applyStashedOp}
|
|
79
|
+
*/
|
|
80
|
+
applyStashedOp(content: unknown): void;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* {@inheritDoc SharedObjectCore.processMessagesCore}
|
|
84
|
+
*/
|
|
85
|
+
processMessagesCore(messagesCollection: IRuntimeMessageCollection): void;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* {@inheritDoc SharedObjectCore.rollback}
|
|
89
|
+
*/
|
|
90
|
+
rollback?(content: unknown, localOpMetadata: unknown): void;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* {@inheritDoc SharedObjectCore.didAttach}
|
|
94
|
+
*/
|
|
95
|
+
didAttach?(): void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* SharedObject implementation that delegates to a SharedKernel.
|
|
100
|
+
* @typeParam TOut - The type of the object exposed to the app.
|
|
101
|
+
* Once initialized, instances of this class forward properties to the `TOut` value provided by the factory.
|
|
102
|
+
* See {@link mergeAPIs} for more limitations.
|
|
103
|
+
*
|
|
104
|
+
* @remarks
|
|
105
|
+
* The App facing API (TOut) needs to be implemented by this object which also has to implement the runtime facing API (ISharedObject).
|
|
106
|
+
*
|
|
107
|
+
* Requiring both of these to be implemented by the same object adds some otherwise unnecessary coupling.
|
|
108
|
+
* This class is a workaround for that, which takes separate implementations of the two APIs and merges them into one using {@link mergeAPIs}.
|
|
109
|
+
*/
|
|
110
|
+
class SharedObjectFromKernel<
|
|
111
|
+
TOut extends object,
|
|
112
|
+
TEvent extends ISharedObjectEvents,
|
|
113
|
+
> extends SharedObject<TEvent> {
|
|
114
|
+
/**
|
|
115
|
+
* Lazy init here so kernel can be constructed in loadCore when loading from existing data.
|
|
116
|
+
*
|
|
117
|
+
* Explicit initialization to undefined is done so Proxy knows this property is from this class (via `Reflect.has`),
|
|
118
|
+
* not from the grafted APIs.
|
|
119
|
+
*/
|
|
120
|
+
#lazyData: FactoryOut<TOut> | undefined = undefined;
|
|
121
|
+
|
|
122
|
+
readonly #kernelArgs: KernelArgs;
|
|
123
|
+
|
|
124
|
+
public constructor(
|
|
125
|
+
id: string,
|
|
126
|
+
runtime: IFluidDataStoreRuntime,
|
|
127
|
+
attributes: IChannelAttributes,
|
|
128
|
+
public readonly factory: SharedKernelFactory<TOut>,
|
|
129
|
+
telemetryContextPrefix: string,
|
|
130
|
+
) {
|
|
131
|
+
super(id, runtime, attributes, telemetryContextPrefix);
|
|
132
|
+
|
|
133
|
+
this.#kernelArgs = {
|
|
134
|
+
sharedObject: this,
|
|
135
|
+
serializer: this.serializer,
|
|
136
|
+
submitLocalMessage: (op, localOpMetadata) =>
|
|
137
|
+
this.submitLocalMessage(op, localOpMetadata),
|
|
138
|
+
eventEmitter: this,
|
|
139
|
+
logger: this.logger,
|
|
140
|
+
idCompressor: runtime.idCompressor,
|
|
141
|
+
lastSequenceNumber: () => this.deltaManager.lastSequenceNumber,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
protected override summarizeCore(
|
|
146
|
+
serializer: IFluidSerializer,
|
|
147
|
+
telemetryContext?: ITelemetryContext,
|
|
148
|
+
incrementalSummaryContext?: IExperimentalIncrementalSummaryContext,
|
|
149
|
+
): ISummaryTreeWithStats {
|
|
150
|
+
return this.#kernel.summarizeCore(serializer, telemetryContext, incrementalSummaryContext);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
protected override initializeLocalCore(): void {
|
|
154
|
+
this.#initializeData(this.factory.create(this.#kernelArgs));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
#initializeData(data: FactoryOut<TOut>): void {
|
|
158
|
+
assert(this.#lazyData === undefined, "initializeData must be called first and only once");
|
|
159
|
+
this.#lazyData = data;
|
|
160
|
+
|
|
161
|
+
// Make `this` implement TOut.
|
|
162
|
+
mergeAPIs(this, data.view);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
get #kernel(): SharedKernel {
|
|
166
|
+
return (this.#lazyData ?? fail("must initializeData first")).kernel;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
protected override async loadCore(storage: IChannelStorageService): Promise<void> {
|
|
170
|
+
this.#initializeData(await this.factory.loadCore(this.#kernelArgs, storage));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
protected override onDisconnect(): void {
|
|
174
|
+
this.#kernel.onDisconnect();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
protected override reSubmitCore(content: unknown, localOpMetadata: unknown): void {
|
|
178
|
+
this.#kernel.reSubmitCore(content, localOpMetadata);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
protected override applyStashedOp(content: unknown): void {
|
|
182
|
+
this.#kernel.applyStashedOp(content);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
protected override processCore(): void {
|
|
186
|
+
fail("processCore should not be called");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
protected override processMessagesCore(messagesCollection: IRuntimeMessageCollection): void {
|
|
190
|
+
this.#kernel.processMessagesCore(messagesCollection);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
protected override rollback(content: unknown, localOpMetadata: unknown): void {
|
|
194
|
+
if (this.#kernel.rollback === undefined) {
|
|
195
|
+
super.rollback(content, localOpMetadata);
|
|
196
|
+
} else {
|
|
197
|
+
this.#kernel.rollback(content, localOpMetadata);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
protected override didAttach(): void {
|
|
202
|
+
this.#kernel.didAttach?.();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* When present on a method, it indicates the methods return value should be replaced with `this` (the wrapper)
|
|
208
|
+
* when wrapping the object with the method.
|
|
209
|
+
* @remarks
|
|
210
|
+
* This is useful when using {@link mergeAPIs} with methods where the return type is `this`, like `Map.set`.
|
|
211
|
+
* @internal
|
|
212
|
+
*/
|
|
213
|
+
export const thisWrap: unique symbol = Symbol("selfWrap");
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* A {@link SharedKernel} providing the implementation of some distributed data structure (DDS) and the needed runtime facing APIs,
|
|
217
|
+
* and a separate view object which exposes the app facing APIs (`T`)
|
|
218
|
+
* for reading and writing data which are specific to this particular data structure.
|
|
219
|
+
* @remarks
|
|
220
|
+
* Output from {@link SharedKernelFactory}.
|
|
221
|
+
* This is an alternative to defining DDSs by sub-classing {@link SharedObject}.
|
|
222
|
+
* @internal
|
|
223
|
+
*/
|
|
224
|
+
export interface FactoryOut<T extends object> {
|
|
225
|
+
readonly kernel: SharedKernel;
|
|
226
|
+
readonly view: T;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A factory for creating DDSs.
|
|
231
|
+
* @remarks
|
|
232
|
+
* Outputs {@link FactoryOut}.
|
|
233
|
+
* This is an alternative to directly implementing {@link @fluidframework/datastore-definitions#IChannelFactory}.
|
|
234
|
+
* Use with {@link makeSharedObjectKind} to create a {@link SharedObjectKind}.
|
|
235
|
+
* @internal
|
|
236
|
+
*/
|
|
237
|
+
export interface SharedKernelFactory<T extends object> {
|
|
238
|
+
create(args: KernelArgs): FactoryOut<T>;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Create combined with {@link SharedObjectCore.loadCore}.
|
|
242
|
+
*/
|
|
243
|
+
loadCore(args: KernelArgs, storage: IChannelStorageService): Promise<FactoryOut<T>>;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Inputs for building a {@link SharedKernel} via {@link SharedKernelFactory}.
|
|
248
|
+
* @internal
|
|
249
|
+
*/
|
|
250
|
+
export interface KernelArgs {
|
|
251
|
+
/**
|
|
252
|
+
* The shared object whose behavior is being implemented.
|
|
253
|
+
*/
|
|
254
|
+
readonly sharedObject: IChannelView & IFluidLoadable;
|
|
255
|
+
/**
|
|
256
|
+
* {@inheritdoc SharedObject.serializer}
|
|
257
|
+
*/
|
|
258
|
+
readonly serializer: IFluidSerializer;
|
|
259
|
+
/**
|
|
260
|
+
* {@inheritdoc SharedObjectCore.submitLocalMessage}
|
|
261
|
+
*/
|
|
262
|
+
readonly submitLocalMessage: (op: unknown, localOpMetadata: unknown) => void;
|
|
263
|
+
/**
|
|
264
|
+
* Top level emitter for events for this object.
|
|
265
|
+
* @remarks
|
|
266
|
+
* This is needed since the separate kernel and view from {@link FactoryOut} currently have to be recombined,
|
|
267
|
+
* and having this as its own thing helps accomplish that.
|
|
268
|
+
*/
|
|
269
|
+
readonly eventEmitter: TypedEventEmitter<ISharedObjectEvents>;
|
|
270
|
+
/**
|
|
271
|
+
* {@inheritdoc SharedObjectCore.logger}
|
|
272
|
+
*/
|
|
273
|
+
readonly logger: ITelemetryLoggerExt;
|
|
274
|
+
/**
|
|
275
|
+
* {@inheritdoc @fluidframework/datastore-definitions#IFluidDataStoreRuntime.idCompressor}
|
|
276
|
+
*/
|
|
277
|
+
readonly idCompressor: IIdCompressor | undefined;
|
|
278
|
+
/**
|
|
279
|
+
* {@inheritdoc @fluidframework/container-definitions#IDeltaManager.lastSequenceNumber}
|
|
280
|
+
*/
|
|
281
|
+
readonly lastSequenceNumber: () => number;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Add getters to `base` which forward own properties from `extra`.
|
|
286
|
+
* @remarks
|
|
287
|
+
* This only handles use of "get" and "has".
|
|
288
|
+
* Therefore, APIs involving setting properties should not be used as `Extra`.
|
|
289
|
+
*
|
|
290
|
+
* Functions from `extra` are bound to the `extra` object and support {@link thisWrap}.
|
|
291
|
+
*
|
|
292
|
+
* Asserts when properties collide.
|
|
293
|
+
* @internal
|
|
294
|
+
*/
|
|
295
|
+
export function mergeAPIs<const Base extends object, const Extra extends object>(
|
|
296
|
+
base: Base,
|
|
297
|
+
extra: Extra,
|
|
298
|
+
): asserts base is Base & Extra {
|
|
299
|
+
for (const [key, descriptor] of Object.entries(Object.getOwnPropertyDescriptors(extra))) {
|
|
300
|
+
assert(!Reflect.has(base, key), "colliding properties");
|
|
301
|
+
|
|
302
|
+
// Detect and special case functions.
|
|
303
|
+
// Currently this is done eagerly (when mergeAPIs is called) rather than lazily (when the property is read):
|
|
304
|
+
// this eager approach should result in slightly better performance,
|
|
305
|
+
// but if functions on `extra` are reassigned over time it will produce incorrect behavior.
|
|
306
|
+
// If this functionality is required, the design can be changed.
|
|
307
|
+
let getter: () => unknown;
|
|
308
|
+
// Bind functions to the extra object and handle thisWrap.
|
|
309
|
+
if (typeof descriptor.value === "function") {
|
|
310
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
311
|
+
const fromExtra: () => Extra | Base = descriptor.value;
|
|
312
|
+
getter = () => forwardMethod(fromExtra, extra, base);
|
|
313
|
+
// To catch (and error on) cases where the function is reassigned and this eager binding approach is not appropriate, make it non-writable.
|
|
314
|
+
Object.defineProperty(extra, key, { ...descriptor, writable: false });
|
|
315
|
+
} else {
|
|
316
|
+
getter = () => extra[key];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
Object.defineProperty(base, key, {
|
|
320
|
+
configurable: false,
|
|
321
|
+
enumerable: descriptor.enumerable,
|
|
322
|
+
get: getter,
|
|
323
|
+
// If setters become required, support them here.
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Wrap a method `f` of `oldThis` to be a method of `newThis`.
|
|
330
|
+
* @remarks
|
|
331
|
+
* The wrapped function will be called with `oldThis` as the `this` parameter.
|
|
332
|
+
* It also accounts for when `f` is marked with {@link thisWrap}.
|
|
333
|
+
*/
|
|
334
|
+
function forwardMethod<TArgs extends [], TReturn>(
|
|
335
|
+
f: (...args: TArgs) => TReturn,
|
|
336
|
+
oldThis: TReturn,
|
|
337
|
+
newThis: TReturn,
|
|
338
|
+
): (...args: TArgs) => TReturn {
|
|
339
|
+
// eslint-disable-next-line unicorn/prefer-ternary
|
|
340
|
+
if (thisWrap in f) {
|
|
341
|
+
return (...args: TArgs) => {
|
|
342
|
+
const result = f.call(oldThis, ...args);
|
|
343
|
+
assert(result === oldThis, "methods returning thisWrap should return this");
|
|
344
|
+
return newThis;
|
|
345
|
+
};
|
|
346
|
+
} else {
|
|
347
|
+
return f.bind(oldThis);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Options for creating a {@link SharedObjectKind} via {@link makeSharedObjectKind}.
|
|
353
|
+
* @typeParam T - The type of the object exposed to the app.
|
|
354
|
+
* This can optionally include members from {@link ISharedObject} which will be provided automatically.
|
|
355
|
+
* @internal
|
|
356
|
+
*/
|
|
357
|
+
export interface SharedObjectOptions<T extends object> {
|
|
358
|
+
/**
|
|
359
|
+
* {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory."type"}
|
|
360
|
+
*/
|
|
361
|
+
readonly type: string;
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.attributes}
|
|
365
|
+
*/
|
|
366
|
+
readonly attributes: IChannelAttributes;
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The factory used to create the kernel and its view.
|
|
370
|
+
* @remarks
|
|
371
|
+
* The view produced by this factory will be grafted onto the {@link SharedObject} using {@link mergeAPIs}.
|
|
372
|
+
* See {@link mergeAPIs} for more information on limitations that apply.
|
|
373
|
+
*/
|
|
374
|
+
readonly factory: SharedKernelFactory<Omit<T, keyof ISharedObject>>;
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* {@inheritDoc SharedObject.telemetryContextPrefix}
|
|
378
|
+
*/
|
|
379
|
+
readonly telemetryContextPrefix: string;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Utility to create a {@link @fluidframework/datastore-definitions#IChannelFactory} classes.
|
|
384
|
+
* @remarks
|
|
385
|
+
* Use {@link makeSharedObjectKind} instead unless exposing the factory is required for legacy API compatibility.
|
|
386
|
+
* @internal
|
|
387
|
+
*/
|
|
388
|
+
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
|
|
389
|
+
function makeChannelFactory<T extends object>(options: SharedObjectOptions<T>) {
|
|
390
|
+
class ChannelFactory implements IChannelFactory<T> {
|
|
391
|
+
/**
|
|
392
|
+
* {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory."type"}
|
|
393
|
+
*/
|
|
394
|
+
public static readonly Type = options.type;
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.attributes}
|
|
398
|
+
*/
|
|
399
|
+
public static readonly Attributes: IChannelAttributes = options.attributes;
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory."type"}
|
|
403
|
+
*/
|
|
404
|
+
public get type(): string {
|
|
405
|
+
return ChannelFactory.Type;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.attributes}
|
|
410
|
+
*/
|
|
411
|
+
public get attributes(): IChannelAttributes {
|
|
412
|
+
return ChannelFactory.Attributes;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.load}
|
|
417
|
+
*/
|
|
418
|
+
public async load(
|
|
419
|
+
runtime: IFluidDataStoreRuntime,
|
|
420
|
+
id: string,
|
|
421
|
+
services: IChannelServices,
|
|
422
|
+
attributes: IChannelAttributes,
|
|
423
|
+
): Promise<T & IChannel> {
|
|
424
|
+
const shared = new SharedObjectFromKernel(
|
|
425
|
+
id,
|
|
426
|
+
runtime,
|
|
427
|
+
attributes,
|
|
428
|
+
options.factory,
|
|
429
|
+
options.telemetryContextPrefix,
|
|
430
|
+
);
|
|
431
|
+
await shared.load(services);
|
|
432
|
+
return shared as unknown as T & IChannel;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* {@inheritDoc @fluidframework/datastore-definitions#IChannelFactory.create}
|
|
437
|
+
*/
|
|
438
|
+
public create(runtime: IFluidDataStoreRuntime, id: string): T & IChannel {
|
|
439
|
+
const shared = new SharedObjectFromKernel(
|
|
440
|
+
id,
|
|
441
|
+
runtime,
|
|
442
|
+
ChannelFactory.Attributes,
|
|
443
|
+
options.factory,
|
|
444
|
+
options.telemetryContextPrefix,
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
shared.initializeLocal();
|
|
448
|
+
|
|
449
|
+
return shared as unknown as T & IChannel;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return ChannelFactory;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Utility to create a {@link SharedObjectKind}.
|
|
458
|
+
* @privateRemarks
|
|
459
|
+
* Using this API avoids having to subclasses any Fluid Framework types,
|
|
460
|
+
* reducing the coupling between the framework and the SharedObject implementation.
|
|
461
|
+
* @internal
|
|
462
|
+
*/
|
|
463
|
+
export function makeSharedObjectKind<T extends object>(
|
|
464
|
+
options: SharedObjectOptions<T>,
|
|
465
|
+
): ISharedObjectKind<T> & SharedObjectKind<T> {
|
|
466
|
+
return createSharedObjectKind<T>(makeChannelFactory(options));
|
|
467
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -53,7 +53,19 @@ export interface ISharedObjectEvents extends IErrorEvent {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
/**
|
|
56
|
-
* Base interface for shared objects from which other interfaces
|
|
56
|
+
* Base interface for shared objects from which other interfaces extend.
|
|
57
|
+
* @remarks
|
|
58
|
+
* This interface is not intended to be implemented outside this repository:
|
|
59
|
+
* implementers should migrate to using an existing implementation instead.
|
|
60
|
+
* @privateRemarks
|
|
61
|
+
* Implemented by {@link SharedObjectCore}.
|
|
62
|
+
*
|
|
63
|
+
* TODO:
|
|
64
|
+
* The relationship between the "shared object" abstraction and "channel" abstraction should be clarified and/or unified.
|
|
65
|
+
* Either there should be a single named abstraction or the docs here need to make it clear why adding events and bindToContext to a channel makes it a "shared object".
|
|
66
|
+
* Additionally the docs here need to define what a shared object is, not just claim this interface is for them.
|
|
67
|
+
* If the intention is that the "shared object" concept `IFluidLoadable` mentions is only ever implemented by this interface then even more concept unification should be done.
|
|
68
|
+
* If not then more clarity is needed on what this interface specifically is, what the other "shared object" concept means and how they relate.
|
|
57
69
|
* @legacy
|
|
58
70
|
* @alpha
|
|
59
71
|
*/
|