@echojs-ecosystem/store 0.1.0
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/README.md +69 -0
- package/dist/index.d.ts +71 -0
- package/dist/index.js +257 -0
- package/dist/index.js.map +1 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# @echojs-ecosystem/store
|
|
4
|
+
|
|
5
|
+
**Structured client state on signals — actions, selectors, and extensions.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@echojs-ecosystem/store)
|
|
8
|
+
[](https://echojs.dev/docs/packages/store)
|
|
9
|
+
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Minimal store layer on [`@echojs-ecosystem/reactivity`](https://www.npmjs.com/package/@echojs-ecosystem/reactivity). Framework-agnostic, SSR-friendly — no `localStorage`, no UI bindings. Persistence lives in [`@echojs-ecosystem/persist`](https://www.npmjs.com/package/@echojs-ecosystem/persist).
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **`createStore`** — `value`, `set`, `update`, `reset`, `subscribe`
|
|
19
|
+
- **Events** — `changed`, `reseted` watchers
|
|
20
|
+
- **`.extend()`** — compose actions, debug, readonly, persist adapters
|
|
21
|
+
- **`select` / `combine`** — derived readonly stores
|
|
22
|
+
- **`withActions`** — typed action factories
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @echojs-ecosystem/store @echojs-ecosystem/reactivity
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick start
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { createStore, withActions, select } from "@echojs-ecosystem/store";
|
|
34
|
+
|
|
35
|
+
const counter = createStore(0, { name: "counter" }).extend(
|
|
36
|
+
withActions({
|
|
37
|
+
increment: (store) => () => store.update((v) => v + 1),
|
|
38
|
+
add: (store) => (n: number) => store.update((v) => v + n),
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
counter.increment();
|
|
43
|
+
counter.subscribe((value, prev) => console.log({ value, prev }));
|
|
44
|
+
|
|
45
|
+
const $isPositive = select(counter, (v) => v > 0);
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API
|
|
49
|
+
|
|
50
|
+
| Export | Description |
|
|
51
|
+
|--------|-------------|
|
|
52
|
+
| `createStore` | Mutable store |
|
|
53
|
+
| `select` | Derived readonly store |
|
|
54
|
+
| `combine` | Merge multiple stores |
|
|
55
|
+
| `withActions` | Action extension |
|
|
56
|
+
| `withDebug` / `withReadonly` / `readonly` | Debug & immutability |
|
|
57
|
+
| `batch` | Re-export from `@echojs-ecosystem/reactivity` |
|
|
58
|
+
|
|
59
|
+
## Persistence
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { withLocalStorage } from "@echojs-ecosystem/persist";
|
|
63
|
+
|
|
64
|
+
const theme = createStore("dark").extend(withLocalStorage({ key: "theme", version: 1 }));
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Documentation
|
|
68
|
+
|
|
69
|
+
[echojs.dev/docs/packages/store](https://echojs.dev/docs/packages/store)
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { Signal, ReadonlySignal } from '@echojs-ecosystem/reactivity';
|
|
2
|
+
export { batch } from '@echojs-ecosystem/reactivity';
|
|
3
|
+
|
|
4
|
+
type EqualsFn<T> = (a: T, b: T) => boolean;
|
|
5
|
+
type EqualsOption<T> = false | EqualsFn<T>;
|
|
6
|
+
type StoreOptions<State> = {
|
|
7
|
+
name?: string;
|
|
8
|
+
equals?: EqualsOption<State>;
|
|
9
|
+
};
|
|
10
|
+
type StoreEventPayload<State> = {
|
|
11
|
+
value: State;
|
|
12
|
+
prevValue: State;
|
|
13
|
+
};
|
|
14
|
+
type StoreEvent<Payload> = {
|
|
15
|
+
watch(listener: (payload: Payload) => void): () => void;
|
|
16
|
+
emit(payload: Payload): void;
|
|
17
|
+
};
|
|
18
|
+
type StoreExtension<State, Result extends object> = (store: Store<State>) => Result;
|
|
19
|
+
type ExtensionResult<Ext> = Ext extends StoreExtension<any, infer Result> ? Result : never;
|
|
20
|
+
type Store<State> = {
|
|
21
|
+
kind: "store";
|
|
22
|
+
name?: string;
|
|
23
|
+
value(): State;
|
|
24
|
+
set(value: State): void;
|
|
25
|
+
update(updater: (value: State) => State): void;
|
|
26
|
+
reset(): void;
|
|
27
|
+
$value: Signal<State>;
|
|
28
|
+
changed: StoreEvent<StoreEventPayload<State>>;
|
|
29
|
+
reseted: StoreEvent<StoreEventPayload<State>>;
|
|
30
|
+
subscribe(listener: (value: State, prevValue: State) => void): () => void;
|
|
31
|
+
extend<Ext extends StoreExtension<State, any>>(extension: Ext): Store<State> & ExtensionResult<Ext>;
|
|
32
|
+
};
|
|
33
|
+
type ReadonlyStore<State> = {
|
|
34
|
+
kind: "readonly-store";
|
|
35
|
+
name?: string;
|
|
36
|
+
value(): State;
|
|
37
|
+
$value: ReadonlySignal<State>;
|
|
38
|
+
changed: StoreEvent<StoreEventPayload<State>>;
|
|
39
|
+
subscribe(listener: (value: State, prevValue: State) => void): () => void;
|
|
40
|
+
};
|
|
41
|
+
type SourceValues<Sources> = {
|
|
42
|
+
[K in keyof Sources]: Sources[K] extends Store<infer V> ? V : Sources[K] extends ReadonlyStore<infer V> ? V : never;
|
|
43
|
+
};
|
|
44
|
+
type CombineOptions<Result> = {
|
|
45
|
+
name?: string;
|
|
46
|
+
equals?: EqualsOption<Result>;
|
|
47
|
+
};
|
|
48
|
+
type SelectOptions<Selected> = {
|
|
49
|
+
name?: string;
|
|
50
|
+
equals?: EqualsOption<Selected>;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
declare const createStore: <State>(initialState: State, options?: StoreOptions<State>) => Store<State>;
|
|
54
|
+
|
|
55
|
+
declare const select: <State, Selected>(store: Store<State>, selector: (state: State) => Selected, options?: SelectOptions<Selected>) => ReadonlyStore<Selected>;
|
|
56
|
+
|
|
57
|
+
type SourceStore = Store<unknown> | ReadonlyStore<unknown>;
|
|
58
|
+
declare const combine: <Sources extends Record<string, SourceStore>, Result>(sources: Sources, combiner: (values: SourceValues<Sources>) => Result, options?: CombineOptions<Result>) => ReadonlyStore<Result>;
|
|
59
|
+
|
|
60
|
+
declare const withActions: <State, const Actions extends Record<string, (store: Store<State>) => unknown>>(actions: Actions) => StoreExtension<State, { [K in keyof Actions]: ReturnType<Actions[K]>; }>;
|
|
61
|
+
|
|
62
|
+
type WithDebugResult = {
|
|
63
|
+
debugName?: string;
|
|
64
|
+
};
|
|
65
|
+
declare const withDebug: <State>() => StoreExtension<State, WithDebugResult>;
|
|
66
|
+
|
|
67
|
+
declare const withReadonly: <State>() => StoreExtension<State, Record<string, never>>;
|
|
68
|
+
|
|
69
|
+
declare const readonly: <State>(store: Store<State>) => ReadonlyStore<State>;
|
|
70
|
+
|
|
71
|
+
export { type CombineOptions, type EqualsOption, type ExtensionResult, type ReadonlyStore, type SelectOptions, type SourceValues, type Store, type StoreEvent, type StoreEventPayload, type StoreExtension, type StoreOptions, combine, createStore, readonly, select, withActions, withDebug, withReadonly };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { signal, computed, effect } from '@echojs-ecosystem/reactivity';
|
|
2
|
+
export { batch } from '@echojs-ecosystem/reactivity';
|
|
3
|
+
|
|
4
|
+
// src/index.ts
|
|
5
|
+
var applyStoreUpdate = (internals, $value, next) => {
|
|
6
|
+
const prev = $value.peek();
|
|
7
|
+
if (internals.equals(prev, next)) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
$value.set(next);
|
|
11
|
+
internals.notifyChanged(next, prev);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// src/assert-extension-keys.ts
|
|
15
|
+
var STORE_RESERVED_KEYS = /* @__PURE__ */ new Set([
|
|
16
|
+
"kind",
|
|
17
|
+
"name",
|
|
18
|
+
"value",
|
|
19
|
+
"set",
|
|
20
|
+
"update",
|
|
21
|
+
"reset",
|
|
22
|
+
"$value",
|
|
23
|
+
"changed",
|
|
24
|
+
"reseted",
|
|
25
|
+
"subscribe",
|
|
26
|
+
"extend"
|
|
27
|
+
]);
|
|
28
|
+
var assertExtensionKeys = (store, extension) => {
|
|
29
|
+
for (const key of Object.keys(extension)) {
|
|
30
|
+
if (STORE_RESERVED_KEYS.has(key) || key in store) {
|
|
31
|
+
throw new Error(`Store extension conflict: key "${key}" already exists`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// src/attach-extend.ts
|
|
37
|
+
var attachExtend = (store) => {
|
|
38
|
+
store.extend = (extension) => {
|
|
39
|
+
const extensionResult = extension(store);
|
|
40
|
+
assertExtensionKeys(store, extensionResult);
|
|
41
|
+
Object.assign(store, extensionResult);
|
|
42
|
+
return store;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// src/create-store-event.ts
|
|
47
|
+
var createStoreEvent = () => {
|
|
48
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
49
|
+
return {
|
|
50
|
+
watch(listener) {
|
|
51
|
+
listeners.add(listener);
|
|
52
|
+
return () => {
|
|
53
|
+
listeners.delete(listener);
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
emit(payload) {
|
|
57
|
+
for (const listener of listeners) {
|
|
58
|
+
listener(payload);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// src/resolve-equals.ts
|
|
65
|
+
var resolveEquals = (equals) => {
|
|
66
|
+
if (equals === false) {
|
|
67
|
+
return () => false;
|
|
68
|
+
}
|
|
69
|
+
if (equals) {
|
|
70
|
+
return equals;
|
|
71
|
+
}
|
|
72
|
+
return Object.is;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/create-store-internals.ts
|
|
76
|
+
var createStoreInternals = (initialState, equalsOption) => {
|
|
77
|
+
const equals = resolveEquals(equalsOption);
|
|
78
|
+
const changed = createStoreEvent();
|
|
79
|
+
const reseted = createStoreEvent();
|
|
80
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
81
|
+
const notifyChanged = (value, prevValue) => {
|
|
82
|
+
changed.emit({ value, prevValue });
|
|
83
|
+
for (const listener of listeners) {
|
|
84
|
+
listener(value, prevValue);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
return {
|
|
88
|
+
initialState,
|
|
89
|
+
equals,
|
|
90
|
+
changed,
|
|
91
|
+
reseted,
|
|
92
|
+
listeners,
|
|
93
|
+
notifyChanged
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
// src/create-store.ts
|
|
98
|
+
var createStore = (initialState, options) => {
|
|
99
|
+
const internals = createStoreInternals(initialState, options?.equals);
|
|
100
|
+
const $value = signal(initialState);
|
|
101
|
+
const store = {
|
|
102
|
+
kind: "store",
|
|
103
|
+
name: options?.name,
|
|
104
|
+
$value,
|
|
105
|
+
changed: internals.changed,
|
|
106
|
+
reseted: internals.reseted,
|
|
107
|
+
value() {
|
|
108
|
+
return $value.value();
|
|
109
|
+
},
|
|
110
|
+
set(value) {
|
|
111
|
+
applyStoreUpdate(internals, $value, value);
|
|
112
|
+
},
|
|
113
|
+
update(updater) {
|
|
114
|
+
const prev = $value.peek();
|
|
115
|
+
applyStoreUpdate(internals, $value, updater(prev));
|
|
116
|
+
},
|
|
117
|
+
reset() {
|
|
118
|
+
const prev = $value.peek();
|
|
119
|
+
const next = internals.initialState;
|
|
120
|
+
internals.reseted.emit({ value: next, prevValue: prev });
|
|
121
|
+
applyStoreUpdate(internals, $value, next);
|
|
122
|
+
},
|
|
123
|
+
subscribe(listener) {
|
|
124
|
+
internals.listeners.add(listener);
|
|
125
|
+
return () => {
|
|
126
|
+
internals.listeners.delete(listener);
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
extend() {
|
|
130
|
+
throw new Error("extend() is not initialized");
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
attachExtend(store);
|
|
134
|
+
return store;
|
|
135
|
+
};
|
|
136
|
+
var createDerivedReadonlyStore = (read, options) => {
|
|
137
|
+
const equals = resolveEquals(options?.equals);
|
|
138
|
+
const $value = computed(read);
|
|
139
|
+
const changed = createStoreEvent();
|
|
140
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
141
|
+
let prev = read();
|
|
142
|
+
effect(() => {
|
|
143
|
+
const next = read();
|
|
144
|
+
if (equals(prev, next)) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const previous = prev;
|
|
148
|
+
prev = next;
|
|
149
|
+
changed.emit({ value: next, prevValue: previous });
|
|
150
|
+
for (const listener of listeners) {
|
|
151
|
+
listener(next, previous);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
kind: "readonly-store",
|
|
156
|
+
name: options?.name,
|
|
157
|
+
$value,
|
|
158
|
+
changed,
|
|
159
|
+
value() {
|
|
160
|
+
return $value.value();
|
|
161
|
+
},
|
|
162
|
+
subscribe(listener) {
|
|
163
|
+
listeners.add(listener);
|
|
164
|
+
return () => {
|
|
165
|
+
listeners.delete(listener);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/select.ts
|
|
172
|
+
var select = (store, selector, options) => {
|
|
173
|
+
return createDerivedReadonlyStore(() => selector(store.value()), {
|
|
174
|
+
name: options?.name,
|
|
175
|
+
equals: options?.equals
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
// src/combine.ts
|
|
180
|
+
var combine = (sources, combiner, options) => {
|
|
181
|
+
const entries = Object.entries(sources);
|
|
182
|
+
const readSources = () => {
|
|
183
|
+
const values = {};
|
|
184
|
+
for (const [key, source] of entries) {
|
|
185
|
+
values[key] = source.value();
|
|
186
|
+
}
|
|
187
|
+
return values;
|
|
188
|
+
};
|
|
189
|
+
return createDerivedReadonlyStore(() => combiner(readSources()), {
|
|
190
|
+
name: options?.name,
|
|
191
|
+
equals: options?.equals
|
|
192
|
+
});
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// src/with-actions.ts
|
|
196
|
+
var withActions = (actions) => {
|
|
197
|
+
return (store) => {
|
|
198
|
+
const result = {};
|
|
199
|
+
for (const key of Object.keys(actions)) {
|
|
200
|
+
const factory = actions[key];
|
|
201
|
+
if (!factory) continue;
|
|
202
|
+
result[key] = factory(store);
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// src/with-debug.ts
|
|
209
|
+
var withDebug = () => {
|
|
210
|
+
return (store) => {
|
|
211
|
+
const debugName = store.name ?? "anonymous";
|
|
212
|
+
store.changed.watch(({ value, prevValue }) => {
|
|
213
|
+
if (typeof console !== "undefined" && typeof console.log === "function") {
|
|
214
|
+
console.log(`[store:${debugName}]`, { prevValue, value });
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
return { debugName };
|
|
218
|
+
};
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// src/with-readonly.ts
|
|
222
|
+
var withReadonly = () => {
|
|
223
|
+
return (store) => {
|
|
224
|
+
const mutating = ["set", "update", "reset"];
|
|
225
|
+
for (const key of mutating) {
|
|
226
|
+
if (key in store) {
|
|
227
|
+
store[key] = () => {
|
|
228
|
+
throw new Error(`Store is readonly: cannot call ${key}()`);
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
store.kind = "readonly-store";
|
|
233
|
+
return {};
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// src/to-readonly-store.ts
|
|
238
|
+
var toReadonlyStore = (store) => {
|
|
239
|
+
const readonlyStore = {
|
|
240
|
+
kind: "readonly-store",
|
|
241
|
+
name: store.name,
|
|
242
|
+
value: () => store.value(),
|
|
243
|
+
$value: store.$value,
|
|
244
|
+
changed: store.changed,
|
|
245
|
+
subscribe: (listener) => store.subscribe(listener)
|
|
246
|
+
};
|
|
247
|
+
return readonlyStore;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// src/readonly.ts
|
|
251
|
+
var readonly = (store) => {
|
|
252
|
+
return toReadonlyStore(store);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export { combine, createStore, readonly, select, withActions, withDebug, withReadonly };
|
|
256
|
+
//# sourceMappingURL=index.js.map
|
|
257
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/apply-store-update.ts","../src/assert-extension-keys.ts","../src/attach-extend.ts","../src/create-store-event.ts","../src/resolve-equals.ts","../src/create-store-internals.ts","../src/create-store.ts","../src/create-derived-readonly-store.ts","../src/select.ts","../src/combine.ts","../src/with-actions.ts","../src/with-debug.ts","../src/with-readonly.ts","../src/to-readonly-store.ts","../src/readonly.ts"],"names":["signal"],"mappings":"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","file":"index.js","sourcesContent":["import { signal } from \"@echojs-ecosystem/reactivity\";\n\nimport type { StoreInternals } from \"./create-store-internals\";\n\nexport const applyStoreUpdate = <State>(\n internals: StoreInternals<State>,\n $value: ReturnType<typeof signal<State>>,\n next: State,\n): void => {\n const prev = $value.peek() as State;\n if (internals.equals(prev, next)) {\n return;\n }\n $value.set(next);\n internals.notifyChanged(next, prev);\n};\n","const STORE_RESERVED_KEYS = new Set([\n \"kind\",\n \"name\",\n \"value\",\n \"set\",\n \"update\",\n \"reset\",\n \"$value\",\n \"changed\",\n \"reseted\",\n \"subscribe\",\n \"extend\",\n]);\n\nexport const assertExtensionKeys = (store: object, extension: object): void => {\n for (const key of Object.keys(extension)) {\n if (STORE_RESERVED_KEYS.has(key) || key in store) {\n throw new Error(`Store extension conflict: key \"${key}\" already exists`);\n }\n }\n};\n","import { assertExtensionKeys } from \"./assert-extension-keys\";\nimport type { ExtensionResult, Store, StoreExtension } from \"./types\";\n\nexport const attachExtend = <State>(store: Store<State>): void => {\n store.extend = <Ext extends StoreExtension<State, any>>(extension: Ext) => {\n const extensionResult = extension(store);\n assertExtensionKeys(store, extensionResult);\n Object.assign(store, extensionResult);\n return store as Store<State> & ExtensionResult<Ext>;\n };\n};\n","import type { StoreEvent } from \"./types\";\n\nexport const createStoreEvent = <Payload>(): StoreEvent<Payload> => {\n const listeners = new Set<(payload: Payload) => void>();\n\n return {\n watch(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n emit(payload) {\n for (const listener of listeners) {\n listener(payload);\n }\n },\n };\n};\n","import type { EqualsFn, EqualsOption } from \"./types\";\n\nexport const resolveEquals = <T>(equals?: EqualsOption<T>): EqualsFn<T> => {\n if (equals === false) {\n return () => false;\n }\n if (equals) {\n return equals;\n }\n return Object.is;\n};\n","import { createStoreEvent } from \"./create-store-event\";\nimport { resolveEquals } from \"./resolve-equals\";\nimport type { EqualsFn, EqualsOption, StoreEvent, StoreEventPayload } from \"./types\";\n\nexport type StoreInternals<State> = {\n initialState: State;\n equals: EqualsFn<State>;\n changed: StoreEvent<StoreEventPayload<State>>;\n reseted: StoreEvent<StoreEventPayload<State>>;\n listeners: Set<(value: State, prevValue: State) => void>;\n notifyChanged(value: State, prevValue: State): void;\n};\n\nexport const createStoreInternals = <State>(\n initialState: State,\n equalsOption?: EqualsOption<State>,\n): StoreInternals<State> => {\n const equals = resolveEquals(equalsOption);\n const changed = createStoreEvent<StoreEventPayload<State>>();\n const reseted = createStoreEvent<StoreEventPayload<State>>();\n const listeners = new Set<(value: State, prevValue: State) => void>();\n\n const notifyChanged = (value: State, prevValue: State) => {\n changed.emit({ value, prevValue });\n for (const listener of listeners) {\n listener(value, prevValue);\n }\n };\n\n return {\n initialState,\n equals,\n changed,\n reseted,\n listeners,\n notifyChanged,\n };\n};\n","import { signal } from \"@echojs-ecosystem/reactivity\";\n\nimport { applyStoreUpdate } from \"./apply-store-update\";\nimport { attachExtend } from \"./attach-extend\";\nimport { createStoreInternals } from \"./create-store-internals\";\nimport type { Store, StoreOptions } from \"./types\";\n\nexport const createStore = <State>(\n initialState: State,\n options?: StoreOptions<State>,\n): Store<State> => {\n const internals = createStoreInternals(initialState, options?.equals);\n const $value = signal(initialState);\n\n const store: Store<State> = {\n kind: \"store\",\n name: options?.name,\n $value,\n changed: internals.changed,\n reseted: internals.reseted,\n\n value() {\n return $value.value() as State;\n },\n\n set(value) {\n applyStoreUpdate(internals, $value, value);\n },\n\n update(updater) {\n const prev = $value.peek() as State;\n applyStoreUpdate(internals, $value, updater(prev));\n },\n\n reset() {\n const prev = $value.peek() as State;\n const next = internals.initialState;\n internals.reseted.emit({ value: next, prevValue: prev });\n applyStoreUpdate(internals, $value, next);\n },\n\n subscribe(listener) {\n internals.listeners.add(listener);\n return () => {\n internals.listeners.delete(listener);\n };\n },\n\n extend() {\n throw new Error(\"extend() is not initialized\");\n },\n };\n\n attachExtend(store);\n return store;\n};\n","import { computed, effect } from \"@echojs-ecosystem/reactivity\";\n\nimport { createStoreEvent } from \"./create-store-event\";\nimport { resolveEquals } from \"./resolve-equals\";\nimport type { EqualsOption, ReadonlyStore, StoreEventPayload } from \"./types\";\n\nexport const createDerivedReadonlyStore = <Selected>(\n read: () => Selected,\n options?: {\n name?: string;\n equals?: EqualsOption<Selected>;\n },\n): ReadonlyStore<Selected> => {\n const equals = resolveEquals(options?.equals);\n const $value = computed(read);\n const changed = createStoreEvent<StoreEventPayload<Selected>>();\n const listeners = new Set<(value: Selected, prevValue: Selected) => void>();\n\n let prev = read();\n\n effect(() => {\n const next = read();\n if (equals(prev, next)) {\n return;\n }\n const previous = prev;\n prev = next;\n changed.emit({ value: next, prevValue: previous });\n for (const listener of listeners) {\n listener(next, previous);\n }\n });\n\n return {\n kind: \"readonly-store\",\n name: options?.name,\n $value,\n changed,\n value() {\n return $value.value() as Selected;\n },\n subscribe(listener) {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n },\n };\n};\n","import { createDerivedReadonlyStore } from \"./create-derived-readonly-store\";\nimport type { ReadonlyStore, SelectOptions, Store } from \"./types\";\n\nexport const select = <State, Selected>(\n store: Store<State>,\n selector: (state: State) => Selected,\n options?: SelectOptions<Selected>,\n): ReadonlyStore<Selected> => {\n return createDerivedReadonlyStore(() => selector(store.value()), {\n name: options?.name,\n equals: options?.equals,\n });\n};\n","import { createDerivedReadonlyStore } from \"./create-derived-readonly-store\";\nimport type { CombineOptions, ReadonlyStore, SourceValues, Store } from \"./types\";\n\ntype SourceStore = Store<unknown> | ReadonlyStore<unknown>;\n\nexport const combine = <Sources extends Record<string, SourceStore>, Result>(\n sources: Sources,\n combiner: (values: SourceValues<Sources>) => Result,\n options?: CombineOptions<Result>,\n): ReadonlyStore<Result> => {\n const entries = Object.entries(sources) as [keyof Sources, SourceStore][];\n\n const readSources = (): SourceValues<Sources> => {\n const values = {} as SourceValues<Sources>;\n for (const [key, source] of entries) {\n values[key] = source.value() as SourceValues<Sources>[typeof key];\n }\n return values;\n };\n\n return createDerivedReadonlyStore(() => combiner(readSources()), {\n name: options?.name,\n equals: options?.equals,\n });\n};\n","import type { Store, StoreExtension } from \"./types\";\n\nexport const withActions = <\n State,\n const Actions extends Record<string, (store: Store<State>) => unknown>,\n>(\n actions: Actions,\n): StoreExtension<\n State,\n { [K in keyof Actions]: ReturnType<Actions[K]> }\n> => {\n return (store) => {\n const result = {} as { [K in keyof Actions]: ReturnType<Actions[K]> };\n for (const key of Object.keys(actions) as (keyof Actions)[]) {\n const factory = actions[key];\n if (!factory) continue;\n result[key] = factory(store) as ReturnType<Actions[typeof key]>;\n }\n return result;\n };\n};\n","import type { StoreExtension } from \"./types\";\n\nexport type WithDebugResult = {\n debugName?: string;\n};\n\nexport const withDebug = <State>(): StoreExtension<State, WithDebugResult> => {\n return (store) => {\n const debugName = store.name ?? \"anonymous\";\n\n store.changed.watch(({ value, prevValue }) => {\n if (typeof console !== \"undefined\" && typeof console.log === \"function\") {\n console.log(`[store:${debugName}]`, { prevValue, value });\n }\n });\n\n return { debugName };\n };\n};\n","import type { StoreExtension } from \"./types\";\n\nexport const withReadonly = <State>(): StoreExtension<State, Record<string, never>> => {\n return (store) => {\n const mutating = [\"set\", \"update\", \"reset\"] as const;\n for (const key of mutating) {\n if (key in store) {\n (store as Record<string, unknown>)[key] = () => {\n throw new Error(`Store is readonly: cannot call ${key}()`);\n };\n }\n }\n (store as { kind: string }).kind = \"readonly-store\";\n return {};\n };\n};\n","import type { ReadonlyStore, Store } from \"./types\";\n\nexport const toReadonlyStore = <State>(store: Store<State>): ReadonlyStore<State> => {\n const readonlyStore: ReadonlyStore<State> = {\n kind: \"readonly-store\",\n name: store.name,\n value: () => store.value(),\n $value: store.$value,\n changed: store.changed,\n subscribe: (listener) => store.subscribe(listener),\n };\n return readonlyStore;\n};\n","import { toReadonlyStore } from \"./to-readonly-store\";\nimport type { ReadonlyStore, Store } from \"./types\";\n\nexport const readonly = <State>(store: Store<State>): ReadonlyStore<State> => {\n return toReadonlyStore(store);\n};\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@echojs-ecosystem/store",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist"
|
|
6
|
+
],
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": false,
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"build": "tsup",
|
|
18
|
+
"check-types": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.typing.json --noEmit",
|
|
19
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.typing.json --noEmit",
|
|
20
|
+
"test:types": "tsc -p tsconfig.typing.json --noEmit",
|
|
21
|
+
"lint": "oxlint .",
|
|
22
|
+
"lint:fix": "oxlint . --fix",
|
|
23
|
+
"format": "oxfmt --check .",
|
|
24
|
+
"format:fix": "oxfmt .",
|
|
25
|
+
"test": "vitest run",
|
|
26
|
+
"test:watch": "vitest",
|
|
27
|
+
"test:coverage": "vitest run --coverage"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@echojs-ecosystem/reactivity": "workspace:*"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@echojs-ecosystem/oxc-config": "workspace:*",
|
|
34
|
+
"typescript": "5.9.2",
|
|
35
|
+
"vitest": "^4.1.4"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public"
|
|
39
|
+
},
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/echojs-ecosystem/echojs-core.git",
|
|
43
|
+
"directory": "packages/store"
|
|
44
|
+
}
|
|
45
|
+
}
|