@azure-net/kit 6.0.0-next.1 → 6.0.0-next.11
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 +335 -2
- package/dist/delivery/index.d.ts +3 -1
- package/dist/delivery/index.js +3 -1
- package/dist/delivery/injectable-dependencies/AsyncHelpers.d.ts +2 -4
- package/dist/delivery/injectable-dependencies/AsyncHelpers.js +96 -102
- package/dist/delivery/presenter/Bound.d.ts +1 -0
- package/dist/delivery/presenter/Bound.js +9 -0
- package/dist/delivery/presenter/RegisterProvider.d.ts +16 -0
- package/dist/delivery/presenter/RegisterProvider.js +99 -0
- package/dist/delivery/schema/rules/Rules.js +2 -1
- package/dist/delivery.d.ts +0 -1
- package/dist/delivery.js +0 -1
- package/dist/i18n/Internal.d.ts +15 -0
- package/dist/i18n/Internal.js +11 -0
- package/dist/i18n/TranslationManager.d.ts +19 -0
- package/dist/i18n/TranslationManager.js +26 -0
- package/dist/i18n/Translations.svelte.d.ts +20 -0
- package/dist/i18n/Translations.svelte.js +254 -0
- package/dist/i18n/Types.d.ts +30 -0
- package/dist/i18n/Types.js +1 -0
- package/dist/i18n/index.d.ts +3 -0
- package/dist/i18n/index.js +2 -0
- package/dist/infra/datasource/BaseDatasource.d.ts +19 -6
- package/dist/infra/datasource/BaseDatasource.js +4 -4
- package/dist/infra/http-service/HttpServiceInstance.d.ts +26 -16
- package/dist/infra/http-service/HttpServiceInstance.js +21 -13
- package/dist/infra/index.d.ts +2 -2
- package/dist/infra/index.js +2 -2
- package/dist/infra/query/QueryBuilder.d.ts +6 -1
- package/dist/infra/query/QueryBuilder.js +14 -12
- package/dist/plugin/AzureNetPlugin.d.ts +1 -4
- package/dist/plugin/AzureNetPlugin.js +21 -64
- package/dist/plugin/index.d.ts +1 -1
- package/dist/server/Context/Runtime.d.ts +3 -0
- package/dist/server/Context/Runtime.js +109 -0
- package/dist/shared/app/App.d.ts +47 -77
- package/dist/shared/app/App.js +80 -106
- package/dist/shared/app/index.d.ts +5 -3
- package/dist/shared/app/middleware/ClientMiddleware.d.ts +14 -8
- package/dist/shared/app/middleware/ClientMiddleware.js +32 -11
- package/dist/shared/app/middleware/ServerMiddleware.d.ts +10 -5
- package/dist/shared/app/middleware/ServerMiddleware.js +3 -3
- package/dist/shared/app/middleware/Types.d.ts +4 -0
- package/dist/shared/app/middleware/Types.js +1 -0
- package/dist/shared/app-error/AppError.d.ts +2 -0
- package/dist/shared/app-error/AppError.js +4 -0
- package/dist/shared/app-error/ErrorRuntime.d.ts +1 -1
- package/dist/shared/app-error/ErrorRuntime.js +4 -3
- package/dist/shared/boundary-provider/CompositionRoot.d.ts +21 -0
- package/dist/shared/boundary-provider/CompositionRoot.js +11 -0
- package/dist/shared/boundary-provider/Provider.d.ts +3 -2
- package/dist/shared/boundary-provider/Provider.js +56 -40
- package/dist/shared/boundary-provider/index.d.ts +1 -0
- package/dist/shared/boundary-provider/index.js +1 -0
- package/dist/shared/cookie/UniversalCookie.js +1 -1
- package/dist/shared/index.d.ts +2 -3
- package/dist/shared/index.js +2 -3
- package/dist/shared/logger/index.js +1 -1
- package/dist/shared/request-context/Internal.d.ts +16 -0
- package/dist/shared/request-context/Internal.js +24 -0
- package/dist/shared/request-context/index.d.ts +1 -0
- package/dist/shared/request-context/index.js +2 -0
- package/dist/svelte/async-signal/AsyncSignal.svelte.d.ts +20 -4
- package/dist/svelte/async-signal/AsyncSignal.svelte.js +53 -13
- package/dist/svelte/async-signal/README.md +89 -0
- package/dist/svelte/index.d.ts +1 -0
- package/dist/svelte/index.js +1 -0
- package/dist/svelte/query/Query.svelte.d.ts +1 -1
- package/dist/svelte/query/Query.svelte.js +14 -6
- package/dist/svelte/query/SearchParamsSync.js +11 -3
- package/dist/{shared → svelte}/redirect/index.d.ts +2 -8
- package/package.json +35 -26
- package/dist/external/edges/Dev.d.ts +0 -1
- package/dist/external/edges/Dev.js +0 -1
- package/dist/external/edges/Edges.d.ts +0 -1
- package/dist/external/edges/Edges.js +0 -1
- package/dist/external/edges/ServerContext.d.ts +0 -1
- package/dist/external/edges/ServerContext.js +0 -1
- package/dist/external/edges/_edges.d.ts +0 -1
- package/dist/external/edges/_edges.js +0 -1
- package/dist/external/i18n/index.d.ts +0 -1
- package/dist/external/i18n/index.js +0 -1
- package/dist/shared/event-bus/EventBus.d.ts +0 -12
- package/dist/shared/event-bus/EventBus.js +0 -3
- package/dist/shared/event-bus/index.d.ts +0 -1
- package/dist/shared/event-bus/index.js +0 -1
- /package/dist/{shared → svelte}/redirect/index.js +0 -0
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getContextInternals } from '../../shared/request-context/Internal.js';
|
|
2
|
+
import { RequestContext } from '../../shared/request-context/Internal.js';
|
|
2
3
|
import { BROWSER } from '../../external/tools/index.js';
|
|
3
4
|
import { AzureNetKitInternalError } from '../app-error/AppError.js';
|
|
4
5
|
const clientCache = new Map();
|
|
@@ -14,30 +15,36 @@ const getProviderCache = (providerName) => {
|
|
|
14
15
|
}
|
|
15
16
|
else {
|
|
16
17
|
const context = RequestContext.current();
|
|
17
|
-
if (!context.
|
|
18
|
-
context.
|
|
18
|
+
if (!getContextInternals(context).azureNetKitBoundaryProviders) {
|
|
19
|
+
getContextInternals(context).azureNetKitBoundaryProviders = new Map();
|
|
19
20
|
}
|
|
20
|
-
const providers = context.
|
|
21
|
+
const providers = getContextInternals(context).azureNetKitBoundaryProviders;
|
|
21
22
|
if (!providers.has(providerName)) {
|
|
22
23
|
providers.set(providerName, new Map());
|
|
23
24
|
}
|
|
24
|
-
if (!context.
|
|
25
|
-
context.
|
|
25
|
+
if (!getContextInternals(context).azureNetKitBoundaryProvidersToCleanup) {
|
|
26
|
+
getContextInternals(context).azureNetKitBoundaryProvidersToCleanup = new Set();
|
|
26
27
|
}
|
|
27
|
-
context.
|
|
28
|
+
getContextInternals(context).azureNetKitBoundaryProvidersToCleanup.add(providerName);
|
|
28
29
|
return providers.get(providerName);
|
|
29
30
|
}
|
|
30
31
|
};
|
|
32
|
+
const getProxyCache = () => {
|
|
33
|
+
if (BROWSER)
|
|
34
|
+
return providerProxyCache;
|
|
35
|
+
const context = RequestContext.current();
|
|
36
|
+
return (getContextInternals(context).azureNetKitBoundaryProviderProxies ??= new Map());
|
|
37
|
+
};
|
|
31
38
|
const getBootFlag = (providerName) => {
|
|
32
39
|
if (BROWSER) {
|
|
33
40
|
return clientBootFlags.get(providerName) ?? false;
|
|
34
41
|
}
|
|
35
42
|
else {
|
|
36
43
|
const context = RequestContext.current();
|
|
37
|
-
if (!context.
|
|
38
|
-
context.
|
|
44
|
+
if (!getContextInternals(context).bootFlags) {
|
|
45
|
+
getContextInternals(context).bootFlags = new Map();
|
|
39
46
|
}
|
|
40
|
-
const bootFlags = context.
|
|
47
|
+
const bootFlags = getContextInternals(context).bootFlags;
|
|
41
48
|
return bootFlags.get(providerName) ?? false;
|
|
42
49
|
}
|
|
43
50
|
};
|
|
@@ -47,10 +54,10 @@ const setBootFlag = (providerName, value) => {
|
|
|
47
54
|
}
|
|
48
55
|
else {
|
|
49
56
|
const context = RequestContext.current();
|
|
50
|
-
if (!context.
|
|
51
|
-
context.
|
|
57
|
+
if (!getContextInternals(context).bootFlags) {
|
|
58
|
+
getContextInternals(context).bootFlags = new Map();
|
|
52
59
|
}
|
|
53
|
-
const bootFlags = context.
|
|
60
|
+
const bootFlags = getContextInternals(context).bootFlags;
|
|
54
61
|
bootFlags.set(providerName, value);
|
|
55
62
|
}
|
|
56
63
|
};
|
|
@@ -58,30 +65,32 @@ const getConstructionStack = () => {
|
|
|
58
65
|
if (BROWSER)
|
|
59
66
|
return clientConstructionStack;
|
|
60
67
|
const context = RequestContext.current();
|
|
61
|
-
return (context.
|
|
68
|
+
return (getContextInternals(context).boundaryProviderConstructionStack ??= []);
|
|
62
69
|
};
|
|
63
|
-
const runWithCycleGuard = (
|
|
70
|
+
const runWithCycleGuard = (provider, member, callback) => {
|
|
64
71
|
const stack = getConstructionStack();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
72
|
+
const cycleStart = stack.findIndex((entry) => entry.provider === provider && entry.member === member);
|
|
73
|
+
const entry = { provider, member };
|
|
74
|
+
if (cycleStart !== -1) {
|
|
75
|
+
const cyclePath = [...stack.slice(cycleStart), entry].map(({ provider, member }) => `${String(provider)}.${member}`).join(' -> ');
|
|
68
76
|
throw new AzureNetKitInternalError(`[BoundaryProvider] Circular provider dependency detected: ${cyclePath}`);
|
|
69
77
|
}
|
|
70
|
-
stack.push(
|
|
78
|
+
stack.push(entry);
|
|
71
79
|
try {
|
|
72
80
|
return callback();
|
|
73
81
|
}
|
|
74
82
|
finally {
|
|
75
|
-
const idx = stack.lastIndexOf(
|
|
83
|
+
const idx = stack.lastIndexOf(entry);
|
|
76
84
|
if (idx !== -1)
|
|
77
85
|
stack.splice(idx, 1);
|
|
78
86
|
}
|
|
79
87
|
};
|
|
80
|
-
export const
|
|
88
|
+
export const CreateServiceContainer = (name, settings) => {
|
|
81
89
|
const { dependsOn = {}, boot, register } = settings;
|
|
82
90
|
const providerFn = () => {
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
const proxies = getProxyCache();
|
|
92
|
+
if (proxies.has(name)) {
|
|
93
|
+
return proxies.get(name);
|
|
85
94
|
}
|
|
86
95
|
const cache = getProviderCache(name);
|
|
87
96
|
let factories = null;
|
|
@@ -101,7 +110,7 @@ export const createBoundaryProvider = (name, settings) => {
|
|
|
101
110
|
}
|
|
102
111
|
});
|
|
103
112
|
}
|
|
104
|
-
factories = register(deps);
|
|
113
|
+
factories = runWithCycleGuard(name, 'register', () => register(deps));
|
|
105
114
|
return factories;
|
|
106
115
|
};
|
|
107
116
|
const providerProxy = new Proxy({}, {
|
|
@@ -117,15 +126,15 @@ export const createBoundaryProvider = (name, settings) => {
|
|
|
117
126
|
return undefined;
|
|
118
127
|
if (Reflect.has(target, key))
|
|
119
128
|
return Reflect.get(target, key, receiver);
|
|
120
|
-
throw new AzureNetKitInternalError(`[BoundaryProvider] Service '${key}' not found in provider '${name}'`);
|
|
129
|
+
throw new AzureNetKitInternalError(`[BoundaryProvider] Service '${key}' not found in provider '${String(name)}'`);
|
|
121
130
|
}
|
|
122
131
|
const factory = factories[key];
|
|
123
132
|
if (typeof factory !== 'function') {
|
|
124
133
|
throw new AzureNetKitInternalError('[BoundaryProvider] Factory must be a function');
|
|
125
134
|
}
|
|
126
|
-
const instance = runWithCycleGuard(
|
|
135
|
+
const instance = runWithCycleGuard(name, key, () => factory());
|
|
127
136
|
if (instance && typeof instance?.then === 'function') {
|
|
128
|
-
throw new AzureNetKitInternalError(`[BoundaryProvider] Service '${key}' in provider '${name}' returned Promise.`);
|
|
137
|
+
throw new AzureNetKitInternalError(`[BoundaryProvider] Service '${key}' in provider '${String(name)}' returned Promise.`);
|
|
129
138
|
}
|
|
130
139
|
cache.set(key, instance);
|
|
131
140
|
return instance;
|
|
@@ -157,17 +166,21 @@ export const createBoundaryProvider = (name, settings) => {
|
|
|
157
166
|
};
|
|
158
167
|
}
|
|
159
168
|
});
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
+
proxies.set(name, providerProxy);
|
|
170
|
+
try {
|
|
171
|
+
if (boot && !getBootFlag(name)) {
|
|
172
|
+
getFactories();
|
|
173
|
+
const bootResult = boot(providerProxy);
|
|
174
|
+
if (bootResult && typeof bootResult.then === 'function') {
|
|
175
|
+
void Promise.resolve(bootResult).catch(() => undefined);
|
|
176
|
+
throw new AzureNetKitInternalError(`[BoundaryProvider] Boot for provider '${String(name)}' must be synchronous.`);
|
|
177
|
+
}
|
|
178
|
+
setBootFlag(name, true);
|
|
169
179
|
}
|
|
170
|
-
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
proxies.delete(name);
|
|
183
|
+
throw error;
|
|
171
184
|
}
|
|
172
185
|
return providerProxy;
|
|
173
186
|
};
|
|
@@ -180,10 +193,10 @@ export async function cleanupProvider(name) {
|
|
|
180
193
|
if (service && typeof service === 'object' && 'dispose' in service) {
|
|
181
194
|
try {
|
|
182
195
|
const disposeResult = service.dispose();
|
|
183
|
-
promises.push(Promise.resolve(disposeResult).catch((err) => console.error(`Error disposing ${name}.${key}:`, err)));
|
|
196
|
+
promises.push(Promise.resolve(disposeResult).catch((err) => console.error(`Error disposing ${String(name)}.${key}:`, err)));
|
|
184
197
|
}
|
|
185
198
|
catch (err) {
|
|
186
|
-
console.error(`Error disposing ${name}.${key}:`, err);
|
|
199
|
+
console.error(`Error disposing ${String(name)}.${key}:`, err);
|
|
187
200
|
}
|
|
188
201
|
}
|
|
189
202
|
}
|
|
@@ -200,7 +213,10 @@ export async function cleanupProvider(name) {
|
|
|
200
213
|
}
|
|
201
214
|
else {
|
|
202
215
|
const context = RequestContext.current();
|
|
203
|
-
const providers = context.
|
|
216
|
+
const providers = getContextInternals(context).azureNetKitBoundaryProviders;
|
|
217
|
+
getProxyCache().delete(name);
|
|
218
|
+
setBootFlag(name, false);
|
|
219
|
+
getContextInternals(context).azureNetKitBoundaryProvidersToCleanup?.delete(name);
|
|
204
220
|
if (providers?.has(name)) {
|
|
205
221
|
const cache = providers.get(name);
|
|
206
222
|
providers.delete(name);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BROWSER, Cookies } from '../../external/tools/index.js';
|
|
2
|
-
import { RequestContext } from '../../
|
|
2
|
+
import { RequestContext } from '../../shared/request-context/Internal.js';
|
|
3
3
|
const cloneCookieOptions = (options) => ({
|
|
4
4
|
...options,
|
|
5
5
|
expires: options.expires instanceof Date ? new Date(options.expires) : options.expires
|
package/dist/shared/index.d.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export { UniversalCookie, UniversalCookieInstance, UniversalNamedCookieInstance, type CookieOptions } from './cookie/index.js';
|
|
2
2
|
export { BackgroundTask } from './background-task/index.js';
|
|
3
|
-
export { AppEvents } from './event-bus/index.js';
|
|
4
3
|
export { ClassMirror } from './class-mirror/index.js';
|
|
5
|
-
export {
|
|
4
|
+
export { CreateServiceContainer, cleanupProvider, type ProviderSettings, type ProviderKey } from './boundary-provider/index.js';
|
|
5
|
+
export { CompositionRoot, type CompositionRootSettings } from './boundary-provider/index.js';
|
|
6
6
|
export * from './app/index.js';
|
|
7
7
|
export { useLogger, LoggerErrors, type ILoggerError, type ILoggerSettings } from './logger/index.js';
|
|
8
|
-
export { useRedirect, type IRedirectSettings, type RedirectStatus } from './redirect/index.js';
|
package/dist/shared/index.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
export { UniversalCookie, UniversalCookieInstance, UniversalNamedCookieInstance } from './cookie/index.js';
|
|
2
2
|
export { BackgroundTask } from './background-task/index.js';
|
|
3
|
-
export { AppEvents } from './event-bus/index.js';
|
|
4
3
|
export { ClassMirror } from './class-mirror/index.js';
|
|
5
|
-
export {
|
|
4
|
+
export { CreateServiceContainer, cleanupProvider } from './boundary-provider/index.js';
|
|
5
|
+
export { CompositionRoot } from './boundary-provider/index.js';
|
|
6
6
|
export * from './app/index.js';
|
|
7
7
|
export { useLogger, LoggerErrors } from './logger/index.js';
|
|
8
|
-
export { useRedirect } from './redirect/index.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AppError, AzureNetKitInternalError } from '../app-error/index.js';
|
|
2
2
|
import { BROWSER } from '../../external/tools/index.js';
|
|
3
|
-
import { RequestContext } from '../../
|
|
3
|
+
import { RequestContext } from '../../shared/request-context/Internal.js';
|
|
4
4
|
import { BackgroundTask } from '../background-task/index.js';
|
|
5
5
|
export var LoggerErrors;
|
|
6
6
|
(function (LoggerErrors) {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { RequestEvent } from '@sveltejs/kit';
|
|
2
|
+
declare global {
|
|
3
|
+
namespace App {
|
|
4
|
+
interface RequestContextState {
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export interface ContextData {
|
|
9
|
+
readonly event: RequestEvent;
|
|
10
|
+
readonly state: App.RequestContextState;
|
|
11
|
+
}
|
|
12
|
+
export declare function initializeRequestContext(resolve: () => ContextData | undefined): void;
|
|
13
|
+
export declare function getContextInternals(context: ContextData): Record<PropertyKey, unknown>;
|
|
14
|
+
export declare const RequestContext: Readonly<{
|
|
15
|
+
current(): ContextData;
|
|
16
|
+
}>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { BROWSER } from '../../external/tools/index.js';
|
|
2
|
+
let getter;
|
|
3
|
+
const internals = new WeakMap();
|
|
4
|
+
export function initializeRequestContext(resolve) {
|
|
5
|
+
getter = resolve;
|
|
6
|
+
}
|
|
7
|
+
export function getContextInternals(context) {
|
|
8
|
+
let data = internals.get(context);
|
|
9
|
+
if (!data) {
|
|
10
|
+
data = {};
|
|
11
|
+
internals.set(context, data);
|
|
12
|
+
}
|
|
13
|
+
return data;
|
|
14
|
+
}
|
|
15
|
+
export const RequestContext = Object.freeze({
|
|
16
|
+
current() {
|
|
17
|
+
if (BROWSER)
|
|
18
|
+
throw new Error('[AzureNetKit] RequestContext is server-only.');
|
|
19
|
+
const context = getter?.();
|
|
20
|
+
if (!context)
|
|
21
|
+
throw new Error('[AzureNetKit] No active RequestContext. Use AzureNetPlugin and resolve services inside a request.');
|
|
22
|
+
return context;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { RequestContext, type ContextData } from './Internal.js';
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
export type AsyncStatus = 'idle' | 'pending' | 'success' | 'error';
|
|
2
2
|
export type AsyncSignalSource = 'auto' | 'manual' | 'global';
|
|
3
|
-
|
|
4
|
-
server?: boolean;
|
|
3
|
+
interface AsyncSignalBaseOptions<TData, TError = Error> {
|
|
5
4
|
immediate?: boolean;
|
|
6
5
|
initialData?: TData | (() => TData);
|
|
7
6
|
beforeSend?: (meta: {
|
|
@@ -12,6 +11,15 @@ export interface AsyncSignalOptions<TData, TError = Error> {
|
|
|
12
11
|
onError?: (error: TError) => void | Promise<void>;
|
|
13
12
|
key?: string;
|
|
14
13
|
}
|
|
14
|
+
export type AsyncSignalOptions<TData, TError = Error> = AsyncSignalBaseOptions<TData, TError> & ({
|
|
15
|
+
server: true;
|
|
16
|
+
key: string;
|
|
17
|
+
} | {
|
|
18
|
+
server?: false;
|
|
19
|
+
});
|
|
20
|
+
type AsyncSignalBatchSignalOptions<TData, TError> = Omit<AsyncSignalBaseOptions<TData, TError>, 'immediate'> & {
|
|
21
|
+
server?: false;
|
|
22
|
+
};
|
|
15
23
|
export interface AsyncSignalSvelte<TData, TError = Error> {
|
|
16
24
|
response?: TData;
|
|
17
25
|
error?: TError;
|
|
@@ -27,13 +35,21 @@ export interface AsyncSignalBatchOptions {
|
|
|
27
35
|
parallel?: boolean;
|
|
28
36
|
}
|
|
29
37
|
export interface AsyncSignalBatchFactory {
|
|
30
|
-
<TData, TError = Error>(handler: (signal?: AbortSignal) => Promise<TData>, options?:
|
|
38
|
+
<TData, TError = Error>(handler: (signal?: AbortSignal) => Promise<TData>, options?: AsyncSignalBatchSignalOptions<TData, TError>): AsyncSignalSvelte<TData, TError>;
|
|
31
39
|
}
|
|
32
40
|
export interface AsyncSignalBatch<TSignals extends readonly AsyncSignalSvelte<unknown, unknown>[]> {
|
|
33
41
|
execute: () => TSignals;
|
|
34
42
|
}
|
|
35
43
|
export declare const isAsyncSignalScheduledForClient: (signal: AsyncSignalSvelte<unknown, unknown>) => boolean;
|
|
36
|
-
export declare
|
|
44
|
+
export declare function createAsyncSignal<TData, TError = Error>(handler: (signal?: AbortSignal) => Promise<TData>, options: AsyncSignalBaseOptions<TData, TError> & {
|
|
45
|
+
server: true;
|
|
46
|
+
key: string;
|
|
47
|
+
}): Promise<AsyncSignalSvelte<TData, TError>>;
|
|
48
|
+
export declare function createAsyncSignal<TData, TError = Error>(handler: (signal?: AbortSignal) => Promise<TData>, options?: AsyncSignalBaseOptions<TData, TError> & {
|
|
49
|
+
server?: false;
|
|
50
|
+
}): AsyncSignalSvelte<TData, TError>;
|
|
51
|
+
export declare function createAsyncSignal<TData, TError = Error>(handler: (signal?: AbortSignal) => Promise<TData>, options: AsyncSignalOptions<TData, TError>): AsyncSignalSvelte<TData, TError> | Promise<AsyncSignalSvelte<TData, TError>>;
|
|
37
52
|
export declare const createAsyncSignalBatch: <const TSignals extends readonly AsyncSignalSvelte<unknown, unknown>[]>(factory: (createSignal: AsyncSignalBatchFactory) => TSignals, options?: AsyncSignalBatchOptions) => AsyncSignalBatch<TSignals>;
|
|
38
53
|
export declare const refreshAsyncSignal: (key: string) => Promise<void>;
|
|
39
54
|
export declare const refreshAllAsyncSignals: () => Promise<void>;
|
|
55
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { untrack } from 'svelte';
|
|
1
|
+
import { hydratable, untrack } from 'svelte';
|
|
2
2
|
import { BROWSER } from '../../external/tools/index.js';
|
|
3
3
|
const ASYNC_SIGNAL_BATCH_INTERNAL = Symbol('async-signal-batch-internal');
|
|
4
4
|
const ASYNC_SIGNAL_SCHEDULED_FOR_CLIENT = Symbol('async-signal-scheduled-for-client');
|
|
@@ -60,7 +60,14 @@ const createAsyncSignalManager = () => {
|
|
|
60
60
|
return { refreshAll, refreshByKey, generateKey, register, unregister };
|
|
61
61
|
};
|
|
62
62
|
const asyncSignalManager = createAsyncSignalManager();
|
|
63
|
-
export
|
|
63
|
+
export function createAsyncSignal(handler, options = {}) {
|
|
64
|
+
if (options.server && (typeof options.key !== 'string' || !options.key.trim())) {
|
|
65
|
+
throw new Error('[AsyncSignal] server: true requires a stable, non-empty key.');
|
|
66
|
+
}
|
|
67
|
+
const signal = createAsyncSignalInstance(handler, options, options.server && options.immediate !== false ? options.key : undefined);
|
|
68
|
+
return options.server ? signal.ready.then(() => signal) : signal;
|
|
69
|
+
}
|
|
70
|
+
const createAsyncSignalInstance = (handler, options = {}, hydrationKey) => {
|
|
64
71
|
const { server = false, immediate = true, initialData = undefined, key } = options;
|
|
65
72
|
const resolvedInitialData = typeof initialData === 'function' ? initialData() : initialData;
|
|
66
73
|
let scheduledForClient = immediate && !BROWSER && !server;
|
|
@@ -105,15 +112,43 @@ export const createAsyncSignal = (handler, options = {}) => {
|
|
|
105
112
|
status = 'pending';
|
|
106
113
|
error = undefined;
|
|
107
114
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
115
|
+
const request = async () => {
|
|
116
|
+
if (options.beforeSend) {
|
|
117
|
+
const beforeSendResult = options.beforeSend({ initial, source });
|
|
118
|
+
if (beforeSendResult && typeof beforeSendResult.then === 'function')
|
|
119
|
+
await beforeSendResult;
|
|
120
|
+
}
|
|
121
|
+
if (runId !== currentRunId || abortController !== localController || localController.signal.aborted) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
return handler(localController.signal);
|
|
125
|
+
};
|
|
126
|
+
let result;
|
|
127
|
+
if (initial && hydrationKey !== undefined) {
|
|
128
|
+
let originalError;
|
|
129
|
+
// Call before yielding: hydratable needs the component's active render context.
|
|
130
|
+
const outcome = await hydratable(`@azure-net/kit/async-signal:${hydrationKey}`, async () => {
|
|
131
|
+
try {
|
|
132
|
+
return { ok: true, data: await request() };
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
originalError = { value: error };
|
|
136
|
+
// Error instances are not devalue-serializable; do not expose server stacks.
|
|
137
|
+
return error instanceof Error
|
|
138
|
+
? { ok: false, native: true, error: { name: error.name, message: error.message } }
|
|
139
|
+
: { ok: false, native: false, error };
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
if (!outcome.ok) {
|
|
143
|
+
if (originalError)
|
|
144
|
+
throw originalError.value;
|
|
145
|
+
throw outcome.native ? Object.assign(new Error(outcome.error.message), { name: outcome.error.name }) : outcome.error;
|
|
146
|
+
}
|
|
147
|
+
result = outcome.data;
|
|
112
148
|
}
|
|
113
|
-
|
|
114
|
-
|
|
149
|
+
else {
|
|
150
|
+
result = await request();
|
|
115
151
|
}
|
|
116
|
-
const result = await handler(localController.signal);
|
|
117
152
|
if (runId !== currentRunId || abortController !== localController || localController.signal.aborted) {
|
|
118
153
|
return undefined;
|
|
119
154
|
}
|
|
@@ -183,21 +218,23 @@ export const createAsyncSignal = (handler, options = {}) => {
|
|
|
183
218
|
if (BROWSER) {
|
|
184
219
|
const signalKey = key ?? asyncSignalManager.generateKey();
|
|
185
220
|
const callback = (source) => startWhenUnlocked(source);
|
|
186
|
-
asyncSignalManager.register(signalKey, callback);
|
|
187
221
|
$effect(() => {
|
|
222
|
+
// Initialization may be abandoned before this effect can own the registration.
|
|
223
|
+
asyncSignalManager.register(signalKey, callback);
|
|
188
224
|
return () => {
|
|
189
225
|
asyncSignalManager.unregister(signalKey, callback);
|
|
190
226
|
};
|
|
191
227
|
});
|
|
192
228
|
}
|
|
193
229
|
if (immediate) {
|
|
230
|
+
// Automatic runs have no caller to catch a rejected onError callback.
|
|
194
231
|
if (!BROWSER && server) {
|
|
195
232
|
untrack(() => {
|
|
196
|
-
void start('auto');
|
|
233
|
+
void start('auto').catch(() => undefined);
|
|
197
234
|
});
|
|
198
235
|
}
|
|
199
236
|
else if (BROWSER) {
|
|
200
|
-
void start('auto');
|
|
237
|
+
void start('auto').catch(() => undefined);
|
|
201
238
|
}
|
|
202
239
|
}
|
|
203
240
|
const asyncSignal = {
|
|
@@ -272,7 +309,10 @@ export const createAsyncSignal = (handler, options = {}) => {
|
|
|
272
309
|
export const createAsyncSignalBatch = (factory, options = {}) => {
|
|
273
310
|
const internals = new Map();
|
|
274
311
|
const createSignal = (handler, signalOptions = {}) => {
|
|
275
|
-
|
|
312
|
+
if (signalOptions.server) {
|
|
313
|
+
throw new Error('[AsyncSignalBatch] server: true is not supported. Await createAsyncSignal during component initialization instead.');
|
|
314
|
+
}
|
|
315
|
+
const signal = createAsyncSignalInstance(handler, { ...signalOptions, immediate: false });
|
|
276
316
|
const internal = signal[ASYNC_SIGNAL_BATCH_INTERNAL];
|
|
277
317
|
internal.lock();
|
|
278
318
|
internals.set(signal, internal);
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# AsyncSignal server hydration
|
|
2
|
+
|
|
3
|
+
`createAsyncSignal(handler)` and `{ server: false }` return a signal synchronously,
|
|
4
|
+
with the existing client-only automatic execution behavior. With `{ server: true }`,
|
|
5
|
+
the return type is `Promise<AsyncSignalSvelte<T>>`; await it to obtain the settled
|
|
6
|
+
initial signal on both the server and the browser.
|
|
7
|
+
|
|
8
|
+
```svelte
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import { createAsyncSignal } from '@azure-net/kit';
|
|
11
|
+
|
|
12
|
+
let { api } = $props<{
|
|
13
|
+
api: { getProfile(signal?: AbortSignal): Promise<{ name: string }> };
|
|
14
|
+
}>();
|
|
15
|
+
|
|
16
|
+
const profile = await createAsyncSignal((signal) => api.getProfile(signal), {
|
|
17
|
+
server: true,
|
|
18
|
+
key: 'profile:current-user'
|
|
19
|
+
});
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<p>{profile.response?.name}</p>
|
|
23
|
+
<button onclick={() => profile.refresh()}>Refresh</button>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Enable Svelte's `compilerOptions.experimental.async`. Call the factory during
|
|
27
|
+
component initialization, directly in the component script as above, or through a
|
|
28
|
+
synchronous helper called there. Do not first await inside an ordinary async helper,
|
|
29
|
+
or create a server-hydratable signal in a module singleton, event handler, `onMount`,
|
|
30
|
+
or `$effect`: Svelte needs the active component render context when `hydratable` is
|
|
31
|
+
called. The request handler itself may await normally. Supply a handler that can run
|
|
32
|
+
in both environments. In this example the parent supplies an API service created
|
|
33
|
+
for the current SSR request, and independently creates its browser counterpart.
|
|
34
|
+
Do not serialize the service through `load` data. Its server implementation must
|
|
35
|
+
capture the current request's fetch/auth context, or use an absolute trusted API
|
|
36
|
+
URL with explicitly scoped credentials. Global server `fetch('/api/profile')`
|
|
37
|
+
cannot resolve a relative URL, and `credentials: 'include'` does not automatically
|
|
38
|
+
forward the incoming user's cookies on the server. Forward only the credentials
|
|
39
|
+
needed by a trusted destination; never put them in the hydration payload.
|
|
40
|
+
|
|
41
|
+
The key must be non-empty, stable between SSR and hydration, and unique for each
|
|
42
|
+
logically distinct request in a render (include parameters when relevant). The
|
|
43
|
+
library namespaces it. Svelte owns the per-render payload; AsyncSignal does not add
|
|
44
|
+
a shared server request cache. This does not make module-global services, mutable
|
|
45
|
+
handler state, or publicly cached personalized HTML safe: the application must
|
|
46
|
+
keep those request-scoped or otherwise isolate users.
|
|
47
|
+
Data and non-Error rejection values must be serializable by
|
|
48
|
+
Svelte's `hydratable`/devalue. Native errors transfer their name and message, not
|
|
49
|
+
stack traces, subclasses, or custom properties. Do not return secrets or sensitive
|
|
50
|
+
error messages: hydratable payloads are sent to the browser.
|
|
51
|
+
|
|
52
|
+
Only the initial automatic request is hydratable. Hydration reuses its result
|
|
53
|
+
without invoking the handler or `beforeSend`; `onSuccess`/`onError` run for the
|
|
54
|
+
settled result in each environment, and `ready` waits for those callbacks. An
|
|
55
|
+
initial request failure yields a signal with `status: 'error'`, as before, rather
|
|
56
|
+
than rejecting the factory promise. Subsequent `execute`, `refresh`, and global
|
|
57
|
+
refreshes use normal request, cancellation, callback, and ready-cycle behavior.
|
|
58
|
+
`beforeSend` sees `initial: false` on a refresh after hydration.
|
|
59
|
+
|
|
60
|
+
If `onSuccess` throws, the signal enters the error state and invokes `onError`,
|
|
61
|
+
retaining the loaded response. If `onError` also throws, `ready` still settles;
|
|
62
|
+
explicit `execute`/`refresh` reject with that callback error, while automatic
|
|
63
|
+
initial loading consumes it to avoid an unhandled rejection. The signal's `error`
|
|
64
|
+
retains the original request or success-callback failure. Hydratable signals join
|
|
65
|
+
the browser global-refresh registry only when their component effect runs, so an
|
|
66
|
+
initialization abandoned before mounting leaves no registry entry. Unmounting is
|
|
67
|
+
not equivalent to calling `abort`: already-running handlers/callbacks may finish.
|
|
68
|
+
|
|
69
|
+
`initialData` (or its synchronous factory) initializes `response` independently in
|
|
70
|
+
each environment. It does not skip the automatic request or change an idle signal
|
|
71
|
+
to success. With `server: true` and the default `immediate: true`, the factory still
|
|
72
|
+
awaits initial loading; success replaces the fallback, while failure retains it.
|
|
73
|
+
Only the request outcome is transferred, not `initialData`, so use matching fallback
|
|
74
|
+
values on the server and browser to avoid hydration mismatches after a failure.
|
|
75
|
+
|
|
76
|
+
`immediate: false` disables initial loading and hydration, including in server mode:
|
|
77
|
+
`await createAsyncSignal(handler, { server: true, key: 'profile', immediate: false,
|
|
78
|
+
initialData })` returns an idle signal whose `response` and resolved `ready` contain
|
|
79
|
+
the initial value (or `undefined` if omitted). The key is still required. Later
|
|
80
|
+
explicit execution is ordinary, non-hydratable work.
|
|
81
|
+
|
|
82
|
+
Batch factories remain synchronous and deferred;
|
|
83
|
+
`createAsyncSignalBatch` explicitly rejects `server: true` in its factory at both
|
|
84
|
+
type-check and runtime. Use individually awaited server signals instead. Ordinary
|
|
85
|
+
batches preserve locks, parallel/sequential execution, cancellation, and ready gates.
|
|
86
|
+
|
|
87
|
+
Outside SvelteKit, include both `head` and `body` returned by `await render(...)` in
|
|
88
|
+
the document: Svelte places the hydration payload in `head`. Configure a CSP nonce
|
|
89
|
+
or hash through Svelte's render options if your application uses CSP.
|
package/dist/svelte/index.d.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { createAsyncSignal, createAsyncSignalBatch, refreshAsyncSignal, refreshA
|
|
|
2
2
|
export { createActiveForm, type ActiveForm, type ActiveFormCallbackContext, type ActiveFormController, type ActiveFormResponse, type ActiveFormStatus, type FormConfig, type InitialValuesHelpers, type LinkPromise, type LinkSignal, type LinkSignalOptions, type ResetBehaviors } from './active-form/ActiveForm.svelte.js';
|
|
3
3
|
export { createEffect, type EffectCallback, type EffectCleanup, type EffectDependency } from './effect/Effect.svelte.js';
|
|
4
4
|
export { createQuery, type CreateQueryOptions, type QueryController, type QueryPath, type QuerySearchParamsSync } from './query/Query.svelte.js';
|
|
5
|
+
export { useRedirect, type IRedirectSettings, type RedirectStatus } from './redirect/index.js';
|
package/dist/svelte/index.js
CHANGED
|
@@ -2,3 +2,4 @@ export { createAsyncSignal, createAsyncSignalBatch, refreshAsyncSignal, refreshA
|
|
|
2
2
|
export { createActiveForm } from './active-form/ActiveForm.svelte.js';
|
|
3
3
|
export { createEffect } from './effect/Effect.svelte.js';
|
|
4
4
|
export { createQuery } from './query/Query.svelte.js';
|
|
5
|
+
export { useRedirect } from './redirect/index.js';
|
|
@@ -19,5 +19,5 @@ export interface QueryController<T extends Record<string, unknown>> {
|
|
|
19
19
|
getInitial: () => T;
|
|
20
20
|
createEffect: (callback: EffectCallback, dependencies?: readonly QueryPath<T>[]) => void;
|
|
21
21
|
}
|
|
22
|
-
export declare const createQuery: <T extends Record<string, unknown>>(initialValue: T, options?: CreateQueryOptions<T>) => QueryController<T>;
|
|
22
|
+
export declare const createQuery: <T extends Record<string, unknown>>(initialValue: T | (() => T), options?: CreateQueryOptions<T>) => QueryController<T>;
|
|
23
23
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { goto } from '$app/navigation';
|
|
2
2
|
import { page } from '$app/state';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
3
4
|
import { createEffect } from '../effect/Effect.svelte.js';
|
|
4
5
|
import { cloneStateValue } from '../shared/cloneStateValue.js';
|
|
5
6
|
import { fromSearchParams, toSearchParams } from './SearchParamsSync.js';
|
|
@@ -53,13 +54,16 @@ const createDeepDependency = (read) => {
|
|
|
53
54
|
};
|
|
54
55
|
};
|
|
55
56
|
export const createQuery = (initialValue, options) => {
|
|
56
|
-
|
|
57
|
+
// Capture defaults once; reset restores this snapshot rather than rerunning the initializer.
|
|
58
|
+
const baseInitial = untrack(() => cloneStateValue(typeof initialValue === 'function' ? initialValue() : initialValue));
|
|
57
59
|
const searchParamsSync = options?.syncWithSearchParams;
|
|
58
60
|
const syncSettings = typeof searchParamsSync === 'object' ? searchParamsSync : undefined;
|
|
59
61
|
const syncEnabled = Boolean(searchParamsSync);
|
|
60
62
|
const parseSearchParams = syncSettings?.fromSearchParams ?? (fromSearchParams);
|
|
61
63
|
const serializeSearchParams = syncSettings?.toSearchParams ?? (toSearchParams);
|
|
62
|
-
const parsedInitial = syncEnabled
|
|
64
|
+
const parsedInitial = syncEnabled
|
|
65
|
+
? parseSearchParams(new URLSearchParams((page.shallow?.url ?? page.url).searchParams.toString()), cloneStateValue(baseInitial))
|
|
66
|
+
: undefined;
|
|
63
67
|
let data = $state(parsedInitial ? { ...cloneStateValue(baseInitial), ...cloneStateValue(parsedInitial) } : cloneStateValue(baseInitial));
|
|
64
68
|
const reset = () => {
|
|
65
69
|
data = cloneStateValue(baseInitial);
|
|
@@ -92,13 +96,17 @@ export const createQuery = (initialValue, options) => {
|
|
|
92
96
|
if (nextParams.toString() === initialParams.toString())
|
|
93
97
|
return;
|
|
94
98
|
}
|
|
95
|
-
const
|
|
99
|
+
const currentUrl = page.shallow?.url ?? page.url;
|
|
100
|
+
const url = new URL(currentUrl.href);
|
|
96
101
|
for (const key of managedKeys)
|
|
97
102
|
url.searchParams.delete(key);
|
|
98
103
|
for (const [key, value] of nextParams)
|
|
99
104
|
url.searchParams.append(key, value);
|
|
100
|
-
if (url.search !==
|
|
101
|
-
|
|
105
|
+
if (url.search !== currentUrl.search) {
|
|
106
|
+
void goto(url, { state: page.state, shallow: true, replace: true, reset: false }).catch((error) => {
|
|
107
|
+
console.error('[createQuery] Failed to synchronize search parameters', error);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
102
110
|
}, [createDeepDependency(() => data)]);
|
|
103
111
|
}
|
|
104
112
|
return {
|