@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 +215 -0
- package/dist/index.d.ts +28 -0
- package/dist/index.js +77 -0
- package/package.json +33 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|