Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
| Issue | Resources |
| --------------------------------------------------------------- | ----------------------------------- |
| [#34258](https://github.com/storybookjs/storybook/issues/34258) | [Report doc](issue-34258-report.md) |
| [#34566](https://github.com/storybookjs/storybook/issues/34566) | [Report doc](issue-34566-report.md) |

## Rest of the Original README

Expand Down
9 changes: 9 additions & 0 deletions code/core/src/manager-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum Category {
MANAGER_CORE_EVENTS = 'MANAGER_CORE-EVENTS',
MANAGER_ROUTER = 'MANAGER_ROUTER',
MANAGER_THEMING = 'MANAGER_THEMING',
MANAGER_UNIVERSAL_STORE = 'MANAGER_UNIVERSAL-STORE',
}

export class ProviderDoesNotExtendBaseProviderError extends StorybookError {
Expand Down Expand Up @@ -47,6 +48,14 @@ export class UncaughtManagerError extends StorybookError {
}
}

export {
UniversalStoreFollowerTimeoutError,
UniversalStoreIdRequiredError,
UniversalStoreMissingSubscribeArgumentError,
UniversalStoreNotConstructableError,
UniversalStoreNotReadyError,
} from './shared/universal-store/errors';

export class StatusTypeIdMismatchError extends StorybookError {
constructor(
public data: {
Expand Down
60 changes: 60 additions & 0 deletions code/core/src/shared/universal-store/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { StorybookError } from '../../storybook-error';

export abstract class UniversalStoreError extends StorybookError {
constructor(props: { code: number; message: string; name: string }) {
super({
...props,
category: 'MANAGER_UNIVERSAL-STORE',
});
}
}

export class UniversalStoreFollowerTimeoutError extends UniversalStoreError {
constructor(public data: { id: string }) {
super({
name: 'UniversalStoreFollowerTimeoutError',
code: 1,
message: `No existing state found for follower with id: '${data.id}'. Make sure a leader with the same id exists before creating a follower.`,
});
}
}

export class UniversalStoreNotConstructableError extends UniversalStoreError {
constructor() {
super({
name: 'UniversalStoreNotConstructableError',
code: 1001,
message: 'UniversalStore is not constructable - use UniversalStore.create() instead',
});
}
}

export class UniversalStoreIdRequiredError extends UniversalStoreError {
constructor() {
super({
name: 'UniversalStoreIdRequiredError',
code: 1002,
message: 'id is required and must be a string, when creating a UniversalStore',
});
}
}

export class UniversalStoreNotReadyError extends UniversalStoreError {
constructor(public data: { id: string; action: 'set state' | 'send event' }) {
super({
name: 'UniversalStoreNotReadyError',
code: 1003,
message: `Cannot ${data.action} before store with id '${data.id}' 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.`,
});
}
}

export class UniversalStoreMissingSubscribeArgumentError extends UniversalStoreError {
constructor(public data: { id: string }) {
super({
name: 'UniversalStoreMissingSubscribeArgumentError',
code: 1004,
message: `Missing first subscribe argument, or second if first is the event type, when subscribing to a UniversalStore with id '${data.id}'`,
});
}
}
40 changes: 5 additions & 35 deletions code/core/src/shared/universal-store/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,14 @@ describe('UniversalStore', () => {
leader: true,
})
).toThrowErrorMatchingInlineSnapshot(
`[TypeError: UniversalStore is not constructable - use UniversalStore.create() instead]`
`[SB_MANAGER_UNIVERSAL-STORE_1001 (UniversalStoreNotConstructableError): UniversalStore is not constructable - use UniversalStore.create() instead]`
);
});

it('should throw when id is not provided', () => {
// Arrange, Act, Assert - creating an instance without an id and expect it to throw
expect(() => (UniversalStore as any).create()).toThrowErrorMatchingInlineSnapshot(
`[TypeError: id is required and must be a string, when creating a UniversalStore]`
`[SB_MANAGER_UNIVERSAL-STORE_1002 (UniversalStoreIdRequiredError): id is required and must be a string, when creating a UniversalStore]`
);
});

Expand Down Expand Up @@ -691,7 +691,7 @@ You should reuse the existing instance instead of trying to create a new one.`);
// Assert - eventually the follower.untilReady() promise should throw an error when the timeout is reached
vi.advanceTimersToNextTimer();
await expect(follower.untilReady()).rejects.toThrowErrorMatchingInlineSnapshot(
`[TypeError: No existing state found for follower with id: 'env1:test'. Make sure a leader with the same id exists before creating a follower.]`
`[SB_MANAGER_UNIVERSAL-STORE_0001 (UniversalStoreFollowerTimeoutError): No existing state found for follower with id: 'env1:test'. Make sure a leader with the same id exists before creating a follower.]`
);
expect(follower.status).toBe(UniversalStore.Status.ERROR);
});
Expand Down Expand Up @@ -942,22 +942,7 @@ You should reuse the existing instance instead of trying to create a new one.`);
expect(follower.status).toBe(UniversalStore.Status.SYNCING);

// Act & Assert - set state on the follower before it is ready and expect it to throw
expect(() => follower.setState({ count: 1 })).toThrowErrorMatchingInlineSnapshot(`
[TypeError: 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.
{
"newState": {
"count": 1
},
"id": "env2:test",
"actor": {
"id": "m7405c0077777777778",
"type": "FOLLOWER",
"environment": "MANAGER"
},
"environment": "MANAGER"
}]
`);
expect(() => follower.setState({ count: 1 })).toThrowErrorMatchingInlineSnapshot(`[SB_MANAGER_UNIVERSAL-STORE_1003 (UniversalStoreNotReadyError): Cannot set state before store with id 'env2:test' 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.]`);
});
});

Expand Down Expand Up @@ -1127,22 +1112,7 @@ You should reuse the existing instance instead of trying to create a new one.`);
expect(follower.status).toBe(UniversalStore.Status.SYNCING);

// Act & Assert - send an event with the follower before it is ready and expect it to throw
expect(() => follower.send({ type: 'TOO_EARLY' })).toThrowErrorMatchingInlineSnapshot(`
[TypeError: 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.
{
"event": {
"type": "TOO_EARLY"
},
"id": "env2:test",
"actor": {
"id": "m7405c0077777777778",
"type": "FOLLOWER",
"environment": "MANAGER"
},
"environment": "MANAGER"
}]
`);
expect(() => follower.send({ type: 'TOO_EARLY' })).toThrowErrorMatchingInlineSnapshot(`[SB_MANAGER_UNIVERSAL-STORE_1003 (UniversalStoreNotReadyError): Cannot send event before store with id 'env2:test' 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.]`);
});
});

Expand Down
55 changes: 14 additions & 41 deletions code/core/src/shared/universal-store/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { dedent } from 'ts-dedent';

import { instances } from './instances';
import {
UniversalStoreFollowerTimeoutError,
UniversalStoreIdRequiredError,
UniversalStoreMissingSubscribeArgumentError,
UniversalStoreNotConstructableError,
UniversalStoreNotReadyError,
} from './errors';
import type {
Actor,
ChannelEvent,
Expand Down Expand Up @@ -251,9 +258,7 @@ export class UniversalStore<
// it can only be called from within the static factory method create()
// See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_propertiessimulating_private_constructors
if (!UniversalStore.isInternalConstructing) {
throw new TypeError(
'UniversalStore is not constructable - use UniversalStore.create() instead'
);
throw new UniversalStoreNotConstructableError();
}
UniversalStore.isInternalConstructing = false;

Expand Down Expand Up @@ -336,7 +341,7 @@ export class UniversalStore<
CustomEvent extends { type: string; payload?: any } = { type: string; payload?: any },
>(options: StoreOptions<State>): UniversalStore<State, CustomEvent> {
if (!options || typeof options?.id !== 'string') {
throw new TypeError('id is required and must be a string, when creating a UniversalStore');
throw new UniversalStoreIdRequiredError();
}
if (options.debug) {
console.debug(
Expand Down Expand Up @@ -390,24 +395,11 @@ export class UniversalStore<
this.debug('setState', { newState, previousState, updater });

if (this.status !== UniversalStore.Status.READY) {
throw new TypeError(
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
)}`
);
throw new UniversalStoreNotReadyError({ id: this.id, action: 'set state' });
}

this.state = newState;
const event = {
const event: SetStateEvent<State> = {
type: UniversalStore.InternalEventType.SET_STATE,
payload: {
state: newState,
Expand Down Expand Up @@ -442,9 +434,7 @@ export class UniversalStore<
this.debug('subscribe', { eventType, listener });

if (!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}'`
);
throw new UniversalStoreMissingSubscribeArgumentError({ id: this.id });
}

if (!this.listeners.has(eventType)) {
Expand Down Expand Up @@ -485,20 +475,7 @@ export class UniversalStore<
public send = (event: CustomEvent) => {
this.debug('send', { event });
if (this.status !== UniversalStore.Status.READY) {
throw new TypeError(
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
)}`
);
throw new UniversalStoreNotReadyError({ id: this.id, action: 'send event' });
}
this.emitToListeners(event, { actor: this.actor });
this.emitToChannel(event, { actor: this.actor });
Expand Down Expand Up @@ -544,11 +521,7 @@ export class UniversalStore<
setTimeout(() => {
// if the state is already resolved by a response before this timeout,
// rejecting it doesn't do anything, it will be ignored
this.syncing!.reject!(
new TypeError(
`No existing state found for follower with id: '${this.id}'. Make sure a leader with the same id exists before creating a follower.`
)
);
this.syncing!.reject!(new UniversalStoreFollowerTimeoutError({ id: this.id }));
}, 1000);
}
}
Expand Down
Loading
Loading