aeon-ui-engine 1.9.0 → 1.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/README.md +33 -0
- package/package.json +1 -1
- package/packages/core/vitest.config.ts +1 -0
- package/packages/primitives/dist/chunk-H5XJGZHT.js +493 -0
- package/packages/primitives/dist/index.d.ts +1 -0
- package/packages/primitives/dist/index.js +23 -1
- package/packages/primitives/dist/machines/app-nav.d.ts +3 -3
- package/packages/primitives/dist/machines/store.d.ts +286 -0
- package/packages/primitives/dist/machines/store.js +25 -0
- package/packages/primitives/dist/machines/tabs.d.ts +2 -2
- package/packages/primitives/src/index.ts +1 -0
- package/packages/primitives/src/machines/store.ts +712 -0
- package/packages/primitives/vitest.config.ts +1 -0
- package/packages/react/dist/index.d.ts +11 -2
- package/packages/react/dist/index.js +31 -2
- package/packages/react/src/hooks/use-aeon-store.ts +50 -0
- package/packages/react/src/index.ts +1 -0
- package/packages/react/vitest.config.ts +1 -0
- package/packages/tree/vitest.config.ts +1 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import * as xstate from 'xstate';
|
|
2
|
+
import { fromCallback } from 'xstate';
|
|
3
|
+
import { CacheState, CacheView } from './cache.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One store per app: every entity lives once, by id, in its slice. Charts and
|
|
7
|
+
* components read through selectors and project state onto `data-aeon-*`
|
|
8
|
+
* attributes; the only way to change anything is an event sent to the store.
|
|
9
|
+
*/
|
|
10
|
+
interface SliceOptions<E, Q> {
|
|
11
|
+
idOf: (entity: E) => string;
|
|
12
|
+
/** Loads the entities a query names. Slices without `load` are filled by events only. */
|
|
13
|
+
load?: (query: Q, context: {
|
|
14
|
+
signal: AbortSignal;
|
|
15
|
+
}) => Promise<readonly E[]>;
|
|
16
|
+
/** Milliseconds a query result stays fresh. Default 30 seconds. */
|
|
17
|
+
ttl?: number | ((query: Q) => number);
|
|
18
|
+
tags?: (query: Q) => readonly string[];
|
|
19
|
+
/** Turns a query into the key its result is stored under. Defaults to `String` for strings, else JSON. */
|
|
20
|
+
queryKey?: (query: Q) => string;
|
|
21
|
+
/** An incoming entity equal to the held one keeps the held object, so selectors stay quiet. Default: shallow equality. */
|
|
22
|
+
equals?: (a: E, b: E) => boolean;
|
|
23
|
+
/** Saved by the store's persistence adapter. Default true. */
|
|
24
|
+
persist?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface SliceDefinition<E, Q> extends SliceOptions<E, Q> {
|
|
27
|
+
readonly __types?: {
|
|
28
|
+
entity: E;
|
|
29
|
+
query: Q;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
declare function defineSlice<E, Q = string>(options: SliceOptions<E, Q>): SliceDefinition<E, Q>;
|
|
33
|
+
type AnySlices = Record<string, SliceDefinition<any, any>>;
|
|
34
|
+
type EntityOf<D> = D extends {
|
|
35
|
+
readonly __types?: {
|
|
36
|
+
entity: infer E;
|
|
37
|
+
};
|
|
38
|
+
} ? E : never;
|
|
39
|
+
type QueryOf<D> = D extends {
|
|
40
|
+
readonly __types?: {
|
|
41
|
+
query: infer Q;
|
|
42
|
+
};
|
|
43
|
+
} ? Q : never;
|
|
44
|
+
/** One query's result: its load state and the ids it returned, in order. */
|
|
45
|
+
interface QueryView {
|
|
46
|
+
readonly state: CacheState;
|
|
47
|
+
readonly ids: readonly string[];
|
|
48
|
+
readonly error: string | null;
|
|
49
|
+
readonly loadedAt: number | null;
|
|
50
|
+
}
|
|
51
|
+
interface SliceState<E> {
|
|
52
|
+
readonly byId: Readonly<Record<string, E>>;
|
|
53
|
+
/** Every held id, in arrival order. */
|
|
54
|
+
readonly ids: readonly string[];
|
|
55
|
+
readonly queries: Readonly<Record<string, QueryView>>;
|
|
56
|
+
/** Entity id → the optimistic write it waits on. */
|
|
57
|
+
readonly pending: Readonly<Record<string, string>>;
|
|
58
|
+
/** Entity id → why its last write was rolled back. Cleared by the next write to it. */
|
|
59
|
+
readonly failed: Readonly<Record<string, string>>;
|
|
60
|
+
}
|
|
61
|
+
type StoreStatus = 'hydrating' | 'ready';
|
|
62
|
+
interface PendingWrite {
|
|
63
|
+
slice: string;
|
|
64
|
+
/** Values before the write, restored on rollback. */
|
|
65
|
+
before: Record<string, unknown>;
|
|
66
|
+
/** Values the write put in place; rollback only restores entities still holding them. */
|
|
67
|
+
after: Record<string, unknown>;
|
|
68
|
+
}
|
|
69
|
+
interface StoreState<S extends AnySlices> {
|
|
70
|
+
readonly status: StoreStatus;
|
|
71
|
+
readonly slices: {
|
|
72
|
+
readonly [N in keyof S]: SliceState<EntityOf<S[N]>>;
|
|
73
|
+
};
|
|
74
|
+
readonly writes: Readonly<Record<string, PendingWrite>>;
|
|
75
|
+
}
|
|
76
|
+
type SliceEvent<N extends string, E, Q> = {
|
|
77
|
+
type: 'ENTITY.UPSERT';
|
|
78
|
+
slice: N;
|
|
79
|
+
entities: readonly E[];
|
|
80
|
+
} | {
|
|
81
|
+
type: 'ENTITY.PATCH';
|
|
82
|
+
slice: N;
|
|
83
|
+
id: string;
|
|
84
|
+
patch: Partial<E>;
|
|
85
|
+
} | {
|
|
86
|
+
type: 'ENTITY.REMOVE';
|
|
87
|
+
slice: N;
|
|
88
|
+
ids: readonly string[];
|
|
89
|
+
} | {
|
|
90
|
+
type: 'QUERY.FETCH';
|
|
91
|
+
slice: N;
|
|
92
|
+
query: Q;
|
|
93
|
+
} | {
|
|
94
|
+
type: 'QUERY.REFRESH';
|
|
95
|
+
slice: N;
|
|
96
|
+
query: Q;
|
|
97
|
+
} | {
|
|
98
|
+
type: 'QUERY.INVALIDATE';
|
|
99
|
+
slice: N;
|
|
100
|
+
query?: Q;
|
|
101
|
+
tag?: string;
|
|
102
|
+
}
|
|
103
|
+
/** Applies at once and marks the entities pending until WRITE.CONFIRM or WRITE.ROLLBACK. */
|
|
104
|
+
| {
|
|
105
|
+
type: 'WRITE.BEGIN';
|
|
106
|
+
slice: N;
|
|
107
|
+
write: string;
|
|
108
|
+
upsert?: readonly E[];
|
|
109
|
+
remove?: readonly string[];
|
|
110
|
+
};
|
|
111
|
+
/** Everything a store accepts. Writes, loads and invalidations all go through here. */
|
|
112
|
+
type StoreEvent<S extends AnySlices> = {
|
|
113
|
+
[N in keyof S & string]: SliceEvent<N, EntityOf<S[N]>, QueryOf<S[N]>>;
|
|
114
|
+
}[keyof S & string] | {
|
|
115
|
+
type: 'WRITE.CONFIRM';
|
|
116
|
+
write: string;
|
|
117
|
+
} | {
|
|
118
|
+
type: 'WRITE.ROLLBACK';
|
|
119
|
+
write: string;
|
|
120
|
+
error?: string;
|
|
121
|
+
}
|
|
122
|
+
/** Marks every query stale; watched ones reload (reconnect, window focus). */
|
|
123
|
+
| {
|
|
124
|
+
type: 'STORE.INVALIDATE';
|
|
125
|
+
}
|
|
126
|
+
/** Drops all data, running loads and saved data (lock, account or network switch). */
|
|
127
|
+
| {
|
|
128
|
+
type: 'STORE.CLEAR';
|
|
129
|
+
};
|
|
130
|
+
interface StoreAdapter {
|
|
131
|
+
load(key: string): string | null | undefined | Promise<string | null | undefined>;
|
|
132
|
+
save(key: string, value: string): void | Promise<void>;
|
|
133
|
+
remove(key: string): void | Promise<void>;
|
|
134
|
+
}
|
|
135
|
+
interface StorePersistOptions {
|
|
136
|
+
adapter: StoreAdapter;
|
|
137
|
+
/** Bump when the saved shape changes; older data goes through `migrate` or is dropped. */
|
|
138
|
+
version: number;
|
|
139
|
+
migrate?: (saved: PersistedSlices, fromVersion: number) => PersistedSlices | null | undefined;
|
|
140
|
+
/** Milliseconds of quiet before a save. Default 250. */
|
|
141
|
+
delayMs?: number;
|
|
142
|
+
}
|
|
143
|
+
type PersistedSlices = Record<string, {
|
|
144
|
+
byId: Record<string, unknown>;
|
|
145
|
+
ids: string[];
|
|
146
|
+
}>;
|
|
147
|
+
interface StoreOptions<S extends AnySlices> {
|
|
148
|
+
/** Names the store and its saved data. Names of secrets, keys or spend decisions are refused. */
|
|
149
|
+
id: string;
|
|
150
|
+
slices: S;
|
|
151
|
+
persist?: StorePersistOptions;
|
|
152
|
+
/** Starts a server or disk feed that sends store events; returns its stop function. */
|
|
153
|
+
sync?: (send: (event: StoreEvent<S>) => void) => (() => void) | void;
|
|
154
|
+
now?: () => number;
|
|
155
|
+
}
|
|
156
|
+
/** A query's view with its entities resolved. Stable until the view or one of its entities changes. */
|
|
157
|
+
interface QueryResult<E> extends QueryView {
|
|
158
|
+
readonly entities: readonly E[];
|
|
159
|
+
}
|
|
160
|
+
interface Selection<T> {
|
|
161
|
+
get(): T;
|
|
162
|
+
/** Calls back only when the selected value changes under `equals`. */
|
|
163
|
+
subscribe(listener: (value: T) => void): () => void;
|
|
164
|
+
}
|
|
165
|
+
/** What the invoked store actor sends its parent chart whenever its selection changes. */
|
|
166
|
+
interface StoreUpdate<T> {
|
|
167
|
+
type: 'STORE.UPDATE';
|
|
168
|
+
store: string;
|
|
169
|
+
value: T;
|
|
170
|
+
}
|
|
171
|
+
interface AeonStore<S extends AnySlices> {
|
|
172
|
+
readonly id: string;
|
|
173
|
+
getSnapshot(): StoreState<S>;
|
|
174
|
+
/** For `useSyncExternalStore`: calls back after every change. */
|
|
175
|
+
subscribe(listener: () => void): () => void;
|
|
176
|
+
send(event: StoreEvent<S>): void;
|
|
177
|
+
select<T>(selector: (state: StoreState<S>) => T, equals?: (a: T, b: T) => boolean): Selection<T>;
|
|
178
|
+
entity<N extends keyof S & string>(slice: N, id: string): EntityOf<S[N]> | undefined;
|
|
179
|
+
query<N extends keyof S & string>(slice: N, query: QueryOf<S[N]>): QueryResult<EntityOf<S[N]>>;
|
|
180
|
+
/** Resolves once saved data has loaded (at once without persistence). */
|
|
181
|
+
readonly ready: Promise<void>;
|
|
182
|
+
/** Saves now instead of after the quiet delay (app quit). */
|
|
183
|
+
flush(): Promise<void>;
|
|
184
|
+
stop(): void;
|
|
185
|
+
/**
|
|
186
|
+
* Invoke from a chart: `invoke: { id: 'store', src: store.actor, input: { select } }`.
|
|
187
|
+
* It sends `STORE.UPDATE` whenever the selection changes and forwards store
|
|
188
|
+
* events the chart sends it (`sendTo('store', …)`).
|
|
189
|
+
*/
|
|
190
|
+
readonly actor: ReturnType<typeof fromCallback<StoreEvent<S>, {
|
|
191
|
+
select: (state: StoreState<S>) => unknown;
|
|
192
|
+
equals?: (a: unknown, b: unknown) => boolean;
|
|
193
|
+
}>>;
|
|
194
|
+
}
|
|
195
|
+
declare function shallowEqual(a: unknown, b: unknown): boolean;
|
|
196
|
+
/** Field and slice names are checked word by word, so `seedPhrase` and `privateKey` are caught. */
|
|
197
|
+
declare function isStorableName(name: string): boolean;
|
|
198
|
+
/** The first field name, at any depth, that names a secret, key or spend decision. */
|
|
199
|
+
declare function unstorableField(value: unknown, depth?: number): string | null;
|
|
200
|
+
/** `data-aeon-*` projection of a query: its load state and how many entities it holds. */
|
|
201
|
+
declare function queryAttrs(result: QueryView): Record<string, string>;
|
|
202
|
+
/** `data-aeon-*` projection of one entity: pending while a write waits on the server, failed after a rollback. */
|
|
203
|
+
declare function entityAttrs<E>(slice: SliceState<E>, sliceName: string, id: string): Record<string, string>;
|
|
204
|
+
declare function memoryAdapter(initial?: Record<string, string>): StoreAdapter & {
|
|
205
|
+
readonly data: Map<string, string>;
|
|
206
|
+
};
|
|
207
|
+
declare function webStorageAdapter(storage?: Storage): StoreAdapter;
|
|
208
|
+
type AnyEntity = unknown;
|
|
209
|
+
type AnySliceState = SliceState<AnyEntity>;
|
|
210
|
+
interface Data {
|
|
211
|
+
status: StoreStatus;
|
|
212
|
+
slices: Record<string, AnySliceState>;
|
|
213
|
+
writes: Record<string, PendingWrite>;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The store's chart: `hydrating` until saved data has loaded, then `ready`.
|
|
217
|
+
* Every event folds into one immutable snapshot (`context.data`) that keeps
|
|
218
|
+
* its identity, and that of every untouched slice and entity, when an event
|
|
219
|
+
* changes nothing.
|
|
220
|
+
*/
|
|
221
|
+
declare function createStoreMachine(defs: AnySlices): xstate.StateMachine<{
|
|
222
|
+
data: Data;
|
|
223
|
+
}, {
|
|
224
|
+
type: "ENTITY.REMOVE";
|
|
225
|
+
slice: string;
|
|
226
|
+
ids: readonly string[];
|
|
227
|
+
} | {
|
|
228
|
+
type: "WRITE.CONFIRM";
|
|
229
|
+
write: string;
|
|
230
|
+
} | {
|
|
231
|
+
type: "WRITE.ROLLBACK";
|
|
232
|
+
write: string;
|
|
233
|
+
error?: string;
|
|
234
|
+
} | {
|
|
235
|
+
type: "STORE.INVALIDATE";
|
|
236
|
+
} | {
|
|
237
|
+
type: "STORE.CLEAR";
|
|
238
|
+
} | {
|
|
239
|
+
type: "ENTITY.UPSERT";
|
|
240
|
+
slice: string;
|
|
241
|
+
entities: readonly any[];
|
|
242
|
+
} | {
|
|
243
|
+
type: "ENTITY.PATCH";
|
|
244
|
+
slice: string;
|
|
245
|
+
id: string;
|
|
246
|
+
patch: Partial<any>;
|
|
247
|
+
} | {
|
|
248
|
+
type: "QUERY.FETCH";
|
|
249
|
+
slice: string;
|
|
250
|
+
query: any;
|
|
251
|
+
} | {
|
|
252
|
+
type: "QUERY.REFRESH";
|
|
253
|
+
slice: string;
|
|
254
|
+
query: any;
|
|
255
|
+
} | {
|
|
256
|
+
type: "QUERY.INVALIDATE";
|
|
257
|
+
slice: string;
|
|
258
|
+
query?: any;
|
|
259
|
+
tag?: string;
|
|
260
|
+
} | {
|
|
261
|
+
type: "WRITE.BEGIN";
|
|
262
|
+
slice: string;
|
|
263
|
+
write: string;
|
|
264
|
+
upsert?: readonly any[] | undefined;
|
|
265
|
+
remove?: readonly string[];
|
|
266
|
+
} | {
|
|
267
|
+
type: "QUERY.UPDATE";
|
|
268
|
+
slice: string;
|
|
269
|
+
key: string;
|
|
270
|
+
view: CacheView<readonly AnyEntity[]>;
|
|
271
|
+
} | {
|
|
272
|
+
type: "STORE.HYDRATED";
|
|
273
|
+
slices: PersistedSlices | null;
|
|
274
|
+
}, {}, never, never, never, never, "ready" | "hydrating" | "boot", string, {
|
|
275
|
+
hydrate: boolean;
|
|
276
|
+
}, xstate.NonReducibleUnknown, xstate.EventObject, xstate.MetaObject, {
|
|
277
|
+
id: "aeonStore";
|
|
278
|
+
states: {
|
|
279
|
+
readonly boot: {};
|
|
280
|
+
readonly hydrating: {};
|
|
281
|
+
readonly ready: {};
|
|
282
|
+
};
|
|
283
|
+
}>;
|
|
284
|
+
declare function createAeonStore<S extends AnySlices>(options: StoreOptions<S>): AeonStore<S>;
|
|
285
|
+
|
|
286
|
+
export { type AeonStore, type AnySlices, type EntityOf, type PersistedSlices, type QueryOf, type QueryResult, type QueryView, type Selection, type SliceDefinition, type SliceOptions, type SliceState, type StoreAdapter, type StoreEvent, type StoreOptions, type StorePersistOptions, type StoreState, type StoreStatus, type StoreUpdate, createAeonStore, createStoreMachine, defineSlice, entityAttrs, isStorableName, memoryAdapter, queryAttrs, shallowEqual, unstorableField, webStorageAdapter };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAeonStore,
|
|
3
|
+
createStoreMachine,
|
|
4
|
+
defineSlice,
|
|
5
|
+
entityAttrs,
|
|
6
|
+
isStorableName,
|
|
7
|
+
memoryAdapter,
|
|
8
|
+
queryAttrs,
|
|
9
|
+
shallowEqual,
|
|
10
|
+
unstorableField,
|
|
11
|
+
webStorageAdapter
|
|
12
|
+
} from "../chunk-H5XJGZHT.js";
|
|
13
|
+
import "../chunk-5BNAV4S2.js";
|
|
14
|
+
export {
|
|
15
|
+
createAeonStore,
|
|
16
|
+
createStoreMachine,
|
|
17
|
+
defineSlice,
|
|
18
|
+
entityAttrs,
|
|
19
|
+
isStorableName,
|
|
20
|
+
memoryAdapter,
|
|
21
|
+
queryAttrs,
|
|
22
|
+
shallowEqual,
|
|
23
|
+
unstorableField,
|
|
24
|
+
webStorageAdapter
|
|
25
|
+
};
|
|
@@ -18,10 +18,10 @@ declare const tabsMachine: xstate.StateMachine<TabsContext, {
|
|
|
18
18
|
type: "SET_VALUE";
|
|
19
19
|
value: string;
|
|
20
20
|
}, {}, never, {
|
|
21
|
-
type: "
|
|
21
|
+
type: "select";
|
|
22
22
|
params: xstate.NonReducibleUnknown;
|
|
23
23
|
} | {
|
|
24
|
-
type: "
|
|
24
|
+
type: "setValue";
|
|
25
25
|
params: xstate.NonReducibleUnknown;
|
|
26
26
|
}, {
|
|
27
27
|
type: "notDisabled";
|