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 CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.10.1] - 2026-10-22
6
+
7
+ ### Changed
8
+
9
+ - Patch release so consumers can install a fresh `aeon-ui-engine` after 1.10.0.
10
+
11
+ ## [1.10.0] - 2026-10-21
12
+
13
+ ### Added
14
+
15
+ - **App store (`@aeon-ui/primitives`).** `createAeonStore({ id, slices, persist?, sync? })` keeps an app's data in one place: each entity once, by id, in its slice (`defineSlice({ idOf, load?, ttl?, tags? })`). Every change is an event: `ENTITY.UPSERT`, `ENTITY.PATCH` and `ENTITY.REMOVE`, plus optimistic `WRITE.BEGIN`, which marks entities pending until `WRITE.CONFIRM` or `WRITE.ROLLBACK` (restores the old value and marks it failed). Queries run on the chart cache (`QUERY.FETCH`, `QUERY.REFRESH`, `QUERY.INVALIDATE`, `STORE.INVALIDATE`). `select`, `query` and `entity` return the same object until their data changes. `queryAttrs` and `entityAttrs` project state onto `data-aeon-state`. Persistence saves only confirmed values through an adapter (`memoryAdapter`, `webStorageAdapter`), with `version` and `migrate`. `sync` feeds server pushes in as events, and `STORE.CLEAR` wipes memory, running loads and the saved copy. Charts read it by invoking `store.actor`. Store ids, slice names and entity fields that name secrets, keys or spend decisions are refused.
16
+ - **`useAeonSelector` and `useAeonQuery` (`@aeon-ui/react`).** Read the store; a component re-renders only when the value it selects changes.
17
+
5
18
  ## [1.9.0] - 2026-10-21
6
19
 
7
20
  ### Added
package/README.md CHANGED
@@ -176,6 +176,39 @@ function AsyncSurface() {
176
176
 
177
177
  See [docs/PRIMITIVE_COVERAGE.md](./docs/PRIMITIVE_COVERAGE.md).
178
178
 
179
+ ## App data: one store
180
+
181
+ Each app keeps its data in one `createAeonStore`: every entity once, by id, in its slice. Components never copy it. They read through selectors and show state through `data-aeon-*` attributes. Every change is an event sent to the store.
182
+
183
+ ```tsx
184
+ import { createAeonStore, defineSlice, entityAttrs, queryAttrs, webStorageAdapter } from '@aeon-ui/primitives'
185
+ import { useAeonQuery, useAeonSelector } from '@aeon-ui/react'
186
+
187
+ export const store = createAeonStore({
188
+ id: 'wallet-app',
189
+ slices: { payments: defineSlice<Payment>({ idOf: (p) => p.id, load: (query) => api.payments(query), ttl: 30_000 }) },
190
+ persist: { adapter: webStorageAdapter(), version: 1 },
191
+ sync: (send) => api.onPayment((p) => send({ type: 'ENTITY.UPSERT', slice: 'payments', entities: [p] })),
192
+ })
193
+
194
+ function Payments() {
195
+ const result = useAeonQuery(store, 'payments', 'recent') // loads once per key, shared by every reader
196
+ return <ul {...queryAttrs(result)}>{result.entities.map((p) => <PaymentRow key={p.id} id={p.id} />)}</ul>
197
+ }
198
+
199
+ function PaymentRow({ id }: { id: string }) {
200
+ const payment = useAeonSelector(store, (s) => s.slices.payments.byId[id]) // re-renders only when this payment changes
201
+ const slice = useAeonSelector(store, (s) => s.slices.payments)
202
+ return <li {...entityAttrs(slice, 'payments', id)}>{payment?.amount}</li>
203
+ }
204
+ ```
205
+
206
+ - **Writes:** `ENTITY.UPSERT`, `ENTITY.PATCH` and `ENTITY.REMOVE` change entities directly. `WRITE.BEGIN` applies a change at once and marks it `pending` until `WRITE.CONFIRM`, or until `WRITE.ROLLBACK` puts the old value back and marks the entity `failed`.
207
+ - **Queries:** `QUERY.FETCH`, `QUERY.REFRESH` and `QUERY.INVALIDATE { query | tag }` run on the chart cache's states (`idle`, `loading`, `ready`, `stale`, `refreshing`, `failed`). `STORE.INVALIDATE` reloads every watched query (for reconnect or window focus). `STORE.CLEAR` drops all data, running loads and the saved copy (for lock or account switch).
208
+ - **Persistence:** only confirmed values are saved, after a short quiet delay. A `version` bump runs `migrate`; without one, older saved data is dropped. Data written during start-up wins over the saved copy. Slices with `persist: false` stay in memory.
209
+ - **Charts:** invoke `store.actor` with `input: { select }` to get `STORE.UPDATE` whenever the selection changes. Store events sent to it are forwarded to the store.
210
+ - **Never stored:** store ids, slice names and entity fields at any depth that name secrets, keys, seeds, passwords or spend are refused.
211
+
179
212
  ## Example
180
213
 
181
214
  ```tsx
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aeon-ui-engine",
3
- "version": "1.9.0",
3
+ "version": "1.10.1",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,6 +3,7 @@ import { defineConfig } from 'vitest/config'
3
3
  export default defineConfig({
4
4
  test: {
5
5
  environment: 'node',
6
+ maxWorkers: '50%',
6
7
  include: ['src/**/*.test.ts'],
7
8
  },
8
9
  })
@@ -0,0 +1,493 @@
1
+ import {
2
+ createChartCache,
3
+ isCacheableId
4
+ } from "./chunk-5BNAV4S2.js";
5
+
6
+ // src/machines/store.ts
7
+ import { stateToAttr } from "@aeon-ui/core";
8
+ import { assign, createActor, fromCallback, setup } from "xstate";
9
+ function defineSlice(options) {
10
+ return options;
11
+ }
12
+ var DEFAULT_TTL = 3e4;
13
+ var ALL_TAG = "*";
14
+ var EMPTY_IDS = Object.freeze([]);
15
+ var IDLE_QUERY = Object.freeze({ state: "idle", ids: EMPTY_IDS, error: null, loadedAt: null });
16
+ var EMPTY_SLICE = Object.freeze({ byId: {}, ids: EMPTY_IDS, queries: {}, pending: {}, failed: {} });
17
+ var defaultKeyOf = (key) => typeof key === "string" ? key : JSON.stringify(key);
18
+ function shallowEqual(a, b) {
19
+ if (Object.is(a, b)) return true;
20
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
21
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
22
+ const keysA = Object.keys(a);
23
+ if (keysA.length !== Object.keys(b).length) return false;
24
+ return keysA.every((key) => Object.hasOwn(b, key) && Object.is(a[key], b[key]));
25
+ }
26
+ function isStorableName(name) {
27
+ return isCacheableId(name.replace(/([a-z0-9])([A-Z])/g, "$1 $2"));
28
+ }
29
+ function unstorableField(value, depth = 0) {
30
+ if (depth > 8 || typeof value !== "object" || value === null) return null;
31
+ for (const [key, inner] of Object.entries(value)) {
32
+ if (!Array.isArray(value) && !isStorableName(key)) return key;
33
+ const nested = unstorableField(inner, depth + 1);
34
+ if (nested !== null) return nested;
35
+ }
36
+ return null;
37
+ }
38
+ function queryAttrs(result) {
39
+ return { "data-aeon-state": stateToAttr(result.state), "data-aeon-count": String(result.ids.length) };
40
+ }
41
+ function entityAttrs(slice, sliceName, id) {
42
+ const state = slice.pending[id] !== void 0 ? "pending" : slice.failed[id] !== void 0 ? "failed" : "settled";
43
+ return { "data-aeon-entity": sliceName, "data-aeon-state": state };
44
+ }
45
+ function memoryAdapter(initial = {}) {
46
+ const data = new Map(Object.entries(initial));
47
+ return {
48
+ data,
49
+ load: (key) => data.get(key) ?? null,
50
+ save: (key, value) => void data.set(key, value),
51
+ remove: (key) => void data.delete(key)
52
+ };
53
+ }
54
+ function webStorageAdapter(storage = globalThis.localStorage) {
55
+ return {
56
+ load: (key) => storage.getItem(key),
57
+ save: (key, value) => storage.setItem(key, value),
58
+ remove: (key) => storage.removeItem(key)
59
+ };
60
+ }
61
+ function omit(record, keys) {
62
+ let next = null;
63
+ for (const key of keys) {
64
+ if (!Object.hasOwn(record, key)) continue;
65
+ next ??= { ...record };
66
+ delete next[key];
67
+ }
68
+ return next ?? record;
69
+ }
70
+ function withSlice(data, name, slice) {
71
+ return slice === data.slices[name] ? data : { ...data, slices: { ...data.slices, [name]: slice } };
72
+ }
73
+ function putEntities(r, name, slice, entities) {
74
+ const def = r.defs[name];
75
+ const equals = def.equals ?? shallowEqual;
76
+ let byId = null;
77
+ let added = null;
78
+ let failed = slice.failed;
79
+ for (const entity of entities) {
80
+ const id = def.idOf(entity);
81
+ const held = (byId ?? slice.byId)[id];
82
+ failed = omit(failed, [id]);
83
+ if (held !== void 0 && (held === entity || equals(held, entity))) continue;
84
+ byId ??= { ...slice.byId };
85
+ if (held === void 0 && !Object.hasOwn(byId, id)) (added ??= []).push(id);
86
+ byId[id] = entity;
87
+ }
88
+ if (!byId && failed === slice.failed) return slice;
89
+ return { ...slice, byId: byId ?? slice.byId, ids: added ? [...slice.ids, ...added] : slice.ids, failed };
90
+ }
91
+ function removeEntities(slice, ids) {
92
+ const byId = omit(slice.byId, ids);
93
+ if (byId === slice.byId) return slice;
94
+ const gone = new Set(ids);
95
+ return { ...slice, byId, ids: slice.ids.filter((id) => !gone.has(id)) };
96
+ }
97
+ function sameIds(a, b) {
98
+ return a.length === b.length && a.every((id, i) => id === b[i]);
99
+ }
100
+ function applyQueryUpdate(r, data, name, key, view) {
101
+ const def = r.defs[name];
102
+ let slice = data.slices[name] ?? EMPTY_SLICE;
103
+ let writes = data.writes;
104
+ const previous = slice.queries[key] ?? IDLE_QUERY;
105
+ let ids = previous.ids;
106
+ if (view.data !== void 0) {
107
+ const incoming = view.data;
108
+ const settled = [];
109
+ for (const entity of incoming) {
110
+ const id = def.idOf(entity);
111
+ const write = slice.pending[id];
112
+ if (write !== void 0 && writes[write]) {
113
+ writes = { ...writes, [write]: { ...writes[write], before: { ...writes[write].before, [id]: entity } } };
114
+ } else settled.push(entity);
115
+ }
116
+ slice = putEntities(r, name, slice, settled);
117
+ const nextIds = incoming.map(def.idOf);
118
+ if (!sameIds(nextIds, ids)) ids = nextIds;
119
+ } else if (view.state === "idle") ids = EMPTY_IDS;
120
+ const query = previous.state === view.state && previous.ids === ids && previous.error === view.error && previous.loadedAt === view.loadedAt ? previous : Object.freeze({ state: view.state, ids, error: view.error, loadedAt: view.loadedAt });
121
+ if (query !== previous) slice = { ...slice, queries: { ...slice.queries, [key]: query } };
122
+ const next = withSlice(data, name, slice);
123
+ return writes === data.writes ? next : { ...next, writes };
124
+ }
125
+ function beginWrite(r, data, event) {
126
+ if (data.writes[event.write]) return data;
127
+ const def = r.defs[event.slice];
128
+ let slice = data.slices[event.slice] ?? EMPTY_SLICE;
129
+ const before = {};
130
+ const after = {};
131
+ const ids = [];
132
+ for (const entity of event.upsert ?? []) {
133
+ const id = def.idOf(entity);
134
+ before[id] = slice.byId[id];
135
+ ids.push(id);
136
+ }
137
+ for (const id of event.remove ?? []) {
138
+ before[id] = slice.byId[id];
139
+ ids.push(id);
140
+ }
141
+ slice = putEntities(r, event.slice, slice, event.upsert ?? []);
142
+ slice = removeEntities(slice, event.remove ?? []);
143
+ for (const id of ids) after[id] = slice.byId[id];
144
+ const pending = { ...slice.pending };
145
+ for (const id of ids) pending[id] = event.write;
146
+ slice = { ...slice, pending, failed: omit(slice.failed, ids) };
147
+ return { ...withSlice(data, event.slice, slice), writes: { ...data.writes, [event.write]: { slice: event.slice, before, after } } };
148
+ }
149
+ function settleWrite(data, writeId, rollback) {
150
+ const write = data.writes[writeId];
151
+ if (!write) return data;
152
+ let slice = data.slices[write.slice] ?? EMPTY_SLICE;
153
+ const ids = Object.keys(write.after);
154
+ const mine = ids.filter((id) => slice.pending[id] === writeId);
155
+ slice = { ...slice, pending: omit(slice.pending, mine) };
156
+ if (rollback !== null) {
157
+ const failed = { ...slice.failed };
158
+ for (const id of ids) {
159
+ if (slice.byId[id] !== write.after[id]) continue;
160
+ const restore = write.before[id];
161
+ if (restore === void 0) slice = removeEntities(slice, [id]);
162
+ else {
163
+ const had = Object.hasOwn(slice.byId, id);
164
+ slice = { ...slice, byId: { ...slice.byId, [id]: restore }, ids: had ? slice.ids : [...slice.ids, id] };
165
+ }
166
+ failed[id] = rollback;
167
+ }
168
+ slice = { ...slice, failed };
169
+ }
170
+ const writes = { ...data.writes };
171
+ delete writes[writeId];
172
+ return { ...withSlice(data, write.slice, slice), writes };
173
+ }
174
+ function hydrate(r, data, saved) {
175
+ let next = { ...data, status: "ready" };
176
+ if (!saved) return next;
177
+ for (const [name, stored] of Object.entries(saved)) {
178
+ const def = r.defs[name];
179
+ if (!def || def.persist === false || !stored || typeof stored.byId !== "object") continue;
180
+ const slice = next.slices[name] ?? EMPTY_SLICE;
181
+ const order = Array.isArray(stored.ids) ? stored.ids : Object.keys(stored.byId);
182
+ const missing = order.map((id) => stored.byId[id]).filter((entity) => entity !== void 0 && unstorableField(entity) === null && !Object.hasOwn(slice.byId, def.idOf(entity)));
183
+ next = withSlice(next, name, putEntities(r, name, slice, missing));
184
+ }
185
+ return next;
186
+ }
187
+ function reduce(r, data, event) {
188
+ switch (event.type) {
189
+ case "ENTITY.UPSERT":
190
+ return withSlice(data, event.slice, putEntities(r, event.slice, data.slices[event.slice] ?? EMPTY_SLICE, event.entities));
191
+ case "ENTITY.PATCH": {
192
+ const slice = data.slices[event.slice] ?? EMPTY_SLICE;
193
+ const held = slice.byId[event.id];
194
+ if (held === void 0) return data;
195
+ return withSlice(data, event.slice, putEntities(r, event.slice, slice, [{ ...held, ...event.patch }]));
196
+ }
197
+ case "ENTITY.REMOVE": {
198
+ const slice = removeEntities(data.slices[event.slice] ?? EMPTY_SLICE, event.ids);
199
+ return withSlice(data, event.slice, { ...slice, failed: omit(slice.failed, event.ids) });
200
+ }
201
+ case "WRITE.BEGIN":
202
+ return beginWrite(r, data, event);
203
+ case "WRITE.CONFIRM":
204
+ return settleWrite(data, event.write, null);
205
+ case "WRITE.ROLLBACK":
206
+ return settleWrite(data, event.write, event.error ?? "write failed");
207
+ case "QUERY.UPDATE":
208
+ return applyQueryUpdate(r, data, event.slice, event.key, event.view);
209
+ case "STORE.HYDRATED":
210
+ return hydrate(r, data, event.slices);
211
+ case "STORE.CLEAR":
212
+ return { ...data, slices: Object.fromEntries(Object.keys(r.defs).map((name) => [name, EMPTY_SLICE])), writes: {} };
213
+ default:
214
+ return data;
215
+ }
216
+ }
217
+ function createStoreMachine(defs) {
218
+ const r = { defs };
219
+ return setup({
220
+ types: {
221
+ context: {},
222
+ events: {},
223
+ input: {}
224
+ }
225
+ }).createMachine({
226
+ id: "aeonStore",
227
+ context: ({ input }) => ({
228
+ data: {
229
+ status: input.hydrate ? "hydrating" : "ready",
230
+ slices: Object.fromEntries(Object.keys(defs).map((name) => [name, EMPTY_SLICE])),
231
+ writes: {}
232
+ }
233
+ }),
234
+ initial: "boot",
235
+ states: {
236
+ boot: { always: [{ guard: ({ context }) => context.data.status === "hydrating", target: "hydrating" }, { target: "ready" }] },
237
+ hydrating: { on: { "STORE.HYDRATED": { target: "ready", actions: assign(({ context, event }) => ({ data: reduce(r, context.data, event) })) } } },
238
+ ready: {}
239
+ },
240
+ on: {
241
+ "*": { actions: assign(({ context, event }) => event.type === "STORE.HYDRATED" ? {} : { data: reduce(r, context.data, event) }) }
242
+ }
243
+ });
244
+ }
245
+ function persistable(defs, data) {
246
+ const out = {};
247
+ for (const [name, def] of Object.entries(defs)) {
248
+ if (def.persist === false) continue;
249
+ const slice = data.slices[name] ?? EMPTY_SLICE;
250
+ const byId = { ...slice.byId };
251
+ const ids = [...slice.ids];
252
+ for (const [id, writeId] of Object.entries(slice.pending)) {
253
+ const before = data.writes[writeId]?.before[id];
254
+ if (before === void 0) {
255
+ delete byId[id];
256
+ const at = ids.indexOf(id);
257
+ if (at >= 0) ids.splice(at, 1);
258
+ } else {
259
+ if (!Object.hasOwn(byId, id)) ids.push(id);
260
+ byId[id] = before;
261
+ }
262
+ }
263
+ out[name] = { byId, ids };
264
+ }
265
+ return out;
266
+ }
267
+ function parseSaved(raw, options) {
268
+ if (!raw) return null;
269
+ try {
270
+ const saved = JSON.parse(raw);
271
+ if (typeof saved !== "object" || saved === null || typeof saved.slices !== "object" || saved.slices === null) return null;
272
+ const slices = saved.slices;
273
+ if (saved.version === options.version) return slices;
274
+ if (typeof saved.version !== "number" || !options.migrate) return null;
275
+ return options.migrate(slices, saved.version) ?? null;
276
+ } catch {
277
+ return null;
278
+ }
279
+ }
280
+ function createAeonStore(options) {
281
+ if (!isStorableName(options.id)) throw new Error(`refusing store "${options.id}": secrets, keys and spend decisions are never stored`);
282
+ for (const name of Object.keys(options.slices)) {
283
+ if (!isStorableName(name)) throw new Error(`refusing slice "${name}" in store "${options.id}": secrets, keys and spend decisions are never stored`);
284
+ }
285
+ const defs = options.slices;
286
+ const persist = options.persist;
287
+ const storageKey = `aeon-store:${options.id}`;
288
+ const actor = createActor(createStoreMachine(defs), { input: { hydrate: persist !== void 0 } });
289
+ const listeners = /* @__PURE__ */ new Set();
290
+ const caches = /* @__PURE__ */ new Map();
291
+ const watched = /* @__PURE__ */ new Map();
292
+ let data = actor.getSnapshot().context.data;
293
+ let stopped = false;
294
+ for (const [name, def] of Object.entries(defs)) {
295
+ if (!def.load) continue;
296
+ const load = def.load;
297
+ const userTags = def.tags;
298
+ caches.set(
299
+ name,
300
+ createChartCache({
301
+ id: `${options.id}.${name}`,
302
+ load: (query2, context) => load(query2, context),
303
+ ttl: def.ttl ?? DEFAULT_TTL,
304
+ tags: (query2) => [ALL_TAG, ...userTags?.(query2) ?? []],
305
+ keyOf: def.queryKey ?? defaultKeyOf,
306
+ // Watched queries are pinned; this bounds the unwatched ones.
307
+ maxEntries: 200,
308
+ now: options.now
309
+ })
310
+ );
311
+ }
312
+ let saveTimer = null;
313
+ const save = async () => {
314
+ if (saveTimer !== null) clearTimeout(saveTimer);
315
+ saveTimer = null;
316
+ if (!persist || stopped || data.status !== "ready") return;
317
+ await persist.adapter.save(storageKey, JSON.stringify({ version: persist.version, slices: persistable(defs, data) }));
318
+ };
319
+ const persisted = Object.keys(defs).filter((name) => defs[name].persist !== false);
320
+ const persistChanged = (previous, next) => persisted.some((name) => previous.slices[name]?.byId !== next.slices[name]?.byId || previous.slices[name]?.pending !== next.slices[name]?.pending);
321
+ const heldBeforeHydration = (previous) => persisted.some((name) => (previous.slices[name]?.ids.length ?? 0) > 0);
322
+ actor.subscribe((snapshot) => {
323
+ const next = snapshot.context.data;
324
+ if (next === data) return;
325
+ const previous = data;
326
+ data = next;
327
+ if (persist && next.status === "ready" && (previous.status === "ready" ? persistChanged(previous, next) : heldBeforeHydration(previous))) {
328
+ if (saveTimer !== null) clearTimeout(saveTimer);
329
+ saveTimer = setTimeout(() => void save().catch(() => {
330
+ }), persist.delayMs ?? 250);
331
+ }
332
+ for (const listener of [...listeners]) listener();
333
+ });
334
+ actor.start();
335
+ const keyOf = (name, query2) => (defs[name].queryKey ?? defaultKeyOf)(query2);
336
+ const watch = (name, query2) => {
337
+ const cache = caches.get(name);
338
+ if (!cache) throw new Error(`slice "${name}" in store "${options.id}" has no load, so it has no queries`);
339
+ const key = keyOf(name, query2);
340
+ let forSlice = watched.get(name);
341
+ if (!forSlice) watched.set(name, forSlice = /* @__PURE__ */ new Map());
342
+ if (!forSlice.has(key)) {
343
+ const forward = (view) => actor.send({ type: "QUERY.UPDATE", slice: name, key, view });
344
+ forSlice.set(key, cache.subscribe(query2, forward));
345
+ forward(cache.peek(query2));
346
+ }
347
+ return cache;
348
+ };
349
+ const unwatchAll = () => {
350
+ for (const forSlice of watched.values()) for (const stop of forSlice.values()) stop();
351
+ watched.clear();
352
+ };
353
+ const checkFields = (entities) => {
354
+ for (const entity of entities) {
355
+ const field = unstorableField(entity);
356
+ if (field !== null) throw new Error(`refusing field "${field}" in store "${options.id}": secrets, keys and spend decisions are never stored`);
357
+ }
358
+ };
359
+ const send = (event) => {
360
+ if (stopped) return;
361
+ const e = event;
362
+ if ("slice" in e && !Object.hasOwn(defs, e.slice)) throw new Error(`store "${options.id}" has no slice "${e.slice}"`);
363
+ switch (e.type) {
364
+ case "ENTITY.UPSERT":
365
+ checkFields(e.entities);
366
+ break;
367
+ case "ENTITY.PATCH":
368
+ checkFields([e.patch]);
369
+ break;
370
+ case "WRITE.BEGIN":
371
+ checkFields(e.upsert ?? []);
372
+ break;
373
+ case "QUERY.FETCH":
374
+ watch(e.slice, e.query).fetch(e.query);
375
+ return;
376
+ case "QUERY.REFRESH":
377
+ watch(e.slice, e.query).refresh(e.query);
378
+ return;
379
+ case "QUERY.INVALIDATE": {
380
+ const cache = caches.get(e.slice);
381
+ if (!cache) return;
382
+ if (e.tag !== void 0) cache.invalidate({ tag: e.tag });
383
+ else if (e.query !== void 0) cache.invalidate({ key: e.query });
384
+ else cache.invalidate({ tag: ALL_TAG });
385
+ return;
386
+ }
387
+ case "STORE.INVALIDATE":
388
+ for (const cache of caches.values()) cache.invalidate({ tag: ALL_TAG });
389
+ return;
390
+ case "STORE.CLEAR":
391
+ unwatchAll();
392
+ for (const cache of caches.values()) cache.clear();
393
+ actor.send(e);
394
+ if (saveTimer !== null) clearTimeout(saveTimer);
395
+ saveTimer = null;
396
+ if (persist) void Promise.resolve(persist.adapter.remove(storageKey)).catch(() => {
397
+ });
398
+ return;
399
+ }
400
+ actor.send(e);
401
+ };
402
+ const results = /* @__PURE__ */ new Map();
403
+ const query = (name, q) => {
404
+ const key = keyOf(name, q);
405
+ const slice = data.slices[name] ?? EMPTY_SLICE;
406
+ const view = slice.queries[key] ?? IDLE_QUERY;
407
+ const memoKey = `${name}\0${key}`;
408
+ const held = results.get(memoKey);
409
+ if (held && held.view === view && held.byId === slice.byId) return held.result;
410
+ const entities = view.ids.map((id) => slice.byId[id]).filter((entity) => entity !== void 0);
411
+ const result = held && held.view === view && sameEntities(held.result.entities, entities) ? held.result : Object.freeze({ ...view, entities: Object.freeze(entities) });
412
+ results.set(memoKey, { view, byId: slice.byId, result });
413
+ return result;
414
+ };
415
+ const select = (selector, equals = Object.is) => {
416
+ let from = data;
417
+ let value = selector(data);
418
+ const get = () => {
419
+ if (from !== data) {
420
+ const next = selector(data);
421
+ from = data;
422
+ if (!equals(value, next)) value = next;
423
+ }
424
+ return value;
425
+ };
426
+ return {
427
+ get,
428
+ subscribe: (listener) => {
429
+ let last = get();
430
+ return store.subscribe(() => {
431
+ const next = get();
432
+ if (next === last) return;
433
+ last = next;
434
+ listener(next);
435
+ });
436
+ }
437
+ };
438
+ };
439
+ let stopSync = void 0;
440
+ const ready = persist ? Promise.resolve().then(() => persist.adapter.load(storageKey)).then((raw) => parseSaved(raw, persist), () => null).then((slices) => {
441
+ if (!stopped) actor.send({ type: "STORE.HYDRATED", slices });
442
+ }) : Promise.resolve();
443
+ const store = {
444
+ id: options.id,
445
+ getSnapshot: () => data,
446
+ subscribe: (listener) => {
447
+ listeners.add(listener);
448
+ return () => void listeners.delete(listener);
449
+ },
450
+ send,
451
+ select,
452
+ entity: (name, id) => (data.slices[name] ?? EMPTY_SLICE).byId[id],
453
+ query: (name, q) => query(name, q),
454
+ ready,
455
+ flush: () => ready.then(save),
456
+ stop: () => {
457
+ if (stopped) return;
458
+ stopped = true;
459
+ if (saveTimer !== null) clearTimeout(saveTimer);
460
+ if (typeof stopSync === "function") stopSync();
461
+ unwatchAll();
462
+ actor.stop();
463
+ listeners.clear();
464
+ },
465
+ actor: fromCallback(
466
+ ({ input, sendBack, receive }) => {
467
+ const selection = select(input.select, input.equals);
468
+ const report = (value) => sendBack({ type: "STORE.UPDATE", store: options.id, value });
469
+ report(selection.get());
470
+ receive((event) => send(event));
471
+ return selection.subscribe(report);
472
+ }
473
+ )
474
+ };
475
+ stopSync = options.sync?.(send);
476
+ return store;
477
+ }
478
+ function sameEntities(a, b) {
479
+ return a.length === b.length && a.every((entity, i) => entity === b[i]);
480
+ }
481
+
482
+ export {
483
+ defineSlice,
484
+ shallowEqual,
485
+ isStorableName,
486
+ unstorableField,
487
+ queryAttrs,
488
+ entityAttrs,
489
+ memoryAdapter,
490
+ webStorageAdapter,
491
+ createStoreMachine,
492
+ createAeonStore
493
+ };
@@ -17,4 +17,5 @@ export { STICKY_BAR_STATES, StickyBarContext, StickyBarEvent, StickyBarState, st
17
17
  export { CONTENT_REGION_STATES, ContentRegionContext, ContentRegionEvent, ContentRegionState, contentRegionMachine } from './machines/content-region.js';
18
18
  export { CLIPBOARD_STATES, ClipboardContext, ClipboardEvent, ClipboardStatus, clipboardMachine } from './machines/clipboard.js';
19
19
  export { CACHE_STATES, CacheCommand, CacheEntryActor, CacheEntryContext, CacheEntryEvent, CacheEntryInput, CacheState, CacheUpdate, CacheView, ChartCache, ChartCacheOptions, cacheEntryMachine, createCacheEntryMachine, createChartCache, isCacheableId } from './machines/cache.js';
20
+ export { AeonStore, AnySlices, EntityOf, PersistedSlices, QueryOf, QueryResult, QueryView, Selection, SliceDefinition, SliceOptions, SliceState, StoreAdapter, StoreEvent, StoreOptions, StorePersistOptions, StoreState, StoreStatus, StoreUpdate, createAeonStore, createStoreMachine, defineSlice, entityAttrs, isStorableName, memoryAdapter, queryAttrs, shallowEqual, unstorableField, webStorageAdapter } from './machines/store.js';
20
21
  import 'xstate';
@@ -2,6 +2,18 @@ import {
2
2
  STICKY_BAR_STATES,
3
3
  stickyBarMachine
4
4
  } from "./chunk-YBPNI2LI.js";
5
+ import {
6
+ createAeonStore,
7
+ createStoreMachine,
8
+ defineSlice,
9
+ entityAttrs,
10
+ isStorableName,
11
+ memoryAdapter,
12
+ queryAttrs,
13
+ shallowEqual,
14
+ unstorableField,
15
+ webStorageAdapter
16
+ } from "./chunk-H5XJGZHT.js";
5
17
  import {
6
18
  tabsMachine
7
19
  } from "./chunk-HONROWUG.js";
@@ -103,24 +115,34 @@ export {
103
115
  clamp as clampSliderValue,
104
116
  clipboardMachine,
105
117
  contentRegionMachine,
118
+ createAeonStore,
106
119
  createAsyncMachine,
107
120
  createCacheEntryMachine,
108
121
  createChartCache,
122
+ createStoreMachine,
123
+ defineSlice,
109
124
  dialogMachine,
125
+ entityAttrs,
110
126
  fieldMachine,
111
127
  fileUploadMachine,
112
128
  getFileUploadPhase,
113
129
  isCacheableId,
114
130
  isEmptyAsyncData,
131
+ isStorableName,
132
+ memoryAdapter,
115
133
  normalizePair as normalizeRangeSliderValues,
116
134
  panelMachine,
117
135
  popoverMachine,
136
+ queryAttrs,
118
137
  rangeSliderMachine,
138
+ shallowEqual,
119
139
  sliderMachine,
120
140
  snap2 as snapRangeSliderValue,
121
141
  snap as snapSliderValue,
122
142
  stickyBarMachine,
123
143
  tabsMachine,
124
144
  toastMachine,
125
- toggleMachine
145
+ toggleMachine,
146
+ unstorableField,
147
+ webStorageAdapter
126
148
  };
@@ -61,13 +61,13 @@ declare const appNavMachine: xstate.StateMachine<AppNavContext, {
61
61
  type: "RESET";
62
62
  section: string;
63
63
  }, {}, never, {
64
- type: "pop";
64
+ type: "replace";
65
65
  params: xstate.NonReducibleUnknown;
66
66
  } | {
67
- type: "push";
67
+ type: "pop";
68
68
  params: xstate.NonReducibleUnknown;
69
69
  } | {
70
- type: "replace";
70
+ type: "push";
71
71
  params: xstate.NonReducibleUnknown;
72
72
  } | {
73
73
  type: "clear";