@ilha/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 ADDED
@@ -0,0 +1,215 @@
1
+ # `@ilha/store`
2
+
3
+ A zustand-shaped reactive store for [Ilha](https://github.com/ilhajs/ilha) islands. Backed by [alien-signals](https://github.com/stackblitz/alien-signals) — the same engine that powers `ilha` core state — for shared global state that lives outside any single island.
4
+
5
+ ---
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ bun add @ilha/store
11
+ ```
12
+
13
+ ---
14
+
15
+ ## Quick Start
16
+
17
+ ```ts
18
+ import { createStore } from "@ilha/store";
19
+
20
+ const store = createStore({ count: 0 });
21
+
22
+ store.setState({ count: 1 });
23
+ store.getState(); // → { count: 1 }
24
+ ```
25
+
26
+ ---
27
+
28
+ ## When to Use
29
+
30
+ `ilha` state is **island-local** — signals are scoped to a single component instance. Use `@ilha/store` when you need state that is:
31
+
32
+ - **Shared across multiple islands** — e.g. a cart, auth session, or theme
33
+ - **Updated from outside an island** — e.g. from a WebSocket handler or a global event bus
34
+ - **Persisted or derived globally** — e.g. synced to `localStorage` via a `subscribe` listener
35
+
36
+ For state that only one island reads and writes, prefer `ilha`'s built-in `.state()`.
37
+
38
+ ---
39
+
40
+ ## API
41
+
42
+ ### `createStore(initialState, actions?)`
43
+
44
+ Creates a store. Optionally accepts an actions creator for encapsulating state mutations.
45
+
46
+ ```ts
47
+ // State only
48
+ const store = createStore({ count: 0, name: "Ada" });
49
+
50
+ // State + actions
51
+ const store = createStore({ count: 0 }, (set, get) => ({
52
+ increment() {
53
+ set({ count: get().count + 1 });
54
+ },
55
+ reset() {
56
+ set({ count: 0 });
57
+ },
58
+ }));
59
+
60
+ store.getState().increment();
61
+ store.getState().count; // → 1
62
+ ```
63
+
64
+ The actions creator receives:
65
+
66
+ | Argument | Description |
67
+ | ----------------------- | ---------------------------------------------------- |
68
+ | `set(patch \| updater)` | Merge a partial patch or apply an updater function |
69
+ | `get()` | Read the current live state (includes other actions) |
70
+ | `getInitialState()` | Read the frozen initial state snapshot |
71
+
72
+ ---
73
+
74
+ ### `store.setState(update)`
75
+
76
+ Merges a partial state update. Accepts a plain object or an updater function.
77
+
78
+ ```ts
79
+ store.setState({ count: 5 });
80
+ store.setState((s) => ({ count: s.count + 1 }));
81
+ ```
82
+
83
+ ---
84
+
85
+ ### `store.getState()`
86
+
87
+ Returns the current state snapshot.
88
+
89
+ ```ts
90
+ store.getState(); // → { count: 5 }
91
+ ```
92
+
93
+ ---
94
+
95
+ ### `store.getInitialState()`
96
+
97
+ Returns the frozen initial state as it was at construction time.
98
+
99
+ ```ts
100
+ store.getInitialState(); // → { count: 0 }
101
+ ```
102
+
103
+ ---
104
+
105
+ ### `store.subscribe(listener)`
106
+
107
+ Subscribes to all state changes. The listener receives the next and previous state. Returns an unsubscribe function.
108
+
109
+ ```ts
110
+ const unsub = store.subscribe((state, prev) => {
111
+ console.log(state.count, prev.count);
112
+ });
113
+
114
+ unsub(); // stop listening
115
+ ```
116
+
117
+ ### `store.subscribe(selector, listener)` — slice subscription
118
+
119
+ Subscribes to a derived slice. The listener only fires when the selected value changes (compared with `Object.is`).
120
+
121
+ ```ts
122
+ const unsub = store.subscribe(
123
+ (s) => s.count,
124
+ (count, prev) => console.log("count changed:", prev, "→", count),
125
+ );
126
+ ```
127
+
128
+ ---
129
+
130
+ ### `store.bind(el, render)`
131
+
132
+ Reactively renders a store-driven HTML string into a DOM element whenever state changes. The render function may return a plain string or an `html\`\`` tagged template.
133
+
134
+ ```ts
135
+ import { html } from "@ilha/store";
136
+
137
+ const unsub = store.bind(
138
+ document.getElementById("counter")!,
139
+ (state) => html`<p>Count: ${state.count}</p>`,
140
+ );
141
+
142
+ unsub(); // detach
143
+ ```
144
+
145
+ ### `store.bind(el, selector, render)` — slice bind
146
+
147
+ Only re-renders when the selected slice changes.
148
+
149
+ ```ts
150
+ store.bind(
151
+ document.getElementById("badge")!,
152
+ (s) => s.count,
153
+ (count) => html`<span>${count}</span>`,
154
+ );
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Usage with Ilha Islands
160
+
161
+ The most common pattern is reading the store inside an island's `.effect()` and calling `store.subscribe()` to drive reactive re-renders:
162
+
163
+ ```ts
164
+ import { createStore, html } from "@ilha/store";
165
+ import ilha from "ilha";
166
+
167
+ export const cartStore = createStore({ items: [] as string[] }, (set, get) => ({
168
+ add(item: string) {
169
+ set({ items: [...get().items, item] });
170
+ },
171
+ remove(item: string) {
172
+ set({ items: get().items.filter((i) => i !== item) });
173
+ },
174
+ }));
175
+
176
+ export const CartIsland = ilha
177
+ .state("items", cartStore.getState().items)
178
+ .effect(({ state }) => {
179
+ return cartStore.subscribe(
180
+ (s) => s.items,
181
+ (items) => state.items(items),
182
+ );
183
+ })
184
+ .render(
185
+ ({ state }) => html`
186
+ <ul>
187
+ ${state.items().map((item) => html`<li>${item}</li>`)}
188
+ </ul>
189
+ `,
190
+ );
191
+ ```
192
+
193
+ ---
194
+
195
+ ## TypeScript
196
+
197
+ Key exported types:
198
+
199
+ ```ts
200
+ import type {
201
+ StoreApi, // the store instance interface
202
+ SetState, // (patch | updater) => void
203
+ GetState, // () => T
204
+ Listener, // (state, prevState) => void
205
+ SliceListener, // (slice, prevSlice) => void
206
+ RenderResult, // string | RawHtml
207
+ Unsub, // () => void
208
+ } from "@ilha/store";
209
+ ```
210
+
211
+ ---
212
+
213
+ ## License
214
+
215
+ MIT
@@ -0,0 +1,28 @@
1
+ import { effectScope } from "alien-signals";
2
+ import { RawHtml } from "ilha";
3
+
4
+ //#region src/index.d.ts
5
+ interface SetState<T> {
6
+ (update: Partial<T>): void;
7
+ (updater: (state: T) => Partial<T>): void;
8
+ }
9
+ type GetState<T> = () => T;
10
+ type Listener<T> = (state: T, prevState: T) => void;
11
+ type SliceListener<_T, S> = (slice: S, prevSlice: S) => void;
12
+ type Unsub = () => void;
13
+ /** Accepted render output — either a plain string or an ilha RawHtml value. */
14
+ type RenderResult = string | RawHtml;
15
+ interface StoreApi<T extends object> {
16
+ setState(update: Partial<T> | ((state: T) => Partial<T>)): void;
17
+ getState(): T;
18
+ getInitialState(): T;
19
+ subscribe(listener: Listener<T>): Unsub;
20
+ subscribe<S>(selector: (state: T) => S, listener: SliceListener<T, S>): Unsub;
21
+ bind(el: Element, render: (state: T) => RenderResult): Unsub;
22
+ bind<S>(el: Element, selector: (state: T) => S, render: (slice: S) => RenderResult): Unsub;
23
+ }
24
+ type ActionsCreator<TState extends object, TActions extends object> = (set: SetState<TState>, get: GetState<any>, getInitialState: () => TState) => TActions;
25
+ declare function createStore<TState extends object>(initialState: TState): StoreApi<TState>;
26
+ declare function createStore<TState extends object, TActions extends object>(initialState: TState, actions: ActionsCreator<TState, TActions>): StoreApi<TState & TActions>;
27
+ //#endregion
28
+ export { GetState, Listener, RenderResult, SetState, SliceListener, StoreApi, Unsub, createStore, effectScope };
package/dist/index.js ADDED
@@ -0,0 +1,77 @@
1
+ import { computed, effect, effectScope, signal } from "alien-signals";
2
+ //#region src/index.ts
3
+ function unwrap(result) {
4
+ if (typeof result === "string") return result;
5
+ return result.value;
6
+ }
7
+ function createStore(initialState, actionsCreator) {
8
+ const stateSignal = signal({});
9
+ function setState(update) {
10
+ const current = stateSignal();
11
+ const patch = typeof update === "function" ? update(current) : update;
12
+ stateSignal({
13
+ ...current,
14
+ ...patch
15
+ });
16
+ }
17
+ function getState() {
18
+ return stateSignal();
19
+ }
20
+ function subscribe(listenerOrSelector, maybeListener) {
21
+ if (maybeListener === void 0) {
22
+ const listener = listenerOrSelector;
23
+ let prev = stateSignal();
24
+ let first = true;
25
+ return effect(() => {
26
+ const current = stateSignal();
27
+ if (first) {
28
+ first = false;
29
+ return;
30
+ }
31
+ listener(current, prev);
32
+ prev = current;
33
+ });
34
+ }
35
+ const selector = listenerOrSelector;
36
+ const sliceComputed = computed(() => selector(stateSignal()));
37
+ let prevSlice = sliceComputed();
38
+ let first = true;
39
+ return effect(() => {
40
+ const currentSlice = sliceComputed();
41
+ if (first) {
42
+ first = false;
43
+ return;
44
+ }
45
+ if (!Object.is(currentSlice, prevSlice)) {
46
+ maybeListener(currentSlice, prevSlice);
47
+ prevSlice = currentSlice;
48
+ }
49
+ });
50
+ }
51
+ function bind(el, renderOrSelector, maybeRender) {
52
+ if (maybeRender === void 0) return effect(() => {
53
+ el.innerHTML = unwrap(renderOrSelector(stateSignal()));
54
+ });
55
+ const sliceComputed = computed(() => renderOrSelector(stateSignal()));
56
+ return effect(() => {
57
+ el.innerHTML = unwrap(maybeRender(sliceComputed()));
58
+ });
59
+ }
60
+ let resolvedInitialState;
61
+ const api = {
62
+ setState,
63
+ getState,
64
+ getInitialState: () => resolvedInitialState,
65
+ subscribe,
66
+ bind
67
+ };
68
+ const resolvedActions = actionsCreator ? actionsCreator(setState, getState, () => resolvedInitialState) : {};
69
+ resolvedInitialState = {
70
+ ...initialState,
71
+ ...resolvedActions
72
+ };
73
+ stateSignal(resolvedInitialState);
74
+ return api;
75
+ }
76
+ //#endregion
77
+ export { createStore, effectScope };
package/package.json ADDED
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "@ilha/store",
3
+ "version": "0.1.0",
4
+ "description": "Typed store.",
5
+ "license": "MIT",
6
+ "author": "Ryuz <ryuzer@proton.me>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/ilhajs/ilha.git"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "type": "module",
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ }
20
+ },
21
+ "scripts": {
22
+ "build": "tsc && tsdown",
23
+ "test": "bun test",
24
+ "cleanup": "rimraf dist node_modules"
25
+ },
26
+ "dependencies": {
27
+ "alien-signals": "3.1.2",
28
+ "ilha": "0.1.0"
29
+ },
30
+ "devDependencies": {
31
+ "zod": "^4.3.6"
32
+ }
33
+ }