diff --git a/change/@microsoft-fast-element-d91894b9-b59a-49fe-9412-e63b309b254a.json b/change/@microsoft-fast-element-d91894b9-b59a-49fe-9412-e63b309b254a.json new file mode 100644 index 00000000000..72991ff581f --- /dev/null +++ b/change/@microsoft-fast-element-d91894b9-b59a-49fe-9412-e63b309b254a.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Skip observer map reprocessing and notification when a generated accessor is assigned the reference it already holds.", + "packageName": "@microsoft/fast-element", + "email": "144495202+AKnassa@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/packages/fast-element/SIZES.md b/packages/fast-element/SIZES.md index 92d854e5126..43698ccb771 100644 --- a/packages/fast-element/SIZES.md +++ b/packages/fast-element/SIZES.md @@ -21,5 +21,5 @@ Bundle sizes for `@microsoft/fast-element` exports. | enableHydration (@microsoft/fast-element/hydration.js) | 46.53 KB | 13.91 KB | 12.49 KB | | declarativeTemplate (@microsoft/fast-element/declarative.js) | 62.15 KB | 19.46 KB | 17.42 KB | | ArrayObserver (@microsoft/fast-element/arrays.js) | 12.55 KB | 4.46 KB | 4.03 KB | -| observerMap (@microsoft/fast-element/observer-map.js) | 21.96 KB | 7.73 KB | 6.97 KB | +| observerMap (@microsoft/fast-element/observer-map.js) | 21.98 KB | 7.74 KB | 6.98 KB | | attributeMap (@microsoft/fast-element/attribute-map.js) | 15.31 KB | 5.41 KB | 4.88 KB | diff --git a/packages/fast-element/src/declarative/extension-subpaths.pw.spec.ts b/packages/fast-element/src/declarative/extension-subpaths.pw.spec.ts index 993dd72ef5c..07376f5e671 100644 --- a/packages/fast-element/src/declarative/extension-subpaths.pw.spec.ts +++ b/packages/fast-element/src/declarative/extension-subpaths.pw.spec.ts @@ -241,6 +241,178 @@ test.describe("extension subpaths", () => { expect(result.hasObservableName).toBe(true); }); + test("observerMap skips reprocessing when the same array reference is assigned", async ({ + page, + }) => { + await page.goto("/"); + + const result = await page.evaluate(async () => { + const { ArrayObserver, Observable, Schema, observerMap } = await import( + // @ts-expect-error: Client module. + "/extension-subpaths-main.js" + ); + + ArrayObserver.enable(); + + class ManualElement { + public someData: any; + } + + const schema = new Schema("manual-same-reference"); + schema.addPath({ + rootPropertyName: "someData", + pathConfig: { + type: "repeat", + path: "someData.users", + currentContext: "user", + parentContext: null, + }, + childrenMap: null, + }); + schema.addPath({ + rootPropertyName: "someData", + pathConfig: { + type: "repeat", + path: "user.orders", + currentContext: "order", + parentContext: "user", + }, + childrenMap: null, + }); + schema.addPath({ + rootPropertyName: "someData", + pathConfig: { + type: "default", + path: "order.id", + currentContext: "order", + parentContext: "user", + }, + childrenMap: null, + }); + + observerMap({ schema })({ type: ManualElement } as any); + + const instance = new ManualElement(); + instance.someData = { + users: [{ orders: [{ id: "first" }] }], + }; + + const user = instance.someData.users[0]; + const orders = user.orders; + + let notifyCount = 0; + Observable.getNotifier(user).subscribe( + { + handleChange() { + notifyCount++; + }, + }, + "orders", + ); + + user.orders = orders; + + user.orders.push({ id: "second" }); + await new Promise(resolve => + requestAnimationFrame(() => requestAnimationFrame(resolve)), + ); + + return { + notifyCount, + preservesReference: user.orders === orders, + hasObservableIdOnPushedItem: + typeof Object.getOwnPropertyDescriptor(user.orders[1], "id")?.get === + "function", + }; + }); + + expect(result.notifyCount).toBe(0); + expect(result.preservesReference).toBe(true); + expect(result.hasObservableIdOnPushedItem).toBe(true); + }); + + test("observerMap processes a replacement array assigned to a nested property", async ({ + page, + }) => { + await page.goto("/"); + + const result = await page.evaluate(async () => { + const { ArrayObserver, Observable, Schema, observerMap } = await import( + // @ts-expect-error: Client module. + "/extension-subpaths-main.js" + ); + + ArrayObserver.enable(); + + class ManualElement { + public someData: any; + } + + const schema = new Schema("manual-replacement-array"); + schema.addPath({ + rootPropertyName: "someData", + pathConfig: { + type: "repeat", + path: "someData.users", + currentContext: "user", + parentContext: null, + }, + childrenMap: null, + }); + schema.addPath({ + rootPropertyName: "someData", + pathConfig: { + type: "repeat", + path: "user.orders", + currentContext: "order", + parentContext: "user", + }, + childrenMap: null, + }); + schema.addPath({ + rootPropertyName: "someData", + pathConfig: { + type: "default", + path: "order.id", + currentContext: "order", + parentContext: "user", + }, + childrenMap: null, + }); + + observerMap({ schema })({ type: ManualElement } as any); + + const instance = new ManualElement(); + instance.someData = { + users: [{ orders: [{ id: "first" }] }], + }; + + const user = instance.someData.users[0]; + + let notifyCount = 0; + Observable.getNotifier(user).subscribe( + { + handleChange() { + notifyCount++; + }, + }, + "orders", + ); + + user.orders = [{ id: "second" }]; + + return { + notifyCount, + hasObservableIdOnReplacementItem: + typeof Object.getOwnPropertyDescriptor(user.orders[0], "id")?.get === + "function", + }; + }); + + expect(result.notifyCount).toBe(1); + expect(result.hasObservableIdOnReplacementItem).toBe(true); + }); + test("observerMap reports missing schemas for non-declarative definitions", async ({ page, }) => { diff --git a/packages/fast-element/src/declarative/observer-map-utilities.ts b/packages/fast-element/src/declarative/observer-map-utilities.ts index 336f150f55a..3ed9139f96a 100644 --- a/packages/fast-element/src/declarative/observer-map-utilities.ts +++ b/packages/fast-element/src/declarative/observer-map-utilities.ts @@ -148,6 +148,11 @@ function defineObservableProperty( }, setValue(source: any, value: any): void { const oldValue = source[field]; + + if (value === oldValue) { + return; + } + const newValue = assignObservables( schema, rootSchema, diff --git a/packages/fast-element/test/extension-subpaths-main.ts b/packages/fast-element/test/extension-subpaths-main.ts index b2bea4da386..3ad0d569e50 100644 --- a/packages/fast-element/test/extension-subpaths-main.ts +++ b/packages/fast-element/test/extension-subpaths-main.ts @@ -10,6 +10,7 @@ export { AttributeMap, attributeMap, } from "@microsoft/fast-element/attribute-map.js"; +export { Observable } from "@microsoft/fast-element/observable.js"; export { ObserverMap, observerMap, diff --git a/sites/website/src/docs/3.x/resources/export-sizes.md b/sites/website/src/docs/3.x/resources/export-sizes.md index 8cc65070e6d..6592d9d512d 100644 --- a/sites/website/src/docs/3.x/resources/export-sizes.md +++ b/sites/website/src/docs/3.x/resources/export-sizes.md @@ -36,5 +36,5 @@ Bundle sizes for `@microsoft/fast-element` exports. | enableHydration (@microsoft/fast-element/hydration.js) | 46.53 KB | 13.91 KB | 12.49 KB | | declarativeTemplate (@microsoft/fast-element/declarative.js) | 62.15 KB | 19.46 KB | 17.42 KB | | ArrayObserver (@microsoft/fast-element/arrays.js) | 12.55 KB | 4.46 KB | 4.03 KB | -| observerMap (@microsoft/fast-element/observer-map.js) | 21.96 KB | 7.73 KB | 6.97 KB | +| observerMap (@microsoft/fast-element/observer-map.js) | 21.98 KB | 7.74 KB | 6.98 KB | | attributeMap (@microsoft/fast-element/attribute-map.js) | 15.31 KB | 5.41 KB | 4.88 KB |