UNPKG

storybook

Version:

Storybook: Develop, document, and test UI components in isolation

410 lines (403 loc) • 16.8 kB
import CJS_COMPAT_NODE_URL_qjinduibsyq from 'node:url'; import CJS_COMPAT_NODE_PATH_qjinduibsyq from 'node:path'; import CJS_COMPAT_NODE_MODULE_qjinduibsyq from "node:module"; var __filename = CJS_COMPAT_NODE_URL_qjinduibsyq.fileURLToPath(import.meta.url); var __dirname = CJS_COMPAT_NODE_PATH_qjinduibsyq.dirname(__filename); var require = CJS_COMPAT_NODE_MODULE_qjinduibsyq.createRequire(import.meta.url); // ------------------------------------------------------------ // end of CJS compatibility banner, injected by Storybook's esbuild configuration // ------------------------------------------------------------ import { UniversalStoreFollowerTimeoutError } from "./chunk-4C4AXOGG.js"; import { require_dist } from "./chunk-FE5AV6EP.js"; import { __toESM } from "./chunk-4US4PNS3.js"; // src/shared/universal-store/index.ts var import_ts_dedent = __toESM(require_dist(), 1); // src/shared/universal-store/instances.ts var instances = /* @__PURE__ */ new Map(); // src/shared/universal-store/index.ts var CHANNEL_EVENT_PREFIX = "UNIVERSAL_STORE:", ProgressState = { PENDING: "PENDING", RESOLVED: "RESOLVED", REJECTED: "REJECTED" }, UniversalStore = class _UniversalStore { constructor(options, environmentOverrides) { /** Enable debug logs for this store */ this.debugging = !1; // TODO: narrow type of listeners based on event type this.listeners = /* @__PURE__ */ new Map([["*", /* @__PURE__ */ new Set()]]); /** Gets the current state */ this.getState = () => (this.debug("getState", { state: this.state }), this.state); /** * Subscribes to store events * * @returns A function to unsubscribe */ this.subscribe = (eventTypeOrListener, maybeListener) => { let subscribesToAllEvents = typeof eventTypeOrListener == "function", eventType = subscribesToAllEvents ? "*" : eventTypeOrListener, listener = subscribesToAllEvents ? eventTypeOrListener : maybeListener; if (this.debug("subscribe", { eventType, listener }), !listener) throw new TypeError( `Missing first subscribe argument, or second if first is the event type, when subscribing to a UniversalStore with id '${this.id}'` ); return this.listeners.has(eventType) || this.listeners.set(eventType, /* @__PURE__ */ new Set()), this.listeners.get(eventType).add(listener), () => { this.debug("unsubscribe", { eventType, listener }), this.listeners.has(eventType) && (this.listeners.get(eventType).delete(listener), this.listeners.get(eventType)?.size === 0 && this.listeners.delete(eventType)); }; }; /** Sends a custom event to the other stores */ this.send = (event) => { if (this.debug("send", { event }), this.status !== _UniversalStore.Status.READY) throw new TypeError( import_ts_dedent.dedent`Cannot send event before store is ready. You can get the current status with store.status, or await store.readyPromise to wait for the store to be ready before sending events. ${JSON.stringify( { event, id: this.id, actor: this.actor, environment: this.environment }, null, 2 )}` ); this.emitToListeners(event, { actor: this.actor }), this.emitToChannel(event, { actor: this.actor }); }; if (this.debugging = options.debug ?? !1, !_UniversalStore.isInternalConstructing) throw new TypeError( "UniversalStore is not constructable - use UniversalStore.create() instead" ); if (_UniversalStore.isInternalConstructing = !1, this.id = options.id, this.actorId = Date.now().toString(36) + Math.random().toString(36).substring(2), this.actorType = options.leader ? _UniversalStore.ActorType.LEADER : _UniversalStore.ActorType.FOLLOWER, this.state = options.initialState, this.channelEventName = `${CHANNEL_EVENT_PREFIX}${this.id}`, this.debug("constructor", { options, environmentOverrides, channelEventName: this.channelEventName }), this.actor.type === _UniversalStore.ActorType.LEADER) this.syncing = { state: ProgressState.RESOLVED, promise: Promise.resolve() }; else { let syncingResolve, syncingReject, syncingPromise = new Promise((resolve, reject) => { syncingResolve = () => { this.syncing.state === ProgressState.PENDING && (this.syncing.state = ProgressState.RESOLVED, resolve()); }, syncingReject = (reason) => { this.syncing.state === ProgressState.PENDING && (this.syncing.state = ProgressState.REJECTED, reject(reason)); }; }); this.syncing = { state: ProgressState.PENDING, promise: syncingPromise, resolve: syncingResolve, reject: syncingReject }; } this.getState = this.getState.bind(this), this.setState = this.setState.bind(this), this.subscribe = this.subscribe.bind(this), this.onStateChange = this.onStateChange.bind(this), this.send = this.send.bind(this), this.emitToChannel = this.emitToChannel.bind(this), this.prepareThis = this.prepareThis.bind(this), this.emitToListeners = this.emitToListeners.bind(this), this.handleChannelEvents = this.handleChannelEvents.bind(this), this.debug = this.debug.bind(this), this.channel = environmentOverrides?.channel ?? _UniversalStore.preparation.channel, this.environment = environmentOverrides?.environment ?? _UniversalStore.preparation.environment, this.channel && this.environment ? (environmentOverrides || _UniversalStore.preparation.resolve({ channel: this.channel, environment: this.environment }), this.prepareThis({ channel: this.channel, environment: this.environment })) : _UniversalStore.preparation.promise.then(this.prepareThis); } static { /** * Defines the possible actor types in the store system * * @readonly */ this.ActorType = { LEADER: "LEADER", FOLLOWER: "FOLLOWER" }; } static { /** * Defines the possible environments the store can run in * * @readonly */ this.Environment = { SERVER: "SERVER", MANAGER: "MANAGER", PREVIEW: "PREVIEW", UNKNOWN: "UNKNOWN", MOCK: "MOCK" }; } /** The environment this realm was prepared as, or `undefined` before `__prepare`. */ static get preparedEnvironment() { return _UniversalStore.preparation.environment; } static { /** * Internal event types used for store synchronization * * @readonly */ this.InternalEventType = { EXISTING_STATE_REQUEST: "__EXISTING_STATE_REQUEST", EXISTING_STATE_RESPONSE: "__EXISTING_STATE_RESPONSE", SET_STATE: "__SET_STATE", LEADER_CREATED: "__LEADER_CREATED", FOLLOWER_CREATED: "__FOLLOWER_CREATED" }; } static { this.Status = { UNPREPARED: "UNPREPARED", SYNCING: "SYNCING", READY: "READY", ERROR: "ERROR" }; } static { // This is used to check if constructor was called from the static factory create() this.isInternalConstructing = !1; } static { _UniversalStore.setupPreparationPromise(); } static setupPreparationPromise() { let resolveRef, rejectRef, promise = new Promise( (resolve, reject) => { resolveRef = (args) => { resolve(args); }, rejectRef = (...args) => { reject(args); }; } ); _UniversalStore.preparation = { resolve: resolveRef, reject: rejectRef, promise }; } /** The actor object representing the store instance with a unique ID and a type */ get actor() { return Object.freeze({ id: this.actorId, type: this.actorType, environment: this.environment ?? _UniversalStore.Environment.UNKNOWN }); } /** * The current state of the store, that signals both if the store is prepared by Storybook and * also - in the case of a follower - if the state has been synced with the leader's state. */ get status() { if (!this.channel || !this.environment) return _UniversalStore.Status.UNPREPARED; switch (this.syncing?.state) { case ProgressState.PENDING: case void 0: return _UniversalStore.Status.SYNCING; case ProgressState.REJECTED: return _UniversalStore.Status.ERROR; case ProgressState.RESOLVED: default: return _UniversalStore.Status.READY; } } /** * A promise that resolves when the store is fully ready. A leader will be ready when the store * has been prepared by Storybook, which is almost instantly. * * A follower will be ready when the state has been synced with the leader's state, within a few * hundred milliseconds. */ untilReady() { let preparation = this.channel && this.environment ? Promise.resolve() : _UniversalStore.preparation.promise; return Promise.all([preparation, this.syncing?.promise]); } /** Creates a new instance of UniversalStore */ static create(options) { if (!options || typeof options?.id != "string") throw new TypeError("id is required and must be a string, when creating a UniversalStore"); options.debug && console.debug( import_ts_dedent.dedent`[UniversalStore] create`, { options } ); let existing = instances.get(options.id); if (existing) return console.warn(import_ts_dedent.dedent`UniversalStore with id "${options.id}" already exists in this environment, re-using existing. You should reuse the existing instance instead of trying to create a new one.`), existing; _UniversalStore.isInternalConstructing = !0; let store = new _UniversalStore(options); return instances.set(options.id, store), store; } /** * Used by Storybook to set the channel for all instances of UniversalStore in the given * environment. * * @internal */ static __prepare(channel, environment) { _UniversalStore.preparation.channel = channel, _UniversalStore.preparation.environment = environment, _UniversalStore.preparation.resolve({ channel, environment }); } /** * Updates the store's state * * Either a new state or a state updater function can be passed to the method. */ setState(updater) { let previousState = this.state, newState = typeof updater == "function" ? updater(previousState) : updater; if (this.debug("setState", { newState, previousState, updater }), this.status !== _UniversalStore.Status.READY) throw new TypeError( import_ts_dedent.dedent`Cannot set state before store is ready. You can get the current status with store.status, or await store.readyPromise to wait for the store to be ready before sending events. ${JSON.stringify( { newState, id: this.id, actor: this.actor, environment: this.environment }, null, 2 )}` ); this.state = newState; let event = { type: _UniversalStore.InternalEventType.SET_STATE, payload: { state: newState, previousState } }; this.emitToChannel(event, { actor: this.actor }), this.emitToListeners(event, { actor: this.actor }); } /** * Subscribes to state changes * * @returns Unsubscribe function */ onStateChange(listener) { return this.debug("onStateChange", { listener }), this.subscribe( _UniversalStore.InternalEventType.SET_STATE, ({ payload }, eventInfo) => { listener(payload.state, payload.previousState, eventInfo); } ); } emitToChannel(event, eventInfo) { this.debug("emitToChannel", { event, eventInfo, channel: !!this.channel }), this.channel?.emit(this.channelEventName, { event, eventInfo }); } prepareThis({ channel, environment }) { this.channel = channel, this.environment = environment, this.debug("prepared", { channel: !!channel, environment }), this.channel.on(this.channelEventName, this.handleChannelEvents), this.actor.type === _UniversalStore.ActorType.LEADER ? this.emitToChannel( { type: _UniversalStore.InternalEventType.LEADER_CREATED }, { actor: this.actor } ) : (this.emitToChannel( { type: _UniversalStore.InternalEventType.FOLLOWER_CREATED }, { actor: this.actor } ), this.emitToChannel( { type: _UniversalStore.InternalEventType.EXISTING_STATE_REQUEST }, { actor: this.actor } ), setTimeout(() => { this.syncing.reject(new UniversalStoreFollowerTimeoutError(this.id)); }, 1e3)); } emitToListeners(event, eventInfo) { let eventTypeListeners = this.listeners.get(event.type), everythingListeners = this.listeners.get("*"); this.debug("emitToListeners", { event, eventInfo, eventTypeListeners, everythingListeners }), [...eventTypeListeners ?? [], ...everythingListeners ?? []].forEach( (listener) => listener(event, eventInfo) ); } handleChannelEvents(channelEvent) { let { event, eventInfo } = channelEvent; if ([eventInfo.actor.id, eventInfo.forwardingActor?.id].includes(this.actor.id)) { this.debug("handleChannelEvents: Ignoring event from self", { channelEvent }); return; } else if (this.syncing?.state === ProgressState.PENDING && event.type !== _UniversalStore.InternalEventType.EXISTING_STATE_RESPONSE) { this.debug("handleChannelEvents: Ignoring event while syncing", { channelEvent }); return; } if (this.debug("handleChannelEvents", { channelEvent }), this.actor.type === _UniversalStore.ActorType.LEADER) { let shouldForwardEvent = !0; switch (event.type) { case _UniversalStore.InternalEventType.EXISTING_STATE_REQUEST: shouldForwardEvent = !1; let responseEvent = { type: _UniversalStore.InternalEventType.EXISTING_STATE_RESPONSE, payload: this.state }; this.debug("handleChannelEvents: responding to existing state request", { responseEvent }), this.emitToChannel(responseEvent, { actor: this.actor }), this.emitToListeners(responseEvent, { actor: this.actor }); break; case _UniversalStore.InternalEventType.LEADER_CREATED: shouldForwardEvent = !1, this.syncing.state = ProgressState.REJECTED, this.debug("handleChannelEvents: erroring due to second leader being created", { event }), console.error( import_ts_dedent.dedent`Detected multiple UniversalStore leaders created with the same id "${this.id}". Only one leader can exists at a time, your stores are now in an invalid state. Leaders detected: this: ${JSON.stringify(this.actor, null, 2)} other: ${JSON.stringify(eventInfo.actor, null, 2)}` ); break; } shouldForwardEvent && (this.debug("handleChannelEvents: forwarding event", { channelEvent }), this.emitToChannel(event, { actor: eventInfo.actor, forwardingActor: this.actor })); } if (this.actor.type === _UniversalStore.ActorType.FOLLOWER) switch (event.type) { case _UniversalStore.InternalEventType.EXISTING_STATE_RESPONSE: if (this.debug("handleChannelEvents: Setting state from leader's existing state response", { event }), this.syncing?.state !== ProgressState.PENDING) break; this.syncing.resolve?.(); let setStateEvent = { type: _UniversalStore.InternalEventType.SET_STATE, payload: { state: event.payload, previousState: this.state } }; this.state = event.payload, this.emitToListeners(setStateEvent, eventInfo); break; } event.type === _UniversalStore.InternalEventType.SET_STATE && (this.debug("handleChannelEvents: Setting state", { event }), this.state = event.payload.state), this.emitToListeners(event, { actor: eventInfo.actor }); } debug(message, data) { this.debugging && console.debug( import_ts_dedent.dedent`[UniversalStore::${this.id}::${this.environment ?? _UniversalStore.Environment.UNKNOWN}] ${message}`, JSON.stringify( { data, actor: this.actor, state: this.state, status: this.status }, null, 2 ) ); } /** * Used to reset the static fields of the UniversalStore class when cleaning up tests * * @internal */ static __reset() { _UniversalStore.preparation.reject(new Error("reset")), _UniversalStore.setupPreparationPromise(), _UniversalStore.isInternalConstructing = !1; } }; export { instances, UniversalStore };