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