akanjs 3.0.0-alpha.6 → 3.0.0-alpha.8

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.
@@ -75,6 +75,13 @@ globalWithRuntime[CLIENT_RUNTIME_KEY] = state;
75
75
  const missingRuntimeError = () =>
76
76
  new Error("Akan client runtime is not registered. Import the generated app client first.");
77
77
 
78
+ const applyRuntimeErrorConstructor = (runtime: ClientRuntime) => {
79
+ const instance = (runtime.fetch as RuntimeFetch | undefined)?.instance as
80
+ | { setErrorConstructor?: (Err: unknown) => void }
81
+ | undefined;
82
+ if (typeof instance?.setErrorConstructor === "function") instance.setErrorConstructor(runtime.Err);
83
+ };
84
+
78
85
  export const registerClientRuntime = <Runtime>(
79
86
  runtime: Runtime,
80
87
  { scope = "app" }: { scope?: RuntimeScope } = {},
@@ -82,6 +89,7 @@ export const registerClientRuntime = <Runtime>(
82
89
  if (state.scope === "app" && scope === "lib") return runtime;
83
90
  state.runtime = runtime as ClientRuntime;
84
91
  state.scope = scope;
92
+ applyRuntimeErrorConstructor(state.runtime);
85
93
  return runtime;
86
94
  };
87
95
 
@@ -1,9 +1,16 @@
1
1
  import { isDayjs } from "./isDayjs";
2
2
 
3
- export const deepObjectify = <T = unknown>(
4
- obj: T | null | undefined,
5
- option: { serializable?: boolean; convertDate?: "string" | "number" } = {},
6
- ): T => {
3
+ interface DeepObjectifyOption {
4
+ serializable?: boolean;
5
+ convertDate?: "string" | "number";
6
+ }
7
+
8
+ const objectifyChild = (value: unknown, option: DeepObjectifyOption): unknown => {
9
+ const modelValue = value as { __ModelType__?: string } | null | undefined;
10
+ return modelValue?.__ModelType__ && !option.serializable ? value : deepObjectify(value, option);
11
+ };
12
+
13
+ export const deepObjectify = <T = unknown>(obj: T | null | undefined, option: DeepObjectifyOption = {}): T => {
7
14
  if (isDayjs(obj) || obj?.constructor === Date) {
8
15
  if (!option.serializable && !option.convertDate) return obj as T;
9
16
  if (option.convertDate === "string") return obj.toISOString() as T;
@@ -12,13 +19,20 @@ export const deepObjectify = <T = unknown>(
12
19
  else return (isDayjs(obj) ? obj.toDate() : obj) as T;
13
20
  } else if (Array.isArray(obj)) {
14
21
  return obj.map((o: unknown) => deepObjectify(o, option)) as T;
22
+ } else if (obj instanceof Map) {
23
+
24
+ const entries = [...obj.entries()].map(
25
+ ([key, value]: [string, unknown]) => [key, objectifyChild(value, option)] as const,
26
+ );
27
+ return (option.serializable ? Object.fromEntries(entries) : new Map(entries)) as T;
28
+ } else if (obj instanceof Set) {
29
+ const values = [...obj.values()].map((value: unknown) => objectifyChild(value, option));
30
+ return (option.serializable ? values : new Set(values)) as T;
15
31
  } else if (obj && typeof obj === "object") {
16
32
  const val: Record<string, unknown> = {};
17
33
  const objRecord = obj as Record<string, unknown>;
18
34
  Object.keys(obj).forEach((key) => {
19
- const fieldValue = objRecord[key] as { __ModelType__: string } | null | undefined;
20
- if (fieldValue?.__ModelType__ && !option.serializable) val[key] = fieldValue;
21
- else if (typeof objRecord[key] !== "function") val[key] = deepObjectify(fieldValue, option);
35
+ if (typeof objRecord[key] !== "function") val[key] = objectifyChild(objRecord[key], option);
22
36
  });
23
37
  return val as T;
24
38
  } else {
package/common/index.ts CHANGED
@@ -36,12 +36,17 @@ export { randomPicks } from "./randomPicks";
36
36
  export {
37
37
  assertUniqueRoutePatterns,
38
38
  compareRouteSpecificity,
39
+ getRouteExports,
39
40
  isRouteSourceFile,
40
41
  isSpecialRouteLeaf,
42
+ LAYOUT_ROUTE_EXPORTS,
41
43
  matchRoutePattern,
42
44
  normalizeRoutePattern,
45
+ PAGE_ROUTE_EXPORTS,
43
46
  type ParsedRouteModuleKey,
44
47
  parseRouteModuleKey,
48
+ RESERVED_ROUTE_CONFIG_EXPORTS,
49
+ ROOT_LAYOUT_ROUTE_EXPORTS,
45
50
  type RouteModuleKind,
46
51
  routeSegmentToPatternPart,
47
52
  routeSegmentToTreePath,
package/common/pathSet.ts CHANGED
@@ -1,16 +1,28 @@
1
1
  type MutableIndexable = Record<string | number, unknown>;
2
2
  type PathSegment = string | number;
3
+ type Container = MutableIndexable | Map<PathSegment, unknown>;
3
4
 
4
5
  const toPathSegments = (path: string | readonly PathSegment[]) =>
5
6
  Array.isArray(path) ? [...path] : path.toString().match(/[^.[\]]+/g) || [];
6
7
 
8
+ const readChild = (container: Container, key: PathSegment) =>
9
+ container instanceof Map ? container.get(key) : container[key];
10
+
11
+ const writeChild = (container: Container, key: PathSegment, value: unknown) => {
12
+ if (container instanceof Map) container.set(key, value);
13
+ else container[key] = value;
14
+ };
15
+
7
16
  export const pathSet = <T>(obj: T, path: string | readonly PathSegment[], value: unknown): T => {
8
17
  if (Object(obj) !== obj) return obj;
9
18
  const pathSegments = toPathSegments(path);
10
- pathSegments.slice(0, -1).reduce<MutableIndexable>((a, c, i) => {
11
- if (Object(a[c]) === a[c]) return a[c] as MutableIndexable;
12
- a[c] = Math.abs(Number(pathSegments[i + 1])) >> 0 === +pathSegments[i + 1] ? [] : {};
13
- return a[c] as MutableIndexable;
14
- }, obj as MutableIndexable)[pathSegments[pathSegments.length - 1]] = value;
19
+ const parent = pathSegments.slice(0, -1).reduce<Container>((a, c, i) => {
20
+ const child = readChild(a, c);
21
+ if (Object(child) === child) return child as Container;
22
+ const created = Math.abs(Number(pathSegments[i + 1])) >> 0 === +pathSegments[i + 1] ? [] : {};
23
+ writeChild(a, c, created);
24
+ return created as unknown as Container;
25
+ }, obj as Container);
26
+ writeChild(parent, pathSegments[pathSegments.length - 1], value);
15
27
  return obj;
16
28
  };
@@ -9,6 +9,36 @@ const DIRECTORY_SCOPED_LEAVES = new Set(["_layout", "_index", "_overrides"]);
9
9
 
10
10
  export type RouteModuleKind = "page" | "layout" | "overrides";
11
11
 
12
+ export const PAGE_ROUTE_EXPORTS: ReadonlySet<string> = new Set([
13
+ "default",
14
+ "pageConfig",
15
+ "head",
16
+ "metadata",
17
+ "generateHead",
18
+ "generateMetadata",
19
+ "Loading",
20
+ ]);
21
+ export const LAYOUT_ROUTE_EXPORTS: ReadonlySet<string> = new Set([...PAGE_ROUTE_EXPORTS, "NotFound", "Error"]);
22
+ export const ROOT_LAYOUT_ROUTE_EXPORTS: ReadonlySet<string> = new Set([
23
+ ...LAYOUT_ROUTE_EXPORTS,
24
+ "fonts",
25
+ "manifest",
26
+ "theme",
27
+ "reconnect",
28
+ "wsConnect",
29
+ "layoutStyle",
30
+ "gaTrackingId",
31
+ ]);
32
+ /** Root-layout exports that are plain config rather than components, so a PascalCase check cannot allow them. */
33
+ export const RESERVED_ROUTE_CONFIG_EXPORTS: ReadonlySet<string> = new Set(
34
+ [...ROOT_LAYOUT_ROUTE_EXPORTS].filter((name) => name !== "default" && !/^[A-Z]/.test(name)),
35
+ );
36
+
37
+ export function getRouteExports(kind: "page" | "layout", { rootLayout = false } = {}): ReadonlySet<string> {
38
+ if (kind === "page") return PAGE_ROUTE_EXPORTS;
39
+ return rootLayout ? ROOT_LAYOUT_ROUTE_EXPORTS : LAYOUT_ROUTE_EXPORTS;
40
+ }
41
+
12
42
  export interface ParsedRouteModuleKey {
13
43
  key: string;
14
44
  kind: RouteModuleKind;
@@ -30,8 +30,10 @@ export const crystalize = (field: FieldProps, value: unknown): unknown => {
30
30
  isArray: false,
31
31
  arrDepth: 0,
32
32
  };
33
+
34
+ const entries = value instanceof Map ? [...value.entries()] : Object.entries(value as Record<string, unknown>);
33
35
  return new Map(
34
- Object.entries(value as Record<string, unknown>).map(([key, val]) => [
36
+ entries.map(([key, val]: [string, unknown]) => [
35
37
  key,
36
38
  field.of
37
39
  ? applyFnToArrayObjects(val, (v: never) => crystalize(mapValueField, v))
package/document/into.ts CHANGED
@@ -88,6 +88,8 @@ export type Mdl<
88
88
  updateMany(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>): Promise<UpdateResult>;
89
89
  removeOne(query: _RawQuery): Promise<UpdateResult>;
90
90
  removeMany(query: _RawQuery): Promise<UpdateResult>;
91
+ updateById(id: string, update: DocumentUpdateInput<_RawDoc>, options?: DocumentUpdateOptions): Promise<UpdateResult>;
92
+ removeById(id: string): Promise<UpdateResult>;
91
93
  bulkWrite(operations: BulkWriteOperation<Raw, _RawDoc, _RawQuery>[]): Promise<UpdateResult>;
92
94
  /** @deprecated Renamed to `count`. */
93
95
  countDocuments(query: _RawQuery): Promise<number>;
@@ -37,6 +37,16 @@ export type FetchProxy<
37
37
  FetchClient &
38
38
  FetchType & { slice: SliceMetaObj; instance: FetchClient; _FetchType: FetchType; _SliceMetaObj: SliceMetaObj };
39
39
 
40
+ interface SharedClientState {
41
+ proxy: FetchProxy | null;
42
+ origin: string | null;
43
+ }
44
+
45
+ const SHARED_CLIENT_KEY = Symbol.for("akanjs.fetch.sharedClient");
46
+ const globalWithSharedClient = globalThis as typeof globalThis & { [SHARED_CLIENT_KEY]?: SharedClientState };
47
+ const sharedClientState: SharedClientState = globalWithSharedClient[SHARED_CLIENT_KEY] ?? { proxy: null, origin: null };
48
+ globalWithSharedClient[SHARED_CLIENT_KEY] = sharedClientState;
49
+
40
50
  type ClientSignalMap<SigType extends { fetch: any }> = {
41
51
  [K in keyof SigType as SigType[K] extends DatabaseSignal<any, any, any, any>
42
52
  ? K
@@ -80,6 +90,19 @@ export class FetchClient {
80
90
  FetchClient.#sharedSerializedSignal = {};
81
91
  FetchClient.#sharedRegistryVersion++;
82
92
  }
93
+ static resetSharedClient() {
94
+ sharedClientState.proxy = null;
95
+ sharedClientState.origin = null;
96
+ }
97
+ static #resolveSharedClientProxy(origin: string, Err?: ErrorConstructor) {
98
+ if (typeof window === "undefined") return null;
99
+
100
+ if (sharedClientState.proxy) return sharedClientState.origin === origin ? sharedClientState.proxy : null;
101
+ const proxy = FetchClient.#makeProxy<unknown, Record<string, SliceMeta>>(new FetchClient(origin, {}, {}, Err));
102
+ sharedClientState.proxy = proxy;
103
+ sharedClientState.origin = origin;
104
+ return proxy;
105
+ }
83
106
  static #mergeSerializedSignalInto(
84
107
  serializedSignal: { [key: string]: SerializedSignal },
85
108
  refName: string,
@@ -700,10 +723,11 @@ export class FetchClient {
700
723
  sig: ClientSignalMap<SigType>;
701
724
  fetch: SigType["fetch"];
702
725
  } {
703
- if (base) base.instance.applySignal(serializedSignal);
726
+ const shared = base ?? FetchClient.#resolveSharedClientProxy(origin, Err);
727
+ if (shared) shared.instance.applySignal(serializedSignal);
704
728
  if (base && Err) base.instance.setErrorConstructor(Err);
705
729
  const proxy =
706
- base ??
730
+ shared ??
707
731
  FetchClient.#makeProxy<unknown, Record<string, SliceMeta>>(new FetchClient(origin, {}, serializedSignal, Err));
708
732
  if (connect) proxy.instance.connect();
709
733
  const sig = {} as any;
@@ -40,6 +40,8 @@ export class WsClient {
40
40
  #roomSubscribeMap = new Map<string, SubscribeOption>();
41
41
  #listenerMap = new Map<string, Set<Listener>>();
42
42
  #destroyed = false;
43
+ #connectRequested = false;
44
+ #unconnectedWarnTimers = new Map<string, ReturnType<typeof setTimeout>>();
43
45
  #jwt: string | null = null;
44
46
  connected = false;
45
47
 
@@ -70,6 +72,7 @@ export class WsClient {
70
72
  }
71
73
 
72
74
  connect() {
75
+ this.#connectRequested = true;
73
76
  if (this.#ws && this.#ws.readyState !== WebSocket.CLOSED) return;
74
77
  this.logger.debug(`Connecting to ${this.url}`);
75
78
  this.#destroyed = false;
@@ -187,10 +190,13 @@ export class WsClient {
187
190
  destroy() {
188
191
  this.logger.debug(`WebSocket destroying`);
189
192
  this.#destroyed = true;
193
+ this.#connectRequested = false;
190
194
  if (this.#reconnectTimer) {
191
195
  clearTimeout(this.#reconnectTimer);
192
196
  this.#reconnectTimer = null;
193
197
  }
198
+ for (const timer of this.#unconnectedWarnTimers.values()) clearTimeout(timer);
199
+ this.#unconnectedWarnTimers.clear();
194
200
  this.#ws?.close();
195
201
  this.#ws = null;
196
202
  }
@@ -231,9 +237,18 @@ export class WsClient {
231
237
  }
232
238
  #warnNotConnected(action: "emit" | "subscribe", key: string) {
233
239
  console.warn(
234
- `[akanjs] WebSocket is not connected. Call fetch.instance.connect() or enable root layout "wsConnect" before ${action} "${key}".`,
240
+ `[akanjs] WebSocket is not connected. Call fetch.instance.connect(), or drop the root layout "wsConnect = false", before ${action} "${key}".`,
235
241
  );
236
242
  }
243
+ #warnUnconnectedSubscribe(key: string) {
244
+ if (this.#connectRequested || this.#unconnectedWarnTimers.has(key)) return;
245
+ const timer = setTimeout(() => {
246
+ this.#unconnectedWarnTimers.delete(key);
247
+ if (this.#connectRequested || this.#destroyed) return;
248
+ this.#warnNotConnected("subscribe", key);
249
+ }, 0);
250
+ this.#unconnectedWarnTimers.set(key, timer);
251
+ }
237
252
  emit(key: string, data: WsRequestPayload) {
238
253
  if (this.#ws?.readyState !== WebSocket.OPEN) {
239
254
  this.logger.warn("WebSocket not connected");
@@ -246,7 +261,7 @@ export class WsClient {
246
261
  }
247
262
  subscribe(option: { key: string; data: unknown[]; handleEvent: (data: unknown) => void }) {
248
263
  const roomId = WsClient.makeRoomId(option.key, option.data);
249
- if (!this.#ws) this.#warnNotConnected("subscribe", option.key);
264
+ if (!this.#ws) this.#warnUnconnectedSubscribe(option.key);
250
265
  if (!this.#roomSubscribeMap.has(roomId)) {
251
266
  this.#roomSubscribeMap.set(roomId, { key: option.key, data: option.data, listener: new Set() });
252
267
  if (this.#ws?.readyState === WebSocket.OPEN) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.6",
3
+ "version": "3.0.0-alpha.8",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -186,7 +186,7 @@ export class CascadeRunner {
186
186
  }
187
187
  if (!lines.length) return;
188
188
  const bulk = lines.filter((line) => line.endsWith("(bulk)")).length;
189
- this.#logger.info(`${lines.length} cascade edge(s), ${bulk} in one query`);
189
+ this.#logger.verbose(`${lines.length} cascade edge(s), ${bulk} in one query`);
190
190
  for (const line of lines) this.#logger.verbose(line);
191
191
  }
192
192
 
@@ -240,6 +240,9 @@ export class DatabaseResolver {
240
240
  updateMany: (query: QueryOf<any>, update: DocumentUpdateInput) => store.updateManyByQuery(query, update),
241
241
  removeOne: (query: QueryOf<any>) => store.removeOneByQuery(query),
242
242
  removeMany: (query: QueryOf<any>) => store.removeManyByQuery(query),
243
+ updateById: (id: string, update: DocumentUpdateInput, options?: { upsert?: boolean }) =>
244
+ store.updateOneByQuery({ id }, update, options),
245
+ removeById: (id: string) => store.removeOneByQuery({ id }),
243
246
 
244
247
  countDocuments: (query: QueryOf<any>) => store.count(query),
245
248
  bulkWrite: (
@@ -13,6 +13,7 @@ import type {
13
13
  import {
14
14
  assertUniqueRoutePatterns,
15
15
  compareRouteSpecificity,
16
+ getRouteExports,
16
17
  matchRoutePattern,
17
18
  parseBasePaths,
18
19
  parseRouteModuleKey,
@@ -49,44 +50,6 @@ export interface RouteModuleCacheStats {
49
50
  }
50
51
 
51
52
  export class RouteTreeBuilder {
52
- static readonly #pageRouteExports = new Set([
53
- "default",
54
- "pageConfig",
55
- "head",
56
- "metadata",
57
- "generateHead",
58
- "generateMetadata",
59
- "Loading",
60
- ]);
61
- static readonly #rootLayoutExports = new Set([
62
- "default",
63
- "pageConfig",
64
- "head",
65
- "metadata",
66
- "generateHead",
67
- "generateMetadata",
68
- "fonts",
69
- "manifest",
70
- "theme",
71
- "reconnect",
72
- "wsConnect",
73
- "layoutStyle",
74
- "gaTrackingId",
75
- "Loading",
76
- "NotFound",
77
- "Error",
78
- ]);
79
- static readonly #layoutRouteExports = new Set([
80
- "default",
81
- "pageConfig",
82
- "head",
83
- "metadata",
84
- "generateHead",
85
- "generateMetadata",
86
- "Loading",
87
- "NotFound",
88
- "Error",
89
- ]);
90
53
  static readonly #moduleCacheStats: RouteModuleCacheStats = {
91
54
  moduleCount: 0,
92
55
  loadedModuleCount: 0,
@@ -309,12 +272,7 @@ export class RouteTreeBuilder {
309
272
  return;
310
273
  }
311
274
  const parsed = parseRouteModuleKey(key);
312
- const allowed =
313
- kind === "page"
314
- ? RouteTreeBuilder.#pageRouteExports
315
- : parsed.isInternalRootLayout
316
- ? RouteTreeBuilder.#rootLayoutExports
317
- : RouteTreeBuilder.#layoutRouteExports;
275
+ const allowed = getRouteExports(kind, { rootLayout: parsed.isInternalRootLayout });
318
276
  for (const exportName of Object.keys(mod)) {
319
277
  if (!allowed.has(exportName)) {
320
278
  throw new Error(`[route-convention] unsupported export "${exportName}" in ${key}`);
package/store/action.ts CHANGED
@@ -751,7 +751,7 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
751
751
  const id = typeof modelOrId === "string" ? modelOrId : modelOrId.id;
752
752
  this.set({ [names.modelFormLoading]: id, [names.modelModal]: modal ?? "edit" });
753
753
  const model = await (fetch[names.model] as (...args: any[]) => Promise<Full>)(id, { onError });
754
- const modelForm = deepObjectify<Input>(model as unknown as Input);
754
+ const modelForm = immerify(modelRef, deepObjectify<Input>(model as unknown as Input) as object) as Input;
755
755
  this.set({
756
756
  [names.model]: model,
757
757
  [names.modelFormLoading]: false,
@@ -1,13 +1,15 @@
1
1
  import { ACTION_META, STATE_DERIVED_META, STATE_INIT_META } from "akanjs/base";
2
2
  import { Translator } from "akanjs/client";
3
3
  import { capitalize, Logger, parseAkanI18nEnv } from "akanjs/common";
4
- import { produce } from "immer";
4
+ import { enableMapSet, produce } from "immer";
5
5
  import type { RefObject } from "react";
6
6
  import { useEffect, useRef, useSyncExternalStore } from "./hooks";
7
7
  import type { RootStoreCls } from "./rootStore";
8
8
  import type { SliceStateKey } from "./state";
9
9
  import { evaluateInitializers, type SearchParamsState, type StateDerivedMeta } from "./stateBuilder";
10
10
 
11
+ enableMapSet();
12
+
11
13
  type StoreStateRecord = Record<string, unknown>;
12
14
  type StoreAction = (...args: unknown[]) => unknown;
13
15
  type TranslationParam = Record<string, string | number>;
package/store/types.ts CHANGED
@@ -36,6 +36,13 @@ export type Get<State, Actions> = {
36
36
  get: () => State & Actions;
37
37
  };
38
38
 
39
+ type VoidAction<T> = T extends (...args: infer Args) => infer Ret
40
+ ? [Ret] extends [PromiseLike<unknown>]
41
+ ? (...args: Args) => Promise<void>
42
+ : (...args: Args) => void
43
+ : T;
44
+ export type VoidActions<Action> = { [K in keyof Action]: VoidAction<Action[K]> };
45
+
39
46
  export type StoreSliceMap<SlceCls extends SliceCls> = SlceCls[typeof SLICE_META];
40
47
  export type StoreSliceSuffix<SlceCls extends SliceCls, Suffix extends keyof StoreSliceMap<SlceCls>> = Suffix & string;
41
48
  export type StoreSliceSuffixCap<SlceCls extends SliceCls, Suffix extends keyof StoreSliceMap<SlceCls>> = Capitalize<
@@ -2,7 +2,7 @@ import type { Prettify } from "akanjs/base";
2
2
  import type { FieldState } from "akanjs/constant";
3
3
  import type { RefObject } from "react";
4
4
  import type { RootStoreCls } from "./rootStore";
5
- import type { SliceStateAction } from "./types";
5
+ import type { SliceStateAction, VoidActions } from "./types";
6
6
 
7
7
  type SetKey<Key extends string> = `set${Capitalize<Key>}`;
8
8
 
@@ -28,7 +28,7 @@ type WithSelectorsOf<State, WritableState, Action, InternalSliceObj> = {
28
28
  use: {
29
29
  [K in keyof State]: () => State[K];
30
30
  };
31
- do: Action & {
31
+ do: VoidActions<Action> & {
32
32
  [K in keyof WritableState as K extends string ? SetKey<K> : never]: (value: FieldState<WritableState[K]>) => void;
33
33
  };
34
34
  get: () => State;
@@ -52,9 +52,7 @@ export interface SliceSelectors<RefName extends string, State, Action> {
52
52
  [K in keyof State]: () => State[K];
53
53
  };
54
54
  do: Prettify<
55
- {
56
- [K in keyof Action]: Action[K];
57
- } & {
55
+ VoidActions<Action> & {
58
56
  [K in keyof State as K extends string ? SetKey<K> : never]: (value: FieldState<State[K]>) => void;
59
57
  }
60
58
  >;
@@ -1,4 +1,6 @@
1
- export declare const deepObjectify: <T = unknown>(obj: T | null | undefined, option?: {
1
+ interface DeepObjectifyOption {
2
2
  serializable?: boolean;
3
3
  convertDate?: "string" | "number";
4
- }) => T;
4
+ }
5
+ export declare const deepObjectify: <T = unknown>(obj: T | null | undefined, option?: DeepObjectifyOption) => T;
6
+ export {};
@@ -23,7 +23,7 @@ export { pathGet } from "./pathGet.d.ts";
23
23
  export { pathSet } from "./pathSet.d.ts";
24
24
  export { randomPick } from "./randomPick.d.ts";
25
25
  export { randomPicks } from "./randomPicks.d.ts";
26
- export { assertUniqueRoutePatterns, compareRouteSpecificity, isRouteSourceFile, isSpecialRouteLeaf, matchRoutePattern, normalizeRoutePattern, type ParsedRouteModuleKey, parseRouteModuleKey, type RouteModuleKind, routeSegmentToPatternPart, routeSegmentToTreePath, tryParseRouteModuleKey, type ValidatePageSourceFileOptions, type ValidateSubRoutePageKeyOptions, validatePageSourceFile, validateSubRoutePageKey, } from "./routeConvention.d.ts";
26
+ export { assertUniqueRoutePatterns, compareRouteSpecificity, getRouteExports, isRouteSourceFile, isSpecialRouteLeaf, LAYOUT_ROUTE_EXPORTS, matchRoutePattern, normalizeRoutePattern, PAGE_ROUTE_EXPORTS, type ParsedRouteModuleKey, parseRouteModuleKey, RESERVED_ROUTE_CONFIG_EXPORTS, ROOT_LAYOUT_ROUTE_EXPORTS, type RouteModuleKind, routeSegmentToPatternPart, routeSegmentToTreePath, tryParseRouteModuleKey, type ValidatePageSourceFileOptions, type ValidateSubRoutePageKeyOptions, validatePageSourceFile, validateSubRoutePageKey, } from "./routeConvention.d.ts";
27
27
  export { sleep } from "./sleep.d.ts";
28
28
  export { splitVersion } from "./splitVersion.d.ts";
29
29
  export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute.d.ts";
@@ -1,4 +1,12 @@
1
1
  export type RouteModuleKind = "page" | "layout" | "overrides";
2
+ export declare const PAGE_ROUTE_EXPORTS: ReadonlySet<string>;
3
+ export declare const LAYOUT_ROUTE_EXPORTS: ReadonlySet<string>;
4
+ export declare const ROOT_LAYOUT_ROUTE_EXPORTS: ReadonlySet<string>;
5
+ /** Root-layout exports that are plain config rather than components, so a PascalCase check cannot allow them. */
6
+ export declare const RESERVED_ROUTE_CONFIG_EXPORTS: ReadonlySet<string>;
7
+ export declare function getRouteExports(kind: "page" | "layout", { rootLayout }?: {
8
+ rootLayout?: boolean | undefined;
9
+ }): ReadonlySet<string>;
2
10
  export interface ParsedRouteModuleKey {
3
11
  key: string;
4
12
  kind: RouteModuleKind;
@@ -62,6 +62,8 @@ export type Mdl<Doc, Raw, _RawDoc = DocumentModel<Raw>, _RawQuery extends Docume
62
62
  updateMany(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>): Promise<UpdateResult>;
63
63
  removeOne(query: _RawQuery): Promise<UpdateResult>;
64
64
  removeMany(query: _RawQuery): Promise<UpdateResult>;
65
+ updateById(id: string, update: DocumentUpdateInput<_RawDoc>, options?: DocumentUpdateOptions): Promise<UpdateResult>;
66
+ removeById(id: string): Promise<UpdateResult>;
65
67
  bulkWrite(operations: BulkWriteOperation<Raw, _RawDoc, _RawQuery>[]): Promise<UpdateResult>;
66
68
  /** @deprecated Renamed to `count`. */
67
69
  countDocuments(query: _RawQuery): Promise<number>;
@@ -35,6 +35,7 @@ export declare class FetchClient {
35
35
  [key: string]: SerializedSignal;
36
36
  }, ErrorCls?: ErrorConstructor | undefined);
37
37
  static resetSharedRegistry(): void;
38
+ static resetSharedClient(): void;
38
39
  setErrorConstructor(ErrorCls?: ErrorConstructor): void;
39
40
  applySignal(serializedSignal: {
40
41
  [key: string]: SerializedSignal;
@@ -297,7 +297,7 @@ export declare const st: {
297
297
  deviceToken: () => string;
298
298
  currentPath: () => string;
299
299
  };
300
- do: RootStore & {
300
+ do: import("./types.d.ts").VoidActions<RootStore> & {
301
301
  setCsrLoaded: (value: boolean) => void;
302
302
  setPath: (value: string) => void;
303
303
  setPathname: (value: string) => void;
@@ -30,6 +30,10 @@ export interface SetPick<State = any> {
30
30
  export type Get<State, Actions> = {
31
31
  get: () => State & Actions;
32
32
  };
33
+ type VoidAction<T> = T extends (...args: infer Args) => infer Ret ? [Ret] extends [PromiseLike<unknown>] ? (...args: Args) => Promise<void> : (...args: Args) => void : T;
34
+ export type VoidActions<Action> = {
35
+ [K in keyof Action]: VoidAction<Action[K]>;
36
+ };
33
37
  export type StoreSliceMap<SlceCls extends SliceCls> = SlceCls[typeof SLICE_META];
34
38
  export type StoreSliceSuffix<SlceCls extends SliceCls, Suffix extends keyof StoreSliceMap<SlceCls>> = Suffix & string;
35
39
  export type StoreSliceSuffixCap<SlceCls extends SliceCls, Suffix extends keyof StoreSliceMap<SlceCls>> = Capitalize<StoreSliceSuffix<SlceCls, Suffix>>;
@@ -2,7 +2,7 @@ import type { Prettify } from "akanjs/base";
2
2
  import type { FieldState } from "akanjs/constant";
3
3
  import type { RefObject } from "react";
4
4
  import type { RootStoreCls } from "./rootStore.d.ts";
5
- import type { SliceStateAction } from "./types.d.ts";
5
+ import type { SliceStateAction, VoidActions } from "./types.d.ts";
6
6
  type SetKey<Key extends string> = `set${Capitalize<Key>}`;
7
7
  export type WithSelectors<RtStoreCls extends RootStoreCls> = RtStoreCls extends RootStoreCls<any, infer WritableState, infer Action, infer InternalSliceObj, any, any, infer State> ? WithSelectorsOf<State, WritableState, Action, InternalSliceObj> : never;
8
8
  type WithSelectorsOf<State, WritableState, Action, InternalSliceObj> = {
@@ -18,7 +18,7 @@ type WithSelectorsOf<State, WritableState, Action, InternalSliceObj> = {
18
18
  use: {
19
19
  [K in keyof State]: () => State[K];
20
20
  };
21
- do: Action & {
21
+ do: VoidActions<Action> & {
22
22
  [K in keyof WritableState as K extends string ? SetKey<K> : never]: (value: FieldState<WritableState[K]>) => void;
23
23
  };
24
24
  get: () => State;
@@ -32,9 +32,7 @@ export interface SliceSelectors<RefName extends string, State, Action> {
32
32
  use: {
33
33
  [K in keyof State]: () => State[K];
34
34
  };
35
- do: Prettify<{
36
- [K in keyof Action]: Action[K];
37
- } & {
35
+ do: Prettify<VoidActions<Action> & {
38
36
  [K in keyof State as K extends string ? SetKey<K> : never]: (value: FieldState<State[K]>) => void;
39
37
  }>;
40
38
  get: () => State;
@@ -7,7 +7,7 @@ type CsrImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, "alt" | "src"> &
7
7
  imageSize: [number, number];
8
8
  abstractData?: string | null;
9
9
  } | null;
10
- abstractData?: string;
10
+ abstractData?: string | null;
11
11
  priority?: boolean;
12
12
  preload?: boolean;
13
13
  quality?: number;
@@ -19,7 +19,7 @@ type AkanImageProps = NativeImageProps & {
19
19
  /** Akan file object or file-like value with url and imageSize metadata. */
20
20
  file?: ImageLikeFile;
21
21
  /** Low-quality preview data. Overrides file.abstractData when provided. */
22
- abstractData?: string;
22
+ abstractData?: string | null;
23
23
  /** Accessible alt text. Defaults to "image" when omitted. */
24
24
  alt?: string;
25
25
  /** Image optimizer quality. Defaults to 75. */
@@ -34,11 +34,11 @@ type AkanImageProps = NativeImageProps & {
34
34
  export declare const Image: ({ src, file, className, abstractData, alt, quality, priority, preload, unoptimized, ...props }: AkanImageProps & ({
35
35
  src?: string;
36
36
  file?: ProtoFile;
37
- abstractData?: string;
37
+ abstractData?: string | null;
38
38
  alt?: string;
39
39
  } | {
40
40
  src?: undefined;
41
- abstractData?: string;
41
+ abstractData?: string | null;
42
42
  file: {
43
43
  url: string;
44
44
  imageSize: [number, number];
package/ui/CsrImage.tsx CHANGED
@@ -5,7 +5,7 @@ import type { ImgHTMLAttributes } from "react";
5
5
  type CsrImageProps = Omit<ImgHTMLAttributes<HTMLImageElement>, "alt" | "src"> & {
6
6
  src?: string;
7
7
  file?: ProtoFile | { url: string; imageSize: [number, number]; abstractData?: string | null } | null;
8
- abstractData?: string;
8
+ abstractData?: string | null;
9
9
  priority?: boolean;
10
10
  preload?: boolean;
11
11
  quality?: number;
package/ui/Image.tsx CHANGED
@@ -30,7 +30,7 @@ type AkanImageProps = NativeImageProps & {
30
30
  /** Akan file object or file-like value with url and imageSize metadata. */
31
31
  file?: ImageLikeFile;
32
32
  /** Low-quality preview data. Overrides file.abstractData when provided. */
33
- abstractData?: string;
33
+ abstractData?: string | null;
34
34
  /** Accessible alt text. Defaults to "image" when omitted. */
35
35
  alt?: string;
36
36
  /** Image optimizer quality. Defaults to 75. */
@@ -59,12 +59,12 @@ export const Image = ({
59
59
  | {
60
60
  src?: string;
61
61
  file?: ProtoFile;
62
- abstractData?: string;
62
+ abstractData?: string | null;
63
63
  alt?: string;
64
64
  }
65
65
  | {
66
66
  src?: undefined;
67
- abstractData?: string;
67
+ abstractData?: string | null;
68
68
  file: { url: string; imageSize: [number, number]; abstractData?: string | null } | null;
69
69
  alt?: string;
70
70
  }
@@ -18,6 +18,7 @@ import {
18
18
  } from "akanjs/client";
19
19
  import {
20
20
  assertUniqueRoutePatterns,
21
+ getRouteExports,
21
22
  Logger,
22
23
  parseAkanI18nEnv,
23
24
  parseBasePaths,
@@ -303,39 +304,7 @@ function validateRouteModuleExports(key: string, mod: RouteModule) {
303
304
  if (!mod.default) throw new Error(`[route-convention] ${key} generated override wrapper has no default export`);
304
305
  return;
305
306
  }
306
- const allowed =
307
- parsed.kind === "page"
308
- ? new Set(["default", "pageConfig", "head", "metadata", "generateHead", "generateMetadata", "Loading"])
309
- : parsed.isInternalRootLayout
310
- ? new Set([
311
- "default",
312
- "pageConfig",
313
- "head",
314
- "metadata",
315
- "generateHead",
316
- "generateMetadata",
317
- "fonts",
318
- "manifest",
319
- "theme",
320
- "reconnect",
321
- "wsConnect",
322
- "layoutStyle",
323
- "gaTrackingId",
324
- "Loading",
325
- "NotFound",
326
- "Error",
327
- ])
328
- : new Set([
329
- "default",
330
- "pageConfig",
331
- "head",
332
- "metadata",
333
- "generateHead",
334
- "generateMetadata",
335
- "Loading",
336
- "NotFound",
337
- "Error",
338
- ]);
307
+ const allowed = getRouteExports(parsed.kind, { rootLayout: parsed.isInternalRootLayout });
339
308
  for (const exportName of Object.keys(mod)) {
340
309
  if (!allowed.has(exportName)) {
341
310
  throw new Error(`[route-convention] unsupported export "${exportName}" in ${key}`);