Skip to content

Commit

Permalink
Add getter api to @xstate/store
Browse files Browse the repository at this point in the history
  • Loading branch information
expelledboy committed Feb 2, 2025
1 parent 80fe593 commit ac5993d
Show file tree
Hide file tree
Showing 3 changed files with 260 additions and 85 deletions.
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

0 comments on commit ac5993d

Please sign in to comment.