Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enable multiple fast-element instances in browser at once #5695

Merged
merged 16 commits into from
Mar 8, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "feat: enable multiple fast-element instances in browser at once",
"packageName": "@microsoft/fast-element",
"email": "roeisenb@microsoft.com",
"dependentChangeType": "patch"
}
45 changes: 36 additions & 9 deletions packages/web-components/fast-element/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -215,8 +215,8 @@ export const DOM: Readonly<{
createInterpolationPlaceholder(index: number): string;
createCustomAttributePlaceholder(attributeName: string, index: number): string;
createBlockPlaceholder(index: number): string;
queueUpdate(callable: Callable): void;
processUpdates(): void;
queueUpdate: (callable: Callable) => void;
processUpdates: () => void;
nextUpdate(): Promise<void>;
setAttribute(element: HTMLElement, attributeName: string, value: any): void;
setBooleanAttribute(element: HTMLElement, attributeName: string, value: boolean): void;
Expand Down Expand Up @@ -280,8 +280,15 @@ export class ExecutionContext<TParent = any, TGrandparent = any> {
length: number;
parent: TParent;
parentContext: ExecutionContext<TGrandparent>;
// @internal
static setEvent(event: Event | null): void;
}

// Warning: (ae-internal-missing-underscore) The name "FAST" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal
export const FAST: FASTGlobal;

// @public
export interface FASTElement {
$emit(type: string, detail?: any, options?: Omit<CustomEventInit, "detail">): boolean | void;
Expand All @@ -307,7 +314,7 @@ export class FASTElementDefinition<TType extends Function = Function> {
readonly attributes: ReadonlyArray<AttributeDefinition>;
define(registry?: CustomElementRegistry): this;
readonly elementOptions?: ElementDefinitionOptions;
static forType<TType extends Function>(type: TType): FASTElementDefinition | undefined;
static forType: <TType_1 extends Function>(key: TType_1) => FASTElementDefinition<Function> | undefined;
readonly isDefined: boolean;
readonly name: string;
readonly propertyLookup: Record<string, AttributeDefinition>;
Expand All @@ -317,9 +324,20 @@ export class FASTElementDefinition<TType extends Function = Function> {
readonly type: TType;
}

// Warning: (ae-internal-missing-underscore) The name "FASTGlobal" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal
export interface FASTGlobal {
getById<T>(id: string | number): T | null;
// (undocumented)
getById<T>(id: string | number, initialize: () => T): T;
readonly versions: string[];
}

// @public
export type Global = typeof globalThis & {
trustedTypes: TrustedTypes;
readonly FAST: FASTGlobal;
};

// @public
Expand Down Expand Up @@ -359,6 +377,20 @@ export class HTMLView implements ElementView, SyntheticView {
unbind(): void;
}

// Warning: (ae-internal-missing-underscore) The name "KernelServiceId" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal
export const enum KernelServiceId {
// (undocumented)
contextEvent = 3,
// (undocumented)
elementDefinitions = 4,
// (undocumented)
observable = 2,
// (undocumented)
updateQueue = 1
}

// Warning: (ae-internal-missing-underscore) The name "Mutable" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal
Expand Down Expand Up @@ -392,7 +424,7 @@ export const nullableNumberConverter: ValueConverter;
// @public
export const Observable: Readonly<{
setArrayObserverFactory(factory: (collection: any[]) => Notifier): void;
getNotifier(source: any): Notifier;
getNotifier: (source: any) => Notifier;
track(source: unknown, propertyName: string): void;
trackVolatile(): void;
notify(source: unknown, args: any): void;
Expand Down Expand Up @@ -465,11 +497,6 @@ export interface RepeatOptions {
recycle?: boolean;
}

// Warning: (ae-internal-missing-underscore) The name "setCurrentEvent" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal (undocumented)
export function setCurrentEvent(event: Event | null): void;

// @public
export function slotted<T = any>(propertyOrOptions: (keyof T & string) | SlottedBehaviorOptions<keyof T & string>): CaptureType<T>;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { FAST, KernelServiceId } from "../platform";
import type { Mutable } from "../interfaces";
import { Observable } from "../observation/observable";
import { ComposableStyles, ElementStyles } from "../styles/element-styles";
Expand All @@ -6,7 +7,20 @@ import { AttributeConfiguration, AttributeDefinition } from "./attributes";

const defaultShadowOptions: ShadowRootInit = { mode: "open" };
const defaultElementOptions: ElementDefinitionOptions = {};
const fastDefinitions = new Map<Function, FASTElementDefinition>();
const definitions = FAST.getById(KernelServiceId.elementDefinitions, () => {
const lookup = new Map<Function, FASTElementDefinition>();

return Object.freeze({
set(key: Function, value: FASTElementDefinition): void {
if (!lookup.has(key)) {
lookup.set(key, value);
}
},
get<TType extends Function>(key: TType): FASTElementDefinition | undefined {
return lookup.get(key);
},
});
});

/**
* Represents metadata configuration for a custom element.
Expand Down Expand Up @@ -178,7 +192,7 @@ export class FASTElementDefinition<TType extends Function = Function> {
enumerable: true,
});

fastDefinitions.set(type, this);
definitions.set(type, this);
(this as Mutable<this>).isDefined = true;
}

Expand All @@ -193,9 +207,5 @@ export class FASTElementDefinition<TType extends Function = Function> {
* Gets the element definition associated with the specified type.
* @param type - The custom element type to retrieve the definition for.
*/
static forType<TType extends Function>(
type: TType
): FASTElementDefinition | undefined {
return fastDefinitions.get(type);
}
static forType = definitions.get;
}
129 changes: 68 additions & 61 deletions packages/web-components/fast-element/src/dom.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,70 @@
import type { Callable } from "./interfaces";
import { $global, TrustedTypesPolicy } from "./platform";
import { KernelServiceId, $global, TrustedTypesPolicy } from "./platform";

const updateQueue = [] as Callable[];
const updateQueue = $global.FAST.getById(KernelServiceId.updateQueue, () => {
const tasks = [] as Callable[];
const pendingErrors: any[] = [];

function throwFirstError(): void {
if (pendingErrors.length) {
throw pendingErrors.shift();
}
}

function tryRunTask(task: Callable): void {
try {
(task as any).call();
} catch (error) {
pendingErrors.push(error);
setTimeout(throwFirstError, 0);
}
}

function process(): void {
const capacity = 1024;
let index = 0;

while (index < tasks.length) {
tryRunTask(tasks[index]);
index++;

// Prevent leaking memory for long chains of recursive calls to `DOM.queueUpdate`.
// If we call `DOM.queueUpdate` within a task scheduled by `DOM.queueUpdate`, the queue will
// grow, but to avoid an O(n) walk for every task we execute, we don't
// shift tasks off the queue after they have been executed.
// Instead, we periodically shift 1024 tasks off the queue.
if (index > capacity) {
// Manually shift all values starting at the index back to the
// beginning of the queue.
for (
let scan = 0, newLength = tasks.length - index;
scan < newLength;
scan++
) {
tasks[scan] = tasks[scan + index];
}

tasks.length -= index;
index = 0;
}
}

tasks.length = 0;
}

function enqueue(callable: Callable): void {
if (tasks.length < 1) {
$global.requestAnimationFrame(process);
}

tasks.push(callable);
}

return Object.freeze({
enqueue,
process,
});
});

/* eslint-disable */
const fastHTMLPolicy: TrustedTypesPolicy = $global.trustedTypes.createPolicy(
Expand All @@ -14,24 +77,6 @@ const fastHTMLPolicy: TrustedTypesPolicy = $global.trustedTypes.createPolicy(

let htmlPolicy: TrustedTypesPolicy = fastHTMLPolicy;

// We use a queue so we can ensure errors are thrown in order.
const pendingErrors: any[] = [];

function throwFirstError(): void {
if (pendingErrors.length) {
throw pendingErrors.shift();
}
}

function tryRunTask(task: Callable): void {
try {
(task as any).call();
} catch (error) {
pendingErrors.push(error);
setTimeout(throwFirstError, 0);
}
}

const marker = `fast-${Math.random().toString(36).substring(2, 8)}`;

/** @internal */
Expand Down Expand Up @@ -131,13 +176,7 @@ export const DOM = Object.freeze({
* 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) {
window.requestAnimationFrame(DOM.processUpdates);
}

updateQueue.push(callable);
},
queueUpdate: updateQueue.enqueue,

/**
* Immediately processes all work previously scheduled
Expand All @@ -146,45 +185,13 @@ export const DOM = Object.freeze({
* This also forces nextUpdate promises
* to resolve.
*/
processUpdates(): void {
const capacity = 1024;
let index = 0;

while (index < updateQueue.length) {
tryRunTask(updateQueue[index]);
index++;

// Prevent leaking memory for long chains of recursive calls to `DOM.queueUpdate`.
// If we call `DOM.queueUpdate` within a task scheduled by `DOM.queueUpdate`, the queue will
// grow, but to avoid an O(n) walk for every task we execute, we don't
// shift tasks off the queue after they have been executed.
// Instead, we periodically shift 1024 tasks off the queue.
if (index > capacity) {
// Manually shift all values starting at the index back to the
// beginning of the queue.
for (
let scan = 0, newLength = updateQueue.length - index;
scan < newLength;
scan++
) {
updateQueue[scan] = updateQueue[scan + index];
}

updateQueue.length -= index;
index = 0;
}
}

updateQueue.length = 0;
},
processUpdates: updateQueue.process,

/**
* Resolves with the next DOM update.
*/
nextUpdate(): Promise<void> {
return new Promise((resolve: () => void) => {
DOM.queueUpdate(resolve);
});
return new Promise(updateQueue.enqueue);
},

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,6 @@ describe("The Observable", () => {
}

context("facade", () => {
it("can set an array observer factory", () => {
const fakeObserver = new SubscriberSet([]);
Observable.setArrayObserverFactory((array: any[]) => fakeObserver);
const array = [];
const observer = Observable.getNotifier(array);
expect(observer).to.equal(fakeObserver);
});

it("can get a notifier for an object", () => {
const instance = new Model();
const notifier = Observable.getNotifier(instance);
Expand Down
Loading