@vesk/runtime 0.2.59 → 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/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.59",
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.59"
75
+ "@vesk/compiler": "^0.2.60"
55
76
  },
56
77
  "scripts": {
57
78
  "typecheck": "tsc --noEmit",