Skip to content
Closed
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
153 changes: 101 additions & 52 deletions packages/xstate-store/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ import {
ExtractEventsFromPayloadMap,
InteropSubscribable,
Observer,
Producer,
Recipe,
Store,
StoreAssigner,
StoreContext,
StoreEffect,
StoreInspectionEvent,
StoreProducerAssigner,
StoreSnapshot
StoreSnapshot,
StoreGetters,
ResolvedGetters
} from './types';

const symbolObservable: typeof Symbol.observable = (() =>
Expand Down Expand Up @@ -51,31 +54,30 @@ const inspectionObservers = new WeakMap<
function createStoreCore<
TContext extends StoreContext,
TEventPayloadMap extends EventPayloadMap,
TEmitted extends EventObject
TGetters extends Record<string, (context: TContext, getters: any) => any>,
TEmitted extends EventObject = EventObject
>(
initialContext: TContext,
transitions: {
[K in keyof TEventPayloadMap & string]: StoreAssigner<
NoInfer<TContext>,
{ type: K } & TEventPayloadMap[K],
TEmitted
>;
},
producer?: (
context: NoInfer<TContext>,
recipe: (context: NoInfer<TContext>) => void
) => NoInfer<TContext>
): Store<TContext, ExtractEventsFromPayloadMap<TEventPayloadMap>, TEmitted> {
transitions: TransitionsFromEventPayloadMap<
TEventPayloadMap,
TContext,
TEmitted
>,
getters?: TGetters,
producer?: Producer<TContext>
): Store<TContext, any, TEmitted, TGetters> {
type StoreEvent = ExtractEventsFromPayloadMap<TEventPayloadMap>;
let observers: Set<Observer<StoreSnapshot<TContext>>> | undefined;
let observers: Set<Observer<StoreSnapshot<TContext, TGetters>>> | undefined;
let listeners: Map<TEmitted['type'], Set<any>> | undefined;
const initialSnapshot: StoreSnapshot<TContext> = {

const initialSnapshot: StoreSnapshot<TContext, TGetters> = {
context: initialContext,
status: 'active',
output: undefined,
error: undefined
error: undefined,
...computeGetters(initialContext, getters)
};
let currentSnapshot: StoreSnapshot<TContext> = initialSnapshot;
let currentSnapshot: StoreSnapshot<TContext, TGetters> = initialSnapshot;

const emit = (ev: TEmitted) => {
if (!listeners) {
Expand All @@ -91,8 +93,13 @@ function createStoreCore<
const transition = createStoreTransition(transitions, producer);

function receive(event: StoreEvent) {
let effects: StoreEffect<TEmitted>[];
[currentSnapshot, effects] = transition(currentSnapshot, event);
const [newContext, effects] = transition(currentSnapshot.context, event);

currentSnapshot = {
...currentSnapshot,
context: newContext,
...computeGetters(newContext, getters)
} as StoreSnapshot<TContext, TGetters>;

inspectionObservers.get(store)?.forEach((observer) => {
observer.next?.({
Expand All @@ -115,7 +122,7 @@ function createStoreCore<
}
}

const store: Store<TContext, StoreEvent, TEmitted> = {
const store: Store<TContext, StoreEvent, TEmitted, TGetters> = {
on(emittedEventType, handler) {
if (!listeners) {
listeners = new Map();
Expand Down Expand Up @@ -164,7 +171,9 @@ function createStoreCore<
}
};
},
[symbolObservable](): InteropSubscribable<StoreSnapshot<TContext>> {
[symbolObservable](): InteropSubscribable<
StoreSnapshot<TContext, TGetters>
> {
return this;
},
inspect: (observerOrFn) => {
Expand Down Expand Up @@ -199,7 +208,7 @@ function createStoreCore<
};

(store as any).trigger = new Proxy(
{} as Store<TContext, StoreEvent, TEmitted>['trigger'],
{} as Store<TContext, StoreEvent, TEmitted, TGetters>['trigger'],
{
get: (_, eventType: string) => {
return (payload: any) => {
Expand Down Expand Up @@ -232,7 +241,8 @@ export type TransitionsFromEventPayloadMap<
type CreateStoreParameterTypes<
TContext extends StoreContext,
TEventPayloadMap extends EventPayloadMap,
TEmitted extends EventPayloadMap
TEmitted extends EventPayloadMap,
TGetters extends Record<string, any> = {}
> = [
definition: {
context: TContext;
Expand All @@ -246,17 +256,20 @@ type CreateStoreParameterTypes<
ExtractEventsFromPayloadMap<TEmitted>
>;
};
getters?: StoreGetters<TContext, TGetters>;
}
];

type CreateStoreReturnType<
TContext extends StoreContext,
TEventPayloadMap extends EventPayloadMap,
TEmitted extends EventPayloadMap
TEmitted extends EventPayloadMap,
TGetters extends Record<string, any> = {}
> = Store<
TContext,
ExtractEventsFromPayloadMap<TEventPayloadMap>,
ExtractEventsFromPayloadMap<TEmitted>
ExtractEventsFromPayloadMap<TEmitted>,
TGetters
>;

/**
Expand Down Expand Up @@ -291,15 +304,17 @@ type CreateStoreReturnType<
function _createStore<
TContext extends StoreContext,
TEventPayloadMap extends EventPayloadMap,
TEmitted extends EventPayloadMap
TEmitted extends EventPayloadMap,
TGetters extends Record<string, any> = {}
>(
...[{ context, on }]: CreateStoreParameterTypes<
...[{ context, on, getters }]: CreateStoreParameterTypes<
TContext,
TEventPayloadMap,
TEmitted
TEmitted,
TGetters
>
): CreateStoreReturnType<TContext, TEventPayloadMap, TEmitted> {
return createStoreCore(context, on);
): CreateStoreReturnType<TContext, TEventPayloadMap, TEmitted, TGetters> {
return createStoreCore(context, on, getters);
}

export const createStore: {
Expand All @@ -309,17 +324,29 @@ export const createStore: {
<
TContext extends StoreContext,
TEventPayloadMap extends EventPayloadMap,
TEmitted extends EventPayloadMap
TEmitted extends EventPayloadMap,
TGetters extends Record<string, any> = {}
>(
...args: CreateStoreParameterTypes<TContext, TEventPayloadMap, TEmitted>
): CreateStoreReturnType<TContext, TEventPayloadMap, TEmitted>;
...args: CreateStoreParameterTypes<
TContext,
TEventPayloadMap,
TEmitted,
TGetters
>
): CreateStoreReturnType<TContext, TEventPayloadMap, TEmitted, TGetters>;
<
TContext extends StoreContext,
TEventPayloadMap extends EventPayloadMap,
TEmitted extends EventPayloadMap
TEmitted extends EventPayloadMap,
TGetters extends Record<string, any> = {}
>(
...args: CreateStoreParameterTypes<TContext, TEventPayloadMap, TEmitted>
): CreateStoreReturnType<TContext, TEventPayloadMap, TEmitted>;
...args: CreateStoreParameterTypes<
TContext,
TEventPayloadMap,
TEmitted,
TGetters
>
): CreateStoreReturnType<TContext, TEventPayloadMap, TEmitted, TGetters>;
} = _createStore;

/**
Expand Down Expand Up @@ -355,11 +382,10 @@ export const createStore: {
export function createStoreWithProducer<
TContext extends StoreContext,
TEventPayloadMap extends EventPayloadMap,
TEmittedPayloadMap extends EventPayloadMap
TEmittedPayloadMap extends EventPayloadMap,
TGetters extends Record<string, any> = {}
>(
producer: NoInfer<
(context: TContext, recipe: (context: TContext) => void) => TContext
>,
producer: NoInfer<Producer<TContext>>,
config: {
context: TContext;
on: {
Expand All @@ -369,13 +395,15 @@ export function createStoreWithProducer<
enqueue: EnqueueObject<ExtractEventsFromPayloadMap<TEmittedPayloadMap>>
) => void;
};
getters?: StoreGetters<TContext, TGetters>;
}
): Store<
TContext,
ExtractEventsFromPayloadMap<TEventPayloadMap>,
ExtractEventsFromPayloadMap<TEmittedPayloadMap>
ExtractEventsFromPayloadMap<TEmittedPayloadMap>,
TGetters
> {
return createStoreCore(config.context, config.on, producer);
return createStoreCore(config.context, config.on, config.getters, producer);
}

declare global {
Expand Down Expand Up @@ -404,17 +432,13 @@ export function createStoreTransition<
TEmitted
>;
},
producer?: (
context: TContext,
recipe: (context: TContext) => void
) => TContext
producer?: Producer<TContext>
) {
return (
snapshot: StoreSnapshot<TContext>,
currentContext: TContext,
event: ExtractEventsFromPayloadMap<TEventPayloadMap>
): [StoreSnapshot<TContext>, StoreEffect<TEmitted>[]] => {
): [TContext, StoreEffect<TEmitted>[]] => {
type StoreEvent = ExtractEventsFromPayloadMap<TEventPayloadMap>;
let currentContext = snapshot.context;
const assigner = transitions?.[event.type as StoreEvent['type']];
const effects: StoreEffect<TEmitted>[] = [];

Expand All @@ -435,7 +459,7 @@ export function createStoreTransition<
};

if (!assigner) {
return [snapshot, effects];
return [currentContext, effects];
}

if (typeof assigner === 'function') {
Expand Down Expand Up @@ -474,11 +498,36 @@ export function createStoreTransition<
currentContext = Object.assign({}, currentContext, partialUpdate);
}

return [{ ...snapshot, context: currentContext }, effects];
return [currentContext, effects];
};
}

// create a unique 6-char id
function uniqueId() {
return Math.random().toString(36).slice(6);
}

const computeGetters = <
TContext extends StoreContext,
TGetters extends Record<string, (context: TContext, getters: any) => any>
>(
context: TContext,
getters?: TGetters
): ResolvedGetters<TGetters> => {
const computed = {} as ResolvedGetters<TGetters>;

if (!getters) return computed;

Object.entries(getters).forEach(([key, fn]) => {
computed[key as keyof TGetters] = fn(
context,
new Proxy(computed, {
get(target, prop) {
return target[prop as keyof typeof target];
}
})
);
});

return computed;
};
Loading
Loading