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 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
+ }
@@ -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,9 @@
1
+ export const defaultClock = () => performance.now();
2
+ export const noopDebugEmitter = () => { };
3
+ export function flagToObject(value) {
4
+ if (value === true)
5
+ return { enabled: true };
6
+ if (value === false)
7
+ return undefined;
8
+ return value;
9
+ }
@@ -0,0 +1,2 @@
1
+ import type { DebugOptions, SveltiniaPluginContext } from '../internal/types.js';
2
+ export declare function createDebugPlugin(defaults?: DebugOptions): ({ sveltinia, store, options }: SveltiniaPluginContext) => void;
@@ -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
+ }