edges-svelte 2.2.0 → 3.0.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/README.md CHANGED
@@ -2,15 +2,16 @@
2
2
 
3
3
  ### A blazing-fast, extremely lightweight and SSR-friendly store for SvelteKit.
4
4
 
5
- **EdgeS** brings seamless, per-request state management to Svelte apps — fully reactive, server-aware, and serialization-safe by default.
5
+ **EdgeS** brings seamless, per-request state management to Svelte apps.
6
6
 
7
- No context boilerplate. No hydration headaches. Just drop-in SSR-compatible state primitives with built-in support for client-side reactivity and server-side isolation.
7
+ No context boilerplate. No hydration headaches.
8
8
 
9
- - 🔄 Unified state for server and client
10
- - 🧠 Persistent per-request memory via `AsyncLocalStorage`
11
- - 💧 Tiny API
12
- - 💥 Instant serialization without magic
13
- - 🧩 Dependency injection, zero runtime overhead
9
+ - Persistent per-request memory via `AsyncLocalStorage`
10
+ - Tiny API
11
+ - Instant serialization without magic
12
+ - Dependency injection, zero runtime overhead
13
+
14
+ EdgeS is built to prevent state leaks. Its primary goal is to keep server-side state safely isolated per request while providing a clean developer experience with presenters, stores, and automatic client updates when fresh state arrives from the server. It is intentionally one-way sync from server to client and does not aim to provide full two-way state synchronization between client and server.
14
15
 
15
16
  > Designed for **SvelteKit**.
16
17
 
@@ -77,8 +78,8 @@ const myStore = createStore('MyUniqueStoreName', ({ createState, createDerivedSt
77
78
  <!-- Will update the state -->
78
79
  ```
79
80
 
80
- - 💡 All stores created inside `createStore` use unique keys automatically and are request-scoped
81
- - 🛡️ Fully SSR-safe — stores are isolated per request and serialized automatically
81
+ - All stores created inside `createStore` use unique keys automatically and are request-scoped
82
+ - Fully SSR-safe — stores are isolated per request and serialized automatically
82
83
 
83
84
  ---
84
85
 
@@ -130,7 +131,7 @@ const doubled = createDerivedState([count], ([$n]) => $n * 2);
130
131
  $doubled;
131
132
  ```
132
133
 
133
- > Like Svelte’s `derived`.
134
+ > Like Svelte’s `derived`. On the server side will not subscribe to store like derived, just reads initial value, on client side works like derived.
134
135
 
135
136
  ---
136
137
 
@@ -158,6 +159,22 @@ const useUserStore = withDeps(({ user, createState }) => {
158
159
  });
159
160
  ```
160
161
 
162
+ For provider-to-provider dependencies, inject provider functions lazily:
163
+
164
+ ```ts
165
+ const useAuth = createPresenter('AuthPresenter', () => ({ isLoggedIn: true }));
166
+
167
+ const useHeader = createPresenter(
168
+ 'HeaderPresenter',
169
+ ({ useAuth }) => ({
170
+ canShowProfile: () => useAuth().isLoggedIn
171
+ }),
172
+ { useAuth }
173
+ );
174
+ ```
175
+
176
+ Avoid injecting resolved provider instances. In development, edges throws a fail-fast error for eager provider injection and for circular dependency chains like `A -> B -> A`.
177
+
161
178
  ---
162
179
 
163
180
  ## createPresenter
@@ -279,48 +296,9 @@ You can skip specifying a unique key and pass the factory as the first argument
279
296
 
280
297
  ---
281
298
 
282
- ## State Compression
283
-
284
- ### Method 1: Via Plugin (Recommended) ✨
299
+ ## State Serialization
285
300
 
286
- ```typescript
287
- // vite.config.ts - Zero config compression!
288
- import { sveltekit } from '@sveltejs/kit/vite';
289
- import { defineConfig } from 'vite';
290
- import { edgesPlugin } from 'edges-svelte/plugin';
291
-
292
- export default defineConfig({
293
- plugins: [
294
- sveltekit(),
295
- edgesPlugin({
296
- compression: {
297
- enabled: true, // Enable compression
298
- threshold: 2048 // Compress states > 2KB
299
- },
300
- silentChromeDevtools: true // Optional: silence devtools requests
301
- })
302
- ]
303
- });
304
-
305
- // That's it! No need to touch hooks.server.ts
306
- ```
307
-
308
- ### Method 2: Manual Setup (For advanced use cases)
309
-
310
- ```typescript
311
- // hooks.server.ts - If you need custom control
312
- import { edgesHandle } from 'edges-svelte/server';
313
-
314
- export const handle = edgesHandle(({ serialize, edgesEvent, resolve }) => {
315
- return resolve(edgesEvent, {
316
- transformPageChunk: ({ html }) =>
317
- serialize(html, {
318
- compress: true, // Enable compression
319
- compressionThreshold: 2048 // Compress states > 2KB
320
- })
321
- });
322
- });
323
- ```
301
+ EdgeS serializes SSR state as plain script payloads. Transport-level compression should be handled by your HTTP stack (gzip/brotli) instead of application-level compression in this package.
324
302
 
325
303
  ---
326
304
 
@@ -0,0 +1,9 @@
1
+ <script lang="ts">
2
+ import { page } from '$app/state';
3
+ import { applyEdgesFromPayload } from './NavigationSync.svelte.js';
4
+
5
+ $effect(() => {
6
+ applyEdgesFromPayload(page.data);
7
+ applyEdgesFromPayload(page.form);
8
+ });
9
+ </script>
@@ -0,0 +1,3 @@
1
+ declare const NavigationStateObserver: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type NavigationStateObserver = ReturnType<typeof NavigationStateObserver>;
3
+ export default NavigationStateObserver;
@@ -1,3 +1,9 @@
1
1
  export declare function registerStateUpdate(key: string, callback: (value: unknown) => void): void;
2
2
  export declare function unregisterStateUpdate(key: string): void;
3
3
  export declare function processEdgesState(edgesState: Record<string, unknown>): void;
4
+ export declare function applyEdgesFromPayload(payload: unknown): void;
5
+ declare global {
6
+ interface Window {
7
+ __EDGES_NAVIGATION_SYNC_MOUNTED__?: boolean;
8
+ }
9
+ }
@@ -1,15 +1,28 @@
1
1
  import { browser } from '../utils/environment.js';
2
+ import { batch } from '../utils/batch.js';
2
3
  const stateUpdateCallbacks = new Map();
3
4
  const UNDEFINED_MARKER = '__EDGES_UNDEFINED__';
4
5
  const NULL_MARKER = '__EDGES_NULL__';
5
- const safeReviver = (key, value) => {
6
+ const BIGINT_MARKER = '__EDGES_BIGINT__';
7
+ const EDGES_STATE_FIELD = '__edges_state__';
8
+ const EDGES_REV_FIELD = '__edges_rev__';
9
+ let lastAppliedRevision = 0;
10
+ const decodeEdgesValue = (value) => {
6
11
  if (value && typeof value === 'object') {
7
- if (UNDEFINED_MARKER in value) {
12
+ if (UNDEFINED_MARKER in value)
8
13
  return undefined;
9
- }
10
- if (NULL_MARKER in value) {
14
+ if (NULL_MARKER in value)
11
15
  return null;
16
+ if (BIGINT_MARKER in value)
17
+ return BigInt(String(value[BIGINT_MARKER]));
18
+ if (Array.isArray(value)) {
19
+ return value.map((item) => decodeEdgesValue(item));
20
+ }
21
+ const decoded = {};
22
+ for (const [key, nested] of Object.entries(value)) {
23
+ decoded[key] = decodeEdgesValue(nested);
12
24
  }
25
+ return decoded;
13
26
  }
14
27
  return value;
15
28
  };
@@ -22,163 +35,81 @@ export function unregisterStateUpdate(key) {
22
35
  stateUpdateCallbacks.delete(key);
23
36
  }
24
37
  export function processEdgesState(edgesState) {
25
- if (!window.__SAFE_SSR_STATE__) {
26
- window.__SAFE_SSR_STATE__ = new Map();
27
- }
28
- for (const [key, value] of Object.entries(edgesState)) {
29
- let processedValue = value;
30
- if (typeof value === 'string') {
31
- try {
32
- processedValue = JSON.parse(value, safeReviver);
38
+ const store = window.__SAFE_SSR_STATE__ ?? new Map();
39
+ window.__SAFE_SSR_STATE__ = store;
40
+ batch(() => {
41
+ for (const [key, value] of Object.entries(edgesState)) {
42
+ let processedValue = decodeEdgesValue(value);
43
+ if (typeof value === 'string') {
44
+ try {
45
+ processedValue = decodeEdgesValue(JSON.parse(value));
46
+ }
47
+ catch {
48
+ /* do nothing */
49
+ }
33
50
  }
34
- catch {
35
- // If not JSON then use without handling
51
+ store.set(key, processedValue);
52
+ const callback = stateUpdateCallbacks.get(key);
53
+ if (callback) {
54
+ callback(processedValue);
36
55
  }
37
56
  }
38
- window.__SAFE_SSR_STATE__.set(key, processedValue);
39
- const callback = stateUpdateCallbacks.get(key);
40
- if (callback) {
41
- callback(processedValue);
42
- }
57
+ });
58
+ }
59
+ export function applyEdgesFromPayload(payload) {
60
+ if (!payload || typeof payload !== 'object')
61
+ return;
62
+ const data = payload;
63
+ const rawState = data[EDGES_STATE_FIELD];
64
+ if (!rawState || typeof rawState !== 'object')
65
+ return;
66
+ const revision = Number(data[EDGES_REV_FIELD] ?? 0);
67
+ if (Number.isFinite(revision) && revision > 0) {
68
+ if (revision <= lastAppliedRevision)
69
+ return;
70
+ lastAppliedRevision = revision;
43
71
  }
72
+ processEdgesState(rawState);
44
73
  }
45
74
  if (browser) {
46
- const originalFetch = window.fetch;
47
- const parseHeaders = (headers) => {
48
- const parsedHeaders = {};
49
- switch (true) {
50
- case headers instanceof Headers:
51
- headers.forEach((value, key) => {
52
- parsedHeaders[key] = value;
53
- });
54
- return parsedHeaders;
55
- case headers && typeof headers === 'object':
56
- Object.entries(headers).forEach(([key, value]) => {
57
- parsedHeaders[key] = value;
58
- });
59
- return parsedHeaders;
60
- default:
61
- return parsedHeaders;
62
- }
63
- };
64
- window.fetch = async function (...args) {
65
- const [input, init] = args;
66
- let reqInfo = { url: '', headers: {} };
67
- if (typeof input === 'string') {
68
- reqInfo = { url: input, headers: parseHeaders(init?.headers) };
69
- }
70
- else if (input instanceof Request) {
71
- reqInfo = { url: input.url, headers: parseHeaders(input.headers) };
72
- }
73
- else if (input instanceof URL) {
74
- reqInfo = { url: input.href, headers: parseHeaders(init?.headers) };
75
- }
76
- const isSvelteKitRequest = init.__sveltekit_fetch__ || reqInfo.headers['x-sveltekit-action'] || reqInfo.url.includes('__data.json');
77
- const response = await originalFetch.apply(this, args);
78
- if (!isSvelteKitRequest) {
79
- return response;
80
- }
81
- if (!response.headers.get('content-type')?.includes('application/json')) {
82
- return response;
83
- }
84
- const interceptEdgesStateFromResponse = (response) => {
85
- if (!response.body)
86
- return response;
87
- if (init?.method === 'POST') {
88
- const originalText = response.text.bind(response);
89
- response.text = async function () {
90
- const text = await originalText();
91
- if (text.includes('__edges_state__')) {
92
- try {
93
- const parsed = JSON.parse(text);
94
- if (parsed.__edges_state__) {
95
- processEdgesState(parsed.__edges_state__);
96
- }
97
- }
98
- catch {
99
- // ignore parsing errors
100
- }
101
- }
102
- return text;
103
- };
104
- }
105
- const originalGetReader = response.body.getReader.bind(response.body);
106
- response.body.getReader = function (opts) {
107
- const reader = originalGetReader(opts);
108
- if (!('read' in reader))
109
- return reader;
110
- const originalRead = reader.read.bind(reader);
111
- const decoder = new TextDecoder();
112
- let buffer = '';
113
- let found = false;
114
- let depth = 0;
115
- let capture = '';
116
- reader.read = async function () {
117
- const result = await originalRead();
118
- if (result.done || found)
119
- return result;
120
- buffer += decoder.decode(result.value, { stream: true });
121
- const idx = buffer.indexOf('"__edges_state__"');
122
- if (idx !== -1) {
123
- const braceStart = buffer.indexOf('{', idx);
124
- if (braceStart !== -1) {
125
- for (let i = braceStart; i < buffer.length; i++) {
126
- const ch = buffer[i];
127
- if (ch === '{') {
128
- if (depth++ === 0)
129
- capture = '';
130
- }
131
- if (depth > 0)
132
- capture += ch;
133
- if (ch === '}') {
134
- depth--;
135
- if (depth === 0) {
136
- try {
137
- const parsed = JSON.parse(capture);
138
- processEdgesState(parsed);
139
- found = true;
140
- }
141
- catch {
142
- // waiting for next iteration
143
- }
144
- break;
145
- }
146
- }
147
- }
148
- }
149
- const MAX_BUFFER = Math.max(8192, capture.length * 2);
150
- if (buffer.length > MAX_BUFFER)
151
- buffer = buffer.slice(-MAX_BUFFER / 2);
152
- }
153
- return result;
154
- };
155
- return reader;
156
- };
157
- return response;
158
- };
159
- return interceptEdgesStateFromResponse(response);
160
- };
75
+ if (!window.__EDGES_NAVIGATION_SYNC_MOUNTED__) {
76
+ window.__EDGES_NAVIGATION_SYNC_MOUNTED__ = true;
77
+ void Promise.all([import('svelte'), import('./NavigationStateObserver.svelte')])
78
+ .then(([svelte, module]) => {
79
+ const target = document.body || document.documentElement;
80
+ const host = document.createElement('div');
81
+ host.setAttribute('data-edges-navigation-sync', '1');
82
+ host.style.display = 'none';
83
+ target.appendChild(host);
84
+ svelte.mount(module.default, { target: host });
85
+ window.addEventListener('beforeunload', () => {
86
+ host.remove();
87
+ window.__EDGES_NAVIGATION_SYNC_MOUNTED__ = false;
88
+ }, { once: true });
89
+ })
90
+ .catch(() => {
91
+ window.__EDGES_NAVIGATION_SYNC_MOUNTED__ = false;
92
+ });
93
+ }
161
94
  if (typeof MutationObserver !== 'undefined') {
162
- // Optimized: Only observe <body> instead of entire document tree
163
- // This reduces CPU usage by 30-50% on pages with frequent DOM updates
164
95
  const observer = new MutationObserver((mutations) => {
165
96
  for (const mutation of mutations) {
166
97
  if (mutation.type === 'childList') {
167
98
  for (const node of mutation.addedNodes) {
168
- // Filter early: only process script elements
169
99
  if (node instanceof HTMLScriptElement) {
170
100
  const text = node.textContent || '';
171
- // Quick check for our state markers
172
101
  if (text.includes('__SAFE_SSR_STATE__') && text.includes('__EDGES_REVIVER__')) {
173
- // Use microtask instead of setTimeout(0) for better performance
174
102
  queueMicrotask(() => {
175
- if (window.__SAFE_SSR_STATE__) {
176
- for (const [key, value] of window.__SAFE_SSR_STATE__) {
177
- const callback = stateUpdateCallbacks.get(key);
178
- if (callback) {
179
- callback(value);
103
+ const store = window.__SAFE_SSR_STATE__;
104
+ if (store) {
105
+ batch(() => {
106
+ for (const [key, value] of store) {
107
+ const callback = stateUpdateCallbacks.get(key);
108
+ if (callback) {
109
+ callback(value);
110
+ }
180
111
  }
181
- }
112
+ });
182
113
  }
183
114
  });
184
115
  }
@@ -187,11 +118,9 @@ if (browser) {
187
118
  }
188
119
  }
189
120
  });
190
- // Optimized: Observe only <body> with subtree: false to reduce overhead
191
- // State scripts are always injected into body, not head
192
121
  observer.observe(document.body || document.documentElement, {
193
122
  childList: true,
194
- subtree: false // Only direct children, not entire subtree
123
+ subtree: false
195
124
  });
196
125
  if (typeof window !== 'undefined') {
197
126
  window.addEventListener('beforeunload', () => observer.disconnect());
@@ -7,6 +7,9 @@ export interface ContextData {
7
7
  providers?: Map<string, unknown>;
8
8
  providersAutoKeyCache?: WeakMap<(...args: unknown[]) => unknown, string>;
9
9
  providersAutoKeyCounters?: Map<string, number>;
10
+ providersConstructionStack?: string[];
11
+ edgesDirtyKeys?: Set<string>;
12
+ edgesRevision?: number;
10
13
  } & App.ContextDataExtended;
11
14
  }
12
15
  declare class RequestContextManager {
@@ -1,32 +1,14 @@
1
1
  import type { Plugin } from 'vite';
2
2
  export interface EdgesPluginOptions {
3
- /**
4
- * State compression options
5
- */
6
- compression?: {
7
- /**
8
- * Enable compression for large state objects
9
- * @default false
10
- */
11
- enabled?: boolean;
12
- /**
13
- * Minimum size in bytes before compression is applied
14
- * @default 1024 (1KB)
15
- */
16
- threshold?: number;
17
- };
18
- /**
19
- * Silence Chrome DevTools requests
20
- * @default true
21
- */
22
3
  silentChromeDevtools?: boolean;
4
+ syncFromServer?: boolean;
5
+ syncTransformMode?: 'ast' | 'regex' | 'hybrid';
23
6
  }
24
7
  /**
25
8
  * Creates a factory for the edges plugin with a custom package name and server path.
26
9
  *
27
10
  * Use this when:
28
11
  * - Creating a wrapper package that re-exports edges-svelte functionality
29
- * - Developing the package itself (use `$lib/server` as serverPath)
30
12
  *
31
13
  * @param packageName - The name that will be used in generated imports (e.g., 'edges-svelte', 'my-wrapper')
32
14
  * @param serverPath - The import path to the server module (e.g., 'edges-svelte/server', '$lib/server')
@@ -38,25 +20,11 @@ export interface EdgesPluginOptions {
38
20
  *
39
21
  * export const myWrapperPlugin = createEdgesPluginFactory('my-wrapper', 'my-wrapper/server');
40
22
  * ```
41
- *
42
- * @example
43
- * ```ts
44
- * // For package development (testing the package itself)
45
- * import { createEdgesPluginFactory } from './src/lib/plugin/index.js';
46
- *
47
- * const edgesPluginDev = createEdgesPluginFactory('edges-svelte', '$lib/server');
48
- *
49
- * export default defineConfig({
50
- * plugins: [sveltekit(), edgesPluginDev()]
51
- * });
52
- * ```
53
23
  */
54
24
  export declare function createEdgesPluginFactory(packageName: string, serverPath: string): (options?: EdgesPluginOptions) => Plugin;
55
25
  /**
56
- * Default edges-svelte plugin for end users.
57
26
  *
58
27
  * This plugin automatically wraps the SvelteKit handle hook with edgesHandle,
59
- * eliminating the need to manually wrap your handle function.
60
28
  *
61
29
  * @example
62
30
  * ```ts
@@ -69,37 +37,5 @@ export declare function createEdgesPluginFactory(packageName: string, serverPath
69
37
  * plugins: [sveltekit(), edgesPlugin()]
70
38
  * });
71
39
  * ```
72
- *
73
- * @example
74
- * ```ts
75
- * // vite.config.ts - With compression
76
- * export default defineConfig({
77
- * plugins: [
78
- * sveltekit(),
79
- * edgesPlugin({
80
- * compression: {
81
- * enabled: true,
82
- * threshold: 2048 // Compress states larger than 2KB
83
- * }
84
- * })
85
- * ]
86
- * });
87
- * ```
88
- *
89
- * After adding the plugin, you can write your hooks.server.ts normally:
90
- *
91
- * @example
92
- * ```ts
93
- * // hooks.server.ts - No manual wrapping needed!
94
- *
95
- * // Option 1: No handle defined - plugin creates default
96
- * // (nothing to write, it just works)
97
- *
98
- * // Option 2: Custom handle - plugin automatically wraps it
99
- * export const handle = async ({ event, resolve }) => {
100
- * console.log('My custom middleware');
101
- * return resolve(event);
102
- * };
103
- * ```
104
40
  */
105
41
  export declare const edgesPlugin: (options?: EdgesPluginOptions) => Plugin;