Skip to content

Commit

Permalink
WIP: experiment with a light version of event contract
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewKushnir committed Apr 29, 2024
1 parent 5b4970b commit dc5bbca
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {createEventInfoFromParameters, EventInfo} from './event_info';

export class EventContractLight {
public events: EventInfo[] = [];
// ['click', clickHandler, 'input', inputHandler, ...]
public listeners: Array<string | EventListener> = [];

constructor(
public namespace: string,
public container: Element,
) {}

addEvent(eventType: string) {
const container = this.container;
const listener = (event: Event) => {
const target = event.target as Element;
const eventInfo = createEventInfoFromParameters(
/* eventType= */ eventType,
/* event= */ event,
/* targetElement= */ target,
/* container= */ container,
/* timestamp= */ Date.now(),
);
this.events.push(eventInfo);
};
this.listeners.push(eventType, listener);
container.addEventListener(eventType, listener);
}
}

export function disposeEventContract(ec: EventContractLight) {
const listeners = ec.listeners;
for (let i = 0; i < listeners.length; i += 2) {
const eventType = listeners[i] as string;
const listener = listeners[i + 1] as EventListener;
ec.container.removeEventListener(eventType, listener);
}
ec.listeners = [];
ec.events = [];
}
26 changes: 24 additions & 2 deletions packages/core/primitives/event-dispatch/src/eventcontract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import {
MOUSE_SPECIAL_SUPPORT,
STOP_PROPAGATION,
} from './event_contract_defines';
import {EventContractLight} from './event_contract_light';
import * as eventInfoLib from './event_info';
import {EventType} from './event_type';
import {Property} from './property';
Expand Down Expand Up @@ -182,14 +183,18 @@ export class EventContract implements UnrenamedEventContract {
this.handleEventInfo(eventInfo);
}

queueEvent(eventInfo: eventInfoLib.EventInfo) {
eventInfoLib.setIsReplay(eventInfo, true);
this.queuedEventInfos?.push(eventInfo);
}

/**
* Handle an `EventInfo`.
*/
private handleEventInfo(eventInfo: eventInfoLib.EventInfo) {
if (!this.dispatcher) {
// All events are queued when the dispatcher isn't yet loaded.
eventInfoLib.setIsReplay(eventInfo, true);
this.queuedEventInfos?.push(eventInfo);
this.queueEvent(eventInfo);
}
if (
EventContract.CUSTOM_EVENT_SUPPORT &&
Expand Down Expand Up @@ -583,6 +588,23 @@ export class EventContract implements UnrenamedEventContract {
}
}

/**
* Copies over state from the `EventContractLight` -> `EventContract`.
*/
export function ingestEventContractLight(ec: EventContract, ecLight: EventContractLight) {
const seenEventTypes = new Set<string>();
for (const event of ecLight.events) {
const eventType = event.eventType;
// Copy over event types
if (!seenEventTypes.has(eventType)) {
ec.addEvent(eventType);
seenEventTypes.add(eventType);
}
// Copy over captured events
ec.queueEvent(event);
}
}

/**
* Adds a11y click support to the given `EventContract`. Meant to be called
* in a different compilation unit from the `EventContract`. The `EventContract`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/

import {EventContractContainer} from './event_contract_container';
import {EventContract} from './eventcontract';
import {EventContractLight} from './event_contract_light';

/**
* Provides a factory function for bootstrapping an event contract on a
Expand All @@ -30,7 +29,7 @@ export function bootstrapEventContract(
if (!anyWindow[field]) {
anyWindow[field] = {};
}
const eventContract = new EventContract(new EventContractContainer(container));
const eventContract = new EventContractLight(appId, container);
anyWindow[field][appId] = eventContract;
for (const ev of events) {
eventContract.addEvent(ev);
Expand Down
20 changes: 18 additions & 2 deletions packages/core/src/hydration/event_replay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Dispatcher, EventContract, EventInfoWrapper, registerDispatcher} from '@angular/core/primitives/event-dispatch';
import {Dispatcher, EventContract, EventContractContainer, EventInfoWrapper, registerDispatcher} from '@angular/core/primitives/event-dispatch';
import {disposeEventContract, EventContractLight} from '@angular/core/primitives/event-dispatch/src/event_contract_light';
import {ingestEventContractLight} from '@angular/core/primitives/event-dispatch/src/eventcontract';

import {APP_BOOTSTRAP_LISTENER, ApplicationRef, whenStable} from '../application/application_ref';
import {APP_ID} from '../application/application_tokens';
Expand Down Expand Up @@ -54,8 +56,22 @@ export function withEventReplay(): Provider[] {
// This is set in packages/platform-server/src/utils.ts
// Note: globalThis[CONTRACT_PROPERTY] may be undefined in case Event Replay feature
// is enabled, but there are no events configured in an application.
const eventContract = globalThis[CONTRACT_PROPERTY]?.[appId] as EventContract;
let eventContract = globalThis[CONTRACT_PROPERTY]?.[appId] as EventContract;
if (eventContract) {
// If this is an instance of the `EventContractLight` - upgrade to
// full EventContract before invoking replay.
if (eventContract instanceof EventContractLight) {
const eventContractLight = eventContract;
eventContract =
new EventContract(new EventContractContainer(eventContractLight.container));

// Populate main `EventContract` state based on the light version of it.
ingestEventContractLight(eventContract, eventContractLight);

// Clear the `EventContractLight` instance (remove all event listeners,
// clear internal state).
disposeEventContract(eventContractLight);
}
const dispatcher = new Dispatcher();
setEventReplayer(dispatcher);
// Event replay is kicked off as a side-effect of executing this function.
Expand Down

0 comments on commit dc5bbca

Please sign in to comment.