Skip to content

Commit

Permalink
feat: enable multiple fast-element instances in browser at once (micr…
Browse files Browse the repository at this point in the history
…osoft#5695)

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

* Change files

* feat: enable optional services and make global setup more resilient

* feat: track loaded versions

* refactor: improve FAST global API naming and documentation

* refactor: remove magic strings

* refactor: minor cleanup to FAST global and comments

* chore: add built step to inject the version

* refactor: refine kernel services

* test: add unit tests for FAST global API

* fix: remove version injection since it won't work with beachball

Co-authored-by: EisenbergEffect <roeisenb@microsoft.com>
  • Loading branch information
EisenbergEffect and EisenbergEffect authored Mar 8, 2022
1 parent bb9c277 commit 0e506c6
Show file tree
Hide file tree
Showing 9 changed files with 588 additions and 431 deletions.
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"
}
51 changes: 39 additions & 12 deletions packages/web-components/fast-element/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class BindingBehavior implements Behavior {
// @public
export interface BindingObserver<TSource = any, TReturn = any, TParent = any> extends Notifier {
disconnect(): void;
observe(source: TSource, context: ExecutionContext): TReturn;
observe(source: TSource, context: ExecutionContext<TParent>): TReturn;
records(): IterableIterator<ObservationRecord>;
}

Expand Down 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,8 +314,8 @@ 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;
readonly isDefined: boolean;
static readonly forType: <TType_1 extends Function>(key: TType_1) => FASTElementDefinition<Function> | undefined;
get isDefined(): boolean;
readonly name: string;
readonly propertyLookup: Record<string, AttributeDefinition>;
readonly shadowOptions?: ShadowRootInit;
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)
elementRegistry = 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,12 +424,12 @@ 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;
defineProperty(target: {}, nameOrAccessor: string | Accessor): void;
getAccessors(target: {}): Accessor[];
getAccessors: (target: {}) => Accessor[];
binding<TSource = any, TReturn = any, TParent = any>(binding: Binding<TSource, TReturn, TParent>, initialSubscriber?: Subscriber | undefined, isVolatileBinding?: boolean): BindingObserver<TSource, TReturn, TParent>;
isVolatileBinding<TSource_1 = any, TReturn_1 = any, TParent_1 = any>(binding: Binding<TSource_1, TReturn_1, TParent_1>): boolean;
}>;
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,12 +1,28 @@
import type { Mutable } from "../interfaces";
import { FAST, KernelServiceId } from "../platform";
import { Observable } from "../observation/observable";
import { ComposableStyles, ElementStyles } from "../styles/element-styles";
import type { ElementViewTemplate } from "../templating/template";
import { AttributeConfiguration, AttributeDefinition } from "./attributes";

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

return Object.freeze({
register(definition: FASTElementDefinition): boolean {
if (typeToDefinition.has(definition.type)) {
return false;
}

typeToDefinition.set(definition.type, definition);
return true;
},
getByType<TType extends Function>(key: TType): FASTElementDefinition | undefined {
return typeToDefinition.get(key);
},
});
});

/**
* Represents metadata configuration for a custom element.
Expand Down Expand Up @@ -59,7 +75,9 @@ export class FASTElementDefinition<TType extends Function = Function> {
/**
* Indicates if this element has been defined in at least one registry.
*/
public readonly isDefined: boolean;
public get isDefined(): boolean {
return !!fastRegistry.getByType(this.type);
}

/**
* The name of the custom element.
Expand Down Expand Up @@ -165,7 +183,7 @@ export class FASTElementDefinition<TType extends Function = Function> {
public define(registry: CustomElementRegistry = customElements): this {
const type = this.type;

if (!this.isDefined) {
if (fastRegistry.register(this)) {
const attributes = this.attributes;
const proto = type.prototype;

Expand All @@ -177,9 +195,6 @@ export class FASTElementDefinition<TType extends Function = Function> {
value: this.observedAttributes,
enumerable: true,
});

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

if (!registry.get(this.name)) {
Expand All @@ -193,9 +208,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 readonly forType = fastRegistry.getByType;
}
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

0 comments on commit 0e506c6

Please sign in to comment.