edges-svelte 1.1.0 → 1.3.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
@@ -26,23 +26,17 @@ npm install edges-svelte
26
26
 
27
27
  ## Setup
28
28
 
29
- To enable **EdgeS**, wrap your SvelteKit `handle` hook and serialize the state in `transformPageChunks`:
29
+ To enable **EdgeS** install edgesPlugin, it will wrap your SvelteKit `handle` hook with AsyncLocalStorage:
30
30
 
31
31
  ```ts
32
- // hooks.server.ts
33
- import { dev } from '$app/environment';
34
- import { edgesHandle } from 'edges-svelte/server';
35
-
36
- export const handle: Handle = async ({ event, resolve }) => {
37
- return edgesHandle(
38
- event,
39
- ({ serialize, edgesEvent }) => {
40
- //...Your handle code, use edgesEvent as a default svelte event (RequestEvent)
41
- return resolve(edgesEvent, { transformPageChunk: ({ html }) => serialize(html) });
42
- },
43
- dev
44
- );
45
- };
32
+ // vite.config.ts
33
+ import { sveltekit } from '@sveltejs/kit/vite';
34
+ import { defineConfig } from 'vite';
35
+ import { edgesPlugin } from 'edges-svelte/plugin';
36
+
37
+ export default defineConfig({
38
+ plugins: [sveltekit(), edgesPlugin()]
39
+ });
46
40
  ```
47
41
 
48
42
  ---
@@ -53,8 +47,7 @@ export const handle: Handle = async ({ event, resolve }) => {
53
47
 
54
48
  ```ts
55
49
  import { createStore } from 'edges-svelte';
56
- // First argument is a unique name. Each store must havew a unique name.
57
- const myStore = createStore('MyStore', ({ createState, createDerivedState }) => {
50
+ const myStore = createStore(({ createState, createDerivedState }) => {
58
51
  // createState creates a writable, SSR-safe store with a unique key
59
52
  const collection = createState<number[]>([]);
60
53
  // createDerivedState creates a derived store, SSR-safe as well
@@ -94,7 +87,7 @@ const myStore = createStore('MyStore', ({ createState, createDerivedState }) =>
94
87
  Stores are cached per request by their unique name (cache key). Calling the same store multiple times in the same request returns the cached instance.
95
88
 
96
89
  ```ts
97
- const myCachedStore = createStore('MyCachedStore', ({ createState }) => {
90
+ const myCachedStore = createStore(({ createState }) => {
98
91
  const data = createState(() => 'cached data');
99
92
  return { data };
100
93
  });
@@ -154,12 +147,12 @@ counter.value += 1;
154
147
 
155
148
  ## Dependency Injection
156
149
 
157
- You can inject dependencies into providers with `createStoreFactory`:
150
+ You can inject dependencies into stores with `createStoreFactory`:
158
151
 
159
152
  ```ts
160
153
  const withDeps = createStoreFactory({ user: getUserFromSession });
161
154
 
162
- const useUserStore = withDeps('UserStore', ({ user, createState }) => {
155
+ const useUserStore = withDeps(({ user, createState }) => {
163
156
  const userState = createState(user);
164
157
  return { userState };
165
158
  });
@@ -190,30 +183,7 @@ While createStore provides state primitives (createState, createDerivedState, cr
190
183
  | Feature | Import from |
191
184
  | -------------------------------------------------------------------------------- | --------------------- |
192
185
  | `createStore`, `createStoreFactory`, `createPresenter`, `createPresenterFactory` | `edges-svelte` |
193
- | `edgesHandle` | `edges-svelte/server` |
194
-
195
- ---
196
-
197
- ## About `edgesHandle`
198
-
199
- ```ts
200
- /**
201
- * Wraps request handling in an AsyncLocalStorage context and provides a `serialize` function
202
- * for injecting state into the HTML response.
203
- *
204
- * @param event - The SvelteKit RequestEvent for the current request.
205
- * @param callback - A function that receives the edgesEvent and a serialize function,
206
- * and expects resolve of edgesEvent as a return result.
207
- * @param silentChromeDevtools - If true, intercepts requests to
208
- * `/.well-known/appspecific/com.chrome.devtools.json` (triggered by Chrome DevTools)
209
- * and returns a 204 No Content response just to avoid spamming logs.
210
- */
211
- type EdgesHandle = (
212
- event: RequestEvent,
213
- callback: (params: { edgesEvent: RequestEvent; serialize: (html: string) => string }) => Promise<Response> | Response,
214
- silentChromeDevtools?: boolean
215
- ) => Promise<Response>;
216
- ```
186
+ | `edgesPlugin` | `edges-svelte/plugin` |
217
187
 
218
188
  ---
219
189
 
@@ -0,0 +1,3 @@
1
+ export declare function registerStateUpdate(key: string, callback: (value: unknown) => void): void;
2
+ export declare function unregisterStateUpdate(key: string): void;
3
+ export declare function processEdgesState(edgesState: Record<string, unknown>): void;
@@ -0,0 +1,115 @@
1
+ import { browser } from '$app/environment';
2
+ const stateUpdateCallbacks = new Map();
3
+ const UNDEFINED_MARKER = '__EDGES_UNDEFINED__';
4
+ const NULL_MARKER = '__EDGES_NULL__';
5
+ const safeReviver = (key, value) => {
6
+ if (value && typeof value === 'object') {
7
+ if (UNDEFINED_MARKER in value) {
8
+ return undefined;
9
+ }
10
+ if (NULL_MARKER in value) {
11
+ return null;
12
+ }
13
+ }
14
+ return value;
15
+ };
16
+ export function registerStateUpdate(key, callback) {
17
+ if (browser) {
18
+ stateUpdateCallbacks.set(key, callback);
19
+ }
20
+ }
21
+ export function unregisterStateUpdate(key) {
22
+ stateUpdateCallbacks.delete(key);
23
+ }
24
+ 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);
33
+ }
34
+ catch {
35
+ // If not JSON then use without handling
36
+ }
37
+ }
38
+ window.__SAFE_SSR_STATE__.set(key, processedValue);
39
+ const callback = stateUpdateCallbacks.get(key);
40
+ if (callback) {
41
+ callback(processedValue);
42
+ }
43
+ }
44
+ }
45
+ if (browser) {
46
+ const originalFetch = window.fetch;
47
+ let processingResponse = false;
48
+ window.fetch = async function (...args) {
49
+ const response = await originalFetch.apply(this, args);
50
+ if (processingResponse) {
51
+ return response;
52
+ }
53
+ const [input, init] = args;
54
+ const url = typeof input === 'string' ? input : input instanceof Request ? input.url : '';
55
+ const isSvelteKitGet = init.__sveltekit_fetch__ || url.includes('__data.json');
56
+ const isSvelteKitPost = input instanceof URL && input.search.startsWith('?/');
57
+ if (isSvelteKitGet || isSvelteKitPost) {
58
+ const contentType = response.headers.get('content-type');
59
+ if (contentType?.includes('application/json')) {
60
+ processingResponse = true;
61
+ try {
62
+ const cloned = response.clone();
63
+ const text = await cloned.text();
64
+ if (text) {
65
+ try {
66
+ const json = JSON.parse(text);
67
+ if (json && typeof json === 'object' && '__edges_state__' in json) {
68
+ processEdgesState(json.__edges_state__);
69
+ }
70
+ }
71
+ catch {
72
+ // ignore no JSON or parse Errors
73
+ }
74
+ }
75
+ }
76
+ catch {
77
+ // Ошибка клонирования или чтения - игнорируем
78
+ }
79
+ finally {
80
+ processingResponse = false;
81
+ }
82
+ }
83
+ }
84
+ return response;
85
+ };
86
+ if (typeof MutationObserver !== 'undefined') {
87
+ const observer = new MutationObserver((mutations) => {
88
+ for (const mutation of mutations) {
89
+ if (mutation.type === 'childList') {
90
+ for (const node of mutation.addedNodes) {
91
+ if (node instanceof HTMLScriptElement) {
92
+ const text = node.textContent || '';
93
+ if (text.includes('__SAFE_SSR_STATE__') && text.includes('__EDGES_REVIVER__')) {
94
+ setTimeout(() => {
95
+ if (window.__SAFE_SSR_STATE__) {
96
+ for (const [key, value] of window.__SAFE_SSR_STATE__) {
97
+ const callback = stateUpdateCallbacks.get(key);
98
+ if (callback) {
99
+ callback(value);
100
+ }
101
+ }
102
+ }
103
+ }, 0);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ }
109
+ });
110
+ observer.observe(document.documentElement, {
111
+ childList: true,
112
+ subtree: true
113
+ });
114
+ }
115
+ }
@@ -4,8 +4,8 @@ export interface ContextData {
4
4
  symbol?: symbol;
5
5
  data: {
6
6
  providers?: Map<string, unknown>;
7
- [p: string]: unknown;
8
- };
7
+ boundary?: Map<string, unknown>;
8
+ } & App.ContextDataExtended;
9
9
  }
10
10
  declare const _default: {
11
11
  current(): ContextData;
@@ -2,7 +2,7 @@ import { browser } from '$app/environment';
2
2
  export default {
3
3
  current() {
4
4
  if (!browser) {
5
- throw new Error('AsyncLocalStorage has not been initialized');
5
+ throw new Error('[edges] AsyncLocalStorage not initialized');
6
6
  }
7
7
  return { data: {} };
8
8
  }
@@ -0,0 +1,49 @@
1
+ import type { Plugin } from 'vite';
2
+ /**
3
+ * Vite plugin that automatically wraps the SvelteKit handle hook with edgesHandle.
4
+ *
5
+ * This eliminates the need to manually wrap your handle function, while still allowing
6
+ * full customization of the handle logic.
7
+ *
8
+ * @param isPackageDevelopment - Set to `true` when developing the edges-svelte package itself.
9
+ * This uses `$lib/server` imports. For all other cases (production or consuming the package), use `false` (default).
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * // vite.config.ts (consuming the package)
14
+ * import { sveltekit } from '@sveltejs/kit/vite';
15
+ * import { defineConfig } from 'vite';
16
+ * import { edgesPlugin } from 'edges-svelte/plugin';
17
+ *
18
+ * export default defineConfig({
19
+ * plugins: [sveltekit(), edgesPlugin()]
20
+ * });
21
+ * ```
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * // vite.config.ts (developing edges-svelte package)
26
+ * import { edgesPlugin } from './src/lib/plugin/index.js';
27
+ *
28
+ * export default defineConfig({
29
+ * plugins: [sveltekit(), edgesPlugin(true)] // true = package development mode
30
+ * });
31
+ * ```
32
+ *
33
+ * After adding the plugin, you can write your hooks.server.ts normally:
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * // hooks.server.ts - No manual wrapping needed!
38
+ *
39
+ * // Option 1: No handle defined - plugin creates default
40
+ * // (nothing to write, it just works)
41
+ *
42
+ * // Option 2: Custom handle - plugin automatically wraps it
43
+ * export const handle = async ({ event, resolve }) => {
44
+ * console.log('My custom middleware');
45
+ * return resolve(event);
46
+ * };
47
+ * ```
48
+ */
49
+ export declare function edgesPlugin(isPackageDevelopment?: boolean): Plugin;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Vite plugin that automatically wraps the SvelteKit handle hook with edgesHandle.
3
+ *
4
+ * This eliminates the need to manually wrap your handle function, while still allowing
5
+ * full customization of the handle logic.
6
+ *
7
+ * @param isPackageDevelopment - Set to `true` when developing the edges-svelte package itself.
8
+ * This uses `$lib/server` imports. For all other cases (production or consuming the package), use `false` (default).
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // vite.config.ts (consuming the package)
13
+ * import { sveltekit } from '@sveltejs/kit/vite';
14
+ * import { defineConfig } from 'vite';
15
+ * import { edgesPlugin } from 'edges-svelte/plugin';
16
+ *
17
+ * export default defineConfig({
18
+ * plugins: [sveltekit(), edgesPlugin()]
19
+ * });
20
+ * ```
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * // vite.config.ts (developing edges-svelte package)
25
+ * import { edgesPlugin } from './src/lib/plugin/index.js';
26
+ *
27
+ * export default defineConfig({
28
+ * plugins: [sveltekit(), edgesPlugin(true)] // true = package development mode
29
+ * });
30
+ * ```
31
+ *
32
+ * After adding the plugin, you can write your hooks.server.ts normally:
33
+ *
34
+ * @example
35
+ * ```ts
36
+ * // hooks.server.ts - No manual wrapping needed!
37
+ *
38
+ * // Option 1: No handle defined - plugin creates default
39
+ * // (nothing to write, it just works)
40
+ *
41
+ * // Option 2: Custom handle - plugin automatically wraps it
42
+ * export const handle = async ({ event, resolve }) => {
43
+ * console.log('My custom middleware');
44
+ * return resolve(event);
45
+ * };
46
+ * ```
47
+ */
48
+ export function edgesPlugin(isPackageDevelopment = false) {
49
+ return {
50
+ name: 'edges-auto-handle',
51
+ enforce: 'pre', // Run before SvelteKit
52
+ transform(code, id) {
53
+ // Only transform hooks.server.ts
54
+ if (!id.includes('hooks.server.ts'))
55
+ return null;
56
+ // If already wrapped by the plugin, skip
57
+ if (code.includes('__EDGES_AUTO_WRAPPED__'))
58
+ return null;
59
+ // If user is manually using edges-svelte, skip auto-wrapping
60
+ const hasManualEdgesImport = code.includes("from 'edges-svelte/server'") ||
61
+ code.includes('from "edges-svelte/server"') ||
62
+ code.includes("from '$lib/server'") ||
63
+ code.includes('from "$lib/server"');
64
+ if (hasManualEdgesImport) {
65
+ return null;
66
+ }
67
+ // Determine the correct import path
68
+ // If developing the package itself, use $lib
69
+ // Otherwise, use the published package path
70
+ const importPath = isPackageDevelopment ? '$lib/server/index.js' : 'edges-svelte/server';
71
+ // Check if user defined a handle export
72
+ const hasHandleExport = /export\s+const\s+handle/.test(code);
73
+ if (!hasHandleExport) {
74
+ // No handle defined - create default
75
+ return {
76
+ code: `// __EDGES_AUTO_WRAPPED__\n` +
77
+ `import { edgesHandle } from '${importPath}';\n\n` +
78
+ code +
79
+ `\n\n` +
80
+ `export const handle = edgesHandle(({ serialize, edgesEvent, resolve }) => ` +
81
+ `resolve(edgesEvent, { transformPageChunk: ({ html }) => serialize(html) }));`,
82
+ map: null
83
+ };
84
+ }
85
+ // User defined a handle - wrap it
86
+ const wrappedCode = `// __EDGES_AUTO_WRAPPED__\n` +
87
+ `import { __autoWrapHandle } from '${importPath}';\n\n` +
88
+ code.replace(/export\s+const\s+handle/, 'const __userHandle') +
89
+ `\n\n` +
90
+ `export const handle = __autoWrapHandle(__userHandle);`;
91
+ return {
92
+ code: wrappedCode,
93
+ map: null
94
+ };
95
+ }
96
+ };
97
+ }
@@ -0,0 +1 @@
1
+ export { edgesPlugin } from './EdgesAutoHandlePlugin.js';
@@ -0,0 +1 @@
1
+ export { edgesPlugin } from './EdgesAutoHandlePlugin.js';
@@ -4,7 +4,6 @@ type NoConflict<I, D> = {
4
4
  [K in keyof I]: K extends keyof D ? never : I[K];
5
5
  };
6
6
  export declare const clearCache: (pattern?: string) => void;
7
- export declare const createUiProvider: <T, F, D extends Record<string, unknown> | ((cacheKey: string) => Record<string, unknown>), I extends Record<string, unknown> = Record<string, unknown>>(name: string, factory: (deps: F) => T, dependencies: D, inject?: I) => (() => T);
8
7
  type StoreDeps = {
9
8
  createRawState: <T>(initial: T | (() => T)) => {
10
9
  value: T;
@@ -12,8 +11,30 @@ type StoreDeps = {
12
11
  createState: <T>(initial: T | (() => T)) => Writable<T>;
13
12
  createDerivedState: typeof BaseCreateDerivedState;
14
13
  };
15
- export declare const createStore: <T, I extends Record<string, unknown> = Record<string, unknown>>(name: string, factory: (args: StoreDeps & NoConflict<I, StoreDeps>) => T, inject?: I) => (() => T);
16
- export declare const createStoreFactory: <I extends Record<string, unknown>>(inject: I) => <T>(name: string, factory: (args: StoreDeps & NoConflict<I, StoreDeps>) => T) => () => T;
17
- export declare const createPresenter: <T, I extends Record<string, unknown> = Record<string, unknown>>(name: string, factory: (args: I) => T, inject?: I) => (() => T);
18
- export declare const createPresenterFactory: <I extends Record<string, unknown>>(inject: I) => <T>(name: string, factory: (args: I) => T) => () => T;
14
+ /**
15
+ * Creates store without name needed
16
+ * @example
17
+ * export const useUserStore = createStore(({ createState }) => {
18
+ * const user = createState(null);
19
+ * return { user };
20
+ * });
21
+ */
22
+ export declare function createStore<T, I extends Record<string, unknown> = Record<string, unknown>>(factory: (args: StoreDeps & NoConflict<I, StoreDeps>) => T, inject?: I): () => T;
23
+ export declare function createNamedStore<T, I extends Record<string, unknown> = Record<string, unknown>>(name: string, factory: (args: StoreDeps & NoConflict<I, StoreDeps>) => T, inject?: I): () => T;
24
+ /**
25
+ * Store factory
26
+ */
27
+ export declare const createStoreFactory: <I extends Record<string, unknown>>(inject: I) => <T>(factory: (args: StoreDeps & NoConflict<I, StoreDeps>) => T) => () => T;
28
+ /**
29
+ * Presenter without name needed
30
+ */
31
+ export declare function createPresenter<T, I extends Record<string, unknown> = Record<string, unknown>>(factory: (args: I) => T, inject?: I): () => T;
32
+ /**
33
+ * Named presenter
34
+ */
35
+ export declare function createNamedPresenter<T, I extends Record<string, unknown> = Record<string, unknown>>(name: string, factory: (args: I) => T, inject?: I): () => T;
36
+ /**
37
+ * Presenter factory
38
+ */
39
+ export declare const createPresenterFactory: <I extends Record<string, unknown>>(inject: I) => <T>(factory: (args: I) => T) => () => T;
19
40
  export {};
@@ -1,7 +1,37 @@
1
1
  import { createState as BaseCreateState, createDerivedState as BaseCreateDerivedState, createRawState as BaseCreateRawState } from '../store/index.js';
2
2
  import { RequestContext } from '../context/index.js';
3
3
  import { browser } from '$app/environment';
4
+ // Global client cache
4
5
  const globalClientCache = new Map();
6
+ // Key auto-generate
7
+ class AutoKeyGenerator {
8
+ static cache = new WeakMap();
9
+ static counters = new Map();
10
+ static generate(factory) {
11
+ if (this.cache.has(factory)) {
12
+ return this.cache.get(factory);
13
+ }
14
+ const fnString = factory.toString();
15
+ let hash = 0;
16
+ for (let i = 0; i < fnString.length; i++) {
17
+ const char = fnString.charCodeAt(i);
18
+ hash = (hash << 5) - hash + char;
19
+ hash = hash & hash;
20
+ }
21
+ const baseKey = `store_${Math.abs(hash).toString(36)}`;
22
+ let finalKey = baseKey;
23
+ if (this.counters.has(baseKey)) {
24
+ const count = this.counters.get(baseKey) + 1;
25
+ this.counters.set(baseKey, count);
26
+ finalKey = `${baseKey}_${count}`;
27
+ }
28
+ else {
29
+ this.counters.set(baseKey, 0);
30
+ }
31
+ this.cache.set(factory, finalKey);
32
+ return finalKey;
33
+ }
34
+ }
5
35
  export const clearCache = (pattern) => {
6
36
  if (browser) {
7
37
  if (pattern) {
@@ -16,21 +46,25 @@ export const clearCache = (pattern) => {
16
46
  }
17
47
  }
18
48
  };
19
- export const createUiProvider = (name, factory, dependencies, inject) => {
20
- const cacheKey = name;
49
+ const createUiProvider = (cacheKey, factory, dependencies, inject) => {
21
50
  return () => {
22
51
  let contextMap;
23
52
  if (browser) {
24
53
  contextMap = globalClientCache;
25
54
  }
26
55
  else {
27
- const context = RequestContext.current();
28
- if (!context.data.providers) {
29
- context.data.providers = new Map();
56
+ try {
57
+ const context = RequestContext.current();
58
+ if (!context.data.providers) {
59
+ context.data.providers = new Map();
60
+ }
61
+ contextMap = context.data.providers;
62
+ }
63
+ catch {
64
+ contextMap = new Map();
30
65
  }
31
- contextMap = context.data.providers;
32
66
  }
33
- if (cacheKey && contextMap.has(cacheKey)) {
67
+ if (contextMap.has(cacheKey)) {
34
68
  const cached = contextMap.get(cacheKey);
35
69
  if (cached !== undefined) {
36
70
  return cached;
@@ -41,13 +75,39 @@ export const createUiProvider = (name, factory, dependencies, inject) => {
41
75
  ...inject
42
76
  };
43
77
  const instance = factory(deps);
44
- if (cacheKey) {
45
- contextMap.set(cacheKey, instance);
46
- }
78
+ contextMap.set(cacheKey, instance);
47
79
  return instance;
48
80
  };
49
81
  };
50
- export const createStore = (name, factory, inject) => {
82
+ /**
83
+ * Creates store without name needed
84
+ * @example
85
+ * export const useUserStore = createStore(({ createState }) => {
86
+ * const user = createState(null);
87
+ * return { user };
88
+ * });
89
+ */
90
+ export function createStore(factory, inject) {
91
+ const cacheKey = AutoKeyGenerator.generate(factory);
92
+ return createUiProvider(cacheKey, factory, (key) => {
93
+ let stateCounter = 0;
94
+ return {
95
+ createState: (initial) => {
96
+ const stateKey = `${key}::state::${stateCounter++}`;
97
+ const initFn = typeof initial === 'function' ? initial : () => initial;
98
+ return BaseCreateState(stateKey, initFn);
99
+ },
100
+ createRawState: (initial) => {
101
+ const stateKey = `${key}::rawstate::${stateCounter++}`;
102
+ const initFn = typeof initial === 'function' ? initial : () => initial;
103
+ return BaseCreateRawState(stateKey, initFn);
104
+ },
105
+ createDerivedState: BaseCreateDerivedState
106
+ };
107
+ }, inject);
108
+ }
109
+ // Creates store with nmame
110
+ export function createNamedStore(name, factory, inject) {
51
111
  return createUiProvider(name, factory, (cacheKey) => {
52
112
  let stateCounter = 0;
53
113
  return {
@@ -64,17 +124,33 @@ export const createStore = (name, factory, inject) => {
64
124
  createDerivedState: BaseCreateDerivedState
65
125
  };
66
126
  }, inject);
67
- };
127
+ }
128
+ /**
129
+ * Store factory
130
+ */
68
131
  export const createStoreFactory = (inject) => {
69
- return function createInjectedStore(name, factory) {
70
- return createStore(name, factory, inject);
132
+ return function (factory) {
133
+ return createStore(factory, inject);
71
134
  };
72
135
  };
73
- export const createPresenter = (name, factory, inject) => {
136
+ /**
137
+ * Presenter without name needed
138
+ */
139
+ export function createPresenter(factory, inject) {
140
+ const cacheKey = AutoKeyGenerator.generate(factory);
141
+ return createUiProvider(cacheKey, factory, {}, inject);
142
+ }
143
+ /**
144
+ * Named presenter
145
+ */
146
+ export function createNamedPresenter(name, factory, inject) {
74
147
  return createUiProvider(name, factory, {}, inject);
75
- };
148
+ }
149
+ /**
150
+ * Presenter factory
151
+ */
76
152
  export const createPresenterFactory = (inject) => {
77
- return function createInjectedStore(name, factory) {
78
- return createPresenter(name, factory, inject);
153
+ return function (factory) {
154
+ return createPresenter(factory, inject);
79
155
  };
80
156
  };
@@ -0,0 +1,11 @@
1
+ import type { Handle } from '@sveltejs/kit';
2
+ /**
3
+ * Automatically wraps a user-defined handle function with edgesHandle.
4
+ * This is used internally by the Vite plugin to provide automatic state management.
5
+ *
6
+ * @internal This function is called automatically by the Vite plugin. You don't need to use it manually.
7
+ *
8
+ * @param userHandle - Optional user-defined handle function from hooks.server.ts
9
+ * @returns A handle function wrapped with edgesHandle for automatic state serialization
10
+ */
11
+ export declare function __autoWrapHandle(userHandle?: Handle): Handle;
@@ -0,0 +1,31 @@
1
+ import { edgesHandle } from './EdgesHandleSimplified.js';
2
+ /**
3
+ * Automatically wraps a user-defined handle function with edgesHandle.
4
+ * This is used internally by the Vite plugin to provide automatic state management.
5
+ *
6
+ * @internal This function is called automatically by the Vite plugin. You don't need to use it manually.
7
+ *
8
+ * @param userHandle - Optional user-defined handle function from hooks.server.ts
9
+ * @returns A handle function wrapped with edgesHandle for automatic state serialization
10
+ */
11
+ export function __autoWrapHandle(userHandle) {
12
+ if (!userHandle) {
13
+ // No user handle - return default edgesHandle
14
+ return edgesHandle(({ serialize, edgesEvent, resolve }) => resolve(edgesEvent, { transformPageChunk: ({ html }) => serialize(html) }));
15
+ }
16
+ // Wrap user's handle with edgesHandle
17
+ return edgesHandle(({ serialize, edgesEvent, resolve }) => {
18
+ return userHandle({
19
+ event: edgesEvent,
20
+ resolve: (e, opts) => resolve(e, {
21
+ ...opts,
22
+ transformPageChunk: ({ html, done }) => {
23
+ // Apply user's transform first (if any)
24
+ const userTransformed = opts?.transformPageChunk?.({ html, done }) ?? html;
25
+ // Then apply edges serialization
26
+ return typeof userTransformed === 'string' ? serialize(userTransformed) : userTransformed;
27
+ }
28
+ })
29
+ });
30
+ });
31
+ }
@@ -4,15 +4,7 @@ type EdgesHandle = (event: RequestEvent, callback: (params: {
4
4
  serialize: (html: string) => string;
5
5
  }) => Promise<Response> | Response, silentChromeDevtools?: boolean) => Promise<Response>;
6
6
  /**
7
- * Wraps request handling in an AsyncLocalStorage context and provides a `serialize` function
8
- * for injecting state into the HTML response.
9
- *
10
- * @param event - The SvelteKit RequestEvent for the current request.
11
- * @param callback - A function that receives the event and a serialize function,
12
- * and returns a Response or a Promise of one.
13
- * @param silentChromeDevtools - If true, intercepts requests to
14
- * `/.well-known/appspecific/com.chrome.devtools.json` (triggered by Chrome DevTools)
15
- * and returns a 204 No Content response instead of a 404 error.
7
+ * Wraps request handling in an AsyncLocalStorage context
16
8
  */
17
9
  export declare const edgesHandle: EdgesHandle;
18
10
  export {};
@@ -1,20 +1,32 @@
1
- import { stateSerialize } from '../store/State.svelte.js';
2
- import { AsyncLocalStorage } from 'async_hooks';
1
+ import { stateSerialize, getStateMap } from '../store/State.svelte.js';
2
+ import { AsyncLocalStorage } from 'node:async_hooks';
3
3
  import RequestContext, {} from '../context/Context.js';
4
4
  const storage = new AsyncLocalStorage();
5
+ const textEncoder = new TextEncoder();
6
+ // Маркеры для специальных значений
7
+ const UNDEFINED_MARKER = '__EDGES_UNDEFINED__';
8
+ const NULL_MARKER = '__EDGES_NULL__';
9
+ // Безопасный replacer для JSON
10
+ const safeReplacer = (key, value) => {
11
+ if (value === undefined) {
12
+ return { [UNDEFINED_MARKER]: true };
13
+ }
14
+ if (value === null) {
15
+ return { [NULL_MARKER]: true };
16
+ }
17
+ return value;
18
+ };
5
19
  /**
6
- * Wraps request handling in an AsyncLocalStorage context and provides a `serialize` function
7
- * for injecting state into the HTML response.
8
- *
9
- * @param event - The SvelteKit RequestEvent for the current request.
10
- * @param callback - A function that receives the event and a serialize function,
11
- * and returns a Response or a Promise of one.
12
- * @param silentChromeDevtools - If true, intercepts requests to
13
- * `/.well-known/appspecific/com.chrome.devtools.json` (triggered by Chrome DevTools)
14
- * and returns a 204 No Content response instead of a 404 error.
20
+ * Wraps request handling in an AsyncLocalStorage context
15
21
  */
16
22
  export const edgesHandle = async (event, callback, silentChromeDevtools = false) => {
17
- return await storage.run({ event: event, symbol: Symbol(), data: { providers: new Map() } }, async () => {
23
+ const requestSymbol = Symbol('request');
24
+ return await storage.run({
25
+ event: event,
26
+ symbol: requestSymbol,
27
+ data: { providers: new Map() }
28
+ }, async () => {
29
+ // Настраиваем RequestContext
18
30
  RequestContext.current = () => {
19
31
  const context = storage.getStore();
20
32
  if (context === undefined) {
@@ -30,16 +42,61 @@ export const edgesHandle = async (event, callback, silentChromeDevtools = false)
30
42
  }
31
43
  return context;
32
44
  };
45
+ // Chrome DevTools handling
33
46
  if (silentChromeDevtools && event.url.pathname === '/.well-known/appspecific/com.chrome.devtools.json') {
34
47
  return new Response(null, { status: 204 });
35
48
  }
36
- return callback({
49
+ // Выполняем основной callback
50
+ const response = await callback({
37
51
  edgesEvent: event,
38
52
  serialize: (html) => {
39
53
  if (!html)
40
54
  return html ?? '';
41
- return html.replace('</body>', `${stateSerialize()}</body>`);
55
+ // Вставляем сериализованное состояние перед </body>
56
+ const serialized = stateSerialize();
57
+ if (!serialized)
58
+ return html;
59
+ return html.replace('</body>', `${serialized}</body>`);
42
60
  }
43
61
  });
62
+ // Проверяем, нужно ли инжектить состояние в JSON
63
+ const contentType = response.headers.get('content-type');
64
+ if (contentType?.includes('application/json')) {
65
+ const stateMap = getStateMap();
66
+ // Если есть состояние для передачи
67
+ if (stateMap && stateMap.size > 0) {
68
+ try {
69
+ // Клонируем response для чтения
70
+ const clonedResponse = response.clone();
71
+ const json = await clonedResponse.json();
72
+ // Подготавливаем состояние для передачи
73
+ const stateObj = {};
74
+ for (const [key, value] of stateMap) {
75
+ // Сериализуем каждое значение отдельно с поддержкой undefined
76
+ stateObj[key] = JSON.stringify(value, safeReplacer);
77
+ }
78
+ // Добавляем состояние в JSON
79
+ const modifiedJson = {
80
+ ...json,
81
+ __edges_state__: stateObj
82
+ };
83
+ const modifiedBody = JSON.stringify(modifiedJson);
84
+ // Создаем новые заголовки с правильной длиной
85
+ const newHeaders = new Headers(response.headers);
86
+ newHeaders.set('content-length', String(textEncoder.encode(modifiedBody).length));
87
+ return new Response(modifiedBody, {
88
+ status: response.status,
89
+ statusText: response.statusText,
90
+ headers: newHeaders
91
+ });
92
+ }
93
+ catch (e) {
94
+ console.error('[edges] Failed to inject state into JSON response:', e);
95
+ // При ошибке возвращаем оригинальный response
96
+ return response;
97
+ }
98
+ }
99
+ }
100
+ return response;
44
101
  });
45
102
  };
@@ -0,0 +1,25 @@
1
+ import type { Handle, RequestEvent, ResolveOptions } from '@sveltejs/kit';
2
+ type SimplifiedCallback = (params: {
3
+ serialize: (html: string) => string;
4
+ edgesEvent: RequestEvent;
5
+ resolve: (event: RequestEvent, opts?: ResolveOptions) => Response | Promise<Response>;
6
+ }) => Response | Promise<Response>;
7
+ /**
8
+ * Simplified wrapper around edgesHandle that provides a more convenient API.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * // Simple usage with default behavior
13
+ * export const handle = edgesHandle(({ serialize, edgesEvent, resolve }) =>
14
+ * resolve(edgesEvent, { transformPageChunk: ({ html }) => serialize(html) })
15
+ * );
16
+ *
17
+ * // You can still access resolve for custom logic
18
+ * export const handle = edgesHandle(({ serialize, edgesEvent, resolve }) => {
19
+ * // Custom logic here
20
+ * return resolve(edgesEvent, { transformPageChunk: ({ html }) => serialize(html) });
21
+ * });
22
+ * ```
23
+ */
24
+ export declare const edgesHandle: (callback: SimplifiedCallback, silentChromeDevtools?: boolean) => Handle;
25
+ export {};
@@ -0,0 +1,25 @@
1
+ import { edgesHandle as originalEdgesHandle } from './EdgesHandle.js';
2
+ /**
3
+ * Simplified wrapper around edgesHandle that provides a more convenient API.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * // Simple usage with default behavior
8
+ * export const handle = edgesHandle(({ serialize, edgesEvent, resolve }) =>
9
+ * resolve(edgesEvent, { transformPageChunk: ({ html }) => serialize(html) })
10
+ * );
11
+ *
12
+ * // You can still access resolve for custom logic
13
+ * export const handle = edgesHandle(({ serialize, edgesEvent, resolve }) => {
14
+ * // Custom logic here
15
+ * return resolve(edgesEvent, { transformPageChunk: ({ html }) => serialize(html) });
16
+ * });
17
+ * ```
18
+ */
19
+ export const edgesHandle = (callback, silentChromeDevtools = true) => {
20
+ return async ({ event, resolve }) => {
21
+ return originalEdgesHandle(event, ({ serialize, edgesEvent }) => {
22
+ return callback({ serialize, edgesEvent, resolve });
23
+ }, silentChromeDevtools);
24
+ };
25
+ };
@@ -1 +1,3 @@
1
- export * from './EdgesHandle.js';
1
+ export { edgesHandle as edgesHandleRaw } from './EdgesHandle.js';
2
+ export { edgesHandle } from './EdgesHandleSimplified.js';
3
+ export { __autoWrapHandle } from './AutoWrapHandle.js';
@@ -1 +1,3 @@
1
- export * from './EdgesHandle.js';
1
+ export { edgesHandle as edgesHandleRaw } from './EdgesHandle.js';
2
+ export { edgesHandle } from './EdgesHandleSimplified.js';
3
+ export { __autoWrapHandle } from './AutoWrapHandle.js';
@@ -5,6 +5,7 @@ declare global {
5
5
  }
6
6
  }
7
7
  export declare const stateSerialize: () => string;
8
+ export declare const getStateMap: () => Map<string, unknown> | undefined;
8
9
  export declare const createRawState: <T>(key: string, initial: () => T) => {
9
10
  value: T;
10
11
  };
@@ -1,84 +1,177 @@
1
1
  import RequestContext from '../context/Context.js';
2
- import { uneval } from 'devalue';
3
2
  import { browser } from '$app/environment';
4
3
  import { derived, writable } from 'svelte/store';
4
+ import { registerStateUpdate } from '../client/NavigationSync.svelte.js';
5
5
  const RequestStores = new WeakMap();
6
+ const UNDEFINED_MARKER = '__EDGES_UNDEFINED__';
7
+ const NULL_MARKER = '__EDGES_NULL__';
8
+ const safeReplacer = (key, value) => {
9
+ if (value === undefined) {
10
+ return { [UNDEFINED_MARKER]: true };
11
+ }
12
+ if (value === null) {
13
+ return { [NULL_MARKER]: true };
14
+ }
15
+ if (typeof value === 'function' || typeof value === 'symbol') {
16
+ return undefined;
17
+ }
18
+ return value;
19
+ };
20
+ // const safeReviver = (key: string, value: unknown): unknown => {
21
+ // if (value && typeof value === 'object') {
22
+ // if (UNDEFINED_MARKER in value) {
23
+ // return undefined;
24
+ // }
25
+ // if (NULL_MARKER in value) {
26
+ // return null;
27
+ // }
28
+ // }
29
+ // return value;
30
+ // };
6
31
  export const stateSerialize = () => {
7
32
  const map = getRequestContext();
8
- if (map) {
9
- const entries = Array.from(map).map(([key, value]) => [uneval(key), uneval(value)]);
10
- return `<script>
11
- window.__SAFE_SSR_STATE__ = new Map();
12
- ${entries.map(([key, value]) => `window.__SAFE_SSR_STATE__.set(${key}, ${value})`).join(';')}
13
- </script>`;
33
+ if (!map || map.size === 0)
34
+ return '';
35
+ const entries = [];
36
+ for (const [key, value] of map) {
37
+ const serialized = JSON.stringify(value, safeReplacer);
38
+ const escaped = serialized.replace(/'/g, "\\'").replace(/\\/g, '\\\\');
39
+ entries.push(`window.__SAFE_SSR_STATE__.set('${key}',JSON.parse('${escaped}',window.__EDGES_REVIVER__))`);
14
40
  }
15
- return '';
41
+ const reviverCode = `window.__EDGES_REVIVER__=function(k,v){if(v&&typeof v==='object'){if('${UNDEFINED_MARKER}' in v)return undefined;if('${NULL_MARKER}' in v)return null}return v};`;
42
+ return `<script>${reviverCode}window.__SAFE_SSR_STATE__=new Map();${entries.join(';')}</script>`;
16
43
  };
17
44
  const getRequestContext = () => {
18
- const sym = RequestContext.current().symbol;
45
+ const context = RequestContext.current();
46
+ const sym = context.symbol;
19
47
  if (sym) {
20
- return RequestStores.get(sym) ?? RequestStores.set(sym, new Map()).get(sym);
48
+ if (!RequestStores.has(sym)) {
49
+ RequestStores.set(sym, new Map());
50
+ }
51
+ return RequestStores.get(sym);
52
+ }
53
+ };
54
+ export const getStateMap = () => {
55
+ try {
56
+ return getRequestContext();
57
+ }
58
+ catch {
59
+ return undefined;
21
60
  }
22
61
  };
23
62
  const getBrowserState = (key, initial) => {
24
- const state = window.__SAFE_SSR_STATE__?.get(key);
25
- if (state)
63
+ if (!window.__SAFE_SSR_STATE__) {
64
+ return initial;
65
+ }
66
+ const state = window.__SAFE_SSR_STATE__.get(key);
67
+ if (window.__SAFE_SSR_STATE__.has(key)) {
26
68
  return state;
69
+ }
27
70
  return initial;
28
71
  };
29
72
  export const createRawState = (key, initial) => {
30
73
  if (browser) {
31
74
  let state = $state(getBrowserState(key, initial()));
75
+ const callback = (newValue) => {
76
+ state = newValue;
77
+ };
78
+ registerStateUpdate(key, callback);
79
+ const updateWindowState = (val) => {
80
+ if (!window.__SAFE_SSR_STATE__) {
81
+ window.__SAFE_SSR_STATE__ = new Map();
82
+ }
83
+ window.__SAFE_SSR_STATE__.set(key, val);
84
+ };
32
85
  return {
33
86
  get value() {
34
87
  return state;
35
88
  },
36
89
  set value(val) {
37
90
  state = val;
91
+ updateWindowState(val);
38
92
  }
39
93
  };
40
94
  }
41
95
  const map = getRequestContext();
96
+ if (!map) {
97
+ const val = initial();
98
+ return {
99
+ get value() {
100
+ return val;
101
+ },
102
+ set value(_) {
103
+ /* noop */
104
+ }
105
+ };
106
+ }
42
107
  return {
43
108
  get value() {
44
- if (!map)
45
- return initial();
46
- if (!map.has(key))
47
- map.set(key, structuredClone(initial()));
109
+ if (!map.has(key)) {
110
+ map.set(key, initial());
111
+ }
48
112
  return map.get(key);
49
113
  },
50
114
  set value(val) {
51
- if (map) {
52
- map.set(key, val);
53
- }
115
+ map.set(key, val);
54
116
  }
55
117
  };
56
118
  };
57
119
  export const createState = (key, initial) => {
58
120
  if (browser) {
59
- return writable(getBrowserState(key, initial()));
121
+ const initialValue = getBrowserState(key, initial());
122
+ const state = writable(initialValue);
123
+ const callback = (newValue) => {
124
+ state.set(newValue);
125
+ };
126
+ registerStateUpdate(key, callback);
127
+ const originalSet = state.set;
128
+ const originalUpdate = state.update;
129
+ state.set = (value) => {
130
+ originalSet(value);
131
+ if (!window.__SAFE_SSR_STATE__) {
132
+ window.__SAFE_SSR_STATE__ = new Map();
133
+ }
134
+ window.__SAFE_SSR_STATE__.set(key, value);
135
+ };
136
+ state.update = (updater) => {
137
+ originalUpdate((current) => {
138
+ const newValue = updater(current);
139
+ if (!window.__SAFE_SSR_STATE__) {
140
+ window.__SAFE_SSR_STATE__ = new Map();
141
+ }
142
+ window.__SAFE_SSR_STATE__.set(key, newValue);
143
+ return newValue;
144
+ });
145
+ };
146
+ return state;
60
147
  }
61
148
  const map = getRequestContext();
62
- if (!map)
63
- throw new Error('No RequestContext available');
64
- if (!map.has(key))
65
- map.set(key, structuredClone(initial()));
66
- //const subscribers: Set<(val: T) => void> = new Set();
149
+ if (!map) {
150
+ const val = initial();
151
+ return {
152
+ subscribe(run) {
153
+ run(val);
154
+ return () => { };
155
+ },
156
+ set() { },
157
+ update() { }
158
+ };
159
+ }
160
+ if (!map.has(key)) {
161
+ map.set(key, initial());
162
+ }
67
163
  return {
68
164
  subscribe(run) {
69
165
  run(map.get(key));
70
- //subscribers.add(run);
71
166
  return () => { };
72
167
  },
73
168
  set(val) {
74
169
  map.set(key, val);
75
- //subscribers.forEach((fn) => fn(val));
76
170
  },
77
171
  update(updater) {
78
172
  const oldVal = map.get(key);
79
173
  const newVal = updater(oldVal);
80
174
  map.set(key, newVal);
81
- //subscribers.forEach((fn) => fn(newVal));
82
175
  }
83
176
  };
84
177
  };
@@ -1 +1 @@
1
- export { createState, createRawState, createDerivedState } from './State.svelte.js';
1
+ export { createState, createRawState, createDerivedState, getStateMap } from './State.svelte.js';
@@ -1 +1 @@
1
- export { createState, createRawState, createDerivedState } from './State.svelte.js';
1
+ export { createState, createRawState, createDerivedState, getStateMap } from './State.svelte.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edges-svelte",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "license": "MIT",
5
5
  "author": "Pixel1917",
6
6
  "description": "A blazing-fast, extremely lightweight and SSR-friendly store for Svelte",
@@ -34,8 +34,8 @@
34
34
  "svelte": "./dist/index.js"
35
35
  },
36
36
  "./server": {
37
- "types": "./dist/server/EdgesHandle.d.ts",
38
- "svelte": "./dist/server/EdgesHandle.js"
37
+ "types": "./dist/server/index.d.ts",
38
+ "svelte": "./dist/server/index.js"
39
39
  },
40
40
  "./context": {
41
41
  "types": "./dist/context/index.d.ts",
@@ -44,6 +44,10 @@
44
44
  "./state": {
45
45
  "types": "./dist/store/index.d.ts",
46
46
  "svelte": "./dist/store/index.js"
47
+ },
48
+ "./plugin": {
49
+ "types": "./dist/plugin/index.d.ts",
50
+ "default": "./dist/plugin/index.js"
47
51
  }
48
52
  },
49
53
  "peerDependencies": {