storybook
Version:
Storybook: Develop, document, and test UI components in isolation
410 lines (403 loc) • 16.8 kB
JavaScript
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
};