@vesk/runtime 0.2.58 → 0.2.60
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/hydrate.d.ts.map +1 -1
- package/dist/hydrate.js +20 -1
- 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/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
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.60",
|
|
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.60"
|
|
55
76
|
},
|
|
56
77
|
"scripts": {
|
|
57
78
|
"typecheck": "tsc --noEmit",
|