edges-svelte 2.2.1 → 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.
@@ -2,7 +2,11 @@ import { createState as BaseCreateState, createDerivedState as BaseCreateDerived
2
2
  import { RequestContext } from '../context/index.js';
3
3
  import { browser } from '../utils/environment.js';
4
4
  import { DevTools } from '../utils/dev.js';
5
+ import { dev } from '../utils/environment.js';
5
6
  const globalClientCache = new Map();
7
+ const globalConstructionStack = [];
8
+ const PROVIDER_FACTORY_MARK = Symbol.for('edges-svelte.provider.factory');
9
+ const PROVIDER_INSTANCE_MARK = Symbol.for('edges-svelte.provider.instance');
6
10
  class AutoKeyGenerator {
7
11
  static cache = new WeakMap();
8
12
  static counters = new Map();
@@ -68,7 +72,52 @@ export const clearCache = (pattern) => {
68
72
  }
69
73
  };
70
74
  const createUiProvider = (cacheKey, factory, dependencies, inject) => {
71
- return () => {
75
+ const readConstructionStack = () => {
76
+ if (browser)
77
+ return globalConstructionStack;
78
+ try {
79
+ const context = RequestContext.current();
80
+ return (context.data.providersConstructionStack ??= []);
81
+ }
82
+ catch {
83
+ return globalConstructionStack;
84
+ }
85
+ };
86
+ const formatCycleError = (key, stack) => {
87
+ const cycleStart = stack.indexOf(key);
88
+ const chain = cycleStart === -1 ? [...stack, key] : [...stack.slice(cycleStart), key];
89
+ return `[edges-svelte] Circular provider dependency detected while constructing "${key}". Chain: ${chain.join(' -> ')}.`;
90
+ };
91
+ const validateLazyInjection = (ownerKey, injections) => {
92
+ if (!dev || !injections)
93
+ return;
94
+ for (const [depKey, depValue] of Object.entries(injections)) {
95
+ if (depValue && typeof depValue === 'object') {
96
+ const sourceKey = depValue[PROVIDER_INSTANCE_MARK];
97
+ if (sourceKey) {
98
+ throw new Error(`[edges-svelte] Eager provider injection detected in "${ownerKey}" for dependency "${depKey}" from "${sourceKey}". Inject provider functions instead of resolved instances.`);
99
+ }
100
+ }
101
+ }
102
+ };
103
+ const markProviderInstance = (instance) => {
104
+ if (!instance)
105
+ return;
106
+ if (typeof instance !== 'object' && typeof instance !== 'function')
107
+ return;
108
+ try {
109
+ Object.defineProperty(instance, PROVIDER_INSTANCE_MARK, {
110
+ value: cacheKey,
111
+ enumerable: false,
112
+ configurable: false,
113
+ writable: false
114
+ });
115
+ }
116
+ catch {
117
+ /* do nothing */
118
+ }
119
+ };
120
+ const provider = (() => {
72
121
  let contextMap;
73
122
  if (browser) {
74
123
  contextMap = globalClientCache;
@@ -95,10 +144,37 @@ const createUiProvider = (cacheKey, factory, dependencies, inject) => {
95
144
  ...(typeof dependencies === 'function' ? dependencies(cacheKey) : dependencies),
96
145
  ...inject
97
146
  };
98
- const instance = factory(deps);
147
+ validateLazyInjection(cacheKey, inject);
148
+ const constructionStack = readConstructionStack();
149
+ if (constructionStack.includes(cacheKey)) {
150
+ throw new Error(formatCycleError(cacheKey, constructionStack));
151
+ }
152
+ constructionStack.push(cacheKey);
153
+ let instance;
154
+ try {
155
+ instance = factory(deps);
156
+ }
157
+ finally {
158
+ const idx = constructionStack.lastIndexOf(cacheKey);
159
+ if (idx !== -1)
160
+ constructionStack.splice(idx, 1);
161
+ }
162
+ markProviderInstance(instance);
99
163
  contextMap.set(cacheKey, instance);
100
164
  return instance;
101
- };
165
+ });
166
+ try {
167
+ Object.defineProperty(provider, PROVIDER_FACTORY_MARK, {
168
+ value: cacheKey,
169
+ enumerable: false,
170
+ configurable: false,
171
+ writable: false
172
+ });
173
+ }
174
+ catch {
175
+ /* do nothing */
176
+ }
177
+ return provider;
102
178
  };
103
179
  export function createStore(nameOrFactory, factoryOrInject, inject) {
104
180
  const isNameProvided = typeof nameOrFactory === 'string';
@@ -1,8 +1,4 @@
1
1
  import type { Handle } from '@sveltejs/kit';
2
- interface CompressionOptions {
3
- compress?: boolean;
4
- compressionThreshold?: number;
5
- }
6
2
  /**
7
3
  * Automatically wraps a user-defined handle function with edgesHandle.
8
4
  * This is used internally by the Vite plugin to provide automatic state management.
@@ -10,9 +6,7 @@ interface CompressionOptions {
10
6
  * @internal This function is called automatically by the Vite plugin. You don't need to use it manually.
11
7
  *
12
8
  * @param userHandle - Optional user-defined handle function from hooks.server.ts
13
- * @param compressionOptions - Compression configuration from the plugin
14
9
  * @param silentChromeDevtools - Whether to silence Chrome DevTools requests
15
10
  * @returns A handle function wrapped with edgesHandle for automatic state serialization
16
11
  */
17
- export declare function __autoWrapHandle(userHandle?: Handle, compressionOptions?: CompressionOptions, silentChromeDevtools?: boolean): Handle;
18
- export {};
12
+ export declare function __autoWrapHandle(userHandle?: Handle, silentChromeDevtools?: boolean): Handle;
@@ -6,14 +6,13 @@ import { edgesHandle } from './EdgesHandleSimplified.js';
6
6
  * @internal This function is called automatically by the Vite plugin. You don't need to use it manually.
7
7
  *
8
8
  * @param userHandle - Optional user-defined handle function from hooks.server.ts
9
- * @param compressionOptions - Compression configuration from the plugin
10
9
  * @param silentChromeDevtools - Whether to silence Chrome DevTools requests
11
10
  * @returns A handle function wrapped with edgesHandle for automatic state serialization
12
11
  */
13
- export function __autoWrapHandle(userHandle, compressionOptions, silentChromeDevtools = true) {
12
+ export function __autoWrapHandle(userHandle, silentChromeDevtools = true) {
14
13
  if (!userHandle) {
15
14
  return edgesHandle(({ serialize, edgesEvent, resolve }) => resolve(edgesEvent, {
16
- transformPageChunk: ({ html }) => serialize(html, compressionOptions)
15
+ transformPageChunk: ({ html }) => serialize(html)
17
16
  }), silentChromeDevtools);
18
17
  }
19
18
  return edgesHandle(({ serialize, edgesEvent, resolve }) => {
@@ -23,7 +22,7 @@ export function __autoWrapHandle(userHandle, compressionOptions, silentChromeDev
23
22
  ...opts,
24
23
  transformPageChunk: ({ html, done }) => {
25
24
  const userTransformed = opts?.transformPageChunk?.({ html, done }) ?? html;
26
- return typeof userTransformed === 'string' ? serialize(userTransformed, compressionOptions) : userTransformed;
25
+ return typeof userTransformed === 'string' ? serialize(userTransformed) : userTransformed;
27
26
  }
28
27
  })
29
28
  });
@@ -1,11 +1,7 @@
1
1
  import type { RequestEvent } from '@sveltejs/kit';
2
- type SerializeOptions = {
3
- compress?: boolean;
4
- compressionThreshold?: number;
5
- };
6
2
  type EdgesHandle = (event: RequestEvent, callback: (params: {
7
3
  edgesEvent: RequestEvent;
8
- serialize: (html: string, options?: SerializeOptions) => string;
4
+ serialize: (html: string) => string;
9
5
  }) => Promise<Response> | Response, silentChromeDevtools?: boolean) => Promise<Response>;
10
6
  export declare const edgesHandle: EdgesHandle;
11
7
  export {};
@@ -1,25 +1,14 @@
1
- import { stateSerialize, getStateMap } from '../store/State.svelte.js';
1
+ import { stateSerialize } from '../store/State.svelte.js';
2
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
- 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
- return value;
16
- };
5
+ let requestRevision = 0;
17
6
  export const edgesHandle = async (event, callback, silentChromeDevtools = false) => {
18
7
  const requestSymbol = Symbol('request');
19
8
  return await storage.run({
20
9
  event: event,
21
10
  symbol: requestSymbol,
22
- data: { providers: new Map() }
11
+ data: { providers: new Map(), edgesDirtyKeys: new Set(), edgesRevision: ++requestRevision }
23
12
  }, async () => {
24
13
  RequestContext.init(() => {
25
14
  const context = storage.getStore();
@@ -41,55 +30,15 @@ export const edgesHandle = async (event, callback, silentChromeDevtools = false)
41
30
  }
42
31
  const response = await callback({
43
32
  edgesEvent: event,
44
- serialize: (html, options) => {
33
+ serialize: (html) => {
45
34
  if (!html)
46
35
  return html ?? '';
47
- const serialized = stateSerialize({
48
- compress: options?.compress,
49
- threshold: options?.compressionThreshold
50
- });
36
+ const serialized = stateSerialize();
51
37
  if (!serialized)
52
38
  return html;
53
39
  return html.replace('</body>', `${serialized}</body>`);
54
40
  }
55
41
  });
56
- const contentType = response.headers.get('content-type');
57
- if (contentType?.includes('application/json')) {
58
- const stateMap = getStateMap();
59
- if (stateMap && stateMap.size > 0) {
60
- try {
61
- const text = await response.text();
62
- try {
63
- const stateObj = {};
64
- for (const [key, value] of stateMap) {
65
- stateObj[key] = JSON.stringify(value, safeReplacer);
66
- }
67
- const modifiedBody = JSON.stringify({
68
- ...JSON.parse(text),
69
- __edges_state__: stateObj
70
- });
71
- const newHeaders = new Headers(response.headers);
72
- newHeaders.set('content-length', String(textEncoder.encode(modifiedBody).length));
73
- return new Response(modifiedBody, {
74
- status: response.status,
75
- statusText: response.statusText,
76
- headers: newHeaders
77
- });
78
- }
79
- catch {
80
- return new Response(text, {
81
- status: response.status,
82
- statusText: response.statusText,
83
- headers: response.headers
84
- });
85
- }
86
- }
87
- catch (e) {
88
- console.error('[edges] Failed to inject state into JSON response:', e);
89
- return response;
90
- }
91
- }
92
- }
93
42
  return response;
94
43
  });
95
44
  };
@@ -1,10 +1,6 @@
1
1
  import type { Handle, RequestEvent, ResolveOptions } from '@sveltejs/kit';
2
- interface SerializeOptions {
3
- compress?: boolean;
4
- compressionThreshold?: number;
5
- }
6
2
  type SimplifiedCallback = (params: {
7
- serialize: (html: string, options?: SerializeOptions) => string;
3
+ serialize: (html: string) => string;
8
4
  edgesEvent: RequestEvent;
9
5
  resolve: (event: RequestEvent, opts?: ResolveOptions) => Response | Promise<Response>;
10
6
  }) => Response | Promise<Response>;
@@ -0,0 +1,3 @@
1
+ export declare const __withEdgesServerLoad: <T extends (...args: unknown[]) => unknown>(load: T) => T;
2
+ export declare const __withEdgesActions: <T extends Record<string, (...args: unknown[]) => unknown>>(actions: T) => T;
3
+ export declare const __withEdgesUniversalLoad: <T extends (...args: unknown[]) => unknown>(load: T) => T;
@@ -0,0 +1,125 @@
1
+ import { getStateMap } from '../store/State.svelte.js';
2
+ import { RequestContext } from '../context/Context.js';
3
+ import { build, dev } from '../utils/environment.js';
4
+ const UNDEFINED_MARKER = '__EDGES_UNDEFINED__';
5
+ const NULL_MARKER = '__EDGES_NULL__';
6
+ const BIGINT_MARKER = '__EDGES_BIGINT__';
7
+ const EDGES_STATE_FIELD = '__edges_state__';
8
+ const EDGES_REV_FIELD = '__edges_rev__';
9
+ const isObjectRecord = (value) => {
10
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
11
+ };
12
+ const PROFILE_EDGES_DELTA = dev && !build;
13
+ const encodeEdgesValue = (value) => {
14
+ if (value === undefined)
15
+ return { [UNDEFINED_MARKER]: true };
16
+ if (value === null)
17
+ return { [NULL_MARKER]: true };
18
+ if (typeof value === 'bigint')
19
+ return { [BIGINT_MARKER]: value.toString() };
20
+ if (Array.isArray(value)) {
21
+ return value.map((item) => encodeEdgesValue(item));
22
+ }
23
+ if (typeof value === 'object' && value !== null) {
24
+ const encoded = {};
25
+ for (const [key, nested] of Object.entries(value)) {
26
+ encoded[key] = encodeEdgesValue(nested);
27
+ }
28
+ return encoded;
29
+ }
30
+ return value;
31
+ };
32
+ const getEdgesDelta = () => {
33
+ const startedAt = PROFILE_EDGES_DELTA ? performance.now() : 0;
34
+ try {
35
+ const context = RequestContext.current();
36
+ const dirtyKeys = context.data.edgesDirtyKeys;
37
+ const rev = context.data.edgesRevision ?? 0;
38
+ const stateMap = getStateMap();
39
+ if (!dirtyKeys || dirtyKeys.size === 0 || !stateMap || stateMap.size === 0)
40
+ return undefined;
41
+ const state = {};
42
+ for (const key of dirtyKeys) {
43
+ if (!stateMap.has(key))
44
+ continue;
45
+ state[key] = encodeEdgesValue(stateMap.get(key));
46
+ }
47
+ if (Object.keys(state).length === 0)
48
+ return undefined;
49
+ return { state, rev };
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ finally {
55
+ if (PROFILE_EDGES_DELTA) {
56
+ const duration = performance.now() - startedAt;
57
+ if (duration > 4) {
58
+ console.debug(`[edges-svelte] edges delta encode took ${duration.toFixed(2)}ms`);
59
+ }
60
+ }
61
+ }
62
+ };
63
+ const mergePayloadWithEdges = (payload) => {
64
+ const edges = getEdgesDelta();
65
+ if (!edges)
66
+ return payload;
67
+ if (isObjectRecord(payload)) {
68
+ return {
69
+ ...payload,
70
+ [EDGES_STATE_FIELD]: edges.state,
71
+ [EDGES_REV_FIELD]: edges.rev
72
+ };
73
+ }
74
+ if (payload === undefined) {
75
+ return {
76
+ [EDGES_STATE_FIELD]: edges.state,
77
+ [EDGES_REV_FIELD]: edges.rev
78
+ };
79
+ }
80
+ return payload;
81
+ };
82
+ export const __withEdgesServerLoad = (load) => {
83
+ const wrapped = (async (...args) => {
84
+ const result = await load(...args);
85
+ return mergePayloadWithEdges(result);
86
+ });
87
+ return wrapped;
88
+ };
89
+ export const __withEdgesActions = (actions) => {
90
+ const wrapped = {};
91
+ for (const [name, action] of Object.entries(actions)) {
92
+ wrapped[name] = async (...args) => {
93
+ const result = await action(...args);
94
+ return mergePayloadWithEdges(result);
95
+ };
96
+ }
97
+ return wrapped;
98
+ };
99
+ export const __withEdgesUniversalLoad = (load) => {
100
+ const wrapped = (async (...args) => {
101
+ const event = args[0];
102
+ const result = await load(...args);
103
+ if (!isObjectRecord(event?.data))
104
+ return result;
105
+ const inheritedState = event.data[EDGES_STATE_FIELD];
106
+ const inheritedRev = event.data[EDGES_REV_FIELD];
107
+ if (!inheritedState)
108
+ return result;
109
+ if (isObjectRecord(result)) {
110
+ return {
111
+ ...result,
112
+ [EDGES_STATE_FIELD]: inheritedState,
113
+ [EDGES_REV_FIELD]: inheritedRev
114
+ };
115
+ }
116
+ if (result === undefined) {
117
+ return {
118
+ [EDGES_STATE_FIELD]: inheritedState,
119
+ [EDGES_REV_FIELD]: inheritedRev
120
+ };
121
+ }
122
+ return result;
123
+ });
124
+ return wrapped;
125
+ };
@@ -1,3 +1,4 @@
1
1
  export { edgesHandle as edgesHandleRaw } from './EdgesHandle.js';
2
2
  export { edgesHandle } from './EdgesHandleSimplified.js';
3
3
  export { __autoWrapHandle } from './AutoWrapHandle.js';
4
+ export { __withEdgesServerLoad, __withEdgesActions, __withEdgesUniversalLoad } from './ServerSync.js';
@@ -1,3 +1,4 @@
1
1
  export { edgesHandle as edgesHandleRaw } from './EdgesHandle.js';
2
2
  export { edgesHandle } from './EdgesHandleSimplified.js';
3
3
  export { __autoWrapHandle } from './AutoWrapHandle.js';
4
+ export { __withEdgesServerLoad, __withEdgesActions, __withEdgesUniversalLoad } from './ServerSync.js';
@@ -5,10 +5,7 @@ declare global {
5
5
  __EDGES_DEVTOOLS__: Record<string, unknown>;
6
6
  }
7
7
  }
8
- export declare const stateSerialize: (options?: {
9
- compress?: boolean;
10
- threshold?: number;
11
- }) => string;
8
+ export declare const stateSerialize: () => string;
12
9
  export declare const getStateMap: () => Map<string, unknown> | undefined;
13
10
  export declare const createRawState: <T>(key: string, initial: () => T) => {
14
11
  value: T;
@@ -2,10 +2,12 @@ import { RequestContext } from '../context/Context.js';
2
2
  import { browser } from '../utils/environment.js';
3
3
  import { derived, writable } from 'svelte/store';
4
4
  import { registerStateUpdate } from '../client/NavigationSync.svelte.js';
5
+ import { queueUpdate, isBatching } from '../utils/batch.js';
5
6
  const RequestStores = new WeakMap();
6
7
  const UNDEFINED_MARKER = '__EDGES_UNDEFINED__';
7
8
  const NULL_MARKER = '__EDGES_NULL__';
8
- const REVIVER_CODE = `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};`;
9
+ const BIGINT_MARKER = '__EDGES_BIGINT__';
10
+ const REVIVER_CODE = `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;if('${BIGINT_MARKER}' in v)return BigInt(v['${BIGINT_MARKER}'])}return v};`;
9
11
  const safeReplacer = (key, value) => {
10
12
  if (value === undefined) {
11
13
  return { [UNDEFINED_MARKER]: true };
@@ -13,36 +15,33 @@ const safeReplacer = (key, value) => {
13
15
  if (value === null) {
14
16
  return { [NULL_MARKER]: true };
15
17
  }
18
+ if (typeof value === 'bigint') {
19
+ return { [BIGINT_MARKER]: value.toString() };
20
+ }
16
21
  if (typeof value === 'function' || typeof value === 'symbol') {
17
22
  return undefined;
18
23
  }
19
24
  return value;
20
25
  };
21
- export const stateSerialize = (options) => {
26
+ const escapeInlineScriptString = (value) => value
27
+ .replace(/[\\']/g, (ch) => '\\' + ch)
28
+ .replace(/</g, '\\u003C')
29
+ .replace(/>/g, '\\u003E')
30
+ .replace(/&/g, '\\u0026')
31
+ .replace(/\u2028/g, '\\u2028')
32
+ .replace(/\u2029/g, '\\u2029');
33
+ export const stateSerialize = () => {
22
34
  const map = getRequestContext();
23
35
  if (!map || map.size === 0)
24
36
  return '';
25
37
  const entries = [];
26
- const shouldCompress = options?.compress ?? false;
27
- const threshold = options?.threshold ?? 1024; // 1KB default
28
38
  for (const [key, value] of map) {
29
39
  const serialized = JSON.stringify(value, safeReplacer);
30
- if (shouldCompress && serialized.length > threshold) {
31
- const bytes = new TextEncoder().encode(serialized);
32
- const binary = Array.from(bytes, (byte) => String.fromCharCode(byte)).join('');
33
- const encoded = btoa(binary);
34
- entries.push(`{
35
- const binary = atob('${encoded}');
36
- const bytes = new Uint8Array(binary.length);
37
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
38
- const decoded = new TextDecoder().decode(bytes);
39
- window.__SAFE_SSR_STATE__.set('${key}', JSON.parse(decoded, window.__EDGES_REVIVER__));
40
- }`);
41
- }
42
- else {
43
- const escaped = serialized.replace(/[\\']/g, (ch) => '\\' + ch);
44
- entries.push(`window.__SAFE_SSR_STATE__.set('${key}',JSON.parse('${escaped}',window.__EDGES_REVIVER__))`);
45
- }
40
+ if (serialized === undefined)
41
+ continue;
42
+ const escapedKey = escapeInlineScriptString(key);
43
+ const escaped = escapeInlineScriptString(serialized);
44
+ entries.push(`window.__SAFE_SSR_STATE__.set('${escapedKey}',JSON.parse('${escaped}',window.__EDGES_REVIVER__))`);
46
45
  }
47
46
  return `<script>${REVIVER_CODE}window.__SAFE_SSR_STATE__=new Map();${entries.join(';')}</script>`;
48
47
  };
@@ -56,6 +55,17 @@ const getRequestContext = () => {
56
55
  return RequestStores.get(sym);
57
56
  }
58
57
  };
58
+ const markStateDirty = (key) => {
59
+ if (browser)
60
+ return;
61
+ try {
62
+ const context = RequestContext.current();
63
+ (context.data.edgesDirtyKeys ??= new Set()).add(key);
64
+ }
65
+ catch {
66
+ // no request context, ignore
67
+ }
68
+ };
59
69
  export const getStateMap = () => {
60
70
  try {
61
71
  return getRequestContext();
@@ -77,23 +87,30 @@ const getBrowserState = (key, initial) => {
77
87
  export const createRawState = (key, initial) => {
78
88
  if (browser) {
79
89
  let state = $state(getBrowserState(key, initial()));
80
- const callback = (newValue) => {
81
- state = newValue;
82
- };
83
- registerStateUpdate(key, callback);
84
90
  const updateWindowState = (val) => {
85
91
  if (!window.__SAFE_SSR_STATE__) {
86
92
  window.__SAFE_SSR_STATE__ = new Map();
87
93
  }
88
94
  window.__SAFE_SSR_STATE__.set(key, val);
89
95
  };
96
+ const applyValue = (val) => {
97
+ state = val;
98
+ updateWindowState(val);
99
+ };
100
+ const callback = (newValue) => {
101
+ queueUpdate(key, newValue, (next) => applyValue(next));
102
+ };
103
+ registerStateUpdate(key, callback);
90
104
  return {
91
105
  get value() {
92
106
  return state;
93
107
  },
94
108
  set value(val) {
95
- state = val;
96
- updateWindowState(val);
109
+ if (isBatching()) {
110
+ queueUpdate(key, val, (next) => applyValue(next));
111
+ return;
112
+ }
113
+ applyValue(val);
97
114
  }
98
115
  };
99
116
  }
@@ -104,9 +121,7 @@ export const createRawState = (key, initial) => {
104
121
  get value() {
105
122
  return val;
106
123
  },
107
- set value(_) {
108
- /* noop */
109
- }
124
+ set value(_) { }
110
125
  };
111
126
  }
112
127
  return {
@@ -118,6 +133,7 @@ export const createRawState = (key, initial) => {
118
133
  },
119
134
  set value(val) {
120
135
  map.set(key, val);
136
+ markStateDirty(key);
121
137
  }
122
138
  };
123
139
  };
@@ -125,22 +141,45 @@ export const createState = (key, initial) => {
125
141
  if (browser) {
126
142
  const initialValue = getBrowserState(key, initial());
127
143
  const state = writable(initialValue);
144
+ let currentValue = initialValue;
128
145
  const callback = (newValue) => {
129
- state.set(newValue);
146
+ queueUpdate(key, newValue, (next) => {
147
+ applyValue(next);
148
+ });
130
149
  };
131
150
  registerStateUpdate(key, callback);
132
151
  const originalSet = state.set;
133
152
  const originalUpdate = state.update;
134
- state.set = (value) => {
153
+ const applyValue = (value) => {
154
+ currentValue = value;
135
155
  originalSet(value);
136
156
  if (!window.__SAFE_SSR_STATE__) {
137
157
  window.__SAFE_SSR_STATE__ = new Map();
138
158
  }
139
159
  window.__SAFE_SSR_STATE__.set(key, value);
140
160
  };
161
+ state.set = (value) => {
162
+ if (isBatching()) {
163
+ currentValue = value;
164
+ queueUpdate(key, value, (next) => {
165
+ applyValue(next);
166
+ });
167
+ return;
168
+ }
169
+ applyValue(value);
170
+ };
141
171
  state.update = (updater) => {
172
+ if (isBatching()) {
173
+ const nextValue = updater(currentValue);
174
+ currentValue = nextValue;
175
+ queueUpdate(key, nextValue, (next) => {
176
+ applyValue(next);
177
+ });
178
+ return;
179
+ }
142
180
  originalUpdate((current) => {
143
181
  const newValue = updater(current);
182
+ currentValue = newValue;
144
183
  if (!window.__SAFE_SSR_STATE__) {
145
184
  window.__SAFE_SSR_STATE__ = new Map();
146
185
  }
@@ -172,11 +211,13 @@ export const createState = (key, initial) => {
172
211
  },
173
212
  set(val) {
174
213
  map.set(key, val);
214
+ markStateDirty(key);
175
215
  },
176
216
  update(updater) {
177
217
  const oldVal = map.get(key);
178
218
  const newVal = updater(oldVal);
179
219
  map.set(key, newVal);
220
+ markStateDirty(key);
180
221
  }
181
222
  };
182
223
  };
package/dist/types.d.ts CHANGED
@@ -15,11 +15,6 @@ export interface BatchOptions {
15
15
  defer?: boolean;
16
16
  onComplete?: () => void;
17
17
  }
18
- export interface CompressionOptions {
19
- enabled?: boolean;
20
- threshold?: number;
21
- algorithm?: 'base64' | 'gzip' | 'brotli';
22
- }
23
18
  export interface DevToolsConfig {
24
19
  enabled?: boolean;
25
20
  warnLargeState?: boolean;
@@ -29,7 +24,6 @@ export interface DevToolsConfig {
29
24
  }
30
25
  export interface EdgesConfig {
31
26
  devTools?: DevToolsConfig;
32
- compression?: CompressionOptions;
33
27
  batch?: BatchOptions;
34
28
  }
35
29
  export type UnknownFunc = {
@@ -1,4 +1,5 @@
1
1
  export declare const batch: (fn: () => void) => void;
2
2
  export declare const queueUpdate: (key: string, value: unknown, callback?: (value: unknown) => void) => void;
3
3
  export declare const onBatchComplete: (callback: () => void) => void;
4
+ export declare const isBatching: () => boolean;
4
5
  export declare const transaction: <T>(fn: () => Promise<T>) => Promise<T>;