orchestore 0.1.5 → 0.1.7
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 +1308 -17
- package/dist/index.cjs +14 -14
- package/dist/index.d.cts +62 -22
- package/dist/index.d.ts +62 -22
- package/dist/index.js +14 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,9 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
OrcheStore is currently under active development and is not yet ready for production use.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
### 📅 Future Release Plans
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
**Planned First Stable Release (v1.0.0):** **2026-06-30**
|
|
10
|
+
|
|
11
|
+
Development is ongoing, and progress is published regularly. New commits are pushed periodically to the public GitHub repository, and pre-release versions (v0.x.x) may be published to npm before the first stable release.
|
|
12
|
+
|
|
13
|
+
### ⚠️ Pre-release Notice
|
|
14
|
+
|
|
15
|
+
OrcheStore is currently in a pre-release phase.
|
|
16
|
+
|
|
17
|
+
* Not recommended for production use
|
|
18
|
+
* APIs may change before the first stable release
|
|
19
|
+
* Internal architecture and runtime behavior are still evolving
|
|
20
|
+
* Documentation and feature coverage are actively being expanded
|
|
21
|
+
|
|
22
|
+
Stay tuned for updates.
|
|
10
23
|
|
|
11
24
|
---
|
|
12
25
|
|
|
@@ -22,6 +35,56 @@ The goal is simple:
|
|
|
22
35
|
|
|
23
36
|
> ⚡ Spend less time wiring state management infrastructure and more time building application features.
|
|
24
37
|
|
|
38
|
+
## Table of Contents
|
|
39
|
+
|
|
40
|
+
- [Introduction](#orchestore)
|
|
41
|
+
- [Core Principles](#core-principles)
|
|
42
|
+
- [Why OrcheStore?](#why-orchestore)
|
|
43
|
+
- [Redux Toolkit Comparison](#redux-toolkit-comparison)
|
|
44
|
+
- [Architecture Overview](#architecture-overview)
|
|
45
|
+
|
|
46
|
+
- [Quick Example](#quick-example)
|
|
47
|
+
|
|
48
|
+
- [Slice Layers](#slice-layers)
|
|
49
|
+
- [name](#name)
|
|
50
|
+
- [state](#state)
|
|
51
|
+
- [Mutations](#mutations)
|
|
52
|
+
- [Methods](#methods)
|
|
53
|
+
- [Computed State (Planned)](#computed-state-planned)
|
|
54
|
+
- [Nested Slices](#nested-slices)
|
|
55
|
+
- [Reusing Slices](#reusing-slices)
|
|
56
|
+
- [Runtime Paths](#runtime-paths)
|
|
57
|
+
|
|
58
|
+
- [State Access & Subscriptions](#state-access--subscriptions)
|
|
59
|
+
- [State Snapshots](#state-snapshots)
|
|
60
|
+
- [State Subscriptions](#state-subscriptions)
|
|
61
|
+
- [Draft State](#draft-state)
|
|
62
|
+
|
|
63
|
+
- [Store Integration](#store-integration)
|
|
64
|
+
- [Creating the Store](#creating-the-store)
|
|
65
|
+
- [Store Provider](#store-provider)
|
|
66
|
+
- [Accessing Slices through Store](#accessing-slices-through-store)
|
|
67
|
+
- [Accessing Store from Slices](#accessing-store-from-slices)
|
|
68
|
+
- [Root Store Type Extension (Planned)](#root-store-type-extension-planned)
|
|
69
|
+
|
|
70
|
+
- [Lineage & Clones](#lineage--clones)
|
|
71
|
+
- [Automatic Cloning](#automatic-cloning)
|
|
72
|
+
- [Manual Cloning](#manual-cloning)
|
|
73
|
+
- [Inspecting a Lineage](#inspecting-a-lineage)
|
|
74
|
+
- [Definition Type Checking](#definition-type-checking)
|
|
75
|
+
|
|
76
|
+
- [Global Utilities](#global-utilities)
|
|
77
|
+
- [Accessing Global Utilities](#accessing-global-utilities)
|
|
78
|
+
- [Utilities Type Extension](#utilities-type-extension)
|
|
79
|
+
- [Providing Runtime Utilities](#providing-runtime-utilities)
|
|
80
|
+
- [Using Global Utilities in Slices](#using-global-utilities-in-slices)
|
|
81
|
+
|
|
82
|
+
- [TypeScript Inference](#typescript-inference)
|
|
83
|
+
|
|
84
|
+
- [Status](#status)
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
25
88
|
## Core Principles
|
|
26
89
|
|
|
27
90
|
- Simplify state management architecture
|
|
@@ -33,26 +96,1254 @@ The goal is simple:
|
|
|
33
96
|
- Maintain strong TypeScript inference
|
|
34
97
|
- Scale naturally through composition
|
|
35
98
|
|
|
99
|
+
## Why OrcheStore?
|
|
100
|
+
|
|
101
|
+
Redux Toolkit significantly improves the Redux developer experience, but many applications still require developers to coordinate logic across multiple concepts:
|
|
102
|
+
|
|
103
|
+
- reducers
|
|
104
|
+
- action creators
|
|
105
|
+
- thunks
|
|
106
|
+
- selectors
|
|
107
|
+
- middleware
|
|
108
|
+
- hooks
|
|
109
|
+
- utility functions
|
|
110
|
+
|
|
111
|
+
As applications grow, state management often becomes less about solving business problems and more about connecting infrastructure.
|
|
112
|
+
|
|
113
|
+
OrcheStore reduces that coordination overhead by exposing state management through unified slice modules.
|
|
114
|
+
|
|
115
|
+
A slice is more than a state container. It is a runtime module that can encapsulate:
|
|
116
|
+
|
|
117
|
+
- state
|
|
118
|
+
- computed state
|
|
119
|
+
- mutations
|
|
120
|
+
- methods
|
|
121
|
+
- selectors
|
|
122
|
+
- child slices
|
|
123
|
+
- shared utilities
|
|
124
|
+
|
|
125
|
+
This allows state and application logic to evolve together within the same domain boundary.
|
|
126
|
+
|
|
127
|
+
Many common Redux patterns are automated by default:
|
|
128
|
+
|
|
129
|
+
| Traditional Redux Pattern | OrcheStore |
|
|
130
|
+
| ----------------------------- | ----------------------------------------------- |
|
|
131
|
+
| Action creators | Direct callable mutations |
|
|
132
|
+
| Thunks | Built-in methods |
|
|
133
|
+
| Dispatch calls | Direct function calls |
|
|
134
|
+
| `PayloadAction` wrappers | Native function arguments |
|
|
135
|
+
| Cross-slice imports | Root store access |
|
|
136
|
+
| Shared service wiring | Global utilities |
|
|
137
|
+
| Manual state tree composition | Nested slices with automatic cloning & isolation |
|
|
138
|
+
| Complex type declarations | Automatic inference |
|
|
139
|
+
| Instance identity management | Lineage-based slice model (shared definition, isolated mounts) |
|
|
140
|
+
|
|
141
|
+
The result is a simpler architecture with fewer moving parts, less boilerplate, and a more direct development experience.
|
|
142
|
+
|
|
143
|
+
Developers can focus on application behavior rather than framework plumbing.
|
|
144
|
+
|
|
145
|
+
## Redux Toolkit Comparison
|
|
146
|
+
|
|
147
|
+
OrcheStore builds on top of Redux Toolkit while providing a higher-level API for organizing state and behavior.
|
|
148
|
+
|
|
149
|
+
| Feature | OrcheStore | Redux Toolkit |
|
|
150
|
+
| ------------------------------ | ---------- | ------------- |
|
|
151
|
+
| Multiple mutation arguments | ✅ | ❌ |
|
|
152
|
+
| Direct callable mutations | ✅ | ❌ |
|
|
153
|
+
| `PayloadAction` wrappers | ❌ | ✅ |
|
|
154
|
+
| Dispatch required | ❌ | ✅ |
|
|
155
|
+
| Built-in orchestration methods | ✅ | ❌ |
|
|
156
|
+
| Nested slice composition | ✅ (isolated context) | ⚠️ Manual (shared state) |
|
|
157
|
+
| Automatic path generation | ✅ | ⚠️ Manual |
|
|
158
|
+
| Global utilities | ✅ | ❌ |
|
|
159
|
+
| Unified slice API | ✅ | ❌ |
|
|
160
|
+
| Per-slice React hooks | ✅ | ❌ |
|
|
161
|
+
| Deep TypeScript inference | ✅ | ⚠️ Partial |
|
|
162
|
+
| Lineage & cloning model | ✅ | ❌ |
|
|
163
|
+
|
|
164
|
+
OrcheStore does not replace Redux Toolkit. Instead, it builds on top of it by automating common patterns and providing a more cohesive developer experience.
|
|
165
|
+
|
|
166
|
+
## Architecture Overview
|
|
167
|
+
|
|
168
|
+
| Layer | Responsibility |
|
|
169
|
+
| ----------- | ---------------------------------------- |
|
|
170
|
+
| `name` | Unique slice identifier |
|
|
171
|
+
| `path` | Hierarchical slice path |
|
|
172
|
+
| `state` | Slice data storage definition |
|
|
173
|
+
| `mutations` | Synchronous state transitions |
|
|
174
|
+
| `methods` | Orchestration and side effects |
|
|
175
|
+
| `computed` | Derived and computed state |
|
|
176
|
+
| `children` | Nested slice composition |
|
|
177
|
+
| `getState` | Imperative state access |
|
|
178
|
+
| `useSelect` | Reactive state subscriptions |
|
|
179
|
+
| `prototype` | Lineage, cloning, and instance utilities |
|
|
180
|
+
|
|
36
181
|
---
|
|
37
182
|
|
|
38
|
-
|
|
183
|
+
# Quick Example
|
|
39
184
|
|
|
40
|
-
|
|
185
|
+
**Comparing OrcheStore with Redux Toolkit**
|
|
41
186
|
|
|
42
|
-
|
|
187
|
+
## Slice Creation
|
|
43
188
|
|
|
44
|
-
|
|
45
|
-
- APIs may change before the first stable release
|
|
46
|
-
- Internal architecture and runtime behavior are still evolving
|
|
47
|
-
- Documentation and feature coverage are actively being expanded
|
|
189
|
+
✔️ OrcheStore centrelize unified options API.
|
|
48
190
|
|
|
49
|
-
|
|
191
|
+
```tsx
|
|
192
|
+
import { createSlice } from "orchestore";
|
|
50
193
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
| Core Architecture | 🚧 In Progress |
|
|
54
|
-
| Documentation | 🚧 In Progress |
|
|
55
|
-
| Public Preview | 🚧 Planned |
|
|
56
|
-
| First Stable Release | 📅 2026-06-30 |
|
|
194
|
+
export const counter = createSlice({
|
|
195
|
+
name: "counter",
|
|
57
196
|
|
|
58
|
-
|
|
197
|
+
state: {
|
|
198
|
+
value: 0,
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
mutations: {
|
|
202
|
+
// Direct arguments without PayloadAction wrappers
|
|
203
|
+
increment(state, amount: number) {
|
|
204
|
+
state.value += amount;
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
// Multiple typed arguments without payload objects
|
|
208
|
+
incrementLimited(state, amount: number, max = Infinity) {
|
|
209
|
+
state.value = Math.min(state.value + amount, max);
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
methods: {
|
|
214
|
+
// Reusable async method inside slice
|
|
215
|
+
sleep(delay: number) {
|
|
216
|
+
return new Promise((resolve) => setTimeout(resolve, delay));
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
// Orchestration layer with full slice access via `this`
|
|
220
|
+
async incrementAfter(amount: number, delay: number) {
|
|
221
|
+
await this.sleep(delay);
|
|
222
|
+
this.increment(amount);
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
⛔ Redux Toolkit splits logic between reducers, extra reducers, actions, and async workflows.
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
import { createSlice, createAsyncThunk, type PayloadAction } from "@reduxjs/toolkit";
|
|
232
|
+
|
|
233
|
+
export const counter = createSlice({
|
|
234
|
+
name: "counter",
|
|
235
|
+
|
|
236
|
+
initialState: {
|
|
237
|
+
value: 0,
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
reducers: {
|
|
241
|
+
// PayloadAction wrapper required
|
|
242
|
+
increment(state, action: PayloadAction<number>) {
|
|
243
|
+
state.value += action.payload;
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
// Multiple arguments must be wrapped into a payload object
|
|
247
|
+
incrementLimited(state, action: PayloadAction<{ amount: number; max?: number }>) {
|
|
248
|
+
state.value = Math.min(
|
|
249
|
+
state.value + action.payload.amount,
|
|
250
|
+
action.payload.max ?? Infinity
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
// Separate APIs for async workflows
|
|
257
|
+
export const incrementAfter = createAsyncThunk(
|
|
258
|
+
"counter/incrementAfter",
|
|
259
|
+
async (
|
|
260
|
+
{ amount, delay }: { amount: number; delay: number },
|
|
261
|
+
{ dispatch }
|
|
262
|
+
) => {
|
|
263
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
264
|
+
|
|
265
|
+
dispatch(counter.actions.increment(amount));
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## Slice Usage
|
|
271
|
+
|
|
272
|
+
✔️ OrcheStore exposes direct callable mutations and methods.
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
counter.increment(4);
|
|
276
|
+
counter.incrementLimited(1, 50);
|
|
277
|
+
counter.incrementAfter(5, 1000);
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
⛔ Redux Toolkit follows dispatch-based execution
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import { useDispatch } from "react-redux";
|
|
284
|
+
|
|
285
|
+
const dispatch = useDispatch();
|
|
286
|
+
|
|
287
|
+
dispatch(counter.actions.increment(4));
|
|
288
|
+
dispatch(counter.actions.incrementLimited({ amount: 1, max: 50 }));
|
|
289
|
+
dispatch(incrementAfter({ amount: 5, delay: 1000 }));
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Store Integration & Context Providing
|
|
293
|
+
|
|
294
|
+
✔️ OrcheStore exposes fully typed slice APIs directly.
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import { createStore } from "orchestore";
|
|
298
|
+
import { counter } from "./counterSlice";
|
|
299
|
+
|
|
300
|
+
export const store = createStore({
|
|
301
|
+
slices: {
|
|
302
|
+
counter,
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
```tsx
|
|
308
|
+
import { StoreProvider } from "orchestore";
|
|
309
|
+
import { store } from "./store";
|
|
310
|
+
|
|
311
|
+
export default function App() {
|
|
312
|
+
return (
|
|
313
|
+
<StoreProvider store={store}>
|
|
314
|
+
<CounterComponent />
|
|
315
|
+
</StoreProvider>
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
⛔ Redux Toolkit requires manual store configuration and type wiring.
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import { configureStore } from "@reduxjs/toolkit";
|
|
324
|
+
import { counter } from "./counterSlice";
|
|
325
|
+
import { useDispatch, useSelector } from "react-redux";
|
|
326
|
+
|
|
327
|
+
export const store = configureStore({
|
|
328
|
+
reducer: {
|
|
329
|
+
counter: counter.reducer,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
export type RootState = ReturnType<typeof store.getState>;
|
|
334
|
+
export type AppDispatch = typeof store.dispatch;
|
|
335
|
+
|
|
336
|
+
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
|
|
337
|
+
export const useAppSelector = useSelector.withTypes<RootState>();
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
```tsx
|
|
341
|
+
import { Provider } from "react-redux";
|
|
342
|
+
import { store } from "./store/index";
|
|
343
|
+
|
|
344
|
+
export default function App() {
|
|
345
|
+
return (
|
|
346
|
+
<Provider store={store}>
|
|
347
|
+
<CounterComponent />
|
|
348
|
+
</Provider>
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## React Usage
|
|
354
|
+
|
|
355
|
+
OrcheStore exposes direct usable child slices through a unified store instance.
|
|
356
|
+
|
|
357
|
+
```tsx
|
|
358
|
+
import { store } from "./store/index";
|
|
359
|
+
import { counter } from "./store/counterSlice";
|
|
360
|
+
|
|
361
|
+
export function CounterComponent() {
|
|
362
|
+
const value = counter.useSelect((state) => state.value);
|
|
363
|
+
const alias = store.counter.useSelect((state) => state.value);
|
|
364
|
+
|
|
365
|
+
return (
|
|
366
|
+
<>
|
|
367
|
+
<div>Counter: {value}</div>
|
|
368
|
+
|
|
369
|
+
<button onClick={() => counter.increment(1)}>Increment</button>
|
|
370
|
+
|
|
371
|
+
<button onClick={() => store.counter.incrementAfter(1, 1000)}>Increment Later</button>
|
|
372
|
+
</>
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
> 📌 If the slice is mounted only once, `store.counter` and `counter` refer to the same runtime instance and can be used interchangeably.
|
|
378
|
+
>
|
|
379
|
+
> 🔄 When a slice is mounted multiple times, each mount receives its own isolated instance. See [Reusing Slices](#reusing-slices) and [Lineage & Clones](#lineage--clones) for details.
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
# Slice Layers
|
|
384
|
+
|
|
385
|
+
Slices are created using `createSlice`.
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { createSlice } from "orchestore";
|
|
389
|
+
|
|
390
|
+
const counter = createSlice({
|
|
391
|
+
name: "counter",
|
|
392
|
+
|
|
393
|
+
state: {},
|
|
394
|
+
|
|
395
|
+
computed: {}, // Planned
|
|
396
|
+
|
|
397
|
+
mutations: {},
|
|
398
|
+
|
|
399
|
+
methods: {},
|
|
400
|
+
|
|
401
|
+
children: {},
|
|
402
|
+
|
|
403
|
+
subscribe: {}, // Planned
|
|
404
|
+
});
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
## name
|
|
408
|
+
|
|
409
|
+
Every slice requires a unique and stable name.
|
|
410
|
+
|
|
411
|
+
The name primarily exists to ensure slice uniqueness and path generation.
|
|
412
|
+
|
|
413
|
+
```ts
|
|
414
|
+
const counter = createSlice({
|
|
415
|
+
name: "counter",
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
The name is exposed at runtime:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
console.log(counter.name); // "counter"
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
It is also accessible inside methods:
|
|
426
|
+
|
|
427
|
+
```ts
|
|
428
|
+
methods: {
|
|
429
|
+
logName() {
|
|
430
|
+
console.log(this.name);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Rules:**
|
|
436
|
+
|
|
437
|
+
- Slice names should not contain `"."` or `"/"`, because dots are reserved for nested slice paths
|
|
438
|
+
- Two slices cannot share the same name
|
|
439
|
+
- Registering the same slice instance multiple times with the same name is not allowed
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
443
|
+
## state
|
|
444
|
+
|
|
445
|
+
The `state` property defines the initial slice state.
|
|
446
|
+
|
|
447
|
+
```ts
|
|
448
|
+
const counter = createSlice({
|
|
449
|
+
name: "counter",
|
|
450
|
+
|
|
451
|
+
state: {
|
|
452
|
+
value: 0,
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
State can also be initialized lazily by providing a function.
|
|
458
|
+
|
|
459
|
+
The initializer runs only once before the first state access.
|
|
460
|
+
|
|
461
|
+
```ts
|
|
462
|
+
const counter = createSlice({
|
|
463
|
+
name: "counter",
|
|
464
|
+
|
|
465
|
+
state: () => ({
|
|
466
|
+
value: 0,
|
|
467
|
+
}),
|
|
468
|
+
});
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**Useful for:**
|
|
472
|
+
|
|
473
|
+
- expensive initialization
|
|
474
|
+
- persisted state restoration
|
|
475
|
+
- runtime-dependent values
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Mutations
|
|
480
|
+
|
|
481
|
+
Mutations are synchronous state transition functions.
|
|
482
|
+
|
|
483
|
+
**Characteristics:**
|
|
484
|
+
|
|
485
|
+
- receive mutable draft state as the first parameter
|
|
486
|
+
- support multiple user-defined arguments
|
|
487
|
+
- directly callable from the exposed slices or store
|
|
488
|
+
|
|
489
|
+
**Responsibilities:**
|
|
490
|
+
|
|
491
|
+
Mutations should contain:
|
|
492
|
+
|
|
493
|
+
- synchronous state updates
|
|
494
|
+
- deterministic state transitions
|
|
495
|
+
- normalization logic
|
|
496
|
+
|
|
497
|
+
For anything else, use methods instead.
|
|
498
|
+
|
|
499
|
+
**Example:**
|
|
500
|
+
|
|
501
|
+
```ts
|
|
502
|
+
const counter = createSlice({
|
|
503
|
+
name: "counter",
|
|
504
|
+
|
|
505
|
+
state: {
|
|
506
|
+
value: 0,
|
|
507
|
+
},
|
|
508
|
+
|
|
509
|
+
mutations: {
|
|
510
|
+
increment(state, amount = 1, max = Infinity) {
|
|
511
|
+
state.value = Math.min(state.value + amount, max);
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
counter.increment(1, 50);
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
---
|
|
520
|
+
|
|
521
|
+
## Methods
|
|
522
|
+
|
|
523
|
+
Methods are the orchestration layer of a slice.
|
|
524
|
+
|
|
525
|
+
**Characteristics:**
|
|
526
|
+
|
|
527
|
+
- receive any number of arguments
|
|
528
|
+
- can return synchronous values or Promises
|
|
529
|
+
- can access:
|
|
530
|
+
- state, mutations, slibling methods, nested slices (through `this`)
|
|
531
|
+
- Application-wide utilities (`this.global`)
|
|
532
|
+
|
|
533
|
+
Methods are not serialized, replayable, or represented in Redux DevTools action history.
|
|
534
|
+
|
|
535
|
+
**Responsibilities:**
|
|
536
|
+
|
|
537
|
+
Methods are designed for centralizing any slice-related logic:
|
|
538
|
+
|
|
539
|
+
- asynchronous workflows
|
|
540
|
+
- business logic orchestration
|
|
541
|
+
- API communication and network calls
|
|
542
|
+
- timers, delayed or scheduled executions, such as `setTimeout` or event listeners
|
|
543
|
+
- side effects such as `localStorage`, `sessionStorage` and DOM manipulation
|
|
544
|
+
- Slice-related React hooks such as `slice.useSelect`, tanstack `useQuery` and `useMutation`
|
|
545
|
+
|
|
546
|
+
**Restrictions:**
|
|
547
|
+
|
|
548
|
+
Methods should NOT:
|
|
549
|
+
|
|
550
|
+
- include slice-unrelated logic.
|
|
551
|
+
- mutate state directly. use mutations instead.
|
|
552
|
+
|
|
553
|
+
Prefer:
|
|
554
|
+
|
|
555
|
+
```ts
|
|
556
|
+
this.increment(1);
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
Instead of:
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
this.getState().value++;
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Example:**
|
|
566
|
+
|
|
567
|
+
```ts
|
|
568
|
+
const counter = createSlice({
|
|
569
|
+
name: "counter",
|
|
570
|
+
|
|
571
|
+
state: {
|
|
572
|
+
value: 0,
|
|
573
|
+
},
|
|
574
|
+
|
|
575
|
+
mutations: {
|
|
576
|
+
increment(state, amount: number) {
|
|
577
|
+
state.value += amount;
|
|
578
|
+
},
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
methods: {
|
|
582
|
+
async incrementAfter(amount: number, delay = 1000) {
|
|
583
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
584
|
+
this.increment(amount);
|
|
585
|
+
this.global.logger.info("Counter incremented");
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
});
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
---
|
|
592
|
+
|
|
593
|
+
## Computed State (Planned)
|
|
594
|
+
|
|
595
|
+
This currently not supported
|
|
596
|
+
|
|
597
|
+
~~The `computed` property provides reusable derived state.~~
|
|
598
|
+
|
|
599
|
+
```ts
|
|
600
|
+
// const counter = createSlice({
|
|
601
|
+
// name: "counter",
|
|
602
|
+
|
|
603
|
+
// state: {
|
|
604
|
+
// value: 10,
|
|
605
|
+
// },
|
|
606
|
+
|
|
607
|
+
// computed: {
|
|
608
|
+
// doubled(state) {
|
|
609
|
+
// return state.value * 2;
|
|
610
|
+
// },
|
|
611
|
+
|
|
612
|
+
// multiplied(state, amount: number) {
|
|
613
|
+
// return state.value * amount;
|
|
614
|
+
// },
|
|
615
|
+
// },
|
|
616
|
+
// });
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
---
|
|
620
|
+
|
|
621
|
+
## Nested Slices
|
|
622
|
+
|
|
623
|
+
Slices can be composed by registering other slice instances through the `children` property.
|
|
624
|
+
|
|
625
|
+
This allows related state and behavior to be organized into a hierarchical structure while preserving full type inference and ownership isolation.
|
|
626
|
+
|
|
627
|
+
```ts
|
|
628
|
+
import { products } from "./productsSlice";
|
|
629
|
+
import { categories } from "./categoriesSlice";
|
|
630
|
+
|
|
631
|
+
export const shop = createSlice({
|
|
632
|
+
name: "shop",
|
|
633
|
+
|
|
634
|
+
state: {},
|
|
635
|
+
|
|
636
|
+
children: {
|
|
637
|
+
products,
|
|
638
|
+
categories,
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
**Accessing Child Slices:**
|
|
644
|
+
|
|
645
|
+
Child slices are exposed directly on their parent slice.
|
|
646
|
+
|
|
647
|
+
```ts
|
|
648
|
+
shop.products.add(...)
|
|
649
|
+
shop.categories.create(...)
|
|
650
|
+
|
|
651
|
+
console.log(shop.products.getState());
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
Deeply nested slice hierarchies are fully supported.
|
|
655
|
+
|
|
656
|
+
```ts
|
|
657
|
+
admin.users.permissions.grant(...);
|
|
658
|
+
|
|
659
|
+
console.log(admin.users.permissions.getState());
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
### Reusing Slices
|
|
663
|
+
|
|
664
|
+
A slice can be mounted multiple times within the same tree.
|
|
665
|
+
|
|
666
|
+
When the same slice definition is reused, OrcheStore automatically creates a separate mounted instance for each location.
|
|
667
|
+
|
|
668
|
+
```ts
|
|
669
|
+
const paginationSlice = createSlice({ ... });
|
|
670
|
+
|
|
671
|
+
const shopSlice = createSlice({
|
|
672
|
+
name: "shop",
|
|
673
|
+
|
|
674
|
+
state: {},
|
|
675
|
+
|
|
676
|
+
children: {
|
|
677
|
+
categories: paginationSlice,
|
|
678
|
+
products: paginationSlice,
|
|
679
|
+
},
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
const adminSlice = createSlice({
|
|
683
|
+
name: "admin",
|
|
684
|
+
|
|
685
|
+
state: {},
|
|
686
|
+
|
|
687
|
+
children: {
|
|
688
|
+
products: paginationSlice,
|
|
689
|
+
},
|
|
690
|
+
});
|
|
691
|
+
```
|
|
692
|
+
|
|
693
|
+
Each mounted instance has:
|
|
694
|
+
|
|
695
|
+
- its own path
|
|
696
|
+
- its own ownership context
|
|
697
|
+
- its own runtime state
|
|
698
|
+
|
|
699
|
+
**Runtime identity:**
|
|
700
|
+
|
|
701
|
+
Although all mounted slices originate from `paginationSlice`, they are not necessarily the same runtime instance.
|
|
702
|
+
|
|
703
|
+
```ts
|
|
704
|
+
paginationSlice === shopSlice.categories; // First mount uses the original instance
|
|
705
|
+
|
|
706
|
+
paginationSlice !== shopSlice.products; // Different mount location creates a clone
|
|
707
|
+
paginationSlice !== adminSlice.products; // Different mount location creates a clone
|
|
708
|
+
|
|
709
|
+
shopSlice.products !== adminSlice.products; // Independent mounted clones
|
|
710
|
+
```
|
|
711
|
+
|
|
712
|
+
Every mount location receives its own isolated instance.
|
|
713
|
+
|
|
714
|
+
For a deeper explanation of how slice reuse works, see [Lineage & Clones](#lineage--clones).
|
|
715
|
+
|
|
716
|
+
### Runtime Paths
|
|
717
|
+
|
|
718
|
+
Every slice exposes a runtime path through `slice.path`.
|
|
719
|
+
|
|
720
|
+
```ts
|
|
721
|
+
store.counter.name; // "counter"
|
|
722
|
+
store.counter.path; // "counter"
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
Nested slices automatically inherit their parent path.
|
|
726
|
+
|
|
727
|
+
```ts
|
|
728
|
+
store.admin.users.name; // "users"
|
|
729
|
+
store.admin.users.path; // "admin.users"
|
|
730
|
+
|
|
731
|
+
store.admin.users.permissions.name; // "permissions"
|
|
732
|
+
store.admin.users.permissions.path; // "admin.users.permissions"
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
**Notes:**
|
|
736
|
+
|
|
737
|
+
Paths are generated automatically from the slice hierarchy.
|
|
738
|
+
|
|
739
|
+
OrcheStore builds on the same concepts as Redux Toolkit's `reducerPath` and `combineReducers`, but automates path generation, reducer registration, and nested slice composition.
|
|
740
|
+
|
|
741
|
+
No manual path configuration or reducer injection is required.
|
|
742
|
+
|
|
743
|
+
---
|
|
744
|
+
|
|
745
|
+
# State Access & Subscriptions
|
|
746
|
+
|
|
747
|
+
OrcheStore provides multiple ways to access state depending on the context.
|
|
748
|
+
|
|
749
|
+
| API | Purpose |
|
|
750
|
+
| ------------------- | -------------------------------------------------------- |
|
|
751
|
+
| `slice.getState()` | Read the current state snapshot |
|
|
752
|
+
| `slice.useSelect()` | Subscribe to state changes inside React |
|
|
753
|
+
| Draft state | Temporary state access available during state evaluation |
|
|
754
|
+
|
|
755
|
+
## State Snapshots
|
|
756
|
+
|
|
757
|
+
`getState()` returns the latest immutable state snapshot.
|
|
758
|
+
|
|
759
|
+
Use it whenever you need to read state imperatively outside of React subscriptions.
|
|
760
|
+
|
|
761
|
+
```ts
|
|
762
|
+
counter.getState().value;
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
**Available:**
|
|
766
|
+
|
|
767
|
+
- outside React
|
|
768
|
+
- inside methods
|
|
769
|
+
|
|
770
|
+
**Notes:**
|
|
771
|
+
|
|
772
|
+
Each call returns the current state at the moment it is executed.
|
|
773
|
+
|
|
774
|
+
Previously captured snapshots are not updated after future mutations.
|
|
775
|
+
|
|
776
|
+
```ts
|
|
777
|
+
methods: {
|
|
778
|
+
logValue() {
|
|
779
|
+
this.changeValue("John");
|
|
780
|
+
|
|
781
|
+
const snapshot = this.getState();
|
|
782
|
+
|
|
783
|
+
this.changeValue("Alice");
|
|
784
|
+
|
|
785
|
+
console.log(snapshot.value); // "John"
|
|
786
|
+
console.log(this.getState().value); // "Alice"
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
---
|
|
792
|
+
|
|
793
|
+
## State Subscriptions
|
|
794
|
+
|
|
795
|
+
`useSelect()` provides reactive state subscriptions for React components.
|
|
796
|
+
|
|
797
|
+
Internally, it is powered by Redux Toolkit's `useSelector`, while exposing a fully typed, slice-scoped API.
|
|
798
|
+
|
|
799
|
+
**Available:**
|
|
800
|
+
|
|
801
|
+
- inside React components
|
|
802
|
+
- inside slice methods that serve as custom React hooks
|
|
803
|
+
|
|
804
|
+
**Notes:**
|
|
805
|
+
|
|
806
|
+
Components automatically re-render when the selected value changes.
|
|
807
|
+
|
|
808
|
+
```tsx
|
|
809
|
+
const value = counter.useSelect((state) => state.value);
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
Selectors can also be accessed through the store to access multiple slices.
|
|
813
|
+
|
|
814
|
+
```tsx
|
|
815
|
+
const canEdit = store.useSelect((state) => {
|
|
816
|
+
return (
|
|
817
|
+
state.auth.isAuthenticated &&
|
|
818
|
+
state.users.permissions.list.includes("edit_user")
|
|
819
|
+
);
|
|
820
|
+
});
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
|
|
825
|
+
## Draft State
|
|
826
|
+
|
|
827
|
+
Draft state provides temporary access to slice state during state evaluation.
|
|
828
|
+
|
|
829
|
+
It is context-dependent and only available within specific APIs:
|
|
830
|
+
|
|
831
|
+
- Inside Mutations
|
|
832
|
+
|
|
833
|
+
Mutations receive a mutable draft state that can be updated directly.
|
|
834
|
+
|
|
835
|
+
```ts
|
|
836
|
+
mutations: {
|
|
837
|
+
setName(state, name: string) {
|
|
838
|
+
state.name = name;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
```
|
|
842
|
+
|
|
843
|
+
~~- Inside Computed State~~
|
|
844
|
+
|
|
845
|
+
~~Computed functions receive a read-only draft state extended with additional runtime helpers.~~
|
|
846
|
+
|
|
847
|
+
```ts
|
|
848
|
+
// computed: {
|
|
849
|
+
// fullName(state) {
|
|
850
|
+
// return `${state.firstName} ${state.lastName}`;
|
|
851
|
+
// }
|
|
852
|
+
// }
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
- Inside useSelect
|
|
856
|
+
|
|
857
|
+
Selectors receive a read-only draft state extended ~~with additional runtime helpers~~.
|
|
858
|
+
|
|
859
|
+
```tsx
|
|
860
|
+
const displayName = users.useSelect((state) => {
|
|
861
|
+
return state.user.name;
|
|
862
|
+
});
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
**~~Available additions include:~~**
|
|
866
|
+
|
|
867
|
+
~~- `state.computed` — computed state access~~
|
|
868
|
+
|
|
869
|
+
---
|
|
870
|
+
|
|
871
|
+
# Store Integration
|
|
872
|
+
|
|
873
|
+
## Creating the Store
|
|
874
|
+
|
|
875
|
+
Create the root store using `createStore`.
|
|
876
|
+
|
|
877
|
+
```ts
|
|
878
|
+
import { createStore } from "orchestore";
|
|
879
|
+
import { counter } from "./counterSlice";
|
|
880
|
+
import { users } from "./usersSlice";
|
|
881
|
+
|
|
882
|
+
export const store = createStore({
|
|
883
|
+
slices: {
|
|
884
|
+
counter,
|
|
885
|
+
users,
|
|
886
|
+
},
|
|
887
|
+
});
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
## Store Provider
|
|
891
|
+
|
|
892
|
+
Wrapping the application component inside this provider is required.
|
|
893
|
+
|
|
894
|
+
```tsx
|
|
895
|
+
import { StoreProvider } from "orchestore";
|
|
896
|
+
import { store } from "./store";
|
|
897
|
+
|
|
898
|
+
export default function App() {
|
|
899
|
+
return (
|
|
900
|
+
<StoreProvider store={store}>
|
|
901
|
+
<Routes />
|
|
902
|
+
</StoreProvider>
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
## Accessing Slices through Store
|
|
908
|
+
|
|
909
|
+
The root store behaves similarly to a parent slice and exposes all registered slices.
|
|
910
|
+
|
|
911
|
+
```ts
|
|
912
|
+
store.counter.increment(1);
|
|
913
|
+
|
|
914
|
+
console.log(store.counter.getState());
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
## Accessing Store from Slices
|
|
918
|
+
|
|
919
|
+
Every slice has access to the root store instance through `this.root`.
|
|
920
|
+
|
|
921
|
+
```ts
|
|
922
|
+
this.root.auth.getState().isAuthenticated;
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
**Useful for:**
|
|
926
|
+
|
|
927
|
+
- cross-slice coordination
|
|
928
|
+
- avoiding circular imports
|
|
929
|
+
- application-wide orchestration
|
|
930
|
+
|
|
931
|
+
## Root Store Type Extension (Planned)
|
|
932
|
+
|
|
933
|
+
Overriding `OrcheStore.Slots.root` provides full root store typing throughout the application.
|
|
934
|
+
|
|
935
|
+
> 🐞 Under active development: this currently causes a circular type inference limitation.
|
|
936
|
+
|
|
937
|
+
```ts
|
|
938
|
+
import { createStore } from "orchestore";
|
|
939
|
+
|
|
940
|
+
export const store = createStore({
|
|
941
|
+
slices: {
|
|
942
|
+
counter,
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
declare module "orchestore" {
|
|
947
|
+
namespace OrcheStore {
|
|
948
|
+
interface Slots {
|
|
949
|
+
root: typeof store; // Bugfix: Causes a circular type inference
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
```ts
|
|
956
|
+
this.root; // Before: any
|
|
957
|
+
this.root; // After: fully typed store
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
**Rules:**
|
|
961
|
+
|
|
962
|
+
- `root` must be a store instance created using `createStore`
|
|
963
|
+
- `null` and `undefined` are excluded automatically
|
|
964
|
+
- `typeof store | null | undefined` is equivalent to `typeof store`
|
|
965
|
+
- Invalid types fall back to `any`
|
|
966
|
+
|
|
967
|
+
---
|
|
968
|
+
|
|
969
|
+
# Lineage & Clones
|
|
970
|
+
|
|
971
|
+
OrcheStore uses a lineage-based model for slice identity.
|
|
972
|
+
|
|
973
|
+
**Why?**
|
|
974
|
+
|
|
975
|
+
Slices can be used in multiple places in the store tree.
|
|
976
|
+
|
|
977
|
+
When this happens, OrcheStore creates a separate runtime instance for each usage. These instances are called **clones**.
|
|
978
|
+
|
|
979
|
+
A clone is an independent instance copy of a slice at runtime. It has its own state and runs separately from other clones, while still remaining part of a shared lineage.
|
|
980
|
+
|
|
981
|
+
A lineage (or family) is the set of all instances that come from the same slice definition.
|
|
982
|
+
|
|
983
|
+
**This means:**
|
|
984
|
+
|
|
985
|
+
- slices are not singletons
|
|
986
|
+
- a slice can appear multiple times in a tree
|
|
987
|
+
- each clone is fully isolated
|
|
988
|
+
- all instances cloned from the same slice are linked through lineage
|
|
989
|
+
|
|
990
|
+
## Automatic Cloning
|
|
991
|
+
|
|
992
|
+
When a slice is reused through `children` or `slices`, OrcheStore automatically creates a new mounted instance for each usage.
|
|
993
|
+
|
|
994
|
+
```ts
|
|
995
|
+
const paginationSlice = createSlice({ ... });
|
|
996
|
+
|
|
997
|
+
const shopSlice = createSlice({
|
|
998
|
+
name: "shop",
|
|
999
|
+
|
|
1000
|
+
state: {},
|
|
1001
|
+
|
|
1002
|
+
children: {
|
|
1003
|
+
a: paginationSlice,
|
|
1004
|
+
b: paginationSlice,
|
|
1005
|
+
},
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
const adminSlice = createSlice({
|
|
1009
|
+
name: "admin",
|
|
1010
|
+
|
|
1011
|
+
state: {},
|
|
1012
|
+
|
|
1013
|
+
children: {
|
|
1014
|
+
a: paginationSlice,
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
```
|
|
1018
|
+
|
|
1019
|
+
Each mount becomes a separate runtime node:
|
|
1020
|
+
|
|
1021
|
+
```ts
|
|
1022
|
+
shopSlice.a !== shopSlice.b;
|
|
1023
|
+
shopSlice.a !== adminSlice.a;
|
|
1024
|
+
shopSlice.b !== adminSlice.a;
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
Each instance also receives its own path and its own ownership context:
|
|
1028
|
+
|
|
1029
|
+
```ts
|
|
1030
|
+
shopSlice.a.path; // "shop.a"
|
|
1031
|
+
shopSlice.b.path; // "shop.b"
|
|
1032
|
+
adminSlice.a.path; // "admin.a"
|
|
1033
|
+
```
|
|
1034
|
+
|
|
1035
|
+
Although these instances are independent at runtime, they still belong to the same lineage.
|
|
1036
|
+
|
|
1037
|
+
## Manual Cloning
|
|
1038
|
+
|
|
1039
|
+
A new detached clone can be created manually from any slice instance:
|
|
1040
|
+
|
|
1041
|
+
```ts
|
|
1042
|
+
const clone = slice.prototype.clone();
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
The new instance:
|
|
1046
|
+
|
|
1047
|
+
- belongs to the same lineage
|
|
1048
|
+
- starts detached from the tree
|
|
1049
|
+
- has no mounted path initially
|
|
1050
|
+
- has its own ownership context
|
|
1051
|
+
|
|
1052
|
+
## Inspecting a Lineage
|
|
1053
|
+
|
|
1054
|
+
**Get All Related Instances:**
|
|
1055
|
+
|
|
1056
|
+
Returns every instance in the lineage, **including** the current one.
|
|
1057
|
+
|
|
1058
|
+
```ts
|
|
1059
|
+
const lineage = slice.prototype.getLineage();
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
Useful for:
|
|
1063
|
+
|
|
1064
|
+
- debugging slice reuse
|
|
1065
|
+
- inspecting mounted instances
|
|
1066
|
+
- understanding tree distribution
|
|
1067
|
+
|
|
1068
|
+
**Get Clones:**
|
|
1069
|
+
|
|
1070
|
+
Returns all lineage members **except** the current instance.
|
|
1071
|
+
|
|
1072
|
+
```ts
|
|
1073
|
+
const siblings = slice.prototype.getClones();
|
|
1074
|
+
```
|
|
1075
|
+
|
|
1076
|
+
Useful for:
|
|
1077
|
+
|
|
1078
|
+
- communicating between clones
|
|
1079
|
+
- broadcast or synchronization scenarios
|
|
1080
|
+
- comparing mounted instances
|
|
1081
|
+
|
|
1082
|
+
## Definition Type Checking
|
|
1083
|
+
|
|
1084
|
+
You can determine whether two slices belong to the same lineage:
|
|
1085
|
+
|
|
1086
|
+
You can check whether two slices belong to the same lineage:
|
|
1087
|
+
|
|
1088
|
+
```ts
|
|
1089
|
+
const isSameLineage = slice.prototype.isTypeOf(otherSlice);
|
|
1090
|
+
```
|
|
1091
|
+
|
|
1092
|
+
Returns `true` when both slices originate from the same slice definition, even if they are different runtime instances.
|
|
1093
|
+
|
|
1094
|
+
```ts
|
|
1095
|
+
const slice1 = createSlice(...);
|
|
1096
|
+
const slice2 = createSlice(...);
|
|
1097
|
+
|
|
1098
|
+
const clone1 = slice1.prototype.clone();
|
|
1099
|
+
const clone2 = clone1.prototype.clone();
|
|
1100
|
+
|
|
1101
|
+
slice1.prototype.isTypeOf(clone1); // true
|
|
1102
|
+
clone1.prototype.isTypeOf(clone2); // true
|
|
1103
|
+
clone2.prototype.isTypeOf(slice1); // true
|
|
1104
|
+
|
|
1105
|
+
slice1.prototype.isTypeOf(slice2); // false
|
|
1106
|
+
slice2.prototype.isTypeOf(clone1); // false
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
## Summary
|
|
1110
|
+
|
|
1111
|
+
- Reusing a slice automatically creates mounted clones.
|
|
1112
|
+
- `clone()` creates a new detached lineage member.
|
|
1113
|
+
- Every clone is isolated at runtime.
|
|
1114
|
+
- All clones from the same definition belong to a shared lineage.
|
|
1115
|
+
- `getLineage()` returns all instances in a lineage.
|
|
1116
|
+
- `getClones()` returns all related instances except the current one.
|
|
1117
|
+
- `isTypeOf()` checks whether two instances belong to the same lineage.
|
|
1118
|
+
|
|
1119
|
+
---
|
|
1120
|
+
|
|
1121
|
+
# Global Utilities
|
|
1122
|
+
|
|
1123
|
+
Global utilities allow slices and the root store to access shared runtime services through `global`.
|
|
1124
|
+
|
|
1125
|
+
Common use cases include:
|
|
1126
|
+
|
|
1127
|
+
- notifications and toasts
|
|
1128
|
+
- navigation and routing
|
|
1129
|
+
- analytics and tracking
|
|
1130
|
+
- API clients and service wrappers
|
|
1131
|
+
- runtime values that are difficult to access directly from slices
|
|
1132
|
+
- integrations with React hooks and third-party libraries
|
|
1133
|
+
|
|
1134
|
+
Utilities are registered using `provideGlobalUtils` and are accessible from any slice or the root store.
|
|
1135
|
+
|
|
1136
|
+
## Accessing Global Utilities
|
|
1137
|
+
|
|
1138
|
+
**Available:**
|
|
1139
|
+
|
|
1140
|
+
- Through the exposed store or slice instances
|
|
1141
|
+
|
|
1142
|
+
```ts
|
|
1143
|
+
store.global;
|
|
1144
|
+
slice.global;
|
|
1145
|
+
```
|
|
1146
|
+
|
|
1147
|
+
- Inside slice methods
|
|
1148
|
+
|
|
1149
|
+
```ts
|
|
1150
|
+
this.global.notify("success", "Saved!");
|
|
1151
|
+
```
|
|
1152
|
+
|
|
1153
|
+
## Utilities Type Extension
|
|
1154
|
+
|
|
1155
|
+
Overriding `OrcheStore.Slots.global` provides full typing everywhere.
|
|
1156
|
+
|
|
1157
|
+
```ts
|
|
1158
|
+
import type { NavigateFunction } from "react-router";
|
|
1159
|
+
|
|
1160
|
+
declare module "orchestore" {
|
|
1161
|
+
namespace OrcheStore {
|
|
1162
|
+
interface Slots {
|
|
1163
|
+
global: {
|
|
1164
|
+
navigate: NavigateFunction;
|
|
1165
|
+
|
|
1166
|
+
notify(type: "info" | "error" | "success", message: string): void;
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
```
|
|
1172
|
+
|
|
1173
|
+
```ts
|
|
1174
|
+
this.global; // Before: any
|
|
1175
|
+
this.global; // After: fully typed
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
**Rules:**
|
|
1179
|
+
|
|
1180
|
+
- `global` must be an object
|
|
1181
|
+
- `null` and `undefined` are excluded automatically
|
|
1182
|
+
- `object | null | undefined` is equivalent to `object`
|
|
1183
|
+
- Invalid types fall back to `any`
|
|
1184
|
+
|
|
1185
|
+
## Providing Runtime Utilities
|
|
1186
|
+
|
|
1187
|
+
Global utility values can be registered or updated at runtime.
|
|
1188
|
+
|
|
1189
|
+
```ts
|
|
1190
|
+
import { useEffect } from "react";
|
|
1191
|
+
import { useNavigate } from "react-router";
|
|
1192
|
+
import { provideGlobalUtils } from "orchestore";
|
|
1193
|
+
import { feedbacks } from "./ui-feedbacks";
|
|
1194
|
+
import { store } from "./store";
|
|
1195
|
+
|
|
1196
|
+
provideGlobalUtils({
|
|
1197
|
+
notify(type, message) {
|
|
1198
|
+
feedbacks.notify(type, message);
|
|
1199
|
+
},
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
export default function App() {
|
|
1203
|
+
const navigate = useNavigate();
|
|
1204
|
+
|
|
1205
|
+
useEffect(() => {
|
|
1206
|
+
provideGlobalUtils({ navigate });
|
|
1207
|
+
}, [navigate]);
|
|
1208
|
+
|
|
1209
|
+
return (
|
|
1210
|
+
<StoreProvider store={store}>
|
|
1211
|
+
<Routes />
|
|
1212
|
+
</StoreProvider>
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1215
|
+
```
|
|
1216
|
+
|
|
1217
|
+
## Using Global Utilities in Slices
|
|
1218
|
+
|
|
1219
|
+
Global utilities can be used anywhere a slice instance is available.
|
|
1220
|
+
|
|
1221
|
+
```ts
|
|
1222
|
+
methods: {
|
|
1223
|
+
async insertUser(data: UserInput) {
|
|
1224
|
+
try {
|
|
1225
|
+
this.setLoading(true);
|
|
1226
|
+
|
|
1227
|
+
const response = await api.users.add(data);
|
|
1228
|
+
|
|
1229
|
+
this.global.notify("success", "User added successfully!");
|
|
1230
|
+
|
|
1231
|
+
this.setLoading(false);
|
|
1232
|
+
|
|
1233
|
+
this.global.navigate("/users/" + response.id);
|
|
1234
|
+
} catch (error) {
|
|
1235
|
+
this.global.notify("error", "Failed to add user");
|
|
1236
|
+
|
|
1237
|
+
console.error(error);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
---
|
|
1244
|
+
|
|
1245
|
+
# TypeScript Inference
|
|
1246
|
+
|
|
1247
|
+
OrcheStore is designed around deep TypeScript inference.
|
|
1248
|
+
|
|
1249
|
+
```ts
|
|
1250
|
+
const counter = createSlice({
|
|
1251
|
+
name: "counter",
|
|
1252
|
+
|
|
1253
|
+
state: {
|
|
1254
|
+
value: 0,
|
|
1255
|
+
},
|
|
1256
|
+
|
|
1257
|
+
mutations: {
|
|
1258
|
+
increment(state, amount: number) {
|
|
1259
|
+
state.value += amount;
|
|
1260
|
+
},
|
|
1261
|
+
},
|
|
1262
|
+
|
|
1263
|
+
methods: {
|
|
1264
|
+
async incrementAfter(amount: number, delay = 1000) {
|
|
1265
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1266
|
+
this.increment(amount);
|
|
1267
|
+
},
|
|
1268
|
+
},
|
|
1269
|
+
|
|
1270
|
+
children: {
|
|
1271
|
+
subCounter: createSlice({
|
|
1272
|
+
name: "subCounter",
|
|
1273
|
+
|
|
1274
|
+
state: {
|
|
1275
|
+
value: 0,
|
|
1276
|
+
},
|
|
1277
|
+
}),
|
|
1278
|
+
},
|
|
1279
|
+
});
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
Automatically produces:
|
|
1283
|
+
|
|
1284
|
+
```ts
|
|
1285
|
+
counter.getState();
|
|
1286
|
+
// { value: number, subCounter: { value: number } }
|
|
1287
|
+
|
|
1288
|
+
counter.subCounter.getState();
|
|
1289
|
+
// { value: number }
|
|
1290
|
+
|
|
1291
|
+
counter.increment(amount: number): void;
|
|
1292
|
+
|
|
1293
|
+
counter.incrementAfter(amount: number, delay?: number): Promise<number>;
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
No manual type declarations required.
|
|
1297
|
+
|
|
1298
|
+
## Framework Type Extensions
|
|
1299
|
+
|
|
1300
|
+
OrcheStore also exposes user-definable type slots through `OrcheStore.Slots`.
|
|
1301
|
+
|
|
1302
|
+
These slots allow application-specific types to be injected into the framework and become available everywhere with full type safety.
|
|
1303
|
+
|
|
1304
|
+
| Slot | Purpose |
|
|
1305
|
+
| -------------------------- | ----------------------- |
|
|
1306
|
+
| `OrcheStore.Slots.root` | Root store typing |
|
|
1307
|
+
| `OrcheStore.Slots.global` | Global utilities typing |
|
|
1308
|
+
|
|
1309
|
+
```ts
|
|
1310
|
+
declare module "orhestore" {
|
|
1311
|
+
namespace OrcheStore {
|
|
1312
|
+
interface Slots {
|
|
1313
|
+
root: typeof store; // Bugfix: Causes a circular type inference
|
|
1314
|
+
|
|
1315
|
+
global: {
|
|
1316
|
+
navigate: NavigateFunction;
|
|
1317
|
+
notify(type: "info" | "error" | "success", message: string): void;
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
This provides full typing for APIs such as:
|
|
1325
|
+
|
|
1326
|
+
```ts
|
|
1327
|
+
this.root;
|
|
1328
|
+
this.global;
|
|
1329
|
+
|
|
1330
|
+
store.global;
|
|
1331
|
+
counter.global;
|
|
1332
|
+
```
|
|
1333
|
+
|
|
1334
|
+
---
|
|
1335
|
+
|
|
1336
|
+
# Status
|
|
1337
|
+
|
|
1338
|
+
OrcheStore is currently experimental and under active development.
|
|
1339
|
+
|
|
1340
|
+
Planned features:
|
|
1341
|
+
|
|
1342
|
+
- middleware and plugin system
|
|
1343
|
+
- persistence utilities
|
|
1344
|
+
- SSR support
|
|
1345
|
+
- deep readonly enforcement
|
|
1346
|
+
- lifecycle hooks
|
|
1347
|
+
- enhanced DevTools integration
|
|
1348
|
+
|
|
1349
|
+
---
|