sveltinia 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/LICENSE +21 -0
- package/README.md +91 -0
- package/dist/adapters/svelte.d.ts +6 -0
- package/dist/adapters/svelte.js +23 -0
- package/dist/adapters/sveltekit.d.ts +6 -0
- package/dist/adapters/sveltekit.js +21 -0
- package/dist/core.d.ts +8 -0
- package/dist/core.js +69 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/internal/action.d.ts +5 -0
- package/dist/internal/action.js +51 -0
- package/dist/internal/constants.d.ts +21 -0
- package/dist/internal/constants.js +21 -0
- package/dist/internal/reactivity.d.ts +6 -0
- package/dist/internal/reactivity.js +63 -0
- package/dist/internal/store-factory.d.ts +3 -0
- package/dist/internal/store-factory.js +186 -0
- package/dist/internal/types.d.ts +131 -0
- package/dist/internal/types.js +1 -0
- package/dist/internal/util.d.ts +8 -0
- package/dist/internal/util.js +9 -0
- package/dist/plugins/debug.d.ts +2 -0
- package/dist/plugins/debug.js +44 -0
- package/dist/plugins/persist.d.ts +7 -0
- package/dist/plugins/persist.js +111 -0
- package/package.json +25 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dene-
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# Sveltinia
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/dene-/sveltinia/main/apps/docs/public/logo.svg" alt="Sveltinia" width="96" height="96">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Sveltinia stores for Svelte and SvelteKit.</strong><br>
|
|
9
|
+
Typed stores, persistence, plugins, action hooks, mutation subscriptions, request-safe SSR.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/sveltinia"><img alt="npm" src="https://img.shields.io/npm/v/sveltinia?style=for-the-badge&labelColor=090c09&color=3f693a"></a>
|
|
14
|
+
<img alt="package managers" src="https://img.shields.io/badge/npm%20%7C%20yarn%20%7C%20pnpm%20%7C%20bun-ready-3f693a?style=for-the-badge&labelColor=090c09">
|
|
15
|
+
<a href="https://svelte.dev"><img alt="Svelte 5" src="https://img.shields.io/badge/Svelte-5-3f693a?style=for-the-badge&labelColor=090c09"></a>
|
|
16
|
+
<a href="https://www.typescriptlang.org"><img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-ready-3f693a?style=for-the-badge&labelColor=090c09"></a>
|
|
17
|
+
<a href="https://dene-.github.io/sveltinia/"><img alt="docs" src="https://img.shields.io/badge/docs-live-3f693a?style=for-the-badge&labelColor=090c09"></a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
Sveltinia is a Svelte 5 state management library inspired by Pinia. It provides typed stores for Svelte and SvelteKit, persisted Svelte stores, action hooks, mutation subscriptions, plugins, and request-safe SvelteKit SSR state management.
|
|
21
|
+
|
|
22
|
+
Use it when you want a Pinia alternative for Svelte, familiar SvelteKit Pinia-style ergonomics, or a small store API without a framework-sized abstraction.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
Install it with whichever package manager your project already uses:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install sveltinia
|
|
30
|
+
# or: yarn add sveltinia
|
|
31
|
+
# or: pnpm add sveltinia
|
|
32
|
+
# or: bun add sveltinia
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { createSveltinia, defineStore } from 'sveltinia'
|
|
39
|
+
|
|
40
|
+
export const sveltinia = createSveltinia({ debug: import.meta.env.DEV })
|
|
41
|
+
|
|
42
|
+
export const useCounter = defineStore('counter', {
|
|
43
|
+
state: () => ({ count: 0 }),
|
|
44
|
+
getters: { double: (state) => state.count * 2 },
|
|
45
|
+
actions: { increment() { this.count++ } },
|
|
46
|
+
persist: true
|
|
47
|
+
})
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Install first-party plugins once per root:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { createDebugPlugin, createPersistedState } from 'sveltinia'
|
|
54
|
+
|
|
55
|
+
sveltinia.use(createDebugPlugin()).use(createPersistedState())
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use the store in application code:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const counter = useCounter(sveltinia)
|
|
62
|
+
counter.increment()
|
|
63
|
+
console.log(counter.double)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
In a Svelte component, adapt it to a Svelte readable store:
|
|
67
|
+
|
|
68
|
+
```svelte
|
|
69
|
+
<script lang="ts">
|
|
70
|
+
import { useStore } from 'sveltinia/svelte'
|
|
71
|
+
import { fromStore } from 'svelte/store'
|
|
72
|
+
import { useCounter } from '$lib/stores/counter'
|
|
73
|
+
|
|
74
|
+
const counter = fromStore(useStore(useCounter()))
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<button onclick={() => counter.current.increment()}>{counter.current.count}</button>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Docs
|
|
81
|
+
|
|
82
|
+
- [Documentation site](https://dene-.github.io/sveltinia/)
|
|
83
|
+
- [GitHub repository](https://github.com/dene-/sveltinia)
|
|
84
|
+
|
|
85
|
+
## License
|
|
86
|
+
|
|
87
|
+
MIT
|
|
88
|
+
|
|
89
|
+
## Contributor
|
|
90
|
+
|
|
91
|
+
Built and maintained by [dene-](https://github.com/dene-).
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type Readable } from 'svelte/store';
|
|
2
|
+
import type { Sveltinia, Store } from '../internal/types.js';
|
|
3
|
+
export declare const provideSveltinia: (sveltinia: Sveltinia) => Sveltinia;
|
|
4
|
+
export declare const useSveltinia: () => Sveltinia;
|
|
5
|
+
export declare function toSvelteStore<T extends Store>(store: T): Readable<T>;
|
|
6
|
+
export declare const useStore: <T extends Store>(store: T) => Readable<T>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readable } from 'svelte/store';
|
|
2
|
+
import { createContext } from 'svelte';
|
|
3
|
+
import { setActiveSveltinia } from '../core.js';
|
|
4
|
+
const [getSveltiniaContext, setSveltiniaContext] = createContext();
|
|
5
|
+
export const provideSveltinia = (sveltinia) => {
|
|
6
|
+
setActiveSveltinia(sveltinia);
|
|
7
|
+
setSveltiniaContext(sveltinia);
|
|
8
|
+
return sveltinia;
|
|
9
|
+
};
|
|
10
|
+
export const useSveltinia = () => getSveltiniaContext();
|
|
11
|
+
// Module-level cache keyed by store identity. `useStore(sameStore)` returns
|
|
12
|
+
// the same readable across calls, avoiding duplicate subscriptions. Stores
|
|
13
|
+
// are stable per sveltinia, so a single module-level map is safe.
|
|
14
|
+
const storeCache = new WeakMap();
|
|
15
|
+
export function toSvelteStore(store) {
|
|
16
|
+
const cached = storeCache.get(store);
|
|
17
|
+
if (cached)
|
|
18
|
+
return cached;
|
|
19
|
+
const readableStore = readable(store, (set) => store.$subscribe(() => set(store)));
|
|
20
|
+
storeCache.set(store, readableStore);
|
|
21
|
+
return readableStore;
|
|
22
|
+
}
|
|
23
|
+
export const useStore = (store) => toSvelteStore(store);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Sveltinia, SveltiniaOptions, SveltiniaPlugin, StateTree } from '../internal/types.js';
|
|
2
|
+
export declare function createSveltinia(options?: SveltiniaOptions, plugins?: SveltiniaPlugin[]): {
|
|
3
|
+
create(serialized?: Record<string, StateTree>): Sveltinia;
|
|
4
|
+
serialize(sveltinia: Sveltinia): Record<string, StateTree>;
|
|
5
|
+
hydrate(sveltinia: Sveltinia, state: Record<string, StateTree>): void;
|
|
6
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createSveltinia as createRoot } from '../core.js';
|
|
2
|
+
import { clone } from '../internal/reactivity.js';
|
|
3
|
+
import { createPersistedState } from '../plugins/persist.js';
|
|
4
|
+
import { createDebugPlugin } from '../plugins/debug.js';
|
|
5
|
+
export function createSveltinia(options = {}, plugins = [createDebugPlugin(), createPersistedState()]) {
|
|
6
|
+
return {
|
|
7
|
+
create(serialized) {
|
|
8
|
+
const sveltinia = createRoot({ ...options, state: serialized ?? options.state });
|
|
9
|
+
for (const plugin of plugins)
|
|
10
|
+
sveltinia.use(plugin);
|
|
11
|
+
return sveltinia;
|
|
12
|
+
},
|
|
13
|
+
serialize(sveltinia) {
|
|
14
|
+
return clone(sveltinia.state);
|
|
15
|
+
},
|
|
16
|
+
hydrate(sveltinia, state) {
|
|
17
|
+
sveltinia.state = clone(state);
|
|
18
|
+
sveltinia.forEachStore((store) => store.$patch(state[store.$id] ?? {}));
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ComputedCell, DefineStoreOptions, Sveltinia, SveltiniaOptions, SetupCell, SetupStore, StateTree, Store } from './internal/types.js';
|
|
2
|
+
export declare const setActiveSveltinia: (sveltinia?: Sveltinia) => void;
|
|
3
|
+
export declare const getActiveSveltinia: () => Sveltinia | undefined;
|
|
4
|
+
export declare function createSveltinia(options?: SveltiniaOptions): Sveltinia;
|
|
5
|
+
export declare const state: <T>(value: T) => SetupCell<T>;
|
|
6
|
+
export declare const computed: <T>(fn: () => T) => ComputedCell<T>;
|
|
7
|
+
export declare function defineStore<S extends StateTree>(id: string, options: DefineStoreOptions<S>): (sveltinia?: Sveltinia) => Store<S>;
|
|
8
|
+
export declare function defineStore(id: string, setup: SetupStore, options?: DefineStoreOptions | Record<string, unknown>): (sveltinia?: Sveltinia) => Store;
|
package/dist/core.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createStore } from './internal/store-factory.js';
|
|
2
|
+
import { SVELTINIA_PROVISION_KEY, CELL_KIND } from './internal/constants.js';
|
|
3
|
+
import { clone } from './internal/reactivity.js';
|
|
4
|
+
// Module-level active sveltinia is intentional: Sveltinia's ergonomic API lets stores
|
|
5
|
+
// resolve their root implicitly via `defineStore(id, ...)()` without passing
|
|
6
|
+
// the root every time. Only one sveltinia is active at a time per module instance.
|
|
7
|
+
let activeSveltinia;
|
|
8
|
+
export const setActiveSveltinia = (sveltinia) => {
|
|
9
|
+
activeSveltinia = sveltinia;
|
|
10
|
+
};
|
|
11
|
+
export const getActiveSveltinia = () => activeSveltinia;
|
|
12
|
+
export function createSveltinia(options = {}) {
|
|
13
|
+
const sveltinia = {
|
|
14
|
+
state: clone(options.state ?? {}),
|
|
15
|
+
_stores: new Map(),
|
|
16
|
+
_plugins: [],
|
|
17
|
+
_options: options,
|
|
18
|
+
use(plugin) {
|
|
19
|
+
this._plugins.push(plugin);
|
|
20
|
+
return this;
|
|
21
|
+
},
|
|
22
|
+
install(app) {
|
|
23
|
+
setActiveSveltinia(this);
|
|
24
|
+
app?.provide(SVELTINIA_PROVISION_KEY, this);
|
|
25
|
+
},
|
|
26
|
+
dispose() {
|
|
27
|
+
this.forEachStore((store) => store.$dispose());
|
|
28
|
+
this._stores.clear();
|
|
29
|
+
},
|
|
30
|
+
registerStore(id, store, state) {
|
|
31
|
+
this.state[id] = state;
|
|
32
|
+
this._stores.set(id, store);
|
|
33
|
+
},
|
|
34
|
+
unregisterStore(id) {
|
|
35
|
+
this._stores.delete(id);
|
|
36
|
+
},
|
|
37
|
+
getStore(id) {
|
|
38
|
+
return this._stores.get(id);
|
|
39
|
+
},
|
|
40
|
+
option(name) {
|
|
41
|
+
return this._options[name];
|
|
42
|
+
},
|
|
43
|
+
forEachStore(callback) {
|
|
44
|
+
for (const store of this._stores.values())
|
|
45
|
+
callback(store);
|
|
46
|
+
},
|
|
47
|
+
pluginContext(store, options) {
|
|
48
|
+
return { sveltinia: this, store, options };
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
return sveltinia;
|
|
52
|
+
}
|
|
53
|
+
export const state = (value) => ({
|
|
54
|
+
__sveltiniaCell: CELL_KIND.STATE,
|
|
55
|
+
value,
|
|
56
|
+
});
|
|
57
|
+
export const computed = (fn) => ({
|
|
58
|
+
__sveltiniaCell: CELL_KIND.COMPUTED,
|
|
59
|
+
get value() {
|
|
60
|
+
return fn();
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
export function defineStore(id, definition, options) {
|
|
64
|
+
return (sveltinia = activeSveltinia) => {
|
|
65
|
+
if (!sveltinia)
|
|
66
|
+
throw new Error(`No active Sveltinia for store "${id}". Pass a root or call setActiveSveltinia().`);
|
|
67
|
+
return createStore(id, sveltinia, definition, options);
|
|
68
|
+
};
|
|
69
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from './internal/types.js';
|
|
2
|
+
export { createSveltinia, defineStore, setActiveSveltinia, getActiveSveltinia, state, computed } from './core.js';
|
|
3
|
+
export { createPersistedState, browserStorage } from './plugins/persist.js';
|
|
4
|
+
export { createDebugPlugin } from './plugins/debug.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from './internal/types.js';
|
|
2
|
+
export { createSveltinia, defineStore, setActiveSveltinia, getActiveSveltinia, state, computed } from './core.js';
|
|
3
|
+
export { createPersistedState, browserStorage } from './plugins/persist.js';
|
|
4
|
+
export { createDebugPlugin } from './plugins/debug.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Clock } from './util.js';
|
|
2
|
+
import type { ActionSubscription, DebuggableStore } from './types.js';
|
|
3
|
+
type ActionFn = (...args: unknown[]) => unknown;
|
|
4
|
+
export declare function instrumentAction(name: string, action: ActionFn, store: DebuggableStore & Record<PropertyKey, unknown>, storeId: string, actionSubscribers: Set<ActionSubscription>, clock: Clock): (...args: unknown[]) => unknown;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { DEBUG_KIND } from './constants.js';
|
|
2
|
+
import { noopDebugEmitter } from './util.js';
|
|
3
|
+
export function instrumentAction(name, action, store, storeId, actionSubscribers, clock) {
|
|
4
|
+
return (...args) => {
|
|
5
|
+
const afterCallbacks = [];
|
|
6
|
+
const onErrorCallbacks = [];
|
|
7
|
+
const start = clock();
|
|
8
|
+
const actionContext = {
|
|
9
|
+
name,
|
|
10
|
+
args,
|
|
11
|
+
store: store,
|
|
12
|
+
after: (cb) => afterCallbacks.push(cb),
|
|
13
|
+
onError: (cb) => onErrorCallbacks.push(cb),
|
|
14
|
+
};
|
|
15
|
+
actionSubscribers.forEach((cb) => cb(actionContext));
|
|
16
|
+
const emitDebug = store._emitDebug ?? noopDebugEmitter;
|
|
17
|
+
const notifyActionSuccess = (value) => {
|
|
18
|
+
afterCallbacks.forEach((cb) => cb(value));
|
|
19
|
+
emitDebug({
|
|
20
|
+
kind: DEBUG_KIND.ACTION,
|
|
21
|
+
storeId,
|
|
22
|
+
name,
|
|
23
|
+
duration: clock() - start,
|
|
24
|
+
});
|
|
25
|
+
return value;
|
|
26
|
+
};
|
|
27
|
+
const notifyActionError = (error) => {
|
|
28
|
+
onErrorCallbacks.forEach((cb) => cb(error));
|
|
29
|
+
emitDebug({
|
|
30
|
+
kind: DEBUG_KIND.ACTION,
|
|
31
|
+
storeId,
|
|
32
|
+
name,
|
|
33
|
+
duration: clock() - start,
|
|
34
|
+
error,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
const result = action.apply(store, args);
|
|
39
|
+
return result instanceof Promise
|
|
40
|
+
? result.then(notifyActionSuccess, (error) => {
|
|
41
|
+
notifyActionError(error);
|
|
42
|
+
throw error;
|
|
43
|
+
})
|
|
44
|
+
: notifyActionSuccess(result);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
notifyActionError(error);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare const SVELTINIA_PROVISION_KEY = "sveltinia";
|
|
2
|
+
export declare const CELL_KIND: {
|
|
3
|
+
readonly STATE: "state";
|
|
4
|
+
readonly COMPUTED: "computed";
|
|
5
|
+
};
|
|
6
|
+
export declare const MUTATION_TYPE: {
|
|
7
|
+
readonly DIRECT: "direct";
|
|
8
|
+
readonly PATCH_OBJECT: "patch object";
|
|
9
|
+
readonly PATCH_FUNCTION: "patch function";
|
|
10
|
+
readonly HYDRATE: "hydrate";
|
|
11
|
+
readonly RESTORE: "restore";
|
|
12
|
+
};
|
|
13
|
+
export declare const DEBUG_KIND: {
|
|
14
|
+
readonly MUTATION: "mutation";
|
|
15
|
+
readonly ACTION: "action";
|
|
16
|
+
readonly PERSISTENCE: "persistence";
|
|
17
|
+
readonly LIFECYCLE: "lifecycle";
|
|
18
|
+
};
|
|
19
|
+
export declare const REDACTED = "[redacted]";
|
|
20
|
+
export declare const DEFAULT_PERSIST_VERSION = 1;
|
|
21
|
+
export declare const LEGACY_PERSIST_VERSION = 0;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export const SVELTINIA_PROVISION_KEY = 'sveltinia';
|
|
2
|
+
export const CELL_KIND = {
|
|
3
|
+
STATE: 'state',
|
|
4
|
+
COMPUTED: 'computed',
|
|
5
|
+
};
|
|
6
|
+
export const MUTATION_TYPE = {
|
|
7
|
+
DIRECT: 'direct',
|
|
8
|
+
PATCH_OBJECT: 'patch object',
|
|
9
|
+
PATCH_FUNCTION: 'patch function',
|
|
10
|
+
HYDRATE: 'hydrate',
|
|
11
|
+
RESTORE: 'restore',
|
|
12
|
+
};
|
|
13
|
+
export const DEBUG_KIND = {
|
|
14
|
+
MUTATION: 'mutation',
|
|
15
|
+
ACTION: 'action',
|
|
16
|
+
PERSISTENCE: 'persistence',
|
|
17
|
+
LIFECYCLE: 'lifecycle',
|
|
18
|
+
};
|
|
19
|
+
export const REDACTED = '[redacted]';
|
|
20
|
+
export const DEFAULT_PERSIST_VERSION = 1;
|
|
21
|
+
export const LEGACY_PERSIST_VERSION = 0;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { StateTree } from './types.js';
|
|
2
|
+
export declare const isObject: (value: unknown) => value is Record<PropertyKey, unknown>;
|
|
3
|
+
export declare const isSafeKey: (key: PropertyKey) => boolean;
|
|
4
|
+
export declare const clone: <T>(value: T) => T;
|
|
5
|
+
export declare function merge(target: StateTree, patch: StateTree): StateTree;
|
|
6
|
+
export declare function makeObservable<T extends StateTree>(value: T, notify: (path: string, oldValue: unknown, newValue: unknown) => void, base?: string): T;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export const isObject = (value) => value !== null && typeof value === 'object';
|
|
2
|
+
const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
|
|
3
|
+
export const isSafeKey = (key) => typeof key !== 'string' || !UNSAFE_KEYS.has(key);
|
|
4
|
+
export const clone = (value) => {
|
|
5
|
+
if (value === undefined)
|
|
6
|
+
return value;
|
|
7
|
+
try {
|
|
8
|
+
return structuredClone(value);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return JSON.parse(JSON.stringify(value));
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
export function merge(target, patch) {
|
|
15
|
+
for (const key of Object.keys(patch)) {
|
|
16
|
+
if (!isSafeKey(key))
|
|
17
|
+
continue;
|
|
18
|
+
const source = patch[key];
|
|
19
|
+
if (isObject(source) &&
|
|
20
|
+
!Array.isArray(source) &&
|
|
21
|
+
isObject(target[key]) &&
|
|
22
|
+
!Array.isArray(target[key]))
|
|
23
|
+
merge(target[key], source);
|
|
24
|
+
else
|
|
25
|
+
target[key] = clone(source);
|
|
26
|
+
}
|
|
27
|
+
return target;
|
|
28
|
+
}
|
|
29
|
+
export function makeObservable(value, notify, base = '') {
|
|
30
|
+
if (!isObject(value))
|
|
31
|
+
return value;
|
|
32
|
+
const target = value;
|
|
33
|
+
for (const key of Object.keys(target)) {
|
|
34
|
+
if (!isSafeKey(key)) {
|
|
35
|
+
delete target[key];
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
target[key] = makeObservable(target[key], notify, base ? `${base}.${key}` : key);
|
|
39
|
+
}
|
|
40
|
+
return new Proxy(value, {
|
|
41
|
+
set(target, prop, next, receiver) {
|
|
42
|
+
if (!isSafeKey(prop))
|
|
43
|
+
return false;
|
|
44
|
+
const path = base ? `${base}.${String(prop)}` : String(prop);
|
|
45
|
+
const old = Reflect.get(target, prop, receiver);
|
|
46
|
+
const wrapped = makeObservable(next, notify, path);
|
|
47
|
+
const ok = Reflect.set(target, prop, wrapped, receiver);
|
|
48
|
+
if (ok && !Object.is(old, next))
|
|
49
|
+
notify(path, old, next);
|
|
50
|
+
return ok;
|
|
51
|
+
},
|
|
52
|
+
deleteProperty(target, prop) {
|
|
53
|
+
if (!isSafeKey(prop))
|
|
54
|
+
return false;
|
|
55
|
+
const path = base ? `${base}.${String(prop)}` : String(prop);
|
|
56
|
+
const old = target[prop];
|
|
57
|
+
const ok = Reflect.deleteProperty(target, prop);
|
|
58
|
+
if (ok)
|
|
59
|
+
notify(path, old, undefined);
|
|
60
|
+
return ok;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type Clock } from './util.js';
|
|
2
|
+
import type { DefineStoreOptions, Sveltinia, SetupStore, StateTree, Store } from './types.js';
|
|
3
|
+
export declare function createStore<S extends StateTree>(id: string, sveltinia: Sveltinia, definition: DefineStoreOptions<S> | SetupStore, setupOptions?: DefineStoreOptions | Record<string, unknown>, clock?: Clock): Store<S>;
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { instrumentAction } from './action.js';
|
|
2
|
+
import { CELL_KIND, DEBUG_KIND, MUTATION_TYPE } from './constants.js';
|
|
3
|
+
import { clone, makeObservable, merge } from './reactivity.js';
|
|
4
|
+
import { defaultClock, noopDebugEmitter } from './util.js';
|
|
5
|
+
const isStateCell = (value) => value?.__sveltiniaCell === CELL_KIND.STATE;
|
|
6
|
+
const isComputedCell = (value) => value?.__sveltiniaCell === CELL_KIND.COMPUTED;
|
|
7
|
+
function defineStateAccessor(store, key, reactiveState) {
|
|
8
|
+
Object.defineProperty(store, key, {
|
|
9
|
+
enumerable: true,
|
|
10
|
+
get: () => reactiveState[key],
|
|
11
|
+
set: (value) => {
|
|
12
|
+
reactiveState[key] = value;
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function defineGetter(store, name, getter, reactiveState) {
|
|
17
|
+
Object.defineProperty(store, name, {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
get: () => getter.call(store, reactiveState),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function createStoreShell(id, sveltinia, initialState, clock) {
|
|
23
|
+
let pendingBatch;
|
|
24
|
+
let disposed = false;
|
|
25
|
+
const subscribers = new Set();
|
|
26
|
+
const actionSubscribers = new Set();
|
|
27
|
+
const emitMutation = (mutation) => {
|
|
28
|
+
for (const callback of subscribers)
|
|
29
|
+
callback(mutation, reactiveState);
|
|
30
|
+
sveltinia.state[id] = reactiveState;
|
|
31
|
+
store._emitDebug({ kind: DEBUG_KIND.MUTATION, storeId: id, mutation });
|
|
32
|
+
};
|
|
33
|
+
const notify = (path, oldValue, newValue) => {
|
|
34
|
+
if (disposed)
|
|
35
|
+
return;
|
|
36
|
+
const event = { path, oldValue: clone(oldValue), newValue: clone(newValue) };
|
|
37
|
+
if (pendingBatch) {
|
|
38
|
+
pendingBatch.events.push(event);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
emitMutation({ type: MUTATION_TYPE.DIRECT, storeId: id, events: [event] });
|
|
42
|
+
};
|
|
43
|
+
const reactiveState = makeObservable(clone(initialState), notify);
|
|
44
|
+
const store = { $id: id, _emitDebug: noopDebugEmitter };
|
|
45
|
+
Object.defineProperty(store, '$state', {
|
|
46
|
+
get: () => reactiveState,
|
|
47
|
+
set: (value) => store.$patch(value),
|
|
48
|
+
});
|
|
49
|
+
Object.keys(reactiveState).forEach((key) => defineStateAccessor(store, key, reactiveState));
|
|
50
|
+
store.$patch = (patch) => {
|
|
51
|
+
const type = typeof patch === 'function' ? MUTATION_TYPE.PATCH_FUNCTION : MUTATION_TYPE.PATCH_OBJECT;
|
|
52
|
+
pendingBatch = {
|
|
53
|
+
type,
|
|
54
|
+
storeId: id,
|
|
55
|
+
payload: typeof patch === 'function' ? undefined : clone(patch),
|
|
56
|
+
events: [],
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
if (typeof patch === 'function')
|
|
60
|
+
patch(reactiveState);
|
|
61
|
+
else
|
|
62
|
+
merge(reactiveState, patch);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
const mutation = pendingBatch;
|
|
66
|
+
pendingBatch = undefined;
|
|
67
|
+
if (mutation)
|
|
68
|
+
emitMutation(mutation);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
store.$subscribe = (callback) => {
|
|
72
|
+
subscribers.add(callback);
|
|
73
|
+
return () => subscribers.delete(callback);
|
|
74
|
+
};
|
|
75
|
+
store.$onAction = (callback) => {
|
|
76
|
+
actionSubscribers.add(callback);
|
|
77
|
+
return () => actionSubscribers.delete(callback);
|
|
78
|
+
};
|
|
79
|
+
store.$dispose = () => {
|
|
80
|
+
disposed = true;
|
|
81
|
+
subscribers.clear();
|
|
82
|
+
actionSubscribers.clear();
|
|
83
|
+
sveltinia.unregisterStore(id);
|
|
84
|
+
store._emitDebug({ kind: DEBUG_KIND.LIFECYCLE, storeId: id, name: 'dispose' });
|
|
85
|
+
};
|
|
86
|
+
const bindAction = (name, action) => {
|
|
87
|
+
store[name] = instrumentAction(name, action, store, id, actionSubscribers, clock);
|
|
88
|
+
};
|
|
89
|
+
const bindGetter = (name, getter) => {
|
|
90
|
+
defineGetter(store, name, getter, reactiveState);
|
|
91
|
+
};
|
|
92
|
+
const bindStateCell = (name, cell) => {
|
|
93
|
+
reactiveState[name] = makeObservable(clone(cell.value), notify, name);
|
|
94
|
+
// Wire the user's cell `.value` to the reactive state so `cell.value++` inside
|
|
95
|
+
// a setup store reads from and writes to the tracked proxy. This in-place
|
|
96
|
+
// redirect is intentional: the cell is the user-facing handle, the proxy is
|
|
97
|
+
// the source of truth.
|
|
98
|
+
Object.defineProperty(cell, 'value', {
|
|
99
|
+
configurable: true,
|
|
100
|
+
get: () => reactiveState[name],
|
|
101
|
+
set: (value) => {
|
|
102
|
+
reactiveState[name] = value;
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
defineStateAccessor(store, name, reactiveState);
|
|
106
|
+
};
|
|
107
|
+
const bindComputedCell = (name, cell) => {
|
|
108
|
+
Object.defineProperty(store, name, {
|
|
109
|
+
enumerable: true,
|
|
110
|
+
get: () => cell.value,
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
const bindPlainStateValue = (name, value) => {
|
|
114
|
+
reactiveState[name] = makeObservable(clone(value), notify, name);
|
|
115
|
+
defineStateAccessor(store, name, reactiveState);
|
|
116
|
+
};
|
|
117
|
+
return {
|
|
118
|
+
store,
|
|
119
|
+
reactiveState,
|
|
120
|
+
bindAction,
|
|
121
|
+
bindGetter,
|
|
122
|
+
bindStateCell,
|
|
123
|
+
bindComputedCell,
|
|
124
|
+
bindPlainStateValue,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const optionsStrategy = {
|
|
128
|
+
extractInitialState(definition, sveltinia, id) {
|
|
129
|
+
const opts = definition;
|
|
130
|
+
return sveltinia.state[id] ?? opts.state();
|
|
131
|
+
},
|
|
132
|
+
populate(definition, _id, shell) {
|
|
133
|
+
const opts = definition;
|
|
134
|
+
const { store, bindAction, bindGetter } = shell;
|
|
135
|
+
for (const [name, getter] of Object.entries(opts.getters ?? {}))
|
|
136
|
+
bindGetter(name, getter);
|
|
137
|
+
for (const [name, action] of Object.entries(opts.actions ?? {}))
|
|
138
|
+
bindAction(name, action);
|
|
139
|
+
store.$reset = () => store.$patch(clone(opts.state()));
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
const setupStrategy = {
|
|
143
|
+
extractInitialState() {
|
|
144
|
+
return {};
|
|
145
|
+
},
|
|
146
|
+
populate(definition, id, shell) {
|
|
147
|
+
const setup = definition;
|
|
148
|
+
const { store, bindAction, bindStateCell, bindComputedCell, bindPlainStateValue } = shell;
|
|
149
|
+
const setupResult = setup();
|
|
150
|
+
for (const [name, value] of Object.entries(setupResult)) {
|
|
151
|
+
if (isStateCell(value))
|
|
152
|
+
bindStateCell(name, value);
|
|
153
|
+
else if (isComputedCell(value))
|
|
154
|
+
bindComputedCell(name, value);
|
|
155
|
+
else if (typeof value === 'function')
|
|
156
|
+
bindAction(name, value);
|
|
157
|
+
else
|
|
158
|
+
bindPlainStateValue(name, value);
|
|
159
|
+
}
|
|
160
|
+
store.$reset = () => {
|
|
161
|
+
throw new Error(`Setup store "${id}" must expose its own reset action.`);
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
const strategies = {
|
|
166
|
+
options: optionsStrategy,
|
|
167
|
+
setup: setupStrategy,
|
|
168
|
+
};
|
|
169
|
+
function applyPlugins(sveltinia, store, options) {
|
|
170
|
+
for (const plugin of sveltinia._plugins)
|
|
171
|
+
Object.assign(store, plugin(sveltinia.pluginContext(store, options)) ?? {});
|
|
172
|
+
}
|
|
173
|
+
export function createStore(id, sveltinia, definition, setupOptions = {}, clock = defaultClock) {
|
|
174
|
+
const existing = sveltinia.getStore(id);
|
|
175
|
+
if (existing)
|
|
176
|
+
return existing;
|
|
177
|
+
const kind = typeof definition === 'function' ? 'setup' : 'options';
|
|
178
|
+
const strategy = strategies[kind];
|
|
179
|
+
const options = kind === 'options' ? definition : setupOptions;
|
|
180
|
+
const initialState = strategy.extractInitialState(definition, sveltinia, id);
|
|
181
|
+
const shell = createStoreShell(id, sveltinia, initialState, clock);
|
|
182
|
+
strategy.populate(definition, id, shell);
|
|
183
|
+
sveltinia.registerStore(id, shell.store, shell.reactiveState);
|
|
184
|
+
applyPlugins(sveltinia, shell.store, options);
|
|
185
|
+
return shell.store;
|
|
186
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
export type StateTree = Record<string, unknown>;
|
|
2
|
+
export type MutationType = 'direct' | 'patch object' | 'patch function' | 'hydrate' | 'restore';
|
|
3
|
+
export interface Mutation {
|
|
4
|
+
type: MutationType;
|
|
5
|
+
storeId: string;
|
|
6
|
+
events?: Array<{
|
|
7
|
+
path: string;
|
|
8
|
+
oldValue: unknown;
|
|
9
|
+
newValue: unknown;
|
|
10
|
+
}>;
|
|
11
|
+
payload?: unknown;
|
|
12
|
+
}
|
|
13
|
+
export type Subscription = (mutation: Mutation, state: StateTree) => void;
|
|
14
|
+
export interface ActionContext {
|
|
15
|
+
name: string;
|
|
16
|
+
args: unknown[];
|
|
17
|
+
store: Store;
|
|
18
|
+
after(cb: (value: unknown) => void): void;
|
|
19
|
+
onError(cb: (error: unknown) => void): void;
|
|
20
|
+
}
|
|
21
|
+
export type ActionSubscription = (context: ActionContext) => void;
|
|
22
|
+
export interface StorageAdapter {
|
|
23
|
+
getItem(key: string): string | null | Promise<string | null>;
|
|
24
|
+
setItem(key: string, value: string): void | Promise<void>;
|
|
25
|
+
removeItem?(key: string): void | Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
export type Serializer = {
|
|
28
|
+
serialize(v: unknown): string;
|
|
29
|
+
deserialize(v: string): unknown;
|
|
30
|
+
};
|
|
31
|
+
export interface PersistOptions {
|
|
32
|
+
enabled?: boolean;
|
|
33
|
+
key?: string;
|
|
34
|
+
storage?: 'localStorage' | 'sessionStorage' | StorageAdapter;
|
|
35
|
+
paths?: string[];
|
|
36
|
+
version?: number;
|
|
37
|
+
migrate?: (state: StateTree, fromVersion: number) => StateTree | Promise<StateTree>;
|
|
38
|
+
serializer?: Serializer;
|
|
39
|
+
lazy?: boolean;
|
|
40
|
+
beforeRestore?: (ctx: {
|
|
41
|
+
store: Store;
|
|
42
|
+
}) => void;
|
|
43
|
+
afterRestore?: (ctx: {
|
|
44
|
+
store: Store;
|
|
45
|
+
}) => void;
|
|
46
|
+
}
|
|
47
|
+
export interface DebugEvent {
|
|
48
|
+
kind: 'mutation' | 'action' | 'persistence' | 'lifecycle';
|
|
49
|
+
storeId: string;
|
|
50
|
+
name?: string;
|
|
51
|
+
mutation?: Mutation;
|
|
52
|
+
duration?: number;
|
|
53
|
+
error?: unknown;
|
|
54
|
+
detail?: unknown;
|
|
55
|
+
}
|
|
56
|
+
export interface DebugOptions {
|
|
57
|
+
enabled?: boolean;
|
|
58
|
+
logger?: (event: DebugEvent) => void;
|
|
59
|
+
redact?: string[];
|
|
60
|
+
}
|
|
61
|
+
export interface SveltiniaOptions {
|
|
62
|
+
state?: Record<string, StateTree>;
|
|
63
|
+
debug?: boolean | DebugOptions;
|
|
64
|
+
persist?: PersistOptions;
|
|
65
|
+
}
|
|
66
|
+
export interface DefineStoreOptions<S extends StateTree = StateTree> {
|
|
67
|
+
state: () => S;
|
|
68
|
+
getters?: Record<string, (this: Store<S>, state: S) => unknown>;
|
|
69
|
+
actions?: Record<string, (...args: unknown[]) => unknown>;
|
|
70
|
+
persist?: boolean | PersistOptions;
|
|
71
|
+
debug?: boolean | DebugOptions;
|
|
72
|
+
}
|
|
73
|
+
export interface SetupCell<T> {
|
|
74
|
+
readonly __sveltiniaCell: 'state';
|
|
75
|
+
value: T;
|
|
76
|
+
}
|
|
77
|
+
export interface ComputedCell<T> {
|
|
78
|
+
readonly __sveltiniaCell: 'computed';
|
|
79
|
+
readonly value: T;
|
|
80
|
+
}
|
|
81
|
+
export type SetupStore = () => Record<string, unknown>;
|
|
82
|
+
export interface ReadableStore<S extends StateTree = StateTree> {
|
|
83
|
+
$id: string;
|
|
84
|
+
$state: S;
|
|
85
|
+
$subscribe(cb: Subscription): () => void;
|
|
86
|
+
$onAction(cb: ActionSubscription): () => void;
|
|
87
|
+
}
|
|
88
|
+
export interface WritableStore<S extends StateTree = StateTree> {
|
|
89
|
+
$patch(patch: Partial<S> | ((state: S) => void)): void;
|
|
90
|
+
$reset(): void;
|
|
91
|
+
}
|
|
92
|
+
export interface PersistableStore {
|
|
93
|
+
$persist(): Promise<void>;
|
|
94
|
+
$restore(): Promise<void>;
|
|
95
|
+
$restored: Promise<void>;
|
|
96
|
+
}
|
|
97
|
+
export interface DisposableStore {
|
|
98
|
+
$dispose(): void;
|
|
99
|
+
}
|
|
100
|
+
export interface DebuggableStore {
|
|
101
|
+
_emitDebug?: (event: DebugEvent) => void;
|
|
102
|
+
}
|
|
103
|
+
export interface StoreExtensions {
|
|
104
|
+
}
|
|
105
|
+
export type Store<S extends StateTree = StateTree> = S & ReadableStore<S> & WritableStore<S> & DisposableStore & DebuggableStore & StoreExtensions;
|
|
106
|
+
export type SveltiniaPlugin = (ctx: SveltiniaPluginContext) => void | Record<string, unknown>;
|
|
107
|
+
export interface SveltiniaPluginContext {
|
|
108
|
+
sveltinia: Sveltinia;
|
|
109
|
+
store: Store;
|
|
110
|
+
options: DefineStoreOptions | Record<string, unknown>;
|
|
111
|
+
}
|
|
112
|
+
export interface App {
|
|
113
|
+
provide(key: string | symbol, value: unknown): void;
|
|
114
|
+
}
|
|
115
|
+
export interface SveltiniaPublic {
|
|
116
|
+
state: Record<string, StateTree>;
|
|
117
|
+
use(plugin: SveltiniaPlugin): Sveltinia;
|
|
118
|
+
install(app?: App): void;
|
|
119
|
+
dispose(): void;
|
|
120
|
+
option<K extends keyof SveltiniaOptions>(name: K): SveltiniaOptions[K] | undefined;
|
|
121
|
+
forEachStore(callback: (store: Store) => void): void;
|
|
122
|
+
}
|
|
123
|
+
export interface Sveltinia extends SveltiniaPublic {
|
|
124
|
+
_stores: Map<string, Store>;
|
|
125
|
+
_plugins: SveltiniaPlugin[];
|
|
126
|
+
_options: SveltiniaOptions;
|
|
127
|
+
registerStore(id: string, store: Store, state: StateTree): void;
|
|
128
|
+
unregisterStore(id: string): void;
|
|
129
|
+
getStore(id: string): Store | undefined;
|
|
130
|
+
pluginContext(store: Store, options: DefineStoreOptions | Record<string, unknown>): SveltiniaPluginContext;
|
|
131
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { DebugEvent } from './types.js';
|
|
2
|
+
export type Clock = () => number;
|
|
3
|
+
export declare const defaultClock: Clock;
|
|
4
|
+
export type DebugEmitter = (event: DebugEvent) => void;
|
|
5
|
+
export declare const noopDebugEmitter: DebugEmitter;
|
|
6
|
+
export declare function flagToObject<T extends {
|
|
7
|
+
enabled?: boolean;
|
|
8
|
+
}>(value: boolean | T | undefined): T | undefined;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { DEBUG_KIND, REDACTED } from '../internal/constants.js';
|
|
2
|
+
import { flagToObject } from '../internal/util.js';
|
|
3
|
+
export function createDebugPlugin(defaults = {}) {
|
|
4
|
+
return ({ sveltinia, store, options }) => {
|
|
5
|
+
const root = flagToObject(sveltinia.option('debug'));
|
|
6
|
+
const local = flagToObject(options.debug);
|
|
7
|
+
const config = {
|
|
8
|
+
...defaults,
|
|
9
|
+
...root,
|
|
10
|
+
...local,
|
|
11
|
+
enabled: local?.enabled ?? root?.enabled ?? defaults.enabled ?? false,
|
|
12
|
+
};
|
|
13
|
+
if (!config.enabled)
|
|
14
|
+
return;
|
|
15
|
+
const logger = config.logger ?? consoleDebug;
|
|
16
|
+
const previousEmitter = store._emitDebug;
|
|
17
|
+
// Chain with any existing emitter so multiple debug plugins stack instead
|
|
18
|
+
// of overwriting each other.
|
|
19
|
+
store._emitDebug = (event) => {
|
|
20
|
+
previousEmitter?.(event);
|
|
21
|
+
logger(redact(event, config.redact ?? []));
|
|
22
|
+
};
|
|
23
|
+
store._emitDebug({ kind: DEBUG_KIND.LIFECYCLE, storeId: store.$id, name: 'create' });
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function redact(event, paths) {
|
|
27
|
+
if (!paths.length || !event.mutation?.events)
|
|
28
|
+
return event;
|
|
29
|
+
return {
|
|
30
|
+
...event,
|
|
31
|
+
mutation: {
|
|
32
|
+
...event.mutation,
|
|
33
|
+
events: event.mutation.events.map((e) => paths.some((p) => e.path === p || e.path.startsWith(`${p}.`))
|
|
34
|
+
? { ...e, oldValue: REDACTED, newValue: REDACTED }
|
|
35
|
+
: e),
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function consoleDebug(event) {
|
|
40
|
+
const label = `[sveltinia] ${event.storeId}${event.name ? `/${event.name}` : ''}`;
|
|
41
|
+
console.groupCollapsed?.(label);
|
|
42
|
+
console.log(event);
|
|
43
|
+
console.groupEnd?.();
|
|
44
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PersistOptions, SveltiniaPluginContext, StorageAdapter } from '../internal/types.js';
|
|
2
|
+
declare module '../internal/types.js' {
|
|
3
|
+
interface StoreExtensions extends PersistableStore {
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
export declare const browserStorage: (kind?: "localStorage" | "sessionStorage") => StorageAdapter | undefined;
|
|
7
|
+
export declare function createPersistedState(defaults?: PersistOptions): ({ sveltinia, store, options }: SveltiniaPluginContext) => void;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { clone, isSafeKey, merge } from '../internal/reactivity.js';
|
|
2
|
+
import { DEBUG_KIND, DEFAULT_PERSIST_VERSION, LEGACY_PERSIST_VERSION, } from '../internal/constants.js';
|
|
3
|
+
import { flagToObject } from '../internal/util.js';
|
|
4
|
+
const JSON_SERIALIZER = { serialize: JSON.stringify, deserialize: JSON.parse };
|
|
5
|
+
export const browserStorage = (kind = 'localStorage') => {
|
|
6
|
+
if (typeof window === 'undefined')
|
|
7
|
+
return undefined;
|
|
8
|
+
return window[kind];
|
|
9
|
+
};
|
|
10
|
+
function resolvePersistConfig(value, root) {
|
|
11
|
+
const local = flagToObject(value);
|
|
12
|
+
if (!local && !root?.enabled)
|
|
13
|
+
return undefined;
|
|
14
|
+
return { ...root, ...local, enabled: local?.enabled ?? root?.enabled ?? true };
|
|
15
|
+
}
|
|
16
|
+
function resolveStorage(config) {
|
|
17
|
+
if (typeof config.storage === 'object')
|
|
18
|
+
return config.storage;
|
|
19
|
+
if (config.storage !== undefined && config.storage !== 'localStorage' && config.storage !== 'sessionStorage')
|
|
20
|
+
throw new Error(`Unsupported persistence storage "${config.storage}". Use browserStorage('localStorage'), browserStorage('sessionStorage'), or a custom adapter.`);
|
|
21
|
+
return browserStorage(config.storage ?? 'localStorage');
|
|
22
|
+
}
|
|
23
|
+
function pickPaths(state, paths) {
|
|
24
|
+
if (!paths?.length)
|
|
25
|
+
return clone(state);
|
|
26
|
+
const out = {};
|
|
27
|
+
for (const path of paths) {
|
|
28
|
+
const segments = path.split('.');
|
|
29
|
+
let source = state;
|
|
30
|
+
let destination = out;
|
|
31
|
+
for (let i = 0; i < segments.length; i++) {
|
|
32
|
+
const segment = segments[i];
|
|
33
|
+
if (!isSafeKey(segment) || !Object.hasOwn(source, segment))
|
|
34
|
+
break;
|
|
35
|
+
if (i === segments.length - 1) {
|
|
36
|
+
destination[segment] = clone(source[segment]);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
destination[segment] ||= {};
|
|
40
|
+
destination = destination[segment];
|
|
41
|
+
source = source[segment];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
function installPersistence(store, config) {
|
|
48
|
+
const storage = resolveStorage(config);
|
|
49
|
+
if (!storage)
|
|
50
|
+
return;
|
|
51
|
+
const key = config.key ?? `sveltinia:${store.$id}`;
|
|
52
|
+
const serializer = config.serializer ?? JSON_SERIALIZER;
|
|
53
|
+
let restoring = false;
|
|
54
|
+
store.$restore = async () => {
|
|
55
|
+
config.beforeRestore?.({ store });
|
|
56
|
+
const value = await storage.getItem(key);
|
|
57
|
+
if (value) {
|
|
58
|
+
const envelope = serializer.deserialize(value);
|
|
59
|
+
let incoming = envelope.state ?? envelope;
|
|
60
|
+
if (config.version !== undefined && envelope.version !== config.version && config.migrate)
|
|
61
|
+
incoming = await config.migrate(incoming, envelope.version ?? LEGACY_PERSIST_VERSION);
|
|
62
|
+
restoring = true;
|
|
63
|
+
try {
|
|
64
|
+
store.$patch((state) => merge(state, incoming));
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
restoring = false;
|
|
68
|
+
}
|
|
69
|
+
store._emitDebug?.({
|
|
70
|
+
kind: DEBUG_KIND.PERSISTENCE,
|
|
71
|
+
storeId: store.$id,
|
|
72
|
+
name: 'restore',
|
|
73
|
+
detail: key,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
config.afterRestore?.({ store });
|
|
77
|
+
};
|
|
78
|
+
store.$persist = async () => {
|
|
79
|
+
const state = pickPaths(store.$state, config.paths);
|
|
80
|
+
await storage.setItem(key, serializer.serialize({ version: config.version ?? DEFAULT_PERSIST_VERSION, state }));
|
|
81
|
+
store._emitDebug?.({
|
|
82
|
+
kind: DEBUG_KIND.PERSISTENCE,
|
|
83
|
+
storeId: store.$id,
|
|
84
|
+
name: 'write',
|
|
85
|
+
detail: key,
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
store.$subscribe(() => {
|
|
89
|
+
if (!restoring)
|
|
90
|
+
void store.$persist();
|
|
91
|
+
});
|
|
92
|
+
if (config.lazy === false) {
|
|
93
|
+
const restoredPromise = store.$restore();
|
|
94
|
+
store.$restored = restoredPromise;
|
|
95
|
+
restoredPromise.catch((error) => {
|
|
96
|
+
console.error(`[sveltinia] persistence restore failed for "${store.$id}"`, error);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
store.$restored = Promise.resolve();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export function createPersistedState(defaults = {}) {
|
|
104
|
+
return ({ sveltinia, store, options }) => {
|
|
105
|
+
const rootConfig = sveltinia.option('persist');
|
|
106
|
+
const config = resolvePersistConfig(options.persist, rootConfig ? { ...defaults, ...rootConfig } : defaults);
|
|
107
|
+
if (!config?.enabled)
|
|
108
|
+
return;
|
|
109
|
+
installPersistence(store, config);
|
|
110
|
+
};
|
|
111
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sveltinia",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Typed stores for Svelte and SvelteKit",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"homepage": "https://dene-.github.io/sveltinia/",
|
|
9
|
+
"repository": {"type":"git","url":"git+https://github.com/dene-/sveltinia.git","directory":"packages/sveltinia"},
|
|
10
|
+
"bugs": {"url":"https://github.com/dene-/sveltinia/issues"},
|
|
11
|
+
"keywords": ["svelte", "svelte5", "sveltekit", "svelte-pinia", "sveltekit-pinia", "pinia", "pinia-alternative", "state-management", "svelte-state-management", "svelte-stores", "persisted-stores", "persisted-svelte-stores", "ssr", "sveltekit-ssr"],
|
|
12
|
+
"main": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {"types":"./dist/index.d.ts","default":"./dist/index.js"},
|
|
16
|
+
"./svelte": {"types":"./dist/adapters/svelte.d.ts","default":"./dist/adapters/svelte.js"},
|
|
17
|
+
"./sveltekit": {"types":"./dist/adapters/sveltekit.d.ts","default":"./dist/adapters/sveltekit.js"},
|
|
18
|
+
"./persist": {"types":"./dist/plugins/persist.d.ts","default":"./dist/plugins/persist.js"},
|
|
19
|
+
"./debug": {"types":"./dist/plugins/debug.d.ts","default":"./dist/plugins/debug.js"}
|
|
20
|
+
},
|
|
21
|
+
"files": ["dist", "README.md", "LICENSE"],
|
|
22
|
+
"publishConfig": {"access":"public"},
|
|
23
|
+
"scripts": {"build":"node ../../scripts/clean-dir.mjs dist && tsc -p tsconfig.json"},
|
|
24
|
+
"peerDependencies": {"svelte":">=5.40.0 <6"}
|
|
25
|
+
}
|