@fluid-experimental/attributor 2.1.1 → 2.2.1

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +2 -2
  3. package/dist/attributorContracts.d.ts +49 -0
  4. package/dist/attributorContracts.d.ts.map +1 -0
  5. package/dist/attributorContracts.js +22 -0
  6. package/dist/attributorContracts.js.map +1 -0
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +6 -4
  10. package/dist/index.js.map +1 -1
  11. package/dist/mixinAttributor.d.ts +8 -49
  12. package/dist/mixinAttributor.d.ts.map +1 -1
  13. package/dist/mixinAttributor.js +35 -140
  14. package/dist/mixinAttributor.js.map +1 -1
  15. package/dist/runtimeAttributor.d.ts +19 -0
  16. package/dist/runtimeAttributor.d.ts.map +1 -0
  17. package/dist/runtimeAttributor.js +71 -0
  18. package/dist/runtimeAttributor.js.map +1 -0
  19. package/dist/runtimeAttributorDataStoreChannel.d.ts +92 -0
  20. package/dist/runtimeAttributorDataStoreChannel.d.ts.map +1 -0
  21. package/dist/runtimeAttributorDataStoreChannel.js +177 -0
  22. package/dist/runtimeAttributorDataStoreChannel.js.map +1 -0
  23. package/dist/runtimeAttributorDataStoreFactory.d.ts +15 -0
  24. package/dist/runtimeAttributorDataStoreFactory.d.ts.map +1 -0
  25. package/dist/runtimeAttributorDataStoreFactory.js +39 -0
  26. package/dist/runtimeAttributorDataStoreFactory.js.map +1 -0
  27. package/lib/attributorContracts.d.ts +49 -0
  28. package/lib/attributorContracts.d.ts.map +1 -0
  29. package/lib/attributorContracts.js +19 -0
  30. package/lib/attributorContracts.js.map +1 -0
  31. package/lib/index.d.ts +2 -1
  32. package/lib/index.d.ts.map +1 -1
  33. package/lib/index.js +2 -1
  34. package/lib/index.js.map +1 -1
  35. package/lib/mixinAttributor.d.ts +8 -49
  36. package/lib/mixinAttributor.d.ts.map +1 -1
  37. package/lib/mixinAttributor.js +33 -138
  38. package/lib/mixinAttributor.js.map +1 -1
  39. package/lib/runtimeAttributor.d.ts +19 -0
  40. package/lib/runtimeAttributor.d.ts.map +1 -0
  41. package/lib/runtimeAttributor.js +67 -0
  42. package/lib/runtimeAttributor.js.map +1 -0
  43. package/lib/runtimeAttributorDataStoreChannel.d.ts +92 -0
  44. package/lib/runtimeAttributorDataStoreChannel.d.ts.map +1 -0
  45. package/lib/runtimeAttributorDataStoreChannel.js +173 -0
  46. package/lib/runtimeAttributorDataStoreChannel.js.map +1 -0
  47. package/lib/runtimeAttributorDataStoreFactory.d.ts +15 -0
  48. package/lib/runtimeAttributorDataStoreFactory.d.ts.map +1 -0
  49. package/lib/runtimeAttributorDataStoreFactory.js +35 -0
  50. package/lib/runtimeAttributorDataStoreFactory.js.map +1 -0
  51. package/package.json +20 -19
  52. package/src/attributorContracts.ts +61 -0
  53. package/src/index.ts +3 -3
  54. package/src/mixinAttributor.ts +45 -267
  55. package/src/runtimeAttributor.ts +111 -0
  56. package/src/runtimeAttributorDataStoreChannel.ts +261 -0
  57. package/src/runtimeAttributorDataStoreFactory.ts +59 -0
@@ -3,11 +3,7 @@
3
3
  * Licensed under the MIT License.
4
4
  */
5
5
 
6
- import { bufferToString } from "@fluid-internal/client-utils";
7
- import {
8
- type IDeltaManager,
9
- type IContainerContext,
10
- } from "@fluidframework/container-definitions/internal";
6
+ import { type IContainerContext } from "@fluidframework/container-definitions/internal";
11
7
  import { ContainerRuntime } from "@fluidframework/container-runtime/internal";
12
8
  import type { IContainerRuntimeOptions } from "@fluidframework/container-runtime/internal";
13
9
  import { type IContainerRuntime } from "@fluidframework/container-runtime-definitions/internal";
@@ -16,102 +12,39 @@ import {
16
12
  type IRequest,
17
13
  type IResponse,
18
14
  } from "@fluidframework/core-interfaces";
19
- import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
20
- import { type IQuorumClients } from "@fluidframework/driver-definitions";
21
- import {
22
- type IDocumentMessage,
23
- type ISnapshotTree,
24
- type ISequencedDocumentMessage,
25
- } from "@fluidframework/driver-definitions/internal";
26
- import {
27
- type ISummaryTreeWithStats,
28
- type ITelemetryContext,
29
- type AttributionInfo,
30
- type AttributionKey,
31
- type NamedFluidDataStoreRegistryEntries,
32
- } from "@fluidframework/runtime-definitions/internal";
33
- import {
34
- SummaryTreeBuilder,
35
- addSummarizeResultToSummary,
36
- } from "@fluidframework/runtime-utils/internal";
37
- import {
38
- PerformanceEvent,
39
- UsageError,
40
- createChildLogger,
41
- loggerToMonitoringContext,
42
- } from "@fluidframework/telemetry-utils/internal";
43
-
44
- import { Attributor, type IAttributor, OpStreamAttributor } from "./attributor.js";
45
- import { AttributorSerializer, type Encoder, chain, deltaEncoder } from "./encoders.js";
46
- import { makeLZ4Encoder } from "./lz4Encoder.js";
47
-
48
- // Summary tree keys
49
- const attributorTreeName = ".attributor";
50
- const opBlobName = "op";
51
-
52
- /**
53
- * @internal
54
- */
55
- export const enableOnNewFileKey = "Fluid.Attribution.EnableOnNewFile";
56
-
57
- /**
58
- * @internal
59
- */
60
- export const IRuntimeAttributor: keyof IProvideRuntimeAttributor = "IRuntimeAttributor";
61
-
62
- /**
63
- * @internal
64
- */
65
- export interface IProvideRuntimeAttributor {
66
- readonly IRuntimeAttributor: IRuntimeAttributor;
67
- }
68
-
69
- /**
70
- * Provides access to attribution information stored on the container runtime.
71
- *
72
- * @remarks Attributors are only populated after the container runtime into which they are being injected has initialized.
73
- *
74
- * @sealed
75
- * @internal
76
- */
77
- export interface IRuntimeAttributor extends IProvideRuntimeAttributor {
78
- /**
79
- * @throws - If no AttributionInfo exists for this key.
80
- */
81
- get(key: AttributionKey): AttributionInfo;
15
+ import { assert } from "@fluidframework/core-utils/internal";
16
+ import { type NamedFluidDataStoreRegistryEntries } from "@fluidframework/runtime-definitions/internal";
17
+ import { loggerToMonitoringContext } from "@fluidframework/telemetry-utils/internal";
82
18
 
83
- /**
84
- * @returns Whether any AttributionInfo exists for the provided key.
85
- */
86
- has(key: AttributionKey): boolean;
87
-
88
- /**
89
- * @returns Whether the runtime is currently tracking attribution information for the loaded container.
90
- * If enabled, the runtime attributor can be asked for the attribution info for different keys.
91
- * See {@link mixinAttributor} for more details on when this happens.
92
- */
93
- readonly isEnabled: boolean;
94
- }
19
+ import {
20
+ attributorDataStoreAlias,
21
+ enableOnNewFileKey,
22
+ type IProvideRuntimeAttributor,
23
+ type IRuntimeAttributor,
24
+ } from "./attributorContracts.js";
25
+ import { RuntimeAttributorFactory } from "./runtimeAttributorDataStoreFactory.js";
95
26
 
96
27
  /**
97
- * Creates an `IRuntimeAttributor` for usage with {@link mixinAttributor}.
98
- *
99
- * @remarks The attributor will only be populated with data once it's passed via scope to a container runtime load flow.
100
- *
28
+ * Utility function to get the runtime attributor from the container runtime.
29
+ * @param runtime - container runtime from which attributor is to be fetched.
30
+ * @returns IRuntimeAttributor if it exists, otherwise undefined.
101
31
  * @internal
102
32
  */
103
- export function createRuntimeAttributor(): IRuntimeAttributor {
104
- return new RuntimeAttributor();
33
+ export async function getRuntimeAttributor(
34
+ runtime: IContainerRuntime,
35
+ ): Promise<IRuntimeAttributor | undefined> {
36
+ const entryPoint = await runtime.getAliasedDataStoreEntryPoint(attributorDataStoreAlias);
37
+ const runtimeAttributor = (await entryPoint?.get()) as
38
+ | FluidObject<IProvideRuntimeAttributor>
39
+ | undefined;
40
+ return runtimeAttributor?.IRuntimeAttributor;
105
41
  }
106
42
 
107
43
  /**
108
44
  * Mixes in logic to load and store runtime-based attribution functionality.
109
45
  *
110
- * The `scope` passed to `load` should implement `IProvideRuntimeAttributor`.
111
- *
112
- * Existing documents without stored attributors will not start storing attribution information: if an
113
- * IRuntimeAttributor is passed via scope to load a document that never previously had attribution information,
114
- * that attributor's `has` method will always return `false`.
46
+ * Existing documents without stored attributor will not start storing attribution information. We only create the attributor
47
+ * if its tracking is enabled and we are creating a new document.
115
48
  * @param Base - base class, inherits from FluidAttributorRuntime
116
49
  * @internal
117
50
  */
@@ -143,29 +76,12 @@ export const mixinAttributor = (
143
76
  containerRuntimeCtor = ContainerRuntimeWithAttributor as unknown as typeof ContainerRuntime,
144
77
  } = params;
145
78
 
146
- const runtimeAttributor = (
147
- containerScope as FluidObject<IProvideRuntimeAttributor> | undefined
148
- )?.IRuntimeAttributor;
149
- if (!runtimeAttributor) {
150
- throw new UsageError(
151
- "ContainerRuntimeWithAttributor must be passed a scope implementing IProvideRuntimeAttributor",
152
- );
153
- }
154
-
155
- const pendingRuntimeState = context.pendingLocalState as {
156
- baseSnapshot?: ISnapshotTree;
157
- };
158
- const baseSnapshot: ISnapshotTree | undefined =
159
- pendingRuntimeState?.baseSnapshot ?? context.baseSnapshot;
160
-
161
- const { quorum, deltaManager, taggedLogger } = context;
162
- assert(
163
- quorum !== undefined,
164
- 0x968 /* quorum must exist when instantiating attribution-providing runtime */,
165
- );
166
-
167
- const mc = loggerToMonitoringContext(taggedLogger);
168
-
79
+ const mc = loggerToMonitoringContext(context.taggedLogger);
80
+ const factory = new RuntimeAttributorFactory();
81
+ const registryEntriesCopy: NamedFluidDataStoreRegistryEntries = [
82
+ ...registryEntries,
83
+ [RuntimeAttributorFactory.type, Promise.resolve(factory)],
84
+ ];
169
85
  const shouldTrackAttribution = mc.config.getBoolean(enableOnNewFileKey) ?? false;
170
86
  if (shouldTrackAttribution) {
171
87
  const { options } = context;
@@ -176,170 +92,32 @@ export const mixinAttributor = (
176
92
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
177
93
  const runtime = (await Base.loadRuntime({
178
94
  context,
179
- registryEntries,
95
+ registryEntries: registryEntriesCopy,
180
96
  requestHandler,
181
97
  provideEntryPoint,
182
- // ! This prop is needed for back-compat. Can be removed in 2.0.0-internal.8.0.0
183
- initializeEntryPoint: provideEntryPoint,
184
98
  runtimeOptions,
185
99
  containerScope,
186
100
  existing,
187
101
  containerRuntimeCtor,
188
102
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
103
  } as any)) as ContainerRuntimeWithAttributor;
190
- runtime.runtimeAttributor = runtimeAttributor as RuntimeAttributor;
191
-
192
- const logger = createChildLogger({
193
- logger: runtime.baseLogger,
194
- namespace: "Attributor",
195
- });
196
104
 
197
- // Note: this fetches attribution blobs relatively eagerly in the load flow; we may want to optimize
198
- // this to avoid blocking on such information until application actually requests some op-based attribution
199
- // info or we need to summarize. All that really needs to happen immediately is to start recording
200
- // op seq# -> attributionInfo for new ops.
201
- await PerformanceEvent.timedExecAsync(
202
- logger,
203
- {
204
- eventName: "initialize",
205
- },
206
- async (event) => {
207
- await runtime.runtimeAttributor?.initialize(
208
- deltaManager,
209
- quorum,
210
- baseSnapshot,
211
- async (id) => runtime.storage.readBlob(id),
212
- shouldTrackAttribution,
105
+ let runtimeAttributor: IRuntimeAttributor | undefined;
106
+ if (shouldTrackAttribution) {
107
+ if (existing) {
108
+ runtimeAttributor = await getRuntimeAttributor(runtime);
109
+ } else {
110
+ const datastore = await runtime.createDataStore(RuntimeAttributorFactory.type);
111
+ const result = await datastore.trySetAlias(attributorDataStoreAlias);
112
+ assert(
113
+ result === "Success",
114
+ 0xa1b /* Failed to set alias for attributor data store */,
213
115
  );
214
- event.end({
215
- attributionEnabledInConfig: shouldTrackAttribution,
216
- attributionEnabledInDoc: runtime.runtimeAttributor
217
- ? runtime.runtimeAttributor.isEnabled
218
- : false,
219
- });
220
- },
221
- );
116
+ runtimeAttributor = (await datastore.entryPoint.get()) as IRuntimeAttributor;
117
+ assert(runtimeAttributor !== undefined, 0xa1c /* Attributor should be defined */);
118
+ }
119
+ }
222
120
 
223
121
  return runtime;
224
122
  }
225
-
226
- private runtimeAttributor: RuntimeAttributor | undefined;
227
-
228
- protected addContainerStateToSummary(
229
- summaryTree: ISummaryTreeWithStats,
230
- fullTree: boolean,
231
- trackState: boolean,
232
- telemetryContext?: ITelemetryContext,
233
- ): void {
234
- super.addContainerStateToSummary(summaryTree, fullTree, trackState, telemetryContext);
235
- const attributorSummary = this.runtimeAttributor?.summarize();
236
- if (attributorSummary) {
237
- addSummarizeResultToSummary(summaryTree, attributorTreeName, attributorSummary);
238
- }
239
- }
240
123
  } as unknown as typeof ContainerRuntime;
241
-
242
- class RuntimeAttributor implements IRuntimeAttributor {
243
- public get IRuntimeAttributor(): IRuntimeAttributor {
244
- return this;
245
- }
246
-
247
- public get(key: AttributionKey): AttributionInfo {
248
- assert(
249
- this.opAttributor !== undefined,
250
- 0x509 /* RuntimeAttributor must be initialized before getAttributionInfo can be called */,
251
- );
252
-
253
- if (key.type === "detached") {
254
- throw new Error("Attribution of detached keys is not yet supported.");
255
- }
256
-
257
- if (key.type === "local") {
258
- // Note: we can *almost* orchestrate this correctly with internal-only changes by looking up the current
259
- // client id in the audience. However, for read->write client transition, the container might have not yet
260
- // received a client id. This is left as a TODO as it might be more easily solved once the detached case
261
- // is settled (e.g. if it's reasonable for the host to know the current user information at container
262
- // creation time, we could just use that here as well).
263
- throw new Error("Attribution of local keys is not yet supported.");
264
- }
265
-
266
- return this.opAttributor.getAttributionInfo(key.seq);
267
- }
268
-
269
- public has(key: AttributionKey): boolean {
270
- if (key.type === "detached") {
271
- return false;
272
- }
273
-
274
- if (key.type === "local") {
275
- return false;
276
- }
277
-
278
- return this.opAttributor?.tryGetAttributionInfo(key.seq) !== undefined;
279
- }
280
-
281
- private encoder: Encoder<IAttributor, string> = {
282
- encode: unreachableCase,
283
- decode: unreachableCase,
284
- };
285
-
286
- private opAttributor: IAttributor | undefined;
287
- public isEnabled = false;
288
-
289
- public async initialize(
290
- deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
291
- quorum: IQuorumClients,
292
- baseSnapshot: ISnapshotTree | undefined,
293
- readBlob: (id: string) => Promise<ArrayBufferLike>,
294
- shouldAddAttributorOnNewFile: boolean,
295
- ): Promise<void> {
296
- const attributorTree = baseSnapshot?.trees[attributorTreeName];
297
- // Existing documents that don't already have a snapshot containing runtime attribution info shouldn't
298
- // inject any for now--this causes some back-compat integration problems that aren't fully worked out.
299
- const shouldExcludeAttributor =
300
- (baseSnapshot !== undefined && attributorTree === undefined) ||
301
- (baseSnapshot === undefined && !shouldAddAttributorOnNewFile);
302
- if (shouldExcludeAttributor) {
303
- // This gives a consistent error for calls to `get` on keys that don't exist.
304
- this.opAttributor = new Attributor();
305
- return;
306
- }
307
-
308
- this.isEnabled = true;
309
- this.encoder = chain(
310
- new AttributorSerializer(
311
- (entries) => new OpStreamAttributor(deltaManager, quorum, entries),
312
- deltaEncoder,
313
- ),
314
- makeLZ4Encoder(),
315
- );
316
-
317
- if (attributorTree === undefined) {
318
- this.opAttributor = new OpStreamAttributor(deltaManager, quorum);
319
- } else {
320
- const id = attributorTree.blobs[opBlobName];
321
- assert(
322
- id !== undefined,
323
- 0x50a /* Attributor tree should have op attributor summary blob. */,
324
- );
325
- const blobContents = await readBlob(id);
326
- const attributorSnapshot = bufferToString(blobContents, "utf8");
327
- this.opAttributor = this.encoder.decode(attributorSnapshot);
328
- }
329
- }
330
-
331
- public summarize(): ISummaryTreeWithStats | undefined {
332
- if (!this.isEnabled) {
333
- // Loaded existing document without attributor data: avoid injecting any data.
334
- return undefined;
335
- }
336
-
337
- assert(
338
- this.opAttributor !== undefined,
339
- 0x50b /* RuntimeAttributor should be initialized before summarization */,
340
- );
341
- const builder = new SummaryTreeBuilder();
342
- builder.addBlob(opBlobName, this.encoder.encode(this.opAttributor));
343
- return builder.getSummaryTree();
344
- }
345
- }
@@ -0,0 +1,111 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+
6
+ import { bufferToString } from "@fluid-internal/client-utils";
7
+ import { IDeltaManager } from "@fluidframework/container-definitions/internal";
8
+ import { assert, unreachableCase } from "@fluidframework/core-utils/internal";
9
+ import {
10
+ IDocumentMessage,
11
+ type ISnapshotTree,
12
+ ISequencedDocumentMessage,
13
+ IQuorumClients,
14
+ } from "@fluidframework/driver-definitions/internal";
15
+ import {
16
+ type AttributionInfo,
17
+ type AttributionKey,
18
+ type ISummaryTreeWithStats,
19
+ } from "@fluidframework/runtime-definitions/internal";
20
+ import { SummaryTreeBuilder } from "@fluidframework/runtime-utils/internal";
21
+
22
+ import { OpStreamAttributor, type IAttributor } from "./attributor.js";
23
+ import { opBlobName, type IRuntimeAttributor } from "./attributorContracts.js";
24
+ import { AttributorSerializer, chain, deltaEncoder, type Encoder } from "./encoders.js";
25
+ import { makeLZ4Encoder } from "./lz4Encoder.js";
26
+
27
+ export class RuntimeAttributor implements IRuntimeAttributor {
28
+ public get IRuntimeAttributor(): IRuntimeAttributor {
29
+ return this;
30
+ }
31
+
32
+ public get(key: AttributionKey): AttributionInfo {
33
+ assert(
34
+ this.opAttributor !== undefined,
35
+ 0x509 /* RuntimeAttributor must be initialized before getAttributionInfo can be called */,
36
+ );
37
+
38
+ if (key.type === "detached") {
39
+ throw new Error("Attribution of detached keys is not yet supported.");
40
+ }
41
+
42
+ if (key.type === "local") {
43
+ // Note: we can *almost* orchestrate this correctly with internal-only changes by looking up the current
44
+ // client id in the audience. However, for read->write client transition, the container might have not yet
45
+ // received a client id. This is left as a TODO as it might be more easily solved once the detached case
46
+ // is settled (e.g. if it's reasonable for the host to know the current user information at container
47
+ // creation time, we could just use that here as well).
48
+ throw new Error("Attribution of local keys is not yet supported.");
49
+ }
50
+
51
+ return this.opAttributor.getAttributionInfo(key.seq);
52
+ }
53
+
54
+ public has(key: AttributionKey): boolean {
55
+ if (key.type === "detached") {
56
+ return false;
57
+ }
58
+
59
+ if (key.type === "local") {
60
+ return false;
61
+ }
62
+
63
+ return this.opAttributor?.tryGetAttributionInfo(key.seq) !== undefined;
64
+ }
65
+
66
+ private encoder: Encoder<IAttributor, string> = {
67
+ encode: unreachableCase,
68
+ decode: unreachableCase,
69
+ };
70
+
71
+ private opAttributor: IAttributor | undefined;
72
+ public isEnabled = true;
73
+
74
+ public async initialize(
75
+ deltaManager: IDeltaManager<ISequencedDocumentMessage, IDocumentMessage>,
76
+ quorum: IQuorumClients,
77
+ baseSnapshotForAttributorTree: ISnapshotTree | undefined,
78
+ readBlob: (id: string) => Promise<ArrayBufferLike>,
79
+ ): Promise<void> {
80
+ this.encoder = chain(
81
+ new AttributorSerializer(
82
+ (entries) => new OpStreamAttributor(deltaManager, quorum, entries),
83
+ deltaEncoder,
84
+ ),
85
+ makeLZ4Encoder(),
86
+ );
87
+
88
+ if (baseSnapshotForAttributorTree === undefined) {
89
+ this.opAttributor = new OpStreamAttributor(deltaManager, quorum);
90
+ } else {
91
+ const id = baseSnapshotForAttributorTree.blobs[opBlobName];
92
+ assert(
93
+ id !== undefined,
94
+ 0x50a /* Attributor tree should have op attributor summary blob. */,
95
+ );
96
+ const blobContents = await readBlob(id);
97
+ const attributorSnapshot = bufferToString(blobContents, "utf8");
98
+ this.opAttributor = this.encoder.decode(attributorSnapshot);
99
+ }
100
+ }
101
+
102
+ public summarizeOpAttributor(): ISummaryTreeWithStats {
103
+ assert(
104
+ this.opAttributor !== undefined,
105
+ 0xa1d /* RuntimeAttributor should be initialized before summarization */,
106
+ );
107
+ const builder = new SummaryTreeBuilder();
108
+ builder.addBlob(opBlobName, this.encoder.encode(this.opAttributor));
109
+ return builder.getSummaryTree();
110
+ }
111
+ }