Skip to content

Commit

Permalink
refactor: Introducing useSyncExternalStore (#127)
Browse files Browse the repository at this point in the history
* Updated useAreModuleRegistered and Ready

* Using useSyncExternalStore also for MSW
  • Loading branch information
patricklafrance authored Dec 8, 2023
1 parent 8fb1b4b commit 7f4cdc5
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 117 deletions.
38 changes: 34 additions & 4 deletions packages/core/src/federation/registerLocalModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ export interface RegisterLocalModulesOptions<TContext> {
context?: TContext;
}

export type LocalModuleRegistrationStatusChangedListener = () => void;

export interface LocalModuleRegistrationError {
// The registration error.
error: unknown;
Expand All @@ -21,6 +23,8 @@ export class LocalModuleRegistry {
#registrationStatus: ModuleRegistrationStatus = "none";
#deferredRegistrations: DeferredRegistration[] = [];

readonly #statusChangedListeners = new Set<LocalModuleRegistrationStatusChangedListener>();

async registerModules<TRuntime extends Runtime = Runtime, TContext = unknown, TData = unknown>(registerFunctions: ModuleRegisterFunction<TRuntime, TContext, TData>[], runtime: TRuntime, { context }: RegisterLocalModulesOptions<TContext> = {}) {
const errors: LocalModuleRegistrationError[] = [];

Expand All @@ -30,7 +34,7 @@ export class LocalModuleRegistry {

runtime.logger.debug(`[squide] Found ${registerFunctions.length} local module${registerFunctions.length !== 1 ? "s" : ""} to register.`);

this.#registrationStatus = "in-progress";
this.#setRegistrationStatus("in-progress");

await Promise.allSettled(registerFunctions.map(async (x, index) => {
runtime.logger.debug(`[squide] ${index + 1}/${registerFunctions.length} Registering local module.`);
Expand Down Expand Up @@ -58,7 +62,7 @@ export class LocalModuleRegistry {
runtime.logger.debug(`[squide] ${index + 1}/${registerFunctions.length} Local module registration completed.`);
}));

this.#registrationStatus = this.#deferredRegistrations.length > 0 ? "registered" : "ready";
this.#setRegistrationStatus(this.#deferredRegistrations.length > 0 ? "registered" : "ready");

return errors;
}
Expand All @@ -79,7 +83,7 @@ export class LocalModuleRegistry {
return Promise.resolve(errors);
}

this.#registrationStatus = "in-completion";
this.#setRegistrationStatus("in-completion");

await Promise.allSettled(this.#deferredRegistrations.map(async ({ index, fct: deferredRegister }) => {
runtime.logger.debug(`[squide] ${index} Completing local module deferred registration.`, "Data:", data);
Expand All @@ -100,19 +104,37 @@ export class LocalModuleRegistry {
runtime.logger.debug(`[squide] ${index} Completed local module deferred registration.`);
}));

this.#registrationStatus = "ready";
this.#setRegistrationStatus("ready");

return errors;
}

registerStatusChangedListener(callback: LocalModuleRegistrationStatusChangedListener) {
this.#statusChangedListeners.add(callback);
}

removeStatusChangedListener(callback: LocalModuleRegistrationStatusChangedListener) {
this.#statusChangedListeners.delete(callback);
}

#setRegistrationStatus(status: ModuleRegistrationStatus) {
this.#registrationStatus = status;

this.#statusChangedListeners.forEach(x => {
x();
});
}

get registrationStatus() {
return this.#registrationStatus;
}

// Strictly for Jest tests, this is NOT ideal.
__reset() {
// Bypass the "setRegistrationStatus" function to prevent calling the listeners.
this.#registrationStatus = "none";
this.#deferredRegistrations = [];
this.#statusChangedListeners.clear();
}
}

Expand All @@ -130,6 +152,14 @@ export function getLocalModuleRegistrationStatus() {
return localModuleRegistry.registrationStatus;
}

export function addLocalModuleRegistrationStatusChangedListener(callback: LocalModuleRegistrationStatusChangedListener) {
localModuleRegistry.registerStatusChangedListener(callback);
}

export function removeLocalModuleRegistrationStatusChangedListener(callback: LocalModuleRegistrationStatusChangedListener) {
localModuleRegistry.removeStatusChangedListener(callback);
}

// Strictly for Jest tests, this is NOT ideal.
export function __resetLocalModuleRegistrations() {
localModuleRegistry.__reset();
Expand Down
6 changes: 3 additions & 3 deletions packages/firefly/tests/AppRouter.test.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Not all permutations are tested because there are simply too many. The code path that we deem the most important to test
// has been handled and additional tests will be added once bugs are discovered.

import { RuntimeContext, __resetLocalModuleRegistrations, registerLocalModules } from "@squide/core";
import { __resetMswStatus, setMswAsStarted } from "@squide/msw";
import { ReactRouterRuntime } from "@squide/react-router";
Expand All @@ -6,9 +9,6 @@ import { render, screen } from "@testing-library/react";
import type { ReactElement, ReactNode } from "react";
import { AppRouter } from "../src/AppRouter.tsx";

// Not all permutations are testedbecause there are simply too many. The code path that we deem the most important to test
// has been handled and additional tests will be added once bugs are discovered.

function Loading() {
return (
<div data-testid="loading">Loading...</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/msw/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export * from "./mswPlugin.ts";
export * from "./mswState.ts";
export * from "./requestHandlerRegistry.ts";
export * from "./setMswAsStarted.ts";
export * from "./useIsMswStarted.ts";

57 changes: 57 additions & 0 deletions packages/msw/src/mswState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export type MswStateChangedListener = () => void;

export class MswState {
#isStarted = false;

readonly #stateChangedListeners = new Set<MswStateChangedListener>();

addStateChangedListener(callback: MswStateChangedListener) {
this.#stateChangedListeners.add(callback);
}

removeStateChangedListener(callback: MswStateChangedListener) {
this.#stateChangedListeners.delete(callback);
}

setAsStarted() {
if (!this.#isStarted) {
this.#isStarted = true;

this.#stateChangedListeners.forEach(x => {
x();
});
}
}

get isStarted() {
return this.#isStarted;
}

// Strictly for Jest tests, this is NOT ideal.
_reset() {
this.#isStarted = false;
}
}

const mswState = new MswState();

export function setMswAsStarted() {
mswState.setAsStarted();
}

export function isMswStarted() {
return mswState.isStarted;
}

export function addMswStateChangedListener(callback: MswStateChangedListener) {
mswState.addStateChangedListener(callback);
}

export function removeMswStateChangedListener(callback: MswStateChangedListener) {
mswState.removeStateChangedListener(callback);
}

// Strictly for Jest tests, this is NOT ideal.
export function __resetMswStatus() {
mswState._reset();
}
2 changes: 1 addition & 1 deletion packages/msw/src/requestHandlerRegistry.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { RequestHandler } from "msw";
import { isMswStarted } from "./setMswAsStarted.ts";
import { isMswStarted } from "./mswState.ts";

export class RequestHandlerRegistry {
readonly #handlers: RequestHandler[] = [];
Expand Down
14 changes: 0 additions & 14 deletions packages/msw/src/setMswAsStarted.ts

This file was deleted.

40 changes: 13 additions & 27 deletions packages/msw/src/useIsMswStarted.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,23 @@
import { useLogger } from "@squide/core";
import { useEffect, useState } from "react";
import { isMswStarted } from "./setMswAsStarted.ts";
import { useEffect, useSyncExternalStore } from "react";
import { addMswStateChangedListener, isMswStarted, removeMswStateChangedListener } from "./mswState.ts";

export interface UseIsMswStartedOptions {
// The interval is in milliseconds.
interval?: number;
function subscribe(callback: () => void) {
addMswStateChangedListener(callback);

return () => removeMswStateChangedListener(callback);
}

export function useIsMswStarted(enabled: boolean, { interval = 10 }: UseIsMswStartedOptions = {}) {
const logger = useLogger();
export function useIsMswStarted(enabled: boolean) {
const isStarted = useSyncExternalStore(subscribe, isMswStarted);

// Using a state hook to force a rerender once MSW is started.
const [value, setIsStarted] = useState(!enabled);
const logger = useLogger();

// Perform a reload once MSW is started.
useEffect(() => {
if (enabled) {
const intervalId = setInterval(() => {
if (isMswStarted()) {
logger.debug("[squide] %cMSW is ready%c.", "color: white; background-color: green;", "");

clearInterval(intervalId);
setIsStarted(true);
}
}, interval);

return () => {
if (intervalId) {
clearInterval(intervalId);
}
};
if (isStarted) {
logger.debug("[squide] %cMSW is ready%c.", "color: white; background-color: green;", "");
}
}, [enabled, interval, logger]);
}, [isStarted, logger]);

return value;
return isStarted || !enabled;
}
39 changes: 35 additions & 4 deletions packages/webpack-module-federation/src/registerRemoteModules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export interface RegisterRemoteModulesOptions<TContext> {
context?: TContext;
}

export type RemoteModuleRegistrationStatusChangedListener = () => void;

export interface RemoteModuleRegistrationError {
// The remote base URL.
url: string;
Expand All @@ -29,6 +31,7 @@ export class RemoteModuleRegistry {

readonly #deferredRegistrations: DeferredRegistration[] = [];
readonly #loadRemote: LoadRemoteFunction;
readonly #statusChangedListeners = new Set<RemoteModuleRegistrationStatusChangedListener>();

constructor(loadRemote: LoadRemoteFunction) {
this.#loadRemote = loadRemote;
Expand Down Expand Up @@ -56,7 +59,7 @@ export class RemoteModuleRegistry {

runtime.logger.debug(`[squide] Found ${remotes.length} remote module${remotes.length !== 1 ? "s" : ""} to register.`);

this.#registrationStatus = "in-progress";
this.#setRegistrationStatus("in-progress");

await Promise.allSettled(remotes.map(async (x, index) => {
let remoteUrl;
Expand Down Expand Up @@ -104,8 +107,12 @@ export class RemoteModuleRegistry {
}
}));

this.#registrationStatus = this.#deferredRegistrations.length > 0 ? "registered" : "ready";
this.#setRegistrationStatus(this.#deferredRegistrations.length > 0 ? "registered" : "ready");

// After introducting the "setRegistrationStatus" method, TypeScript seems to think that the only possible
// values for registrationStatus is "none" and now complains about the lack of overlapping between "none" and "ready".
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (this.#registrationStatus === "ready") {
this.#logSharedScope(runtime.logger);
}
Expand All @@ -129,7 +136,7 @@ export class RemoteModuleRegistry {
return Promise.resolve(errors);
}

this.#registrationStatus = "in-completion";
this.#setRegistrationStatus("in-completion");

await Promise.allSettled(this.#deferredRegistrations.map(async ({ url, containerName, index, fct: deferredRegister }) => {
runtime.logger.debug(`[squide] ${index} Completing registration for module "${RemoteModuleName}" from container "${containerName}" of remote "${url}".`);
Expand All @@ -153,13 +160,29 @@ export class RemoteModuleRegistry {
runtime.logger.debug(`[squide] ${index} Completed registration for module "${RemoteModuleName}" from container "${containerName}" of remote "${url}".`);
}));

this.#registrationStatus = "ready";
this.#setRegistrationStatus("ready");

this.#logSharedScope(runtime.logger);

return errors;
}

registerStatusChangedListener(callback: RemoteModuleRegistrationStatusChangedListener) {
this.#statusChangedListeners.add(callback);
}

removeStatusChangedListener(callback: RemoteModuleRegistrationStatusChangedListener) {
this.#statusChangedListeners.delete(callback);
}

#setRegistrationStatus(status: ModuleRegistrationStatus) {
this.#registrationStatus = status;

this.#statusChangedListeners.forEach(x => {
x();
});
}

get registrationStatus() {
return this.#registrationStatus;
}
Expand All @@ -183,3 +206,11 @@ export function completeRemoteModuleRegistrations<TRuntime extends Runtime = Run
export function getRemoteModuleRegistrationStatus() {
return remoteModuleRegistry.registrationStatus;
}

export function addRemoteModuleRegistrationStatusChangedListener(callback: RemoteModuleRegistrationStatusChangedListener) {
remoteModuleRegistry.registerStatusChangedListener(callback);
}

export function removeRemoteModuleRegistrationStatusChangedListener(callback: RemoteModuleRegistrationStatusChangedListener) {
remoteModuleRegistry.removeStatusChangedListener(callback);
}
Loading

0 comments on commit 7f4cdc5

Please sign in to comment.