@vesk/runtime 0.2.59 → 0.2.61
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/dist/path-store.d.ts +123 -0
- package/dist/path-store.d.ts.map +1 -0
- package/dist/path-store.js +966 -0
- package/dist/ripple-blocks.d.ts.map +1 -1
- package/dist/ripple-blocks.js +12 -1
- package/dist/ripple-runtime.d.ts +36 -0
- package/dist/ripple-runtime.d.ts.map +1 -1
- package/dist/ripple-runtime.js +120 -3
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +20 -5
- package/dist/store-persistence.d.ts +90 -0
- package/dist/store-persistence.d.ts.map +1 -0
- package/dist/store-persistence.js +154 -0
- package/dist/store.d.ts +61 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +275 -0
- package/package.json +23 -2
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
let serverReadHook = null;
|
|
2
|
+
/**
|
|
3
|
+
* Install server-side persistence hooks (called by the adapter). `read`
|
|
4
|
+
* returns the persisted raw JSON string (typically from a request cookie) or
|
|
5
|
+
* null. The hook is a module-level singleton that reads request-scoped state
|
|
6
|
+
* itself (e.g. `globalThis.__vesk_request`), so installing it once per module
|
|
7
|
+
* load is safe under concurrent requests.
|
|
8
|
+
*/
|
|
9
|
+
export function installPersistedStoreServerHooks(hooks) {
|
|
10
|
+
serverReadHook = typeof hooks.read === 'function' ? hooks.read : null;
|
|
11
|
+
}
|
|
12
|
+
/** The request token scoping persisted-key registrations, or '' outside a render. */
|
|
13
|
+
function currentSsrToken() {
|
|
14
|
+
const g = globalThis;
|
|
15
|
+
return typeof g.__vsk_ssr_token === 'string' ? g.__vsk_ssr_token : '';
|
|
16
|
+
}
|
|
17
|
+
/** The cookie name a persisted store is mirrored to, for SSR continuity. */
|
|
18
|
+
export function persistedStoreCookieName(storageKey) {
|
|
19
|
+
return 'vsk_store_' + encodeURIComponent(storageKey);
|
|
20
|
+
}
|
|
21
|
+
const pendingKeys = new Map();
|
|
22
|
+
function bucketFor(token) {
|
|
23
|
+
const set = pendingKeys.get(token);
|
|
24
|
+
if (set)
|
|
25
|
+
return set;
|
|
26
|
+
const fresh = new Set();
|
|
27
|
+
pendingKeys.set(token, fresh);
|
|
28
|
+
return fresh;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The storage keys of persisted stores constructed in this SSR render.
|
|
32
|
+
* `token` overrides the ambient request token for callers that drain the
|
|
33
|
+
* registry outside the render scope (the no-layout streaming path captures
|
|
34
|
+
* the render token at its `renderPageStream` call site and passes it here).
|
|
35
|
+
*/
|
|
36
|
+
export function getPersistedStoreKeys(token) {
|
|
37
|
+
const set = pendingKeys.get(token ?? currentSsrToken());
|
|
38
|
+
return set ? Array.from(set) : [];
|
|
39
|
+
}
|
|
40
|
+
/** Clear the constructed-key registry (called per SSR render by the adapter). */
|
|
41
|
+
export function resetPersistedStoreKeys(token) {
|
|
42
|
+
pendingKeys.delete(token ?? currentSsrToken());
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Read a persisted store's raw JSON for the current environment:
|
|
46
|
+
* - server: register the key (so the adapter emits the pre-paint script) and
|
|
47
|
+
* ask the installed read hook,
|
|
48
|
+
* - client: consume `window.__VESK_STORE_INIT__[key]` if present (then write
|
|
49
|
+
* it back to localStorage so a later client pickup still works), else read
|
|
50
|
+
* localStorage.
|
|
51
|
+
*/
|
|
52
|
+
export function readPersistedStorage(storageKey) {
|
|
53
|
+
if (!storageKey)
|
|
54
|
+
return null;
|
|
55
|
+
if (typeof window === 'undefined') {
|
|
56
|
+
bucketFor(currentSsrToken()).add(storageKey);
|
|
57
|
+
return serverReadHook ? (serverReadHook(storageKey) ?? null) : null;
|
|
58
|
+
}
|
|
59
|
+
const init = globalThis.__VESK_STORE_INIT__;
|
|
60
|
+
if (init && init[storageKey] !== undefined) {
|
|
61
|
+
const raw = init[storageKey];
|
|
62
|
+
delete init[storageKey];
|
|
63
|
+
try {
|
|
64
|
+
localStorage.setItem(storageKey, raw);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
/* storage blocked — the in-memory value still applies */
|
|
68
|
+
}
|
|
69
|
+
return raw;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
return localStorage.getItem(storageKey);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Persist a store's raw JSON on the client: localStorage plus a same-site
|
|
80
|
+
* cookie mirror (best effort, small stores) so the next server render sees it.
|
|
81
|
+
*/
|
|
82
|
+
export function writePersistedStorage(storageKey, raw) {
|
|
83
|
+
if (!storageKey)
|
|
84
|
+
return;
|
|
85
|
+
if (typeof window !== 'undefined') {
|
|
86
|
+
try {
|
|
87
|
+
localStorage.setItem(storageKey, raw);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
/* storage blocked — the cookie mirror still applies */
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (typeof document !== 'undefined') {
|
|
94
|
+
try {
|
|
95
|
+
document.cookie =
|
|
96
|
+
persistedStoreCookieName(storageKey) +
|
|
97
|
+
'=' +
|
|
98
|
+
encodeURIComponent(raw) +
|
|
99
|
+
'; path=/; max-age=31536000; SameSite=Lax';
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
/* cookie blocked — client still persists via localStorage */
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** Remove a persisted store's localStorage record and expire its cookie mirror. */
|
|
107
|
+
export function clearPersistedStorage(storageKey) {
|
|
108
|
+
if (!storageKey)
|
|
109
|
+
return;
|
|
110
|
+
if (typeof window !== 'undefined') {
|
|
111
|
+
try {
|
|
112
|
+
localStorage.removeItem(storageKey);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* ignore */
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (typeof document !== 'undefined') {
|
|
119
|
+
try {
|
|
120
|
+
document.cookie = persistedStoreCookieName(storageKey) + '=; path=/; max-age=0';
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
/* ignore */
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Plain JS that copies each persisted store's localStorage value into
|
|
129
|
+
* `window.__VESK_STORE_INIT__` before first paint. `serverValues` maps
|
|
130
|
+
* storageKey → the value the server rendered with (cookie read), used as a
|
|
131
|
+
* fallback so the very first client visit does not flash defaults.
|
|
132
|
+
*
|
|
133
|
+
* This is served as an origin-hosted script file (`/store-init.js?t=<token>`),
|
|
134
|
+
* never inlined into the HTML, so strict CSP without `unsafe-inline` cannot
|
|
135
|
+
* block it. Because the file has no HTML context, JSON.stringify output can be
|
|
136
|
+
* embedded directly (JSON is a valid JS literal) — no escaping needed. Safe
|
|
137
|
+
* for `</script>`-style content precisely because it never sits inside a
|
|
138
|
+
* script element.
|
|
139
|
+
*/
|
|
140
|
+
export function buildStorePrePaintPayload(serverValues) {
|
|
141
|
+
return ('globalThis.__VESK_STORE_INIT__=(function(){' +
|
|
142
|
+
'var S=' +
|
|
143
|
+
JSON.stringify(serverValues) +
|
|
144
|
+
';' +
|
|
145
|
+
'var E={};' +
|
|
146
|
+
'for(var k in S){var v=null;try{v=localStorage.getItem(k);}catch(e){}' +
|
|
147
|
+
'if(v===null)v=S[k];if(v!=null)E[k]=v;}' +
|
|
148
|
+
'return E;' +
|
|
149
|
+
'})();\n');
|
|
150
|
+
}
|
|
151
|
+
/** The `<script src>` reference for one store-init payload token. */
|
|
152
|
+
export function storeInitScriptTag(token) {
|
|
153
|
+
return '<script src="/store-init.js?t=' + token + '" data-vesk-store-init></script>';
|
|
154
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { StoreMiddleware } from '@vesk/types';
|
|
2
|
+
/**
|
|
3
|
+
* @typedef Subscriber
|
|
4
|
+
* A callback that receives the current store state or derived value.
|
|
5
|
+
* @example
|
|
6
|
+
* const unsub = store.subscribe(state => console.log(state));
|
|
7
|
+
*/
|
|
8
|
+
type Subscriber<T> = (value: T) => void;
|
|
9
|
+
/**
|
|
10
|
+
* @typedef StoreOptions
|
|
11
|
+
* Options for creating a store.
|
|
12
|
+
* @property persist Whether to persist store in localStorage (default: false)
|
|
13
|
+
* @property storageKey The key to use in localStorage if persist is true
|
|
14
|
+
* @property devtools Whether to send devtools events (default: false)
|
|
15
|
+
*/
|
|
16
|
+
interface StoreOptions {
|
|
17
|
+
persist?: boolean;
|
|
18
|
+
storageKey?: string;
|
|
19
|
+
devtools?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* @typedef Middleware
|
|
23
|
+
* A function that runs before or after each update.
|
|
24
|
+
* @param state Current state of the store
|
|
25
|
+
* @param action Optional string describing the event
|
|
26
|
+
* @param payload Optional data passed to update/set
|
|
27
|
+
*/
|
|
28
|
+
type Middleware<T> = StoreMiddleware<T>;
|
|
29
|
+
/**
|
|
30
|
+
* @function createStore
|
|
31
|
+
* Creates a reactive store with optional persistence and tools like watch & middleware.
|
|
32
|
+
*
|
|
33
|
+
* @param initial The initial state object
|
|
34
|
+
* @param options Optional store options
|
|
35
|
+
* @returns Store API with get, set, update, delete, subscribe, derive, watch, use, clear, batch, reset, rehydrate, toJSON
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* const counterStore = createStore({ count: 0 });
|
|
39
|
+
*/
|
|
40
|
+
export declare function createStore<T extends object>(initial: T, options?: StoreOptions): {
|
|
41
|
+
get: () => T;
|
|
42
|
+
set: (next: T, action?: string) => void;
|
|
43
|
+
update: {
|
|
44
|
+
(partial: Partial<T>): void;
|
|
45
|
+
(partialFn: (prev: T) => Partial<T>): void;
|
|
46
|
+
};
|
|
47
|
+
delete: (keys: (keyof T) | (keyof T)[], action?: string) => void;
|
|
48
|
+
clear: () => void;
|
|
49
|
+
subscribe: (fn: Subscriber<T>, selector?: (state: T) => any, equals?: (a: any, b: any) => boolean) => () => boolean;
|
|
50
|
+
watch: <K>(selector: (s: T) => K, callback: (n: K, o: K) => void, equals?: (a: K, b: K) => boolean) => () => void;
|
|
51
|
+
use: (middleware: Middleware<T>) => () => void;
|
|
52
|
+
derive: <K>(selector: (s: T) => K, equals?: (a: K, b: K) => boolean) => {
|
|
53
|
+
subscribe(cb: (v: K) => void): () => boolean;
|
|
54
|
+
};
|
|
55
|
+
batch: <R>(fn: () => R) => R;
|
|
56
|
+
reset: () => void;
|
|
57
|
+
rehydrate: (newInitial: Partial<T>) => void;
|
|
58
|
+
toJSON: () => T;
|
|
59
|
+
};
|
|
60
|
+
export {};
|
|
61
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAGnD;;;;;GAKG;AACH,KAAK,UAAU,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;AAExC;;;;;;GAMG;AACH,UAAU,YAAY;IACpB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;GAMG;AACH,KAAK,UAAU,CAAC,CAAC,IAAI,eAAe,CAAC,CAAC,CAAC,CAAC;AAExC;;;;;;;;;;GAUG;AACH,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,EAC1C,OAAO,EAAE,CAAC,EACV,OAAO,GAAE,YAAiB;eAqFV,CAAC;gBAKE,CAAC;;kBAaK,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;oBACf,CAAC,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,IAAI;;mBAe/B,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE;;oBA2D3C,UAAU,CAAC,CAAC,CAAC,aACN,CAAC,KAAK,EAAE,CAAC,KAAK,GAAG,WACnB,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,KAAK,OAAO;YAgCvB,CAAC,YACJ,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,YACX,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,WACrB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO;sBAoBT,UAAU,CAAC,CAAC,CAAC;aAgBtB,CAAC,YACL,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,WACZ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,OAAO;sBAGhB,CAAC,CAAC,EAAE,CAAC,KAAK,IAAI;;YAtGjB,CAAC,MAAM,MAAM,CAAC,KAAG,CAAC;;4BAhBF,OAAO,CAAC,CAAC,CAAC;;EAoJ1C"}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { track, flush_sync } from './ripple-runtime.js';
|
|
2
|
+
import { readPersistedStorage, writePersistedStorage, clearPersistedStorage } from './store-persistence.js';
|
|
3
|
+
/**
|
|
4
|
+
* @function createStore
|
|
5
|
+
* Creates a reactive store with optional persistence and tools like watch & middleware.
|
|
6
|
+
*
|
|
7
|
+
* @param initial The initial state object
|
|
8
|
+
* @param options Optional store options
|
|
9
|
+
* @returns Store API with get, set, update, delete, subscribe, derive, watch, use, clear, batch, reset, rehydrate, toJSON
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const counterStore = createStore({ count: 0 });
|
|
13
|
+
*/
|
|
14
|
+
export function createStore(initial, options = {}) {
|
|
15
|
+
const { persist = false, storageKey, devtools = false } = options;
|
|
16
|
+
// Load persisted state if enabled — on the server this reads through the
|
|
17
|
+
// adapter-installed hook (request cookie) and registers the key for the
|
|
18
|
+
// pre-paint script; on the client it prefers the pre-painted
|
|
19
|
+
// `window.__VESK_STORE_INIT__` value, then localStorage. Fallback: seed.
|
|
20
|
+
let value = { ...initial };
|
|
21
|
+
if (persist && storageKey) {
|
|
22
|
+
const stored = readPersistedStorage(storageKey);
|
|
23
|
+
if (stored) {
|
|
24
|
+
try {
|
|
25
|
+
value = JSON.parse(stored);
|
|
26
|
+
}
|
|
27
|
+
catch { }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Reactive cell holding the state
|
|
31
|
+
const stateCell = track(value);
|
|
32
|
+
const subs = new Set();
|
|
33
|
+
const watchers = [];
|
|
34
|
+
const middlewares = [];
|
|
35
|
+
let prevValue = { ...value };
|
|
36
|
+
// Batching state
|
|
37
|
+
let batchDepth = 0;
|
|
38
|
+
let dirty = false;
|
|
39
|
+
/** Notify subscribers and watchers immediately (used when not batching) */
|
|
40
|
+
function notifyNow(action, payload) {
|
|
41
|
+
const current = stateCell.get();
|
|
42
|
+
// Only notify if state changed (reference equality)
|
|
43
|
+
if (current === prevValue)
|
|
44
|
+
return;
|
|
45
|
+
// Notify subscribers
|
|
46
|
+
subs.forEach(fn => fn(current));
|
|
47
|
+
// Notify watchers
|
|
48
|
+
watchers.forEach(({ selector, cb, equals }) => {
|
|
49
|
+
const eq = equals ?? Object.is;
|
|
50
|
+
const newVal = selector(current);
|
|
51
|
+
const oldVal = selector(prevValue);
|
|
52
|
+
if (!eq(newVal, oldVal))
|
|
53
|
+
cb(newVal, oldVal);
|
|
54
|
+
});
|
|
55
|
+
// Notify middlewares
|
|
56
|
+
middlewares.forEach(fn => fn(current, action, payload));
|
|
57
|
+
// Devtools hook
|
|
58
|
+
if (devtools && typeof window !== "undefined") {
|
|
59
|
+
const dt = window.__VESK_STORE_DEVTOOLS__;
|
|
60
|
+
if (dt && typeof dt.send === "function") {
|
|
61
|
+
dt.send({ type: action, payload, state: structuredClone(current) });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Update prevValue
|
|
65
|
+
prevValue = { ...current };
|
|
66
|
+
}
|
|
67
|
+
/** Called by mutators to signal that a change occurred */
|
|
68
|
+
function notifyDirty(action, payload) {
|
|
69
|
+
if (batchDepth > 0) {
|
|
70
|
+
dirty = true;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
notifyNow(action, payload);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Persist current state if enabled — localStorage plus a same-site cookie
|
|
77
|
+
* mirror so the next SSR render serves the user's actual value. */
|
|
78
|
+
function persistToStorage() {
|
|
79
|
+
if (persist && storageKey) {
|
|
80
|
+
try {
|
|
81
|
+
writePersistedStorage(storageKey, JSON.stringify(stateCell.get()));
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
console.log("error saving to localStorage: ", err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/** @function get — Returns the current store state */
|
|
89
|
+
function get() {
|
|
90
|
+
return stateCell.get();
|
|
91
|
+
}
|
|
92
|
+
/** @function set — Replaces the entire store state */
|
|
93
|
+
function set(next, action = "set") {
|
|
94
|
+
stateCell.set(next);
|
|
95
|
+
persistToStorage();
|
|
96
|
+
notifyDirty(action, next);
|
|
97
|
+
}
|
|
98
|
+
function update(partialOrFn, action = "update") {
|
|
99
|
+
const current = stateCell.get();
|
|
100
|
+
const patch = typeof partialOrFn === "function" ? partialOrFn(current) : partialOrFn;
|
|
101
|
+
const next = { ...current, ...patch };
|
|
102
|
+
stateCell.set(next);
|
|
103
|
+
persistToStorage();
|
|
104
|
+
notifyDirty(action, patch);
|
|
105
|
+
}
|
|
106
|
+
/** @function delete — Removes one or multiple keys from the store */
|
|
107
|
+
function deleteKeys(keys, action = "delete") {
|
|
108
|
+
const current = stateCell.get();
|
|
109
|
+
const kArray = Array.isArray(keys) ? keys : [keys];
|
|
110
|
+
const next = { ...current };
|
|
111
|
+
kArray.forEach(k => {
|
|
112
|
+
delete next[k];
|
|
113
|
+
});
|
|
114
|
+
stateCell.set(next);
|
|
115
|
+
persistToStorage();
|
|
116
|
+
notifyDirty(action, keys);
|
|
117
|
+
}
|
|
118
|
+
/** @function reset — Resets state to the original initial value (does not affect persistence) */
|
|
119
|
+
function reset() {
|
|
120
|
+
stateCell.set({ ...initial });
|
|
121
|
+
notifyDirty("reset");
|
|
122
|
+
}
|
|
123
|
+
/** @function rehydrate — Merges new initial state and updates store (keeps same identity) */
|
|
124
|
+
function rehydrate(newInitial) {
|
|
125
|
+
Object.assign(initial, newInitial);
|
|
126
|
+
stateCell.set({ ...initial });
|
|
127
|
+
notifyDirty("rehydrate");
|
|
128
|
+
}
|
|
129
|
+
/** @function clear — Clears both state and its localStorage record */
|
|
130
|
+
function clear() {
|
|
131
|
+
stateCell.set({ ...initial });
|
|
132
|
+
if (persist && storageKey) {
|
|
133
|
+
clearPersistedStorage(storageKey);
|
|
134
|
+
}
|
|
135
|
+
notifyDirty("clear");
|
|
136
|
+
}
|
|
137
|
+
/** @function batch — Runs a batch of synchronous store operations, delaying subscribers until after. */
|
|
138
|
+
function batch(fn) {
|
|
139
|
+
batchDepth++;
|
|
140
|
+
try {
|
|
141
|
+
return flush_sync(fn);
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
batchDepth--;
|
|
145
|
+
if (batchDepth === 0 && dirty) {
|
|
146
|
+
notifyDirty("batch");
|
|
147
|
+
dirty = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/** @function subscribe
|
|
152
|
+
* Subscribes to all store changes (full state or a selected part).
|
|
153
|
+
*
|
|
154
|
+
* @param {Function} fn Callback to receive state or selected part
|
|
155
|
+
* @param {Function} [selector] Optional selector to receive subset
|
|
156
|
+
*
|
|
157
|
+
* @example
|
|
158
|
+
* store.subscribe(s => console.log(s)); // full state
|
|
159
|
+
* store.subscribe(s => console.log(s.theme), s => s.theme); // only theme
|
|
160
|
+
*/
|
|
161
|
+
function subscribe(fn, selector, equals) {
|
|
162
|
+
const eq = equals ?? Object.is;
|
|
163
|
+
let hasPrev = false;
|
|
164
|
+
let prev;
|
|
165
|
+
const wrapper = selector
|
|
166
|
+
? (state) => {
|
|
167
|
+
const selected = selector(state);
|
|
168
|
+
if (!hasPrev || !eq(selected, prev)) {
|
|
169
|
+
hasPrev = true;
|
|
170
|
+
prev = selected;
|
|
171
|
+
fn(selected);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
: fn;
|
|
175
|
+
subs.add(wrapper);
|
|
176
|
+
wrapper(stateCell.get()); // initial call
|
|
177
|
+
return () => subs.delete(wrapper);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* @function watch
|
|
181
|
+
* Watches a specific value in the store and runs callback when it changes.
|
|
182
|
+
* Provides both new and old values.
|
|
183
|
+
*
|
|
184
|
+
* @param {Function} selector Function selecting what to watch
|
|
185
|
+
* @param {Function} callback Called with (newValue, oldValue)
|
|
186
|
+
* @param {Function} [equals] Optional equality function (defaults to Object.is)
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* store.watch(s => s.count, (n, o) => console.log(`count: ${o} → ${n}`));
|
|
190
|
+
*/
|
|
191
|
+
function watch(selector, callback, equals) {
|
|
192
|
+
watchers.push({ selector, cb: callback, equals });
|
|
193
|
+
return () => {
|
|
194
|
+
const i = watchers.findIndex(w => w.cb === callback);
|
|
195
|
+
if (i >= 0)
|
|
196
|
+
watchers.splice(i, 1);
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* @function use
|
|
201
|
+
* Adds middleware to run on every state change.
|
|
202
|
+
*
|
|
203
|
+
* Middleware is for logging, analytics, or side effects.
|
|
204
|
+
*
|
|
205
|
+
* @param {Function} middleware Function with (state, action?, payload?)
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* store.use((s, action) => console.log(`[${action}]`, s));
|
|
209
|
+
*/
|
|
210
|
+
function use(middleware) {
|
|
211
|
+
middlewares.push(middleware);
|
|
212
|
+
return () => {
|
|
213
|
+
const i = middlewares.indexOf(middleware);
|
|
214
|
+
if (i >= 0)
|
|
215
|
+
middlewares.splice(i, 1);
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* @function derive
|
|
220
|
+
* Creates a derived store that reacts to changes in selected parts of the state.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* const themeStore = store.derive(s => s.theme);
|
|
224
|
+
* themeStore.subscribe(v => console.log("theme:", v));
|
|
225
|
+
*/
|
|
226
|
+
function derive(selector, equals) {
|
|
227
|
+
return {
|
|
228
|
+
subscribe(cb) {
|
|
229
|
+
return subscribe((v) => cb(v), selector, equals);
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
/** @function toJSON — Returns a deep copy of the current state for debugging */
|
|
234
|
+
function toJSON() {
|
|
235
|
+
return structuredClone(stateCell.get());
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
get,
|
|
239
|
+
set,
|
|
240
|
+
update,
|
|
241
|
+
delete: deleteKeys,
|
|
242
|
+
clear,
|
|
243
|
+
subscribe,
|
|
244
|
+
watch,
|
|
245
|
+
use,
|
|
246
|
+
derive,
|
|
247
|
+
batch,
|
|
248
|
+
reset,
|
|
249
|
+
rehydrate,
|
|
250
|
+
toJSON,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// -------------------- Example Stores --------------------
|
|
254
|
+
//
|
|
255
|
+
// Commented out on purpose: a top-level `createStore(...)` is a side effect,
|
|
256
|
+
// so shipping ready-made example stores here would force `store.ts` into every
|
|
257
|
+
// client runtime bundle — the tree-shaker cannot drop a module whose top level
|
|
258
|
+
// runs anything. `createStore` is imported on demand and stays tree-shaken.
|
|
259
|
+
// Build your own with `createStore({ ... })`.
|
|
260
|
+
// /**
|
|
261
|
+
// * This provides a basic store for your app like theme, user etc.
|
|
262
|
+
// * It's an example which you can delete;
|
|
263
|
+
// */
|
|
264
|
+
// export const appStore = createStore(
|
|
265
|
+
// {
|
|
266
|
+
// user: { name: "Joe", location: "unknown", preferences: [] },
|
|
267
|
+
// count: 0,
|
|
268
|
+
// theme: "dark",
|
|
269
|
+
// },
|
|
270
|
+
// { persist: true, storageKey: "appStore" }
|
|
271
|
+
// );
|
|
272
|
+
// export const routeStore = createStore(
|
|
273
|
+
// { path: "/" },
|
|
274
|
+
// { persist: true, storageKey: "routeStore" }
|
|
275
|
+
// );
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vesk/runtime",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.61",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vesk runtime — reactivity, blocks, DOM operations, hydration",
|
|
6
6
|
"main": "./dist/index-client.js",
|
|
@@ -22,6 +22,18 @@
|
|
|
22
22
|
"types": "./dist/index-router.d.ts",
|
|
23
23
|
"default": "./dist/index-router.js"
|
|
24
24
|
},
|
|
25
|
+
"./store": {
|
|
26
|
+
"types": "./dist/store.d.ts",
|
|
27
|
+
"default": "./dist/store.js"
|
|
28
|
+
},
|
|
29
|
+
"./path-store": {
|
|
30
|
+
"types": "./dist/path-store.d.ts",
|
|
31
|
+
"default": "./dist/path-store.js"
|
|
32
|
+
},
|
|
33
|
+
"./store-persistence": {
|
|
34
|
+
"types": "./dist/store-persistence.d.ts",
|
|
35
|
+
"default": "./dist/store-persistence.js"
|
|
36
|
+
},
|
|
25
37
|
"./src/*": "./dist/*.js",
|
|
26
38
|
"./package.json": "./package.json"
|
|
27
39
|
},
|
|
@@ -30,6 +42,15 @@
|
|
|
30
42
|
"router": [
|
|
31
43
|
"dist/index-router.d.ts"
|
|
32
44
|
],
|
|
45
|
+
"store": [
|
|
46
|
+
"dist/store.d.ts"
|
|
47
|
+
],
|
|
48
|
+
"path-store": [
|
|
49
|
+
"dist/path-store.d.ts"
|
|
50
|
+
],
|
|
51
|
+
"store-persistence": [
|
|
52
|
+
"dist/store-persistence.d.ts"
|
|
53
|
+
],
|
|
33
54
|
"client": [
|
|
34
55
|
"dist/index-client.d.ts"
|
|
35
56
|
],
|
|
@@ -51,7 +72,7 @@
|
|
|
51
72
|
}
|
|
52
73
|
},
|
|
53
74
|
"dependencies": {
|
|
54
|
-
"@vesk/compiler": "^0.2.
|
|
75
|
+
"@vesk/compiler": "^0.2.61"
|
|
55
76
|
},
|
|
56
77
|
"scripts": {
|
|
57
78
|
"typecheck": "tsc --noEmit",
|