@azure-net/kit 6.0.0-next.5 → 6.0.0-next.7

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
@@ -68,27 +68,51 @@ import type { OrdersUseCases } from '#contexts/Orders/Application/UseCases/Order
68
68
  export class OrdersPresenter {
69
69
  constructor(private readonly useCases: OrdersUseCases) {}
70
70
 
71
- collection = () => createAsyncResource(() => this.useCases.collection());
71
+ collection() {
72
+ return createAsyncResource(() => this.useCases.collection());
73
+ }
72
74
  }
73
75
 
74
- export const useOrdersPresenter = RegisterProvider(() => {
75
- const { Orders } = ApplicationProvider();
76
- return new OrdersPresenter(Orders);
77
- }, 'scoped');
76
+ export const useOrdersPresenter = RegisterProvider(
77
+ () => {
78
+ const { Orders } = ApplicationProvider();
79
+ return new OrdersPresenter(Orders);
80
+ },
81
+ { lifecycle: 'scoped', bindMethods: true }
82
+ );
78
83
  ```
79
84
 
80
85
  Call `useOrdersPresenter()` during component initialization, not at module scope.
81
86
  The registration itself is safe at module scope and does not resolve dependencies.
82
- Arrow fields remain callable after destructuring. `RegisterProvider` returns the
83
- actual class instance, without a proxy or automatic method binding.
87
+ `RegisterProvider` returns the actual class instance, without a proxy. The optional
88
+ second argument is an options object: `{ lifecycle?: ..., bindMethods?: ... }`.
89
+ Defaults are `lifecycle: 'scoped'` and `bindMethods: false`. The old string argument
90
+ is no longer supported. With binding disabled, methods and property descriptors are
91
+ left untouched.
92
+
93
+ Use `bindMethods: true` to bind function-valued data properties, including inherited
94
+ methods, or `bindMethods: ['collection']` to select typed method names. Binding runs
95
+ only after the factory returns, once for each newly created instance. Accessors are
96
+ never evaluated or replaced, including Svelte rune accessors. Constructors and
97
+ Object.prototype methods are excluded. A selected accessor (even one returning a
98
+ function), missing method, or protected property throws instead of silently failing.
99
+ TypeScript cannot distinguish a function-returning getter from a method, so the
100
+ accessor check also happens at runtime.
101
+
102
+ Method references are stable after binding. Frozen/non-extensible instances cannot
103
+ receive inherited bound methods. Arbitrary proxies and exotic built-in objects are
104
+ not supported binding targets. Function-valued fields are also bound with `true`;
105
+ use an explicit list if the instance holds component constructors or other callable
106
+ dependencies. No global cache retains instances or bound functions. Binding does not
107
+ apply retroactively to callbacks captured during construction or methods replaced later.
84
108
  Classes using Svelte runes such as `$state` must be placed in a `.svelte.ts` module
85
109
  (or declared inside a Svelte component), not a regular `.ts` file.
86
110
 
87
- | Lifecycle | Server | Browser |
88
- | ------------------------------------------ | ----------------------------------------- | ----------------------------- |
89
- | `scoped` (default) | One instance per registration per request | New instance per call |
90
- | `transient` (`trancient` is also accepted) | New instance per call | New instance per call |
91
- | `singleton` | One instance per registration per request | One instance per registration |
111
+ | Lifecycle | Server | Browser |
112
+ | ------------------ | ----------------------------------------- | ----------------------------- |
113
+ | `scoped` (default) | One instance per registration per request | New instance per call |
114
+ | `transient` | New instance per call | New instance per call |
115
+ | `singleton` | One instance per registration per request | One instance per registration |
92
116
 
93
117
  Server caching requires `AzureNetPlugin` request context and fails
94
118
  outside it. There is no process-wide server singleton. Request caches have weak
@@ -1,5 +1,5 @@
1
1
  export { createAsyncAction, createAsyncResource, type AsyncActionSettings, type AsyncHelperRetry, type AsyncResourceSettings, type AsyncActionResponse } from './injectable-dependencies/index.js';
2
- export { RegisterProvider, type ProviderLifecycle } from './presenter/RegisterProvider.js';
2
+ export { RegisterProvider, type ProviderLifecycle, type RegisterProviderOptions, type ProviderMethodKey } from './presenter/RegisterProvider.js';
3
3
  export { Bound } from './presenter/Bound.js';
4
4
  export { createSchemaFactory, schema, type Schema, type RequestErrors, type ISchemaError, type ValidationResult, type ValidationRuleResult, type ValidationParams, type ValidationMessage, type ValidationErrorsMap } from './schema/index.js';
5
5
  export { createRules, validationMessagesRu, validationMessagesI18n, validationMessagesEn, type ValidationRuleParams, type BaseValidationMessages } from './schema/rules/index.js';
@@ -1,4 +1,11 @@
1
1
  export type ProviderLifecycle = 'scoped' | 'transient' | 'singleton';
2
+ export type ProviderMethodKey<T> = Exclude<{
3
+ [K in keyof T]-?: T[K] extends (...args: never[]) => unknown ? K : never;
4
+ }[keyof T], 'constructor'>;
5
+ export interface RegisterProviderOptions<T> {
6
+ lifecycle?: ProviderLifecycle;
7
+ bindMethods?: boolean | readonly ProviderMethodKey<T>[];
8
+ }
2
9
  /**
3
10
  * Registration is safe at module scope; the factory is called only by the returned accessor.
4
11
  * scoped: per request on the server, per call in the browser.
@@ -6,4 +13,4 @@ export type ProviderLifecycle = 'scoped' | 'transient' | 'singleton';
6
13
  * transient: a new instance on every call.
7
14
  * No hydration, component context, effects or disposal are managed by this function.
8
15
  */
9
- export declare function RegisterProvider<T extends object>(factory: () => T extends PromiseLike<unknown> ? never : T, lifecycle?: ProviderLifecycle): () => T;
16
+ export declare function RegisterProvider<T extends object>(factory: () => T extends PromiseLike<unknown> ? never : T, options?: RegisterProviderOptions<NoInfer<T>>): () => T;
@@ -1,5 +1,45 @@
1
1
  import { RequestContext } from '../../shared/request-context/Internal.js';
2
2
  import { BROWSER } from '../../external/tools/index.js';
3
+ const bindInstanceMethods = (instance, selection) => {
4
+ const descriptors = new Map();
5
+ const selected = selection === true ? undefined : new Set(selection);
6
+ const seen = new Set();
7
+ let target = instance;
8
+ while (target && target !== Object.prototype && target !== Function.prototype) {
9
+ for (const key of selected ?? Reflect.ownKeys(target)) {
10
+ if (key === 'constructor' || seen.has(key))
11
+ continue;
12
+ const descriptor = Object.getOwnPropertyDescriptor(target, key);
13
+ if (!descriptor)
14
+ continue;
15
+ seen.add(key);
16
+ // Never evaluate accessors, including getters returning functions or Svelte state.
17
+ if ('value' in descriptor && typeof descriptor.value === 'function')
18
+ descriptors.set(key, descriptor);
19
+ else if (selected)
20
+ throw new TypeError(`[RegisterProvider] Cannot bind non-method ${String(key)}.`);
21
+ }
22
+ if (selected && seen.size === selected.size)
23
+ break;
24
+ target = Object.getPrototypeOf(target);
25
+ }
26
+ if (selected) {
27
+ for (const key of selected) {
28
+ if (!descriptors.has(key))
29
+ throw new TypeError(`[RegisterProvider] Method ${String(key)} was not found or cannot be bound.`);
30
+ }
31
+ }
32
+ // Validate every write before changing ordinary instances, avoiding partial binding on errors.
33
+ for (const key of descriptors.keys()) {
34
+ const own = Object.getOwnPropertyDescriptor(instance, key);
35
+ if (own ? !own.configurable && !own.writable : !Object.isExtensible(instance)) {
36
+ throw new TypeError(`[RegisterProvider] Cannot bind protected method ${String(key)}.`);
37
+ }
38
+ }
39
+ for (const [key, descriptor] of descriptors) {
40
+ Object.defineProperty(instance, key, { ...descriptor, value: Function.prototype.bind.call(descriptor.value, instance) });
41
+ }
42
+ };
3
43
  /**
4
44
  * Registration is safe at module scope; the factory is called only by the returned accessor.
5
45
  * scoped: per request on the server, per call in the browser.
@@ -7,10 +47,16 @@ import { BROWSER } from '../../external/tools/index.js';
7
47
  * transient: a new instance on every call.
8
48
  * No hydration, component context, effects or disposal are managed by this function.
9
49
  */
10
- export function RegisterProvider(factory, lifecycle = 'scoped') {
50
+ export function RegisterProvider(factory, options = {}) {
51
+ if (!options || typeof options !== 'object' || Array.isArray(options))
52
+ throw new TypeError('[RegisterProvider] Options must be an object.');
53
+ const { lifecycle = 'scoped', bindMethods = false } = options;
11
54
  if (!['scoped', 'transient', 'singleton'].includes(lifecycle)) {
12
55
  throw new TypeError(`[RegisterProvider] Unknown lifecycle: ${lifecycle}`);
13
56
  }
57
+ if (typeof bindMethods !== 'boolean' && !Array.isArray(bindMethods))
58
+ throw new TypeError('[RegisterProvider] bindMethods must be a boolean or method list.');
59
+ const selection = Array.isArray(bindMethods) ? [...bindMethods] : bindMethods;
14
60
  const requests = new WeakMap();
15
61
  let clientInstance;
16
62
  let constructing = false;
@@ -28,6 +74,8 @@ export function RegisterProvider(factory, lifecycle = 'scoped') {
28
74
  void Promise.resolve(instance).catch(() => undefined);
29
75
  throw new TypeError('[RegisterProvider] Factory must be synchronous.');
30
76
  }
77
+ if (selection === true || (Array.isArray(selection) && selection.length > 0))
78
+ bindInstanceMethods(instance, selection);
31
79
  return instance;
32
80
  }
33
81
  finally {
@@ -249,6 +249,6 @@ export const createTranslations = (params) => {
249
249
  '">')
250
250
  });
251
251
  return manager;
252
- }, 'singleton');
252
+ }, { lifecycle: 'singleton' });
253
253
  return factory;
254
254
  };
@@ -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 { replaceState } from '$app/navigation';
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,14 +54,15 @@ const createDeepDependency = (read) => {
53
54
  };
54
55
  };
55
56
  export const createQuery = (initialValue, options) => {
56
- const baseInitial = cloneStateValue(initialValue);
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
64
  const parsedInitial = syncEnabled
63
- ? parseSearchParams(new URLSearchParams(page.url.searchParams.toString()), cloneStateValue(baseInitial))
65
+ ? parseSearchParams(new URLSearchParams((page.shallow?.url ?? page.url).searchParams.toString()), cloneStateValue(baseInitial))
64
66
  : undefined;
65
67
  let data = $state(parsedInitial ? { ...cloneStateValue(baseInitial), ...cloneStateValue(parsedInitial) } : cloneStateValue(baseInitial));
66
68
  const reset = () => {
@@ -94,13 +96,17 @@ export const createQuery = (initialValue, options) => {
94
96
  if (nextParams.toString() === initialParams.toString())
95
97
  return;
96
98
  }
97
- const url = new URL(page.url.href);
99
+ const currentUrl = page.shallow?.url ?? page.url;
100
+ const url = new URL(currentUrl.href);
98
101
  for (const key of managedKeys)
99
102
  url.searchParams.delete(key);
100
103
  for (const [key, value] of nextParams)
101
104
  url.searchParams.append(key, value);
102
- if (url.search !== page.url.search)
103
- replaceState(url, page.state);
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
+ }
104
110
  }, [createDeepDependency(() => data)]);
105
111
  }
106
112
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-net/kit",
3
- "version": "6.0.0-next.5",
3
+ "version": "6.0.0-next.7",
4
4
  "license": "MIT",
5
5
  "author": "Pixel1917",
6
6
  "description": "Universal toolkit and library collection, developed as part of the Azure-net ecosystem",