@fluid-experimental/dds-interceptions 2.0.0-dev-rc.2.0.0.245554

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 (83) hide show
  1. package/.eslintrc.cjs +14 -0
  2. package/.mocharc.cjs +12 -0
  3. package/CHANGELOG.md +93 -0
  4. package/LICENSE +21 -0
  5. package/README.md +95 -0
  6. package/api-extractor-cjs.json +8 -0
  7. package/api-extractor-lint.json +4 -0
  8. package/api-extractor.json +4 -0
  9. package/api-report/dds-interceptions.api.md +24 -0
  10. package/dist/dds-interceptions-alpha.d.ts +13 -0
  11. package/dist/dds-interceptions-beta.d.ts +19 -0
  12. package/dist/dds-interceptions-public.d.ts +19 -0
  13. package/dist/dds-interceptions-untrimmed.d.ts +67 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +13 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/map/index.d.ts +7 -0
  19. package/dist/map/index.d.ts.map +1 -0
  20. package/dist/map/index.js +12 -0
  21. package/dist/map/index.js.map +1 -0
  22. package/dist/map/sharedDirectoryWithInterception.d.ts +29 -0
  23. package/dist/map/sharedDirectoryWithInterception.d.ts.map +1 -0
  24. package/dist/map/sharedDirectoryWithInterception.js +117 -0
  25. package/dist/map/sharedDirectoryWithInterception.js.map +1 -0
  26. package/dist/map/sharedMapWithInterception.d.ts +23 -0
  27. package/dist/map/sharedMapWithInterception.d.ts.map +1 -0
  28. package/dist/map/sharedMapWithInterception.js +49 -0
  29. package/dist/map/sharedMapWithInterception.js.map +1 -0
  30. package/dist/package.json +3 -0
  31. package/dist/sequence/index.d.ts +6 -0
  32. package/dist/sequence/index.d.ts.map +1 -0
  33. package/dist/sequence/index.js +10 -0
  34. package/dist/sequence/index.js.map +1 -0
  35. package/dist/sequence/sharedStringWithInterception.d.ts +27 -0
  36. package/dist/sequence/sharedStringWithInterception.d.ts.map +1 -0
  37. package/dist/sequence/sharedStringWithInterception.js +207 -0
  38. package/dist/sequence/sharedStringWithInterception.js.map +1 -0
  39. package/dist/tsdoc-metadata.json +11 -0
  40. package/lib/dds-interceptions-alpha.d.ts +13 -0
  41. package/lib/dds-interceptions-beta.d.ts +19 -0
  42. package/lib/dds-interceptions-public.d.ts +19 -0
  43. package/lib/dds-interceptions-untrimmed.d.ts +67 -0
  44. package/lib/index.d.ts +7 -0
  45. package/lib/index.d.ts.map +1 -0
  46. package/lib/index.js +7 -0
  47. package/lib/index.js.map +1 -0
  48. package/lib/map/index.d.ts +7 -0
  49. package/lib/map/index.d.ts.map +1 -0
  50. package/lib/map/index.js +7 -0
  51. package/lib/map/index.js.map +1 -0
  52. package/lib/map/sharedDirectoryWithInterception.d.ts +29 -0
  53. package/lib/map/sharedDirectoryWithInterception.d.ts.map +1 -0
  54. package/lib/map/sharedDirectoryWithInterception.js +113 -0
  55. package/lib/map/sharedDirectoryWithInterception.js.map +1 -0
  56. package/lib/map/sharedMapWithInterception.d.ts +23 -0
  57. package/lib/map/sharedMapWithInterception.d.ts.map +1 -0
  58. package/lib/map/sharedMapWithInterception.js +45 -0
  59. package/lib/map/sharedMapWithInterception.js.map +1 -0
  60. package/lib/sequence/index.d.ts +6 -0
  61. package/lib/sequence/index.d.ts.map +1 -0
  62. package/lib/sequence/index.js +6 -0
  63. package/lib/sequence/index.js.map +1 -0
  64. package/lib/sequence/sharedStringWithInterception.d.ts +27 -0
  65. package/lib/sequence/sharedStringWithInterception.d.ts.map +1 -0
  66. package/lib/sequence/sharedStringWithInterception.js +203 -0
  67. package/lib/sequence/sharedStringWithInterception.js.map +1 -0
  68. package/lib/test/sharedDirectoryWithInterception.spec.js +282 -0
  69. package/lib/test/sharedDirectoryWithInterception.spec.js.map +1 -0
  70. package/lib/test/sharedMapWithInterception.spec.js +105 -0
  71. package/lib/test/sharedMapWithInterception.spec.js.map +1 -0
  72. package/lib/test/sharedStringWithInterception.spec.js +147 -0
  73. package/lib/test/sharedStringWithInterception.spec.js.map +1 -0
  74. package/package.json +151 -0
  75. package/prettier.config.cjs +8 -0
  76. package/src/index.ts +7 -0
  77. package/src/map/index.ts +7 -0
  78. package/src/map/sharedDirectoryWithInterception.ts +172 -0
  79. package/src/map/sharedMapWithInterception.ts +58 -0
  80. package/src/sequence/index.ts +6 -0
  81. package/src/sequence/sharedStringWithInterception.ts +288 -0
  82. package/tsconfig.cjs.json +7 -0
  83. package/tsconfig.json +9 -0
@@ -0,0 +1,282 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ import { strict as assert } from "assert";
6
+ import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils";
7
+ import { SharedDirectory, DirectoryFactory } from "@fluidframework/map";
8
+ import { createDirectoryWithInterception } from "../map/index.js";
9
+ describe("Shared Directory with Interception", () => {
10
+ describe("Simple User Attribution", () => {
11
+ const userAttributes = { userId: "Fake User" };
12
+ const documentId = "fakeId";
13
+ const attributionDirectoryName = "attribution";
14
+ const attributionKey = (key) => `${key}.attribution`;
15
+ let sharedDirectory;
16
+ let dataStoreContext;
17
+ // This function gets / creates the attribution directory for the given subdirectory path.
18
+ function getAttributionDirectory(root, path) {
19
+ if (!root.hasSubDirectory(attributionDirectoryName)) {
20
+ root.createSubDirectory(attributionDirectoryName);
21
+ }
22
+ let currentSubDir = root.getSubDirectory(attributionDirectoryName);
23
+ assert(currentSubDir);
24
+ if (path === "/") {
25
+ return currentSubDir;
26
+ }
27
+ let prevSubDir = currentSubDir;
28
+ const subdirs = path.substr(1).split("/");
29
+ for (const subdir of subdirs) {
30
+ currentSubDir = currentSubDir.getSubDirectory(subdir);
31
+ if (currentSubDir === undefined) {
32
+ currentSubDir = prevSubDir.createSubDirectory(subdir);
33
+ break;
34
+ }
35
+ prevSubDir = currentSubDir;
36
+ }
37
+ return currentSubDir;
38
+ }
39
+ /**
40
+ * This callback creates / gets an attribution directory that mirrors the actual directory. It sets the
41
+ * user attribute in the attribution directory against the same key used in the original set.
42
+ * For example - For directory /foo, it sets the attribute in /attribution/foo.
43
+ */
44
+ function mirrorDirectoryInterceptionCb(baseDirectory, subDirectory, key, value) {
45
+ const attributionDirectory = getAttributionDirectory(baseDirectory, subDirectory.absolutePath);
46
+ attributionDirectory.set(key, userAttributes);
47
+ }
48
+ /**
49
+ * This callback creates / gets an attribution directory that is a subdirectory of the given directory. It sets
50
+ * the user attribute in the attribution directory against the same key used in the original set.
51
+ * For example - For directory /foo, it sets the attribute in /foo/attribute
52
+ */
53
+ function subDirectoryinterceptionCb(baseDirectory, subDirectory, key, value) {
54
+ if (!subDirectory.hasSubDirectory(attributionDirectoryName)) {
55
+ subDirectory.createSubDirectory(attributionDirectoryName);
56
+ }
57
+ const attributionDirectory = subDirectory.getSubDirectory(attributionDirectoryName);
58
+ assert(attributionDirectory);
59
+ attributionDirectory.set(key, userAttributes);
60
+ }
61
+ // This callback sets the user attribution in the subdirectory against a key derived from the original key.
62
+ function setInterceptionCb(baseDirectory, subDirectory, key, value) {
63
+ subDirectory.set(attributionKey(key), userAttributes);
64
+ }
65
+ function orderSequentially(callback) {
66
+ callback();
67
+ }
68
+ beforeEach(() => {
69
+ const dataStoreRuntime = new MockFluidDataStoreRuntime();
70
+ sharedDirectory = new SharedDirectory(documentId, dataStoreRuntime, DirectoryFactory.Attributes);
71
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
72
+ dataStoreContext = {
73
+ containerRuntime: { orderSequentially },
74
+ };
75
+ });
76
+ // Verifies that the props are stored correctly in the attribution sub directory - a sub directory
77
+ // of the given directory with name `attributionDirectoryName`.
78
+ function verifySubDirectoryAttribution(directory, key, value, props) {
79
+ assert.equal(directory.get(key), value, "The retrieved value should match the value that was set");
80
+ const attributionDir = directory.getSubDirectory(attributionDirectoryName);
81
+ assert(attributionDir);
82
+ if (props === undefined) {
83
+ assert.equal(attributionDir, undefined, "The attribution directory should not exist because there was no interception");
84
+ }
85
+ else {
86
+ assert.deepEqual(attributionDir.get(key), props, "The user attributes set via the interception callback should exist.");
87
+ }
88
+ }
89
+ // Verifies that the props are stored correctly in the given directory under a key derived from the
90
+ // given key - under attributionKey(key).
91
+ function verifyDirectoryAttribution(directory, key, value, props) {
92
+ assert.equal(directory.get(key), value, "The retrieved value should match the value that was set");
93
+ if (props === undefined) {
94
+ assert.equal(directory.get(attributionKey(key)), undefined, "The user attributes should not exist because there was no interception");
95
+ }
96
+ else {
97
+ assert.deepEqual(directory.get(attributionKey(key)), props, "The user attributes set via the interception callback should exist.");
98
+ }
99
+ }
100
+ /**
101
+ * This test create two levels of directories as shown below:
102
+ * /
103
+ * /foo
104
+ * /foo/bar
105
+ *
106
+ * It mirrors this directory structure for storing attributes as shown below. It uses the baseDirectory
107
+ * in the interception callback to create this.
108
+ * /attribution
109
+ * /attribution/foo
110
+ * /attribution/foo/bar
111
+ *
112
+ * It tests that the wrapper returns the correct baseDirectory (root in this case). It also tests that the
113
+ * subdirectory created via the wrapper calls is wrapped and calls the interception callback.
114
+ */
115
+ it("should be able to create an attribution directory tree mirroring the actual directory tree", async () => {
116
+ const root = createDirectoryWithInterception(sharedDirectory, dataStoreContext, mirrorDirectoryInterceptionCb);
117
+ const key = "level";
118
+ let value = "root";
119
+ root.set(key, value);
120
+ assert.equal(root.get(key), value, "The retrieved value should match the value that was set");
121
+ // Verify that attribution directory `/attribution` was created for root and the user attribute
122
+ // set on it.
123
+ const rootAttribution = root.getSubDirectory(attributionDirectoryName);
124
+ assert(rootAttribution);
125
+ assert.equal(rootAttribution.get(key), userAttributes, "The user attrributes set via callback should exist");
126
+ // Create the level 1 directory `/foo`.
127
+ const foo = root.createSubDirectory("foo");
128
+ value = "level1";
129
+ foo.set(key, value);
130
+ assert.equal(foo.get(key), value, "The retrieved value should match the value that was set");
131
+ // Verify that attribution directory `/attribution/foo` was created for /foo and the user attribute
132
+ // set on it.
133
+ const fooAttribution = rootAttribution.getSubDirectory("foo");
134
+ assert(fooAttribution);
135
+ assert.equal(fooAttribution.get(key), userAttributes, "The user attributes set via callback should exist");
136
+ // Create the level 2 directory `/foo/bar`.
137
+ const bar = foo.createSubDirectory("bar");
138
+ value = "level2";
139
+ bar.set(key, value);
140
+ assert.equal(bar.get(key), value, "The retrieved value should match the value that was set");
141
+ // Verify that attribution directory `/attribution/foo/bar` was created for /foo/bar and the user
142
+ // attribute set on it.
143
+ const barAttribution = fooAttribution.getSubDirectory("bar");
144
+ assert(barAttribution);
145
+ assert.equal(barAttribution.get(key), userAttributes, "The user attributes set via callback should exist");
146
+ });
147
+ /**
148
+ * This test create two levels of directories as shown below:
149
+ * /
150
+ * /foo
151
+ * /foo/bar
152
+ *
153
+ * It creates an attribution subdirectory for each of the subdirectories as shown below:
154
+ * /attribution
155
+ * /foo/attribution
156
+ * /foo/bar/attribution
157
+ *
158
+ * It tests that the wrapper returns the correct subDirectory.
159
+ */
160
+ it("should be able to create an attribution directory for each subdirectory", async () => {
161
+ const root = createDirectoryWithInterception(sharedDirectory, dataStoreContext, subDirectoryinterceptionCb);
162
+ const key = "level";
163
+ let value = "root";
164
+ root.set(key, value);
165
+ verifySubDirectoryAttribution(root, key, value, userAttributes);
166
+ // Create the level 1 directory `/foo`.
167
+ const foo = root.createSubDirectory("foo");
168
+ value = "level1";
169
+ foo.set(key, value);
170
+ verifySubDirectoryAttribution(foo, key, value, userAttributes);
171
+ // Create the level 2 directory `/foo/bar`.
172
+ const bar = foo.createSubDirectory("bar");
173
+ value = "level2";
174
+ bar.set(key, value);
175
+ verifySubDirectoryAttribution(bar, key, value, userAttributes);
176
+ });
177
+ it("should be able to get a wrapped subDirectory via getSubDirectory/getWorkingDirectory", async () => {
178
+ const root = createDirectoryWithInterception(sharedDirectory, dataStoreContext, subDirectoryinterceptionCb);
179
+ // Create a sub directory and get it via getSubDirectory.
180
+ root.createSubDirectory("foo");
181
+ const foo = root.getSubDirectory("foo");
182
+ assert(foo);
183
+ // Set a key and verify that user attribute is set via the interception callback.
184
+ let key = "color";
185
+ let value = "green";
186
+ foo.set(key, value);
187
+ verifySubDirectoryAttribution(foo, key, value, userAttributes);
188
+ // Create a sub directory via the unwrapped object and get its working directory via the wrapper.
189
+ sharedDirectory.createSubDirectory("bar");
190
+ const bar = root.getWorkingDirectory("bar");
191
+ assert(bar);
192
+ // Set a key and verify that user attribute is set via the interception callback.
193
+ key = "permission";
194
+ value = "read";
195
+ bar.set(key, value);
196
+ verifySubDirectoryAttribution(bar, key, value, userAttributes);
197
+ });
198
+ it("should get undefined for non-existent subDirectory via getSubDirectory/getWorkingDirectory", async () => {
199
+ const root = createDirectoryWithInterception(sharedDirectory, dataStoreContext, subDirectoryinterceptionCb);
200
+ const foo = root.getSubDirectory("foo");
201
+ assert.strictEqual(foo, undefined);
202
+ const bar = root.getWorkingDirectory("bar");
203
+ assert.strictEqual(bar, undefined);
204
+ });
205
+ /**
206
+ * This test creates a wrapped shared directory. It then creates a subdirectory and creates another wrapper
207
+ * from the subdirectory. It verifies that the callback for both the root directory and subdirectory is
208
+ * called on a set on the wrapped subdirectory.
209
+ */
210
+ it("should be able to wrap a subDirectory in another interception wrapper", async () => {
211
+ const root = createDirectoryWithInterception(sharedDirectory, dataStoreContext, setInterceptionCb);
212
+ // Create a sub directory via the wrapper and wrap it in another interception wrapper.
213
+ const foo = root.createSubDirectory("foo");
214
+ const userEmail = "test@microsoft.com";
215
+ // Interception callback for wrapping the subdirectory that adds user email to the attribution.
216
+ function interceptionCb(baseDirectory, subDirectory, key, value) {
217
+ const attributes = subDirectory.get(attributionKey(key));
218
+ subDirectory.set(attributionKey(key), { ...attributes, userEmail });
219
+ }
220
+ const fooWithAttribution = createDirectoryWithInterception(foo, dataStoreContext, interceptionCb);
221
+ // Set a key and verify that user id and user email are set via the interception callbacks.
222
+ const permKey = "permission";
223
+ const permValue = "write";
224
+ fooWithAttribution.set(permKey, permValue);
225
+ verifyDirectoryAttribution(fooWithAttribution, permKey, permValue, {
226
+ ...userAttributes,
227
+ userEmail,
228
+ });
229
+ });
230
+ it("should be able to see changes made by the wrapper from the underlying shared directory", async () => {
231
+ const sharedDirectoryWithInterception = createDirectoryWithInterception(sharedDirectory, dataStoreContext, setInterceptionCb);
232
+ const key = "style";
233
+ const value = "bold";
234
+ sharedDirectoryWithInterception.set(key, value);
235
+ verifyDirectoryAttribution(sharedDirectory, key, value, userAttributes);
236
+ });
237
+ it("should be able to see changes made by the underlying shared directory from the wrapper", async () => {
238
+ const sharedDirectoryWithInterception = createDirectoryWithInterception(sharedDirectory, dataStoreContext, setInterceptionCb);
239
+ const key = "font";
240
+ const value = "Arial";
241
+ sharedDirectory.set(key, value);
242
+ verifyDirectoryAttribution(sharedDirectoryWithInterception, key, value);
243
+ });
244
+ /**
245
+ * This test calls set on the wrapper from the interception callback which will cause an infinite
246
+ * recursion. Verify that the wrapper detects this and asserts.
247
+ * Also, verify that the object is not unusable after the assert.
248
+ */
249
+ it("should assert if set is called on the wrapper from the callback causing infinite recursion", async () => {
250
+ // eslint-disable-next-line prefer-const
251
+ let sharedDirectoryWithInterception;
252
+ let useWrapper = true;
253
+ // If useWrapper above is true, this interception callback that calls a set on the wrapped object
254
+ // causing an infinite recursion.
255
+ // If useWrapper is false, it uses the passed subDirectory which does not cause recursion.
256
+ function recursiveInterceptionCb(baseDirectory, subDirectory, key, value) {
257
+ const directory = useWrapper ? sharedDirectoryWithInterception : subDirectory;
258
+ directory.set(attributionKey(key), userAttributes);
259
+ }
260
+ // Create the interception wrapper with the above callback. The set method should throw an assertion as this
261
+ // will cause infinite recursion.
262
+ sharedDirectoryWithInterception = createDirectoryWithInterception(sharedDirectory, dataStoreContext, recursiveInterceptionCb);
263
+ let asserted = false;
264
+ try {
265
+ sharedDirectoryWithInterception.set("color", "green");
266
+ }
267
+ catch (error) {
268
+ assert.strictEqual(error.message, "0x0bf", "We should have caught an assert in replaceText because it detects an infinite recursion");
269
+ asserted = true;
270
+ }
271
+ assert.equal(asserted, true, "The set call should have asserted because it detects inifinite recursion");
272
+ // Set useWrapper to false and call set on the wrapper again. Verify that the object is still usable and
273
+ // we do not get an assert anymore.
274
+ useWrapper = false;
275
+ const colorKey = "color";
276
+ const colorValue = "red";
277
+ sharedDirectoryWithInterception.set(colorKey, colorValue);
278
+ verifyDirectoryAttribution(sharedDirectoryWithInterception, colorKey, colorValue, userAttributes);
279
+ });
280
+ });
281
+ });
282
+ //# sourceMappingURL=sharedDirectoryWithInterception.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharedDirectoryWithInterception.spec.js","sourceRoot":"","sources":["../../src/test/sharedDirectoryWithInterception.spec.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { strict as assert } from \"assert\";\nimport { MockFluidDataStoreRuntime } from \"@fluidframework/test-runtime-utils\";\nimport { IDirectory, SharedDirectory, DirectoryFactory } from \"@fluidframework/map\";\nimport { IFluidDataStoreContext } from \"@fluidframework/runtime-definitions\";\nimport { createDirectoryWithInterception } from \"../map/index.js\";\n\ndescribe(\"Shared Directory with Interception\", () => {\n\tdescribe(\"Simple User Attribution\", () => {\n\t\tconst userAttributes = { userId: \"Fake User\" };\n\t\tconst documentId = \"fakeId\";\n\t\tconst attributionDirectoryName = \"attribution\";\n\t\tconst attributionKey = (key: string) => `${key}.attribution`;\n\t\tlet sharedDirectory: SharedDirectory;\n\t\tlet dataStoreContext: IFluidDataStoreContext;\n\n\t\t// This function gets / creates the attribution directory for the given subdirectory path.\n\t\tfunction getAttributionDirectory(root: IDirectory, path: string) {\n\t\t\tif (!root.hasSubDirectory(attributionDirectoryName)) {\n\t\t\t\troot.createSubDirectory(attributionDirectoryName);\n\t\t\t}\n\n\t\t\tlet currentSubDir = root.getSubDirectory(attributionDirectoryName);\n\t\t\tassert(currentSubDir);\n\t\t\tif (path === \"/\") {\n\t\t\t\treturn currentSubDir;\n\t\t\t}\n\n\t\t\tlet prevSubDir = currentSubDir;\n\t\t\tconst subdirs = path.substr(1).split(\"/\");\n\t\t\tfor (const subdir of subdirs) {\n\t\t\t\tcurrentSubDir = currentSubDir.getSubDirectory(subdir);\n\t\t\t\tif (currentSubDir === undefined) {\n\t\t\t\t\tcurrentSubDir = prevSubDir.createSubDirectory(subdir);\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\t\t\t\tprevSubDir = currentSubDir;\n\t\t\t}\n\t\t\treturn currentSubDir;\n\t\t}\n\n\t\t/**\n\t\t * This callback creates / gets an attribution directory that mirrors the actual directory. It sets the\n\t\t * user attribute in the attribution directory against the same key used in the original set.\n\t\t * For example - For directory /foo, it sets the attribute in /attribution/foo.\n\t\t */\n\t\tfunction mirrorDirectoryInterceptionCb(\n\t\t\tbaseDirectory: IDirectory,\n\t\t\tsubDirectory: IDirectory,\n\t\t\tkey: string,\n\t\t\tvalue: any,\n\t\t): void {\n\t\t\tconst attributionDirectory: IDirectory = getAttributionDirectory(\n\t\t\t\tbaseDirectory,\n\t\t\t\tsubDirectory.absolutePath,\n\t\t\t);\n\t\t\tattributionDirectory.set(key, userAttributes);\n\t\t}\n\n\t\t/**\n\t\t * This callback creates / gets an attribution directory that is a subdirectory of the given directory. It sets\n\t\t * the user attribute in the attribution directory against the same key used in the original set.\n\t\t * For example - For directory /foo, it sets the attribute in /foo/attribute\n\t\t */\n\t\tfunction subDirectoryinterceptionCb(\n\t\t\tbaseDirectory: IDirectory,\n\t\t\tsubDirectory: IDirectory,\n\t\t\tkey: string,\n\t\t\tvalue: any,\n\t\t): void {\n\t\t\tif (!subDirectory.hasSubDirectory(attributionDirectoryName)) {\n\t\t\t\tsubDirectory.createSubDirectory(attributionDirectoryName);\n\t\t\t}\n\t\t\tconst attributionDirectory = subDirectory.getSubDirectory(attributionDirectoryName);\n\t\t\tassert(attributionDirectory);\n\t\t\tattributionDirectory.set(key, userAttributes);\n\t\t}\n\n\t\t// This callback sets the user attribution in the subdirectory against a key derived from the original key.\n\t\tfunction setInterceptionCb(\n\t\t\tbaseDirectory: IDirectory,\n\t\t\tsubDirectory: IDirectory,\n\t\t\tkey: string,\n\t\t\tvalue: any,\n\t\t): void {\n\t\t\tsubDirectory.set(attributionKey(key), userAttributes);\n\t\t}\n\n\t\tfunction orderSequentially(callback: () => void): void {\n\t\t\tcallback();\n\t\t}\n\n\t\tbeforeEach(() => {\n\t\t\tconst dataStoreRuntime = new MockFluidDataStoreRuntime();\n\t\t\tsharedDirectory = new SharedDirectory(\n\t\t\t\tdocumentId,\n\t\t\t\tdataStoreRuntime,\n\t\t\t\tDirectoryFactory.Attributes,\n\t\t\t);\n\n\t\t\t// eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n\t\t\tdataStoreContext = {\n\t\t\t\tcontainerRuntime: { orderSequentially },\n\t\t\t} as IFluidDataStoreContext;\n\t\t});\n\n\t\t// Verifies that the props are stored correctly in the attribution sub directory - a sub directory\n\t\t// of the given directory with name `attributionDirectoryName`.\n\t\tfunction verifySubDirectoryAttribution(\n\t\t\tdirectory: IDirectory,\n\t\t\tkey: string,\n\t\t\tvalue: string,\n\t\t\tprops?: any,\n\t\t) {\n\t\t\tassert.equal(\n\t\t\t\tdirectory.get(key),\n\t\t\t\tvalue,\n\t\t\t\t\"The retrieved value should match the value that was set\",\n\t\t\t);\n\n\t\t\tconst attributionDir = directory.getSubDirectory(attributionDirectoryName);\n\t\t\tassert(attributionDir);\n\t\t\tif (props === undefined) {\n\t\t\t\tassert.equal(\n\t\t\t\t\tattributionDir,\n\t\t\t\t\tundefined,\n\t\t\t\t\t\"The attribution directory should not exist because there was no interception\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tassert.deepEqual(\n\t\t\t\t\tattributionDir.get(key),\n\t\t\t\t\tprops,\n\t\t\t\t\t\"The user attributes set via the interception callback should exist.\",\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t// Verifies that the props are stored correctly in the given directory under a key derived from the\n\t\t// given key - under attributionKey(key).\n\t\tfunction verifyDirectoryAttribution(\n\t\t\tdirectory: IDirectory,\n\t\t\tkey: string,\n\t\t\tvalue: string,\n\t\t\tprops?: any,\n\t\t) {\n\t\t\tassert.equal(\n\t\t\t\tdirectory.get(key),\n\t\t\t\tvalue,\n\t\t\t\t\"The retrieved value should match the value that was set\",\n\t\t\t);\n\n\t\t\tif (props === undefined) {\n\t\t\t\tassert.equal(\n\t\t\t\t\tdirectory.get(attributionKey(key)),\n\t\t\t\t\tundefined,\n\t\t\t\t\t\"The user attributes should not exist because there was no interception\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tassert.deepEqual(\n\t\t\t\t\tdirectory.get(attributionKey(key)),\n\t\t\t\t\tprops,\n\t\t\t\t\t\"The user attributes set via the interception callback should exist.\",\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\t/**\n\t\t * This test create two levels of directories as shown below:\n\t\t * /\n\t\t * /foo\n\t\t * /foo/bar\n\t\t *\n\t\t * It mirrors this directory structure for storing attributes as shown below. It uses the baseDirectory\n\t\t * in the interception callback to create this.\n\t\t * /attribution\n\t\t * /attribution/foo\n\t\t * /attribution/foo/bar\n\t\t *\n\t\t * It tests that the wrapper returns the correct baseDirectory (root in this case). It also tests that the\n\t\t * subdirectory created via the wrapper calls is wrapped and calls the interception callback.\n\t\t */\n\t\tit(\"should be able to create an attribution directory tree mirroring the actual directory tree\", async () => {\n\t\t\tconst root = createDirectoryWithInterception(\n\t\t\t\tsharedDirectory,\n\t\t\t\tdataStoreContext,\n\t\t\t\tmirrorDirectoryInterceptionCb,\n\t\t\t);\n\n\t\t\tconst key: string = \"level\";\n\t\t\tlet value: string = \"root\";\n\t\t\troot.set(key, value);\n\t\t\tassert.equal(\n\t\t\t\troot.get(key),\n\t\t\t\tvalue,\n\t\t\t\t\"The retrieved value should match the value that was set\",\n\t\t\t);\n\n\t\t\t// Verify that attribution directory `/attribution` was created for root and the user attribute\n\t\t\t// set on it.\n\t\t\tconst rootAttribution = root.getSubDirectory(attributionDirectoryName);\n\t\t\tassert(rootAttribution);\n\t\t\tassert.equal(\n\t\t\t\trootAttribution.get(key),\n\t\t\t\tuserAttributes,\n\t\t\t\t\"The user attrributes set via callback should exist\",\n\t\t\t);\n\n\t\t\t// Create the level 1 directory `/foo`.\n\t\t\tconst foo = root.createSubDirectory(\"foo\");\n\t\t\tvalue = \"level1\";\n\t\t\tfoo.set(key, value);\n\t\t\tassert.equal(\n\t\t\t\tfoo.get(key),\n\t\t\t\tvalue,\n\t\t\t\t\"The retrieved value should match the value that was set\",\n\t\t\t);\n\n\t\t\t// Verify that attribution directory `/attribution/foo` was created for /foo and the user attribute\n\t\t\t// set on it.\n\t\t\tconst fooAttribution = rootAttribution.getSubDirectory(\"foo\");\n\t\t\tassert(fooAttribution);\n\t\t\tassert.equal(\n\t\t\t\tfooAttribution.get(key),\n\t\t\t\tuserAttributes,\n\t\t\t\t\"The user attributes set via callback should exist\",\n\t\t\t);\n\n\t\t\t// Create the level 2 directory `/foo/bar`.\n\t\t\tconst bar = foo.createSubDirectory(\"bar\");\n\t\t\tvalue = \"level2\";\n\t\t\tbar.set(key, value);\n\t\t\tassert.equal(\n\t\t\t\tbar.get(key),\n\t\t\t\tvalue,\n\t\t\t\t\"The retrieved value should match the value that was set\",\n\t\t\t);\n\n\t\t\t// Verify that attribution directory `/attribution/foo/bar` was created for /foo/bar and the user\n\t\t\t// attribute set on it.\n\t\t\tconst barAttribution = fooAttribution.getSubDirectory(\"bar\");\n\t\t\tassert(barAttribution);\n\t\t\tassert.equal(\n\t\t\t\tbarAttribution.get(key),\n\t\t\t\tuserAttributes,\n\t\t\t\t\"The user attributes set via callback should exist\",\n\t\t\t);\n\t\t});\n\n\t\t/**\n\t\t * This test create two levels of directories as shown below:\n\t\t * /\n\t\t * /foo\n\t\t * /foo/bar\n\t\t *\n\t\t * It creates an attribution subdirectory for each of the subdirectories as shown below:\n\t\t * /attribution\n\t\t * /foo/attribution\n\t\t * /foo/bar/attribution\n\t\t *\n\t\t * It tests that the wrapper returns the correct subDirectory.\n\t\t */\n\t\tit(\"should be able to create an attribution directory for each subdirectory\", async () => {\n\t\t\tconst root = createDirectoryWithInterception(\n\t\t\t\tsharedDirectory,\n\t\t\t\tdataStoreContext,\n\t\t\t\tsubDirectoryinterceptionCb,\n\t\t\t);\n\t\t\tconst key: string = \"level\";\n\t\t\tlet value: string = \"root\";\n\t\t\troot.set(key, value);\n\t\t\tverifySubDirectoryAttribution(root, key, value, userAttributes);\n\n\t\t\t// Create the level 1 directory `/foo`.\n\t\t\tconst foo = root.createSubDirectory(\"foo\");\n\t\t\tvalue = \"level1\";\n\t\t\tfoo.set(key, value);\n\t\t\tverifySubDirectoryAttribution(foo, key, value, userAttributes);\n\n\t\t\t// Create the level 2 directory `/foo/bar`.\n\t\t\tconst bar = foo.createSubDirectory(\"bar\");\n\t\t\tvalue = \"level2\";\n\t\t\tbar.set(key, value);\n\t\t\tverifySubDirectoryAttribution(bar, key, value, userAttributes);\n\t\t});\n\n\t\tit(\"should be able to get a wrapped subDirectory via getSubDirectory/getWorkingDirectory\", async () => {\n\t\t\tconst root = createDirectoryWithInterception(\n\t\t\t\tsharedDirectory,\n\t\t\t\tdataStoreContext,\n\t\t\t\tsubDirectoryinterceptionCb,\n\t\t\t);\n\n\t\t\t// Create a sub directory and get it via getSubDirectory.\n\t\t\troot.createSubDirectory(\"foo\");\n\t\t\tconst foo = root.getSubDirectory(\"foo\");\n\t\t\tassert(foo);\n\n\t\t\t// Set a key and verify that user attribute is set via the interception callback.\n\t\t\tlet key: string = \"color\";\n\t\t\tlet value: string = \"green\";\n\t\t\tfoo.set(key, value);\n\t\t\tverifySubDirectoryAttribution(foo, key, value, userAttributes);\n\n\t\t\t// Create a sub directory via the unwrapped object and get its working directory via the wrapper.\n\t\t\tsharedDirectory.createSubDirectory(\"bar\");\n\t\t\tconst bar = root.getWorkingDirectory(\"bar\");\n\t\t\tassert(bar);\n\n\t\t\t// Set a key and verify that user attribute is set via the interception callback.\n\t\t\tkey = \"permission\";\n\t\t\tvalue = \"read\";\n\t\t\tbar.set(key, value);\n\t\t\tverifySubDirectoryAttribution(bar, key, value, userAttributes);\n\t\t});\n\n\t\tit(\"should get undefined for non-existent subDirectory via getSubDirectory/getWorkingDirectory\", async () => {\n\t\t\tconst root = createDirectoryWithInterception(\n\t\t\t\tsharedDirectory,\n\t\t\t\tdataStoreContext,\n\t\t\t\tsubDirectoryinterceptionCb,\n\t\t\t);\n\n\t\t\tconst foo = root.getSubDirectory(\"foo\");\n\t\t\tassert.strictEqual(foo, undefined);\n\n\t\t\tconst bar = root.getWorkingDirectory(\"bar\");\n\t\t\tassert.strictEqual(bar, undefined);\n\t\t});\n\n\t\t/**\n\t\t * This test creates a wrapped shared directory. It then creates a subdirectory and creates another wrapper\n\t\t * from the subdirectory. It verifies that the callback for both the root directory and subdirectory is\n\t\t * called on a set on the wrapped subdirectory.\n\t\t */\n\t\tit(\"should be able to wrap a subDirectory in another interception wrapper\", async () => {\n\t\t\tconst root = createDirectoryWithInterception(\n\t\t\t\tsharedDirectory,\n\t\t\t\tdataStoreContext,\n\t\t\t\tsetInterceptionCb,\n\t\t\t);\n\n\t\t\t// Create a sub directory via the wrapper and wrap it in another interception wrapper.\n\t\t\tconst foo = root.createSubDirectory(\"foo\");\n\t\t\tconst userEmail = \"test@microsoft.com\";\n\n\t\t\t// Interception callback for wrapping the subdirectory that adds user email to the attribution.\n\t\t\tfunction interceptionCb(baseDirectory, subDirectory, key, value) {\n\t\t\t\tconst attributes = subDirectory.get(attributionKey(key));\n\t\t\t\tsubDirectory.set(attributionKey(key), { ...attributes, userEmail });\n\t\t\t}\n\t\t\tconst fooWithAttribution = createDirectoryWithInterception(\n\t\t\t\tfoo,\n\t\t\t\tdataStoreContext,\n\t\t\t\tinterceptionCb,\n\t\t\t);\n\n\t\t\t// Set a key and verify that user id and user email are set via the interception callbacks.\n\t\t\tconst permKey: string = \"permission\";\n\t\t\tconst permValue: string = \"write\";\n\t\t\tfooWithAttribution.set(permKey, permValue);\n\t\t\tverifyDirectoryAttribution(fooWithAttribution, permKey, permValue, {\n\t\t\t\t...userAttributes,\n\t\t\t\tuserEmail,\n\t\t\t});\n\t\t});\n\n\t\tit(\"should be able to see changes made by the wrapper from the underlying shared directory\", async () => {\n\t\t\tconst sharedDirectoryWithInterception = createDirectoryWithInterception(\n\t\t\t\tsharedDirectory,\n\t\t\t\tdataStoreContext,\n\t\t\t\tsetInterceptionCb,\n\t\t\t);\n\t\t\tconst key: string = \"style\";\n\t\t\tconst value: string = \"bold\";\n\t\t\tsharedDirectoryWithInterception.set(key, value);\n\t\t\tverifyDirectoryAttribution(sharedDirectory, key, value, userAttributes);\n\t\t});\n\n\t\tit(\"should be able to see changes made by the underlying shared directory from the wrapper\", async () => {\n\t\t\tconst sharedDirectoryWithInterception = createDirectoryWithInterception(\n\t\t\t\tsharedDirectory,\n\t\t\t\tdataStoreContext,\n\t\t\t\tsetInterceptionCb,\n\t\t\t);\n\t\t\tconst key: string = \"font\";\n\t\t\tconst value: string = \"Arial\";\n\t\t\tsharedDirectory.set(key, value);\n\t\t\tverifyDirectoryAttribution(sharedDirectoryWithInterception, key, value);\n\t\t});\n\n\t\t/**\n\t\t * This test calls set on the wrapper from the interception callback which will cause an infinite\n\t\t * recursion. Verify that the wrapper detects this and asserts.\n\t\t * Also, verify that the object is not unusable after the assert.\n\t\t */\n\t\tit(\"should assert if set is called on the wrapper from the callback causing infinite recursion\", async () => {\n\t\t\t// eslint-disable-next-line prefer-const\n\t\t\tlet sharedDirectoryWithInterception: SharedDirectory;\n\n\t\t\tlet useWrapper: boolean = true;\n\t\t\t// If useWrapper above is true, this interception callback that calls a set on the wrapped object\n\t\t\t// causing an infinite recursion.\n\t\t\t// If useWrapper is false, it uses the passed subDirectory which does not cause recursion.\n\t\t\tfunction recursiveInterceptionCb(baseDirectory, subDirectory, key, value) {\n\t\t\t\tconst directory = useWrapper ? sharedDirectoryWithInterception : subDirectory;\n\t\t\t\tdirectory.set(attributionKey(key), userAttributes);\n\t\t\t}\n\n\t\t\t// Create the interception wrapper with the above callback. The set method should throw an assertion as this\n\t\t\t// will cause infinite recursion.\n\t\t\tsharedDirectoryWithInterception = createDirectoryWithInterception(\n\t\t\t\tsharedDirectory,\n\t\t\t\tdataStoreContext,\n\t\t\t\trecursiveInterceptionCb,\n\t\t\t);\n\n\t\t\tlet asserted: boolean = false;\n\t\t\ttry {\n\t\t\t\tsharedDirectoryWithInterception.set(\"color\", \"green\");\n\t\t\t} catch (error: any) {\n\t\t\t\tassert.strictEqual(\n\t\t\t\t\terror.message,\n\t\t\t\t\t\"0x0bf\",\n\t\t\t\t\t\"We should have caught an assert in replaceText because it detects an infinite recursion\",\n\t\t\t\t);\n\t\t\t\tasserted = true;\n\t\t\t}\n\t\t\tassert.equal(\n\t\t\t\tasserted,\n\t\t\t\ttrue,\n\t\t\t\t\"The set call should have asserted because it detects inifinite recursion\",\n\t\t\t);\n\n\t\t\t// Set useWrapper to false and call set on the wrapper again. Verify that the object is still usable and\n\t\t\t// we do not get an assert anymore.\n\t\t\tuseWrapper = false;\n\t\t\tconst colorKey: string = \"color\";\n\t\t\tconst colorValue: string = \"red\";\n\t\t\tsharedDirectoryWithInterception.set(colorKey, colorValue);\n\t\t\tverifyDirectoryAttribution(\n\t\t\t\tsharedDirectoryWithInterception,\n\t\t\t\tcolorKey,\n\t\t\t\tcolorValue,\n\t\t\t\tuserAttributes,\n\t\t\t);\n\t\t});\n\t});\n});\n"]}
@@ -0,0 +1,105 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ import { strict as assert } from "assert";
6
+ import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils";
7
+ import { SharedMap } from "@fluidframework/map";
8
+ import { createSharedMapWithInterception } from "../map/index.js";
9
+ describe("Shared Map with Interception", () => {
10
+ describe("Simple User Attribution", () => {
11
+ /**
12
+ * The following tests test simple user attribution in SharedMap with interception.
13
+ * In the callback function of the SharedMap with inteception, it sets the user
14
+ * attribution information in the underlying SharedMap against <key>.attribution.
15
+ */
16
+ const userAttributes = { userId: "Fake User" };
17
+ const documentId = "fakeId";
18
+ const attributionKey = (key) => `${key}.attribution`;
19
+ let sharedMap;
20
+ let dataStoreContext;
21
+ function orderSequentially(callback) {
22
+ callback();
23
+ }
24
+ function interceptionCb(map, key, value) {
25
+ map.set(attributionKey(key), userAttributes);
26
+ }
27
+ beforeEach(() => {
28
+ const dataStoreRuntime = new MockFluidDataStoreRuntime();
29
+ sharedMap = SharedMap.getFactory().create(dataStoreRuntime, documentId);
30
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
31
+ dataStoreContext = {
32
+ containerRuntime: { orderSequentially },
33
+ };
34
+ });
35
+ // Verifies that the props are stored correctly in the given map under a key derived from the
36
+ // given key - under attributionKey(key).
37
+ function verifyMapAttribution(map, key, value, props) {
38
+ assert.equal(map.get(key), value, "The retrieved value should match the value that was set");
39
+ if (props === undefined) {
40
+ assert.equal(map.get(attributionKey(key)), undefined, "The user attributes should not exist because there was no interception");
41
+ }
42
+ else {
43
+ assert.deepEqual(map.get(attributionKey(key)), props, "The user attributes set via the interception callback should exist.");
44
+ }
45
+ }
46
+ it("should be able to intercept SharedMap set method in the wrapper", async () => {
47
+ const sharedMapWithInterception = createSharedMapWithInterception(sharedMap, dataStoreContext, interceptionCb);
48
+ const key = "color";
49
+ const value = "green";
50
+ sharedMapWithInterception.set(key, value);
51
+ verifyMapAttribution(sharedMapWithInterception, key, value, userAttributes);
52
+ });
53
+ it("should be able to see changes made by the wrapper from the underlying shared map", async () => {
54
+ const sharedMapWithInterception = createSharedMapWithInterception(sharedMap, dataStoreContext, interceptionCb);
55
+ const key = "style";
56
+ const value = "bold";
57
+ sharedMapWithInterception.set(key, value);
58
+ verifyMapAttribution(sharedMap, key, value, userAttributes);
59
+ });
60
+ it("should be able to see changes made by the underlying shared map from the wrapper", async () => {
61
+ const sharedMapWithInterception = createSharedMapWithInterception(sharedMap, dataStoreContext, interceptionCb);
62
+ const key = "font";
63
+ const value = "Arial";
64
+ sharedMap.set(key, value);
65
+ verifyMapAttribution(sharedMapWithInterception, key, value);
66
+ });
67
+ /**
68
+ * This test calls set on the wrapper from the interception callback which will cause an infinite
69
+ * recursion. Verify that the wrapper detects this and asserts.
70
+ * Also, verify that the object is not unusable after the assert.
71
+ */
72
+ it("should assert if set is called on the wrapper from the callback causing infinite recursion", async () => {
73
+ // eslint-disable-next-line prefer-const
74
+ let sharedMapWithInterception;
75
+ let useWrapper = true;
76
+ // If useWrapper above is true, this interception callback that calls a set on the wrapped object
77
+ // causing an infinite recursion.
78
+ // If useWrapper is false, it uses the passed sharedMap which does not cause recursion.
79
+ function recursiveInterceptionCb(map, key, value) {
80
+ const localMap = useWrapper ? sharedMapWithInterception : sharedMap;
81
+ localMap.set(attributionKey(key), userAttributes);
82
+ }
83
+ // Create the interception wrapper with a callback that calls set on the wrapper. The set method should
84
+ // throw an assertion as this will cause infinite recursion.
85
+ sharedMapWithInterception = createSharedMapWithInterception(sharedMap, dataStoreContext, recursiveInterceptionCb);
86
+ let asserted = false;
87
+ try {
88
+ sharedMapWithInterception.set("color", "green");
89
+ }
90
+ catch (error) {
91
+ assert.strictEqual(error.message, "0x0c0", "We should have caught an assert in replaceText because it detects an infinite recursion");
92
+ asserted = true;
93
+ }
94
+ assert.equal(asserted, true, "The set call should have asserted because it detects inifinite recursion");
95
+ // Set useWrapper to false and call set on the wrapper again. Verify that the object is still usable and
96
+ // we do not get an assert anymore.
97
+ useWrapper = false;
98
+ const colorKey = "color";
99
+ const colorValue = "red";
100
+ sharedMapWithInterception.set(colorKey, colorValue);
101
+ verifyMapAttribution(sharedMapWithInterception, colorKey, colorValue, userAttributes);
102
+ });
103
+ });
104
+ });
105
+ //# sourceMappingURL=sharedMapWithInterception.spec.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sharedMapWithInterception.spec.js","sourceRoot":"","sources":["../../src/test/sharedMapWithInterception.spec.ts"],"names":[],"mappings":"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","sourcesContent":["/*!\n * Copyright (c) Microsoft Corporation and contributors. All rights reserved.\n * Licensed under the MIT License.\n */\n\nimport { strict as assert } from \"assert\";\nimport { MockFluidDataStoreRuntime } from \"@fluidframework/test-runtime-utils\";\nimport { ISharedMap, SharedMap } from \"@fluidframework/map\";\nimport { IFluidDataStoreContext } from \"@fluidframework/runtime-definitions\";\nimport { createSharedMapWithInterception } from \"../map/index.js\";\n\ndescribe(\"Shared Map with Interception\", () => {\n\tdescribe(\"Simple User Attribution\", () => {\n\t\t/**\n\t\t * The following tests test simple user attribution in SharedMap with interception.\n\t\t * In the callback function of the SharedMap with inteception, it sets the user\n\t\t * attribution information in the underlying SharedMap against <key>.attribution.\n\t\t */\n\t\tconst userAttributes = { userId: \"Fake User\" };\n\t\tconst documentId = \"fakeId\";\n\t\tconst attributionKey = (key: string) => `${key}.attribution`;\n\t\tlet sharedMap: ISharedMap;\n\t\tlet dataStoreContext: IFluidDataStoreContext;\n\n\t\tfunction orderSequentially(callback: () => void): void {\n\t\t\tcallback();\n\t\t}\n\n\t\tfunction interceptionCb(map: ISharedMap, key: string, value: any): void {\n\t\t\tmap.set(attributionKey(key), userAttributes);\n\t\t}\n\n\t\tbeforeEach(() => {\n\t\t\tconst dataStoreRuntime = new MockFluidDataStoreRuntime();\n\t\t\tsharedMap = SharedMap.getFactory().create(dataStoreRuntime, documentId);\n\n\t\t\t// eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n\t\t\tdataStoreContext = {\n\t\t\t\tcontainerRuntime: { orderSequentially },\n\t\t\t} as IFluidDataStoreContext;\n\t\t});\n\n\t\t// Verifies that the props are stored correctly in the given map under a key derived from the\n\t\t// given key - under attributionKey(key).\n\t\tfunction verifyMapAttribution(map: ISharedMap, key: string, value: string, props?: any) {\n\t\t\tassert.equal(\n\t\t\t\tmap.get(key),\n\t\t\t\tvalue,\n\t\t\t\t\"The retrieved value should match the value that was set\",\n\t\t\t);\n\n\t\t\tif (props === undefined) {\n\t\t\t\tassert.equal(\n\t\t\t\t\tmap.get(attributionKey(key)),\n\t\t\t\t\tundefined,\n\t\t\t\t\t\"The user attributes should not exist because there was no interception\",\n\t\t\t\t);\n\t\t\t} else {\n\t\t\t\tassert.deepEqual(\n\t\t\t\t\tmap.get(attributionKey(key)),\n\t\t\t\t\tprops,\n\t\t\t\t\t\"The user attributes set via the interception callback should exist.\",\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\n\t\tit(\"should be able to intercept SharedMap set method in the wrapper\", async () => {\n\t\t\tconst sharedMapWithInterception = createSharedMapWithInterception(\n\t\t\t\tsharedMap,\n\t\t\t\tdataStoreContext,\n\t\t\t\tinterceptionCb,\n\t\t\t);\n\t\t\tconst key: string = \"color\";\n\t\t\tconst value: string = \"green\";\n\t\t\tsharedMapWithInterception.set(key, value);\n\t\t\tverifyMapAttribution(sharedMapWithInterception, key, value, userAttributes);\n\t\t});\n\n\t\tit(\"should be able to see changes made by the wrapper from the underlying shared map\", async () => {\n\t\t\tconst sharedMapWithInterception = createSharedMapWithInterception(\n\t\t\t\tsharedMap,\n\t\t\t\tdataStoreContext,\n\t\t\t\tinterceptionCb,\n\t\t\t);\n\t\t\tconst key: string = \"style\";\n\t\t\tconst value: string = \"bold\";\n\t\t\tsharedMapWithInterception.set(key, value);\n\t\t\tverifyMapAttribution(sharedMap, key, value, userAttributes);\n\t\t});\n\n\t\tit(\"should be able to see changes made by the underlying shared map from the wrapper\", async () => {\n\t\t\tconst sharedMapWithInterception = createSharedMapWithInterception(\n\t\t\t\tsharedMap,\n\t\t\t\tdataStoreContext,\n\t\t\t\tinterceptionCb,\n\t\t\t);\n\t\t\tconst key: string = \"font\";\n\t\t\tconst value: string = \"Arial\";\n\t\t\tsharedMap.set(key, value);\n\t\t\tverifyMapAttribution(sharedMapWithInterception, key, value);\n\t\t});\n\n\t\t/**\n\t\t * This test calls set on the wrapper from the interception callback which will cause an infinite\n\t\t * recursion. Verify that the wrapper detects this and asserts.\n\t\t * Also, verify that the object is not unusable after the assert.\n\t\t */\n\t\tit(\"should assert if set is called on the wrapper from the callback causing infinite recursion\", async () => {\n\t\t\t// eslint-disable-next-line prefer-const\n\t\t\tlet sharedMapWithInterception: ISharedMap;\n\n\t\t\tlet useWrapper: boolean = true;\n\t\t\t// If useWrapper above is true, this interception callback that calls a set on the wrapped object\n\t\t\t// causing an infinite recursion.\n\t\t\t// If useWrapper is false, it uses the passed sharedMap which does not cause recursion.\n\t\t\tfunction recursiveInterceptionCb(map: ISharedMap, key: string, value: any) {\n\t\t\t\tconst localMap = useWrapper ? sharedMapWithInterception : sharedMap;\n\t\t\t\tlocalMap.set(attributionKey(key), userAttributes);\n\t\t\t}\n\t\t\t// Create the interception wrapper with a callback that calls set on the wrapper. The set method should\n\t\t\t// throw an assertion as this will cause infinite recursion.\n\t\t\tsharedMapWithInterception = createSharedMapWithInterception(\n\t\t\t\tsharedMap,\n\t\t\t\tdataStoreContext,\n\t\t\t\trecursiveInterceptionCb,\n\t\t\t);\n\n\t\t\tlet asserted: boolean = false;\n\t\t\ttry {\n\t\t\t\tsharedMapWithInterception.set(\"color\", \"green\");\n\t\t\t} catch (error: any) {\n\t\t\t\tassert.strictEqual(\n\t\t\t\t\terror.message,\n\t\t\t\t\t\"0x0c0\",\n\t\t\t\t\t\"We should have caught an assert in replaceText because it detects an infinite recursion\",\n\t\t\t\t);\n\t\t\t\tasserted = true;\n\t\t\t}\n\t\t\tassert.equal(\n\t\t\t\tasserted,\n\t\t\t\ttrue,\n\t\t\t\t\"The set call should have asserted because it detects inifinite recursion\",\n\t\t\t);\n\n\t\t\t// Set useWrapper to false and call set on the wrapper again. Verify that the object is still usable and\n\t\t\t// we do not get an assert anymore.\n\t\t\tuseWrapper = false;\n\t\t\tconst colorKey: string = \"color\";\n\t\t\tconst colorValue: string = \"red\";\n\t\t\tsharedMapWithInterception.set(colorKey, colorValue);\n\t\t\tverifyMapAttribution(sharedMapWithInterception, colorKey, colorValue, userAttributes);\n\t\t});\n\t});\n});\n"]}
@@ -0,0 +1,147 @@
1
+ /*!
2
+ * Copyright (c) Microsoft Corporation and contributors. All rights reserved.
3
+ * Licensed under the MIT License.
4
+ */
5
+ import { strict as assert } from "assert";
6
+ import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils";
7
+ import { SharedString, SharedStringFactory } from "@fluidframework/sequence";
8
+ import { createSharedStringWithInterception } from "../sequence/index.js";
9
+ describe("Shared String with Interception", () => {
10
+ /**
11
+ * The following tests test simple user attribution in SharedString with interception.
12
+ * In the callback function of the SharedString with interception, it adds the user
13
+ * information to the passed properties and returns it.
14
+ */
15
+ describe("Simple User Attribution", () => {
16
+ const userAttributes = { userId: "Fake User" };
17
+ const documentId = "fakeId";
18
+ let sharedString;
19
+ let dataStoreContext;
20
+ function orderSequentially(callback) {
21
+ callback();
22
+ }
23
+ // Interception function that adds userProps to the passed props and returns.
24
+ function propertyInterceptionCb(props) {
25
+ const newProps = { ...props, ...userAttributes };
26
+ return newProps;
27
+ }
28
+ // Function that verifies that the given shared string has correct value and the right properties at
29
+ // the given position.
30
+ function verifyString(ss, text, props, position) {
31
+ assert.equal(ss.getText(), text, "The retrieved text should match the inserted text");
32
+ assert.deepEqual({ ...ss.getPropertiesAtPosition(position) }, { ...props }, "The properties set via the interception callback should exist");
33
+ }
34
+ beforeEach(() => {
35
+ const dataStoreRuntime = new MockFluidDataStoreRuntime();
36
+ sharedString = new SharedString(dataStoreRuntime, documentId, SharedStringFactory.Attributes);
37
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
38
+ dataStoreContext = {
39
+ containerRuntime: { orderSequentially },
40
+ };
41
+ });
42
+ it("should be able to intercept SharedString methods by the wrapper", async () => {
43
+ const sharedStringWithInterception = createSharedStringWithInterception(sharedString, dataStoreContext, propertyInterceptionCb);
44
+ // Insert text into shared string.
45
+ let text = "123";
46
+ let syleProps = { style: "bold" };
47
+ sharedStringWithInterception.insertText(0, text, syleProps);
48
+ verifyString(sharedStringWithInterception, text, { ...syleProps, ...userAttributes }, 2);
49
+ // Replace text in the shared string.
50
+ text = "aaa";
51
+ syleProps = { style: "italics " };
52
+ sharedStringWithInterception.replaceText(2, 3, "aaa", syleProps);
53
+ verifyString(sharedStringWithInterception, "12aaa", { ...syleProps, ...userAttributes }, 2);
54
+ // Annotate the shared string.
55
+ const colorProps = { color: "green" };
56
+ sharedStringWithInterception.annotateRange(0, 5, colorProps);
57
+ verifyString(sharedStringWithInterception, "12aaa", { ...syleProps, ...colorProps, ...userAttributes }, 2);
58
+ });
59
+ it("should be able to see changes made by the wrapper from the underlying shared string", async () => {
60
+ const sharedStringWithInterception = createSharedStringWithInterception(sharedString, dataStoreContext, propertyInterceptionCb);
61
+ // Insert text via the shared string with interception wrapper.
62
+ let text = "123";
63
+ let syleProps = { style: "bold" };
64
+ sharedStringWithInterception.insertText(0, text, syleProps);
65
+ // Verify the text and properties via the underlying shared string.
66
+ verifyString(sharedString, text, { ...syleProps, ...userAttributes }, 2);
67
+ // Replace text via the shared string with interception wrapper.
68
+ text = "aaa";
69
+ syleProps = { style: "italics " };
70
+ sharedStringWithInterception.replaceText(2, 3, "aaa", syleProps);
71
+ // Verify the text and properties via the underlying shared string.
72
+ verifyString(sharedString, "12aaa", { ...syleProps, ...userAttributes }, 2);
73
+ // Annotate the shared string.
74
+ const colorProps = { color: "green" };
75
+ sharedStringWithInterception.annotateRange(0, 5, colorProps);
76
+ // Verify the text and properties via the underlying shared string.
77
+ verifyString(sharedString, "12aaa", { ...syleProps, ...colorProps, ...userAttributes }, 2);
78
+ });
79
+ it("should be able to see changes made by the underlying shared string from the wrapper", async () => {
80
+ const sharedStringWithInterception = createSharedStringWithInterception(sharedString, dataStoreContext, propertyInterceptionCb);
81
+ // Insert text via the underlying shared string.
82
+ let text = "123";
83
+ let syleProps = { style: "bold" };
84
+ sharedString.insertText(0, text, syleProps);
85
+ // Verify the text and properties via the interception wrapper. It should not have the user attributes.
86
+ verifyString(sharedStringWithInterception, text, syleProps, 2);
87
+ // Replace text via the underlying shared string.
88
+ text = "aaa";
89
+ syleProps = { style: "italics " };
90
+ sharedString.replaceText(2, 3, "aaa", syleProps);
91
+ // Verify the text and properties via the interception wrapper. It should not have the user attributes.
92
+ verifyString(sharedStringWithInterception, "12aaa", syleProps, 2);
93
+ // Annotate the shared string via the underlying shared string.
94
+ const colorProps = { color: "green" };
95
+ sharedString.annotateRange(0, 5, colorProps);
96
+ // Verify the text and properties via the interception wrapper. It should not have the user attributes.
97
+ verifyString(sharedStringWithInterception, "12aaa", { ...syleProps, ...colorProps }, 2);
98
+ });
99
+ /**
100
+ * This test calls a method on the wrapper from the interception callback which will cause an infinite
101
+ * recursion. Verify that the wrapper detects this and asserts.
102
+ * Also, verify that the object is not unusable after the assert.
103
+ */
104
+ it("should assert if a wrapper method is called from the callback causing infinite recursion", async () => {
105
+ // eslint-disable-next-line prefer-const
106
+ let sharedStringWithInterception;
107
+ const propsInRecursiveCb = { fromRecursiveCb: "true" };
108
+ let useWrapper = true;
109
+ // If useWrapper above is true, this interception callback calls a method on the wrapped object
110
+ // causing an infinite recursion.
111
+ // If useWrapper is false, it uses the passed shared string which does not cause recursion.
112
+ function recursiveInterceptionCb(properties) {
113
+ const ss = useWrapper ? sharedStringWithInterception : sharedString;
114
+ ss.annotateRange(0, 1, propsInRecursiveCb);
115
+ return { ...properties, ...userAttributes };
116
+ }
117
+ // Create the interception wrapper with the above callback. The set method should throw an assertion as this
118
+ // will cause infinite recursion.
119
+ sharedStringWithInterception = createSharedStringWithInterception(sharedString, dataStoreContext, recursiveInterceptionCb);
120
+ let text = "123";
121
+ const props = { style: "bold" };
122
+ // First, insert text via the unwrapped shared string so that we have something to annotate in the
123
+ // recursiveInterceptionCb.
124
+ sharedString.insertText(0, text, props);
125
+ let asserted = false;
126
+ try {
127
+ text = "abc";
128
+ // Try to replace text.
129
+ sharedStringWithInterception.replaceText(1, 2, text);
130
+ }
131
+ catch (error) {
132
+ assert.strictEqual(error.message, "0x0c8", "We should have caught an assert in replaceText because it detects an infinite recursion");
133
+ asserted = true;
134
+ }
135
+ assert.equal(asserted, true, "replaceText should have asserted because it detects inifinite recursion");
136
+ // Verify that the object is still usable:
137
+ // Set useWrapper to false and call replacetext on the wrapper again. Verify that we do not get an assert.
138
+ useWrapper = false;
139
+ text = "test";
140
+ sharedStringWithInterception.replaceText(2, 3, text, props);
141
+ verifyString(sharedStringWithInterception, "12test", { ...props, ...userAttributes }, 2);
142
+ // Verify that the annotate on position 0 in the recursiveInterceptionCb annotated the attributes.
143
+ verifyString(sharedStringWithInterception, "12test", { ...props, ...propsInRecursiveCb }, 0);
144
+ });
145
+ });
146
+ });
147
+ //# sourceMappingURL=sharedStringWithInterception.spec.js.map