diff --git a/change/@microsoft-fast-element-ecbe6511-b19c-4312-91f2-1c354ebd5245.json b/change/@microsoft-fast-element-ecbe6511-b19c-4312-91f2-1c354ebd5245.json new file mode 100644 index 00000000000..6d5e05205b9 --- /dev/null +++ b/change/@microsoft-fast-element-ecbe6511-b19c-4312-91f2-1c354ebd5245.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: enable synchronous dom updates for SSR", + "packageName": "@microsoft/fast-element", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@microsoft-fast-router-08a4dedc-a404-44ca-ac6a-fe3b4b22a2a1.json b/change/@microsoft-fast-router-08a4dedc-a404-44ca-ac6a-fe3b4b22a2a1.json new file mode 100644 index 00000000000..1a0e4e6db55 --- /dev/null +++ b/change/@microsoft-fast-router-08a4dedc-a404-44ca-ac6a-fe3b4b22a2a1.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "fix: update router to work with new template primitives", + "packageName": "@microsoft/fast-router", + "email": "roeisenb@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md index bfb4247549b..fa09944427e 100644 --- a/packages/web-components/fast-element/docs/api-report.md +++ b/packages/web-components/fast-element/docs/api-report.md @@ -209,6 +209,7 @@ export const DOM: Readonly<{ createInterpolationPlaceholder(index: number): string; createCustomAttributePlaceholder(index: number): string; createBlockPlaceholder(index: number): string; + setUpdateMode(isAsync: boolean): void; queueUpdate(callable: Callable): void; nextUpdate(): Promise; processUpdates(): void; diff --git a/packages/web-components/fast-element/src/dom.spec.ts b/packages/web-components/fast-element/src/dom.spec.ts index eec75bb63d4..a201d7808e5 100644 --- a/packages/web-components/fast-element/src/dom.spec.ts +++ b/packages/web-components/fast-element/src/dom.spec.ts @@ -25,7 +25,7 @@ function watchSetTimeoutForErrors() { } describe("The DOM facade", () => { - context("when updating DOM", () => { + context("when updating DOM asynchronously", () => { it("calls task in a future turn", done => { let called = false; @@ -430,4 +430,100 @@ describe("The DOM facade", () => { }, waitMilliseconds); }); }); + + context("when updating DOM synchronously", () => { + beforeEach(() => { + DOM.setUpdateMode(false); + }); + + afterEach(() => { + DOM.setUpdateMode(true); + }); + + it("calls task immediately", () => { + let called = false; + + DOM.queueUpdate(() => { + called = true; + }); + + expect(called).to.equal(true); + }); + + it("calls task.call method immediately", () => { + let called = false; + + DOM.queueUpdate({ + call: () => { + called = true; + } + }); + + expect(called).to.equal(true); + }); + + it("calls multiple tasks in order", () => { + const calls:number[] = []; + + DOM.queueUpdate(() => { + calls.push(0); + }); + DOM.queueUpdate(() => { + calls.push(1); + }); + DOM.queueUpdate(() => { + calls.push(2); + }); + + expect(calls).to.eql([0, 1, 2]); + }); + + it("can schedule tasks recursively", () => { + const steps: number[] = []; + + DOM.queueUpdate(() => { + steps.push(0); + DOM.queueUpdate(() => { + steps.push(2); + DOM.queueUpdate(() => { + steps.push(4); + }); + steps.push(3); + }); + steps.push(1); + }); + + expect(steps).to.eql([0, 1, 2, 3, 4]); + }); + + it(`can recurse ${maxRecursion} tasks deep`, () => { + let recurseCount = 0; + function go() { + if (++recurseCount < maxRecursion) { + DOM.queueUpdate(go); + } + } + + DOM.queueUpdate(go); + + expect(recurseCount).to.equal(maxRecursion); + }); + + it("throws errors immediately", () => { + const calls: number[] = []; + let caught: any; + + try { + DOM.queueUpdate(() => { + calls.push(0); + throw 0; + }); + } catch(error) { + caught = error; + } + + expect(calls).to.eql([0]); + expect(caught).to.eql(0); + }); + }); }); diff --git a/packages/web-components/fast-element/src/dom.ts b/packages/web-components/fast-element/src/dom.ts index f95cd66af56..45942985dbf 100644 --- a/packages/web-components/fast-element/src/dom.ts +++ b/packages/web-components/fast-element/src/dom.ts @@ -13,6 +13,8 @@ const fastHTMLPolicy: TrustedTypesPolicy = $global.trustedTypes.createPolicy( let htmlPolicy: TrustedTypesPolicy = fastHTMLPolicy; const updateQueue: Callable[] = []; const pendingErrors: any[] = []; +const rAF = $global.requestAnimationFrame; +let updateAsync = true; function throwFirstError(): void { if (pendingErrors.length) { @@ -24,8 +26,13 @@ function tryRunTask(task: Callable): void { try { (task as any).call(); } catch (error) { - pendingErrors.push(error); - setTimeout(throwFirstError, 0); + if (updateAsync) { + pendingErrors.push(error); + setTimeout(throwFirstError, 0); + } else { + updateQueue.length = 0; + throw error; + } } } @@ -128,16 +135,30 @@ export const DOM = Object.freeze({ return ``; }, + /** + * Sets the update mode used by queueUpdate. + * @param isAsync Indicates whether DOM updates should be asynchronous. + * @remarks + * By default, the update mode is asynchronous, since that provides the best + * performance in the browser. Passing false to setUpdateMode will instead cause + * the queue to be immediately processed for each call to queueUpdate. However, + * ordering will still be preserved so that nested tasks do not run until + * after parent tasks complete. + */ + setUpdateMode(isAsync: boolean) { + updateAsync = isAsync; + }, + /** * Schedules DOM update work in the next async batch. * @param callable - The callable function or object to queue. */ queueUpdate(callable: Callable) { - if (updateQueue.length < 1) { - $global.requestAnimationFrame(DOM.processUpdates); - } - updateQueue.push(callable); + + if (updateQueue.length < 2) { + updateAsync ? rAF(DOM.processUpdates) : DOM.processUpdates(); + } }, /**