akanjs 2.4.2-rc.1 → 2.4.2-rc.3

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.
Files changed (42) hide show
  1. package/common/fileUpload.ts +1 -1
  2. package/common/index.ts +1 -1
  3. package/common/subRoute.ts +61 -0
  4. package/constant/cascadePaths.ts +32 -0
  5. package/constant/fieldInfo.ts +7 -0
  6. package/constant/index.ts +1 -0
  7. package/constant/purify.ts +9 -10
  8. package/constant/via.ts +6 -0
  9. package/fetch/client/fetchClient.ts +1 -1
  10. package/fetch/client/httpClient.ts +9 -5
  11. package/fetch/fetchType/sliceFetch.type.ts +1 -1
  12. package/package.json +1 -1
  13. package/server/di/diLifecycle.ts +7 -1
  14. package/server/proxy/hostBasePathWebProxy.ts +16 -5
  15. package/server/resolver/service.resolver.ts +27 -5
  16. package/server/webRouter.ts +27 -11
  17. package/signal/endpointInfo.ts +2 -1
  18. package/signal/slice.ts +1 -1
  19. package/signal/sliceInfo.ts +2 -1
  20. package/store/action.ts +18 -2
  21. package/store/state.ts +9 -1
  22. package/store/storeInstance.ts +2 -0
  23. package/test/sampleOf.ts +3 -3
  24. package/types/common/fileUpload.d.ts +1 -1
  25. package/types/common/index.d.ts +1 -1
  26. package/types/common/subRoute.d.ts +19 -0
  27. package/types/constant/cascadePaths.d.ts +11 -0
  28. package/types/constant/fieldInfo.d.ts +4 -0
  29. package/types/constant/index.d.ts +1 -0
  30. package/types/constant/purify.d.ts +3 -2
  31. package/types/constant/via.d.ts +4 -0
  32. package/types/fetch/fetchType/sliceFetch.type.d.ts +1 -1
  33. package/types/server/resolver/service.resolver.d.ts +3 -3
  34. package/types/signal/endpointInfo.d.ts +2 -2
  35. package/types/signal/slice.d.ts +1 -1
  36. package/types/signal/sliceInfo.d.ts +2 -2
  37. package/types/store/action.d.ts +1 -1
  38. package/types/store/state.d.ts +4 -2
  39. package/types/test/sampleOf.d.ts +2 -2
  40. package/types/ui/Load/Units.d.ts +3 -1
  41. package/ui/Load/Units.tsx +17 -0
  42. package/ui/Signal/Arg.tsx +1 -1
@@ -10,7 +10,7 @@ interface FileUploadSerializedSignal {
10
10
  /** Framework-owned file-upload contract shared by client-safe packages. */
11
11
  export const fileUploadContract = {
12
12
  fields: { files: "files", metas: "metas", type: "type", parentId: "parentId" },
13
- buildMetas: (fileList: FileList) =>
13
+ buildMetas: (fileList: FileList | File[]) =>
14
14
  Array.from(fileList).map((f) => ({ lastModifiedAt: new Date(f.lastModified).toISOString(), size: f.size })),
15
15
  } as const;
16
16
 
package/common/index.ts CHANGED
@@ -52,7 +52,7 @@ export {
52
52
  } from "./routeConvention";
53
53
  export { sleep } from "./sleep";
54
54
  export { splitVersion } from "./splitVersion";
55
- export { getBasePathFromPathname, parseBasePaths } from "./subRoute";
55
+ export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute";
56
56
  export type * from "./types";
57
57
  export {
58
58
  type WebsocketAuthAckData,
@@ -12,6 +12,67 @@ export const parseBasePaths = (value: string | string[] | Set<string> | undefine
12
12
  return [...new Set(items.map((basePath) => basePath.trim()).filter(Boolean))];
13
13
  };
14
14
 
15
+ const normalizeSubRouteHost = (host: string): string => host.trim().toLowerCase().replace(/:\d+$/, "");
16
+
17
+ /** A hostname never contains these; a fragment carrying one is a malformed entry, not a host we failed to match. */
18
+ const isSubRouteHost = (host: string): boolean => host.length > 0 && !/[\s/=]/.test(host);
19
+
20
+ /**
21
+ * `"soft=a.com,b.com;office=c.com"` -> `{ soft: ["a.com", "b.com"], office: ["c.com"] }`. A deployment platform
22
+ * renders this value, so a malformed entry is skipped rather than thrown: one bad character must not CrashLoop
23
+ * every pod that received it.
24
+ */
25
+ export const parseSubRouteHosts = (value: string | undefined | null): Record<string, string[]> => {
26
+ const hostsByBasePath: Record<string, string[]> = {};
27
+ for (const group of (value ?? "").split(";")) {
28
+ const separatorIdx = group.indexOf("=");
29
+ if (separatorIdx < 0) continue;
30
+ const basePath = group
31
+ .slice(0, separatorIdx)
32
+ .trim()
33
+ .replace(/^\/+|\/+$/g, "");
34
+ if (!basePath) continue;
35
+ const hosts = group
36
+ .slice(separatorIdx + 1)
37
+ .split(",")
38
+ .map(normalizeSubRouteHost)
39
+ .filter(isSubRouteHost);
40
+ if (!hosts.length) continue;
41
+ hostsByBasePath[basePath] = [...new Set([...(hostsByBasePath[basePath] ?? []), ...hosts])];
42
+ }
43
+ return hostsByBasePath;
44
+ };
45
+
46
+ /**
47
+ * Unions the env mapping onto the one baked into the build artifact, never replacing it — dropping the env is the
48
+ * rollback path. A basePath the build does not serve is reported back instead of honoured: the route tree is a
49
+ * build output, so accepting one would answer every request under it with a 404 and nothing to explain why.
50
+ */
51
+ export const resolveSubRouteHosts = ({
52
+ subRoutes,
53
+ basePaths,
54
+ env,
55
+ }: {
56
+ subRoutes: Record<string, string[]>;
57
+ basePaths: Iterable<string>;
58
+ env?: string | null;
59
+ }): { subRoutes: Record<string, string[]>; ignoredBasePaths: string[] } => {
60
+ const parsed = Object.entries(parseSubRouteHosts(env));
61
+ if (!parsed.length) return { subRoutes, ignoredBasePaths: [] };
62
+
63
+ const configuredBasePaths = new Set(parseBasePaths([...basePaths]));
64
+ const merged: Record<string, string[]> = { ...subRoutes };
65
+ const ignoredBasePaths: string[] = [];
66
+ for (const [basePath, hosts] of parsed) {
67
+ if (!configuredBasePaths.has(basePath)) {
68
+ ignoredBasePaths.push(basePath);
69
+ continue;
70
+ }
71
+ merged[basePath] = [...new Set([...(merged[basePath] ?? []).map(normalizeSubRouteHost), ...hosts])];
72
+ }
73
+ return { subRoutes: merged, ignoredBasePaths };
74
+ };
75
+
15
76
  export const getBasePathFromPathname = (
16
77
  pathname: string,
17
78
  {
@@ -0,0 +1,32 @@
1
+ import type { ConstantField, FieldObject } from "./fieldInfo";
2
+ import type { ConstantModelRef } from "./via";
3
+
4
+ /** What happens to the documents a relation field points at when the owner is removed. */
5
+ export const cascadeActions = ["remove"] as const;
6
+ export type CascadeAction = (typeof cascadeActions)[number];
7
+
8
+ export class CascadePaths {
9
+ /** Field key → the model its ids point at. Resolved to a refName later: the target may not be registered yet. */
10
+ readonly remove = new Map<string, ConstantModelRef>();
11
+
12
+ collect(fieldMap: FieldObject) {
13
+ for (const [key, field] of Object.entries(fieldMap)) {
14
+ if (!field.cascade) continue;
15
+ this.#assertCascadable(key, field.cascade, field);
16
+ this.remove.set(key, field.modelRef);
17
+ }
18
+ return this;
19
+ }
20
+
21
+ #assertCascadable(key: string, action: CascadeAction, field: ConstantField) {
22
+
23
+ if (!cascadeActions.includes(action)) {
24
+ throw new Error(`Cascade field "${key}" declares cascade: "${action}", which is not one of ${cascadeActions}`);
25
+ }
26
+
27
+ if (!field.isClass || field.isScalar) {
28
+ throw new Error(`Cascade field "${key}" is not a model reference and has no document to remove`);
29
+ }
30
+ if (field.arrDepth > 1) throw new Error(`Cascade field "${key}" is a nested array and cannot cascade`);
31
+ }
32
+ }
@@ -16,6 +16,7 @@ import {
16
16
  type SingleValue,
17
17
  type UnCls,
18
18
  } from "akanjs/base";
19
+ import type { CascadeAction } from "./cascadePaths";
19
20
  import { ConstantRegistry } from "./constantRegistry";
20
21
  import type { TextFieldRole } from "./textFieldPaths";
21
22
  import type { BaseObject } from "./types";
@@ -104,6 +105,7 @@ export interface ConstantFieldProps<
104
105
  of?: MapValue;
105
106
  validate?: (value: FieldValue, model: any) => boolean;
106
107
  text?: TextFieldRole;
108
+ cascade?: CascadeAction;
107
109
  meta?: Metadata;
108
110
  }
109
111
  export const fieldPresets = ["email", "password", "url"] as const;
@@ -194,6 +196,7 @@ interface ConstantFieldBuildProps<
194
196
  of?: MapValue;
195
197
  validate?: (value: FieldValue, model: any) => boolean;
196
198
  text?: TextFieldRole;
199
+ cascade?: CascadeAction;
197
200
  modelRef: ConstantModelRef;
198
201
  arrDepth: number;
199
202
  optArrDepth: number;
@@ -317,6 +320,7 @@ export class ConstantField<
317
320
  readonly of?: MapValue;
318
321
  readonly validate?: (value: FieldValue, model: any) => boolean;
319
322
  readonly text?: TextFieldRole;
323
+ readonly cascade?: CascadeAction;
320
324
  readonly modelRef: ConstantModelRef;
321
325
  readonly arrDepth: number;
322
326
  readonly optArrDepth: number;
@@ -348,6 +352,7 @@ export class ConstantField<
348
352
  this.of = props.of;
349
353
  this.validate = props.validate;
350
354
  this.text = props.text;
355
+ this.cascade = props.cascade;
351
356
  this.modelRef = props.modelRef;
352
357
  this.arrDepth = props.arrDepth;
353
358
  this.optArrDepth = props.optArrDepth;
@@ -426,6 +431,7 @@ export class ConstantField<
426
431
  of: option.of,
427
432
  validate: option.validate,
428
433
  text: option.text,
434
+ cascade: option.cascade,
429
435
  modelRef,
430
436
  arrDepth: arrDepth,
431
437
  optArrDepth: optArrDepth,
@@ -465,6 +471,7 @@ export class ConstantField<
465
471
  of: this.of,
466
472
  validate: this.validate,
467
473
  text: this.text,
474
+ cascade: this.cascade,
468
475
  modelRef: this.modelRef,
469
476
  arrDepth: this.arrDepth,
470
477
  optArrDepth: this.optArrDepth,
package/constant/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export * from "./cascadePaths";
1
2
  export * from "./constantRegistry";
2
3
  export * from "./crystalize";
3
4
  export * from "./deserialize";
@@ -10,7 +10,6 @@ import {
10
10
  ID,
11
11
  PrimitiveRegistry,
12
12
  type PrimitiveScalar,
13
- type Upload,
14
13
  } from "akanjs/base";
15
14
  import { Logger } from "akanjs/common";
16
15
 
@@ -37,15 +36,15 @@ type PurifiedWithObjectToId<T, StateKeys extends keyof GetStateObject<T> = keyof
37
36
  } & {
38
37
  [K in StateKeys as null extends T[K] ? K : never]?: Purified<T[K]> | undefined;
39
38
  };
40
- export type PurifiedModel<T> = T extends Upload[]
41
- ? FileList
42
- : T extends (infer S)[]
43
- ? PurifiedModel<S>[]
44
- : T extends string | number | boolean | Dayjs | File
45
- ? T
46
- : T extends Map<infer K, infer V>
47
- ? Map<K, PurifiedModel<V>>
48
- : PurifiedWithObjectToId<T>;
39
+ export type PurifiedModel<T> = T extends (infer S)[]
40
+ ? PurifiedModel<S>[]
41
+ : T extends string | number | boolean | Dayjs | File
42
+ ? T
43
+ : T extends Map<infer K, infer V>
44
+ ? Map<K, PurifiedModel<V>>
45
+ : PurifiedWithObjectToId<T>;
46
+
47
+ export type UploadableClientArg<T> = [T] extends [File[]] ? File[] | FileList : T;
49
48
 
50
49
  export type PurifyFunc<Input, _DefaultInput = DefaultOf<Input>, _PurifiedInput = PurifiedModel<Input>> = (
51
50
  self: _DefaultInput,
package/constant/via.ts CHANGED
@@ -13,6 +13,7 @@ import { applyMixins } from "akanjs/common";
13
13
  import { immerable } from "immer";
14
14
 
15
15
  import { crystalize, getDefault } from ".";
16
+ import { CascadePaths } from "./cascadePaths";
16
17
  import { ConstantRegistry } from "./constantRegistry";
17
18
  import {
18
19
  ConstantField,
@@ -193,6 +194,7 @@ const getBaseConstantClass = (field: FieldObject, modelType: ConstantType = "sca
193
194
  static readonly [FIELD_META]: FieldObject = field;
194
195
  static modelType: ConstantType = modelType;
195
196
  static text: TextFieldPaths = new TextFieldPaths();
197
+ static cascade: CascadePaths = new CascadePaths();
196
198
  static children: Set<ConstantModelRef> = new Set();
197
199
  static relations: Set<ConstantModelRef> = new Set();
198
200
  static enums: Set<EnumInstance> = new Set();
@@ -269,6 +271,7 @@ export interface ConstantStatics<
269
271
  relations: Set<ConstantModelRef>;
270
272
  enums: Set<EnumInstance>;
271
273
  text: TextFieldPaths;
274
+ cascade: CascadePaths;
272
275
  _OptionalKey: OptionalKey;
273
276
  _RelationKey: RelationKey;
274
277
  _PrimitiveKey: PrimitiveKey;
@@ -296,6 +299,7 @@ export interface DatabaseConstantStatics<Schema = any, FieldObj extends FieldObj
296
299
  relations: Set<ConstantModelRef>;
297
300
  enums: Set<EnumInstance>;
298
301
  text: TextFieldPaths;
302
+ cascade: CascadePaths;
299
303
  _DatabaseSchema: {
300
304
  [K in keyof Schema]: K extends keyof FieldObj
301
305
  ? FieldObj[K]["fieldType"] extends "hidden"
@@ -318,6 +322,7 @@ export type ConstantModelRef<
318
322
  relations: Set<ConstantModelRef>;
319
323
  enums: Set<EnumInstance>;
320
324
  text: TextFieldPaths;
325
+ cascade: CascadePaths;
321
326
  }
322
327
  >;
323
328
 
@@ -440,6 +445,7 @@ const applyConstantStatics = <Model>(model: ConstantCls<Model>, fieldMap: FieldO
440
445
  for (const relation of field.modelRef.relations) model.relations.add(relation);
441
446
  });
442
447
  model.text.collect(fieldMap);
448
+ model.cascade.collect(fieldMap);
443
449
  return model as unknown as ConstantCls<Model>;
444
450
  };
445
451
 
@@ -491,7 +491,7 @@ export class FetchClient {
491
491
  this.#setHandlerFactory(
492
492
  names.addModelFiles,
493
493
  () =>
494
- (async (fileList: FileList, parentId?: string, option?: FetchPolicy) => {
494
+ (async (fileList: FileList | File[], parentId?: string, option?: FetchPolicy) => {
495
495
  const cap = resolveFileUploadCapability(this.serializedSignal);
496
496
  const endpoint = cap ? this.serializedSignal[cap.refName]?.endpoint[cap.endpointKey] : undefined;
497
497
  if (!cap || !endpoint)
@@ -125,6 +125,12 @@ export class HttpClient {
125
125
  });
126
126
  return `${paramedPath}${searchPath}`;
127
127
  }
128
+
129
+ static #toUploadValues(argValue: unknown): (Blob | string)[] {
130
+ if (Array.isArray(argValue)) return argValue as (Blob | string)[];
131
+ if (typeof FileList !== "undefined" && argValue instanceof FileList) return Array.from(argValue);
132
+ return [argValue as Blob | string];
133
+ }
128
134
  static makeBody(bodyArgs: SerializedArg[], uploadArgs: SerializedArg[], argMap: Map<string, unknown>) {
129
135
  if (uploadArgs.length > 0) {
130
136
  const formData = new FormData();
@@ -133,11 +139,9 @@ export class HttpClient {
133
139
  if (arg.nullable && (argValue === null || argValue === undefined)) return;
134
140
  if (!arg.nullable && (argValue === null || argValue === undefined))
135
141
  throw new Error(`Argument ${arg.name} is required`);
136
- if (Array.isArray(argValue)) {
137
- argValue.forEach((value) => {
138
- formData.append(arg.name, value as Blob | string);
139
- });
140
- } else formData.append(arg.name, argValue as Blob | string);
142
+ HttpClient.#toUploadValues(argValue).forEach((value) => {
143
+ formData.append(arg.name, value);
144
+ });
141
145
  });
142
146
  bodyArgs.forEach((arg) => {
143
147
  const argValue = argMap.get(arg.name);
@@ -152,7 +152,7 @@ type AppliedBaseSliceFetchType<
152
152
  [K in `get${_CapitalizedRefName}Edit`]: (id: string, option?: FetchPolicy) => Promise<ServerEdit<RefName, Full>>;
153
153
  } & {
154
154
  [K in `add${_CapitalizedRefName}Files`]: (
155
- fileList: FileList,
155
+ fileList: FileList | File[],
156
156
  parentId?: string,
157
157
  option?: FetchPolicy,
158
158
  ) => Promise<ProtoFile[]>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "2.4.2-rc.1",
3
+ "version": "2.4.2-rc.3",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -457,7 +457,13 @@ export class DiLifecycle {
457
457
  if (serviceCls.type === "database") {
458
458
  const databaseModule = this.#database.get(serviceCls.refName);
459
459
  if (!databaseModule) throw new Error(`Database "${serviceCls.refName}" is not registered`);
460
- ServiceResolver.resolveDatabaseService(databaseModule.constant, databaseModule.database, serviceCls);
460
+ ServiceResolver.resolveDatabaseService(
461
+ databaseModule.constant,
462
+ databaseModule.database,
463
+ serviceCls,
464
+
465
+ (refName) => this.getService(refName),
466
+ );
461
467
  }
462
468
  const service = new serviceCls();
463
469
  await InjectInfo.resolveInjection(service, serviceCls, this.registry, this.#env);
@@ -1,12 +1,14 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { getEnv } from "akanjs/base";
4
+ import { Logger, resolveSubRouteHosts } from "akanjs/common";
4
5
  import type { BaseBuildArtifact } from "../types";
5
6
  import { AkanResponse } from "./akanResponse";
6
7
  import type { WebProxy } from "./types";
7
8
 
8
9
  export class HostBasePathWebProxy implements WebProxy {
9
10
  static readonly refName = "HostBasePathWebProxy";
11
+ #logger = new Logger("HostBasePathWebProxy");
10
12
  #domainMap: Map<string, string> | null = null;
11
13
 
12
14
  use(request: Bun.BunRequest) {
@@ -49,7 +51,16 @@ export class HostBasePathWebProxy implements WebProxy {
49
51
 
50
52
  #getDomainMap(): Map<string, string> {
51
53
  if (this.#domainMap) return this.#domainMap;
52
- const subRoutes = loadWebRouteMetadata();
54
+ const metadata = loadWebRouteMetadata();
55
+ const { subRoutes, ignoredBasePaths } = resolveSubRouteHosts({
56
+ subRoutes: metadata.subRoutes,
57
+ basePaths: metadata.basePaths,
58
+ env: process.env.AKAN_SUB_ROUTE_HOSTS,
59
+ });
60
+ if (ignoredBasePaths.length)
61
+ this.#logger.warn(
62
+ `AKAN_SUB_ROUTE_HOSTS names basePaths this build does not serve, ignoring: ${ignoredBasePaths.join(", ")}`,
63
+ );
53
64
  const map = new Map<string, string>();
54
65
  for (const [basePath, domains] of Object.entries(subRoutes)) {
55
66
  for (const domain of domains) map.set(normalizeHost(domain), basePath);
@@ -59,14 +70,14 @@ export class HostBasePathWebProxy implements WebProxy {
59
70
  }
60
71
  }
61
72
 
62
- function loadWebRouteMetadata() {
73
+ function loadWebRouteMetadata(): Pick<BaseBuildArtifact, "subRoutes" | "basePaths"> {
63
74
  const artifactPath = path.join(resolveArtifactDir(), "base-artifact.json");
64
- if (!fs.existsSync(artifactPath)) return { routes: [] };
75
+ if (!fs.existsSync(artifactPath)) return { subRoutes: {}, basePaths: [] };
65
76
  try {
66
77
  const parsed = JSON.parse(fs.readFileSync(artifactPath, "utf8")) as Partial<BaseBuildArtifact>;
67
- return parsed.subRoutes ?? {};
78
+ return { subRoutes: parsed.subRoutes ?? {}, basePaths: parsed.basePaths ?? [] };
68
79
  } catch {
69
- return { routes: [] };
80
+ return { subRoutes: {}, basePaths: [] };
70
81
  }
71
82
  }
72
83
 
@@ -1,6 +1,6 @@
1
1
  import type { PromiseOrObject } from "akanjs/base";
2
2
  import { capitalize } from "akanjs/common";
3
- import type { ConstantModel, QueryOf } from "akanjs/constant";
3
+ import { type ConstantModel, ConstantRegistry, type QueryOf } from "akanjs/constant";
4
4
  import {
5
5
  type CRUDEventType,
6
6
  type DatabaseModel,
@@ -17,7 +17,11 @@ import {
17
17
  import type { DatabaseService, ServiceCls } from "akanjs/service";
18
18
 
19
19
  export class ServiceResolver {
20
- static #getDefaultDbServiceMethods(className: string) {
20
+ static #getDefaultDbServiceMethods(
21
+ className: string,
22
+ cascades: [string, string][],
23
+ getService: (refName: string) => DatabaseService,
24
+ ) {
21
25
  const dbServiceMethods = {
22
26
  async __get(this: DatabaseService, id: string) {
23
27
  return await this.__databaseModel.__get(id);
@@ -95,9 +99,18 @@ export class ServiceResolver {
95
99
  return this.__update(id, data);
96
100
  },
97
101
  async __remove(this: DatabaseService, id: string): Promise<Doc> {
102
+
103
+ const targets = cascades.map(([key, refName]) => [key, getService(refName)] as const);
98
104
  await this.__libsPreRemove(id);
99
105
  const doc = await this.__databaseModel.__remove(id);
100
- return await this.__libsPostRemove(doc);
106
+ const removed = await this.__libsPostRemove(doc);
107
+ for (const [key, target] of targets) {
108
+ const value = (removed as Record<string, unknown>)[key];
109
+ const ids = (Array.isArray(value) ? value : [value]).filter((v): v is string => typeof v === "string");
110
+
111
+ for (const targetId of ids) await target.__remove(targetId);
112
+ }
113
+ return removed;
101
114
  },
102
115
  async [`remove${className}`](this: DatabaseService, id: string): Promise<Doc> {
103
116
  return this.__remove(id);
@@ -105,9 +118,18 @@ export class ServiceResolver {
105
118
  };
106
119
  return dbServiceMethods;
107
120
  }
108
- static resolveDatabaseService(constant: ConstantModel, database: DatabaseModel, srvRef: ServiceCls): ServiceCls {
121
+ static resolveDatabaseService(
122
+ constant: ConstantModel,
123
+ database: DatabaseModel,
124
+ srvRef: ServiceCls,
125
+ getService: (refName: string) => DatabaseService,
126
+ ): ServiceCls {
109
127
  const className = capitalize(database.refName);
110
- Object.assign(srvRef.prototype, ServiceResolver.#getDefaultDbServiceMethods(className));
128
+
129
+ const cascades = [...constant.full.cascade.remove].map(
130
+ ([key, modelRef]) => [key, ConstantRegistry.getRefName(modelRef)] as [string, string],
131
+ );
132
+ Object.assign(srvRef.prototype, ServiceResolver.#getDefaultDbServiceMethods(className, cascades, getService));
111
133
  const getQueryDataFromKey = (queryKey: string, args: any): { query: any; queryOption: any } => {
112
134
  const lastArg = args.at(-1);
113
135
  const hasQueryOption =
@@ -7,6 +7,7 @@ import {
7
7
  getBasePathFromPathname,
8
8
  Logger,
9
9
  parseAkanI18nEnv,
10
+ resolveSubRouteHosts,
10
11
  } from "akanjs/common";
11
12
  import { type AkanRequestStore, createRequestStore, parseCookieHeader } from "akanjs/fetch";
12
13
  import type { AkanMetricsReport } from "akanjs/service";
@@ -267,6 +268,7 @@ export class WebRouter {
267
268
  #logger = new Logger("WebRouter");
268
269
  #artifactDir = WebRouter.#resolveArtifactDir();
269
270
  #artifact: BaseBuildArtifact;
271
+ #subRoutes: Record<string, string[]>;
270
272
  #rsc: RscWorker;
271
273
  #hub: HmrWsHub | null = null;
272
274
  #prodMode = process.env.NODE_ENV === "production";
@@ -293,6 +295,16 @@ export class WebRouter {
293
295
  constructor({ artifact, cssBytesByUrl, rsc, seedIndex, upgradeHmrWs }: WebRouterOptions) {
294
296
  this.#logger.verbose(`[SSR] loaded ${Object.keys(cssBytesByUrl).length} CSS assets`);
295
297
  this.#artifact = artifact;
298
+ const { subRoutes, ignoredBasePaths } = resolveSubRouteHosts({
299
+ subRoutes: artifact.subRoutes,
300
+ basePaths: artifact.basePaths,
301
+ env: process.env.AKAN_SUB_ROUTE_HOSTS,
302
+ });
303
+ this.#subRoutes = subRoutes;
304
+ if (ignoredBasePaths.length)
305
+ this.#logger.warn(
306
+ `AKAN_SUB_ROUTE_HOSTS names basePaths this build does not serve, ignoring: ${ignoredBasePaths.join(", ")}`,
307
+ );
296
308
  this.#rsc = rsc;
297
309
  this.renderState = {
298
310
  buildId: 0,
@@ -424,8 +436,7 @@ export class WebRouter {
424
436
  const clientOrigin = WebRouter.#clientFacingOrigin(req);
425
437
  const target = reqUrl.searchParams.get("url");
426
438
  const rawTargetUrl = target ? new URL(target, clientOrigin) : reqUrl;
427
- const requestBasePath =
428
- req.headers.get("x-base-path") ?? WebRouter.#basePathForRequestHost(req, this.#artifact.subRoutes);
439
+ const requestBasePath = this.#requestBasePath(req);
429
440
  const normalizedTarget = normalizeRscTargetUrlForHostBasePath(rawTargetUrl, {
430
441
  basePath: requestBasePath,
431
442
  basePaths: this.#artifact.basePaths,
@@ -517,11 +528,7 @@ export class WebRouter {
517
528
  });
518
529
  }
519
530
 
520
- const sitemapBasePath = getSitemapBasePath(
521
- url.pathname,
522
- this.#artifact.basePaths,
523
- req.headers.get("x-base-path") ?? WebRouter.#basePathForRequestHost(req, this.#artifact.subRoutes),
524
- );
531
+ const sitemapBasePath = getSitemapBasePath(url.pathname, this.#artifact.basePaths, this.#requestBasePath(req));
525
532
  if (sitemapBasePath !== undefined) {
526
533
  return new Response(
527
534
  createDefaultSitemapXml({
@@ -713,6 +720,17 @@ export class WebRouter {
713
720
  return getClientFacingOrigin(req);
714
721
  }
715
722
 
723
+ /**
724
+ * `x-base-path` is set by `HostBasePathWebProxy`, but it reaches here from the wire too, so it is checked against
725
+ * the basePaths this build serves — the same check `getBasePathFromPathname` already applies to it. An unknown
726
+ * value falls through to host matching instead of routing the request into a basePath that resolves to nothing.
727
+ */
728
+ #requestBasePath(req: Request): string | null {
729
+ const headerBasePath = req.headers.get("x-base-path");
730
+ if (headerBasePath && this.#artifact.basePaths.includes(headerBasePath)) return headerBasePath;
731
+ return WebRouter.#basePathForRequestHost(req, this.#subRoutes);
732
+ }
733
+
716
734
  static #basePathForRequestHost(req: Request, subRoutes: Record<string, string[]>): string | null {
717
735
  const host = (req.headers.get("x-forwarded-host") ?? req.headers.get("host") ?? "")
718
736
  .toLowerCase()
@@ -857,8 +875,7 @@ export class WebRouter {
857
875
  pathname,
858
876
  i18n: this.#artifact.i18n,
859
877
  basePaths: this.#artifact.basePaths,
860
- headerBasePath:
861
- req.headers.get("x-base-path") ?? WebRouter.#basePathForRequestHost(req, this.#artifact.subRoutes),
878
+ headerBasePath: this.#requestBasePath(req),
862
879
  });
863
880
  }
864
881
 
@@ -866,8 +883,7 @@ export class WebRouter {
866
883
  const basePath = getBasePathFromPathname(pathname, {
867
884
  basePaths: Object.keys(this.renderState.cssAssets),
868
885
  i18n: this.#artifact.i18n,
869
- headerBasePath:
870
- req.headers.get("x-base-path") ?? WebRouter.#basePathForRequestHost(req, this.#artifact.subRoutes),
886
+ headerBasePath: this.#requestBasePath(req),
871
887
  });
872
888
  return this.renderState.cssAssets[basePath ?? ""]?.cssUrl ?? null;
873
889
  }
@@ -15,6 +15,7 @@ import type {
15
15
  ParamFieldType,
16
16
  PlainTypeToFieldType,
17
17
  PurifiedModel,
18
+ UploadableClientArg,
18
19
  } from "akanjs/constant";
19
20
  import type { ServiceModel } from "akanjs/service";
20
21
  import type { InternalArgCls } from "./internalArg";
@@ -135,7 +136,7 @@ export class EndpointInfo<
135
136
  Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>,
136
137
  Optional extends boolean = false,
137
138
  _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType,
138
- _ClientArg = PurifiedModel<_ArgType>,
139
+ _ClientArg = UploadableClientArg<PurifiedModel<_ArgType>>,
139
140
  _ServerArg = DocumentModel<_ArgType>,
140
141
  >(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>) {
141
142
  if (this.execFn) throw new Error("Query function is already set");
package/signal/slice.ts CHANGED
@@ -112,7 +112,7 @@ export function slice<
112
112
  _Filter,
113
113
  SrvMap<SrvModule>,
114
114
  ["query"],
115
- [_Query],
115
+ [query?: _Query | null],
116
116
  [],
117
117
  [_Query]
118
118
  >;
@@ -9,6 +9,7 @@ import type {
9
9
  PlainTypeToFieldType,
10
10
  PurifiedModel,
11
11
  QueryOf,
12
+ UploadableClientArg,
12
13
  } from "akanjs/constant";
13
14
  import type { FilterCls, FilterInstance } from "akanjs/document";
14
15
  import type { ServiceModel } from "akanjs/service";
@@ -94,7 +95,7 @@ export class SliceInfo<
94
95
  Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>,
95
96
  Optional extends boolean = false,
96
97
  _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType,
97
- _ClientArg = PurifiedModel<_ArgType>,
98
+ _ClientArg = UploadableClientArg<PurifiedModel<_ArgType>>,
98
99
  _ServerArg = DocumentModel<_ArgType>,
99
100
  >(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>) {
100
101
  if (this.execFn) throw new Error("Query function is already set");
package/store/action.ts CHANGED
@@ -224,7 +224,7 @@ type FormSetter<
224
224
  ? K extends string
225
225
  ? SetterKey<"upload", K, RefName, _CapitalizedRefName>
226
226
  : never
227
- : never]: (fileList: FileList, idx?: number) => Promise<void>;
227
+ : never]: (fileList: FileList | File[], idx?: number) => Promise<void>;
228
228
  } & {
229
229
  [K in `writeOn${_CapitalizedRefName}`]: (path: string | (string | number)[], value: any) => void;
230
230
  };
@@ -376,7 +376,11 @@ export const makeFormSetter = (refName: string, fetch: FetchProxy<any>) => {
376
376
  : {}),
377
377
  ...(field.isClass && !!fileUploadRefName && ConstantRegistry.getRefName(field.modelRef) === fileUploadRefName
378
378
  ? {
379
- [namesOfField.uploadFieldOnModel]: async function (this: SetGet, fileList: FileList, index?: number) {
379
+ [namesOfField.uploadFieldOnModel]: async function (
380
+ this: SetGet,
381
+ fileList: FileList | File[],
382
+ index?: number,
383
+ ) {
380
384
  const form = (this.get() as { [key: string]: any })[names.modelForm] as { [key: string]: any };
381
385
  if (!fileList.length) return;
382
386
  const files = await (fetch[names.addModelFiles] as (...args: any) => Promise<ProtoFile[]>)(
@@ -476,6 +480,7 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
476
480
  initModel: `init${className}`,
477
481
  modelInitList: `${fieldName}InitList`,
478
482
  modelInitAt: `${fieldName}InitAt`,
483
+ modelStaleAt: `${fieldName}StaleAt`,
479
484
  refreshModel: `refresh${className}`,
480
485
  selectModel: `select${className}`,
481
486
  setPageOfModel: `setPageOf${className}`,
@@ -489,6 +494,14 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
489
494
  queryArgsOfModel: `queryArgsOf${className}`,
490
495
  sortOfModel: `sortOf${className}`,
491
496
  };
497
+ const staleAtOfOtherSlices = (createdSliceName: string) => {
498
+ const staleAt = new Date();
499
+ return Object.fromEntries(
500
+ slices
501
+ .filter(({ sliceName }) => sliceName !== createdSliceName)
502
+ .map(({ sliceName }) => [sliceName.replace(names.model, names.modelStaleAt), staleAt]),
503
+ );
504
+ };
492
505
  const baseAction = {
493
506
  [names.createModelInForm]: async function (
494
507
  this: SetGet,
@@ -527,6 +540,7 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
527
540
  [namesOfSlice.modelInsight]: newModelInsight,
528
541
  [names.modelViewAt]: new Date(),
529
542
  [names.modelModal]: modal ?? null,
543
+ ...staleAtOfOtherSlices(sliceName),
530
544
  ...(typeof path === "string" && path ? { [path]: model } : {}),
531
545
  });
532
546
  await onSuccess?.(model);
@@ -609,6 +623,7 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
609
623
  [namesOfSlice.modelInsight]: newModelInsight,
610
624
  [names.modelViewAt]: new Date(),
611
625
  [names.modelModal]: modal ?? null,
626
+ ...staleAtOfOtherSlices(sliceName),
612
627
  ...(typeof path === "string" && path ? { [path]: model } : {}),
613
628
  });
614
629
  await onSuccess?.(model);
@@ -845,6 +860,7 @@ export const makeActions = (refName: string, slice: { [key: string]: SerializedS
845
860
  initModel: SliceName.replace(names.Model, names.initModel),
846
861
  modelInitList: SliceName.replace(names.Model, names.modelInitList),
847
862
  modelInitAt: SliceName.replace(names.Model, names.modelInitAt),
863
+ modelStaleAt: SliceName.replace(names.Model, names.modelStaleAt),
848
864
  refreshModel: SliceName.replace(names.Model, names.refreshModel),
849
865
  selectModel: SliceName.replace(names.Model, names.selectModel),
850
866
  setPageOfModel: SliceName.replace(names.Model, names.setPageOfModel),
package/store/state.ts CHANGED
@@ -23,6 +23,7 @@ export type SliceStateKey =
23
23
  | "modelListLoading"
24
24
  | "modelInitList"
25
25
  | "modelInitAt"
26
+ | "modelStaleAt"
26
27
  | "modelSelection"
27
28
  | "lastPageOfModel"
28
29
  | "pageOfModel"
@@ -78,6 +79,8 @@ export type SliceState<
78
79
  [K in `${RefName}InitList${_CapitalizedSuffix}`]: DataList<Light>;
79
80
  } & {
80
81
  [K in `${RefName}InitAt${_CapitalizedSuffix}`]: Date;
82
+ } & {
83
+ [K in `${RefName}StaleAt${_CapitalizedSuffix}`]: Date;
81
84
  } & {
82
85
  [K in `${RefName}Selection${_CapitalizedSuffix}`]: DataList<Light>;
83
86
  } & {
@@ -112,7 +115,9 @@ type DefaultSliceStateFields<
112
115
  | `${_RefName}InitList${StoreSliceSuffixCap<SlceCls, Suffix>}`
113
116
  | `${_RefName}Selection${StoreSliceSuffixCap<SlceCls, Suffix>}`]: DataList<_Light>;
114
117
  } & {
115
- [Suffix in _Suffixes as `${_RefName}InitAt${StoreSliceSuffixCap<SlceCls, Suffix>}`]: Date;
118
+ [Suffix in _Suffixes as
119
+ | `${_RefName}InitAt${StoreSliceSuffixCap<SlceCls, Suffix>}`
120
+ | `${_RefName}StaleAt${StoreSliceSuffixCap<SlceCls, Suffix>}`]: Date;
116
121
  } & {
117
122
  [Suffix in _Suffixes as `${_RefName}ListLoading${StoreSliceSuffixCap<SlceCls, Suffix>}`]: boolean;
118
123
  } & {
@@ -182,6 +187,7 @@ export const createSliceState = (refName: string, slice: { [key: string]: Serial
182
187
  modelListLoading: `${fieldName}ListLoading`,
183
188
  modelInitList: `${fieldName}InitList`,
184
189
  modelInitAt: `${fieldName}InitAt`,
190
+ modelStaleAt: `${fieldName}StaleAt`,
185
191
  modelSelection: `${fieldName}Selection`,
186
192
  modelInsight: `${fieldName}Insight`,
187
193
  lastPageOfModel: `lastPageOf${className}`,
@@ -200,6 +206,7 @@ export const createSliceState = (refName: string, slice: { [key: string]: Serial
200
206
  modelListLoading: sliceName.replace(names.model, names.modelListLoading),
201
207
  modelInitList: sliceName.replace(names.model, names.modelInitList),
202
208
  modelInitAt: sliceName.replace(names.model, names.modelInitAt),
209
+ modelStaleAt: sliceName.replace(names.model, names.modelStaleAt),
203
210
  modelSelection: sliceName.replace(names.model, names.modelSelection),
204
211
  modelInsight: sliceName.replace(names.model, names.modelInsight),
205
212
  lastPageOfModel: SliceName.replace(names.Model, names.lastPageOfModel),
@@ -214,6 +221,7 @@ export const createSliceState = (refName: string, slice: { [key: string]: Serial
214
221
  [namesOfSlice.modelListLoading]: true,
215
222
  [namesOfSlice.modelInitList]: new DataList(),
216
223
  [namesOfSlice.modelInitAt]: new Date(0),
224
+ [namesOfSlice.modelStaleAt]: new Date(0),
217
225
  [namesOfSlice.modelSelection]: new DataList(),
218
226
  [namesOfSlice.modelInsight]: new cnst.insight(),
219
227
  [namesOfSlice.lastPageOfModel]: 1,
@@ -263,6 +263,7 @@ export class StoreInstance {
263
263
  modelListLoading: `${fieldName}ListLoading`,
264
264
  modelInitList: `${fieldName}InitList`,
265
265
  modelInitAt: `${fieldName}InitAt`,
266
+ modelStaleAt: `${fieldName}StaleAt`,
266
267
  pageOfModel: `pageOf${className}`,
267
268
  limitOfModel: `limitOf${className}`,
268
269
  queryArgsOfModel: `queryArgsOf${className}`,
@@ -286,6 +287,7 @@ export class StoreInstance {
286
287
  modelList: sliceName.replace(names.model, names.modelList),
287
288
  modelListLoading: sliceName.replace(names.model, names.modelListLoading),
288
289
  modelInitAt: SliceName.replace(names.Model, names.modelInitAt),
290
+ modelStaleAt: SliceName.replace(names.Model, names.modelStaleAt),
289
291
  lastPageOfModel: SliceName.replace(names.Model, names.lastPageOfModel),
290
292
  pageOfModel: SliceName.replace(names.Model, names.pageOfModel),
291
293
  limitOfModel: SliceName.replace(names.Model, names.limitOfModel),
package/test/sampleOf.ts CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  Upload,
11
11
  } from "akanjs/base";
12
12
  import { randomPick } from "akanjs/common";
13
- import type { BaseObject, ConstantCls, ConstantField, DocumentModel, FieldPreset } from "akanjs/constant";
13
+ import type { BaseObject, ConstantCls, ConstantField, DocumentModel, FieldObject, FieldPreset } from "akanjs/constant";
14
14
 
15
15
  import { sample } from "./sample";
16
16
 
@@ -57,8 +57,8 @@ const makeSample = (field: ConstantField): any => {
57
57
  export type SampleOf<Model> = DocumentModel<{
58
58
  [K in keyof Model as Model[K] extends BaseObject ? never : K]: NonNullable<Model[K]>;
59
59
  }>;
60
- export const sampleOf = <Model>(
61
- modelRef: ConstantCls<Model>,
60
+ export const sampleOf = <Model, FieldObj extends FieldObject>(
61
+ modelRef: ConstantCls<Model, FieldObj>,
62
62
  ): DocumentModel<{ [K in keyof Model as Model[K] extends BaseObject ? never : K]: NonNullable<Model[K]> }> => {
63
63
 
64
64
  return Object.fromEntries(
@@ -13,7 +13,7 @@ export declare const fileUploadContract: {
13
13
  readonly type: "type";
14
14
  readonly parentId: "parentId";
15
15
  };
16
- readonly buildMetas: (fileList: FileList) => {
16
+ readonly buildMetas: (fileList: FileList | File[]) => {
17
17
  lastModifiedAt: string;
18
18
  size: number;
19
19
  }[];
@@ -25,6 +25,6 @@ export { randomPicks } from "./randomPicks.d.ts";
25
25
  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
26
  export { sleep } from "./sleep.d.ts";
27
27
  export { splitVersion } from "./splitVersion.d.ts";
28
- export { getBasePathFromPathname, parseBasePaths } from "./subRoute.d.ts";
28
+ export { getBasePathFromPathname, parseBasePaths, parseSubRouteHosts, resolveSubRouteHosts } from "./subRoute.d.ts";
29
29
  export type * from "./types.d.ts";
30
30
  export { type WebsocketAuthAckData, type WebsocketAuthRequest, websocketAuthContract, } from "./websocketAuth.d.ts";
@@ -1,5 +1,24 @@
1
1
  import type { AkanI18nConfig } from "./localeConfig.d.ts";
2
2
  export declare const parseBasePaths: (value: string | string[] | Set<string> | undefined | null) => string[];
3
+ /**
4
+ * `"soft=a.com,b.com;office=c.com"` -> `{ soft: ["a.com", "b.com"], office: ["c.com"] }`. A deployment platform
5
+ * renders this value, so a malformed entry is skipped rather than thrown: one bad character must not CrashLoop
6
+ * every pod that received it.
7
+ */
8
+ export declare const parseSubRouteHosts: (value: string | undefined | null) => Record<string, string[]>;
9
+ /**
10
+ * Unions the env mapping onto the one baked into the build artifact, never replacing it — dropping the env is the
11
+ * rollback path. A basePath the build does not serve is reported back instead of honoured: the route tree is a
12
+ * build output, so accepting one would answer every request under it with a 404 and nothing to explain why.
13
+ */
14
+ export declare const resolveSubRouteHosts: ({ subRoutes, basePaths, env, }: {
15
+ subRoutes: Record<string, string[]>;
16
+ basePaths: Iterable<string>;
17
+ env?: string | null;
18
+ }) => {
19
+ subRoutes: Record<string, string[]>;
20
+ ignoredBasePaths: string[];
21
+ };
3
22
  export declare const getBasePathFromPathname: (pathname: string, { basePaths, i18n, headerBasePath, }: {
4
23
  basePaths: Iterable<string>;
5
24
  i18n?: Pick<AkanI18nConfig, "locales" | "defaultLocale">;
@@ -0,0 +1,11 @@
1
+ import type { FieldObject } from "./fieldInfo.d.ts";
2
+ import type { ConstantModelRef } from "./via.d.ts";
3
+ /** What happens to the documents a relation field points at when the owner is removed. */
4
+ export declare const cascadeActions: readonly ["remove"];
5
+ export type CascadeAction = (typeof cascadeActions)[number];
6
+ export declare class CascadePaths {
7
+ #private;
8
+ /** Field key → the model its ids point at. Resolved to a refName later: the target may not be registered yet. */
9
+ readonly remove: Map<string, ConstantModelRef>;
10
+ collect(fieldMap: FieldObject): this;
11
+ }
@@ -1,4 +1,5 @@
1
1
  import { type Any, CLIENT_VALUE, type Cls, type Dayjs, type EnumInstance, type Float, Int, type PrimitiveScalar, SERVER_VALUE, type SingleValue, type UnCls } from "akanjs/base";
2
+ import type { CascadeAction } from "./cascadePaths.d.ts";
2
3
  import type { TextFieldRole } from "./textFieldPaths.d.ts";
3
4
  import type { BaseObject } from "./types.d.ts";
4
5
  import type { ConstantModelRef } from "./via.d.ts";
@@ -45,6 +46,7 @@ export interface ConstantFieldProps<FieldType extends ConstantFieldKind = Consta
45
46
  of?: MapValue;
46
47
  validate?: (value: FieldValue, model: any) => boolean;
47
48
  text?: TextFieldRole;
49
+ cascade?: CascadeAction;
48
50
  meta?: Metadata;
49
51
  }
50
52
  export declare const fieldPresets: readonly ["email", "password", "url"];
@@ -91,6 +93,7 @@ interface ConstantFieldBuildProps<FieldType extends ConstantFieldKind = any, Fie
91
93
  of?: MapValue;
92
94
  validate?: (value: FieldValue, model: any) => boolean;
93
95
  text?: TextFieldRole;
96
+ cascade?: CascadeAction;
94
97
  modelRef: ConstantModelRef;
95
98
  arrDepth: number;
96
99
  optArrDepth: number;
@@ -134,6 +137,7 @@ export declare class ConstantField<FieldType extends ConstantFieldKind = Constan
134
137
  readonly of?: MapValue;
135
138
  readonly validate?: (value: FieldValue, model: any) => boolean;
136
139
  readonly text?: TextFieldRole;
140
+ readonly cascade?: CascadeAction;
137
141
  readonly modelRef: ConstantModelRef;
138
142
  readonly arrDepth: number;
139
143
  readonly optArrDepth: number;
@@ -1,3 +1,4 @@
1
+ export * from "./cascadePaths.d.ts";
1
2
  export * from "./constantRegistry.d.ts";
2
3
  export * from "./crystalize.d.ts";
3
4
  export * from "./deserialize.d.ts";
@@ -1,4 +1,4 @@
1
- import { type Dayjs, type GetStateObject, type Upload } from "akanjs/base";
1
+ import { type Dayjs, type GetStateObject } from "akanjs/base";
2
2
  import { type BaseObject, type ConstantModelRef, type DefaultOf, type DefaultOfSchema } from ".";
3
3
  type Purified<O> = O extends BaseObject ? string : O extends BaseObject[] ? string[] : O extends Dayjs ? Dayjs : O extends object ? PurifiedModel<O> : O;
4
4
  type PurifiedWithObjectToId<T, StateKeys extends keyof GetStateObject<T> = keyof GetStateObject<T>> = {
@@ -6,7 +6,8 @@ type PurifiedWithObjectToId<T, StateKeys extends keyof GetStateObject<T> = keyof
6
6
  } & {
7
7
  [K in StateKeys as null extends T[K] ? K : never]?: Purified<T[K]> | undefined;
8
8
  };
9
- export type PurifiedModel<T> = T extends Upload[] ? FileList : T extends (infer S)[] ? PurifiedModel<S>[] : T extends string | number | boolean | Dayjs | File ? T : T extends Map<infer K, infer V> ? Map<K, PurifiedModel<V>> : PurifiedWithObjectToId<T>;
9
+ export type PurifiedModel<T> = T extends (infer S)[] ? PurifiedModel<S>[] : T extends string | number | boolean | Dayjs | File ? T : T extends Map<infer K, infer V> ? Map<K, PurifiedModel<V>> : PurifiedWithObjectToId<T>;
10
+ export type UploadableClientArg<T> = [T] extends [File[]] ? File[] | FileList : T;
10
11
  export type PurifyFunc<Input, _DefaultInput = DefaultOf<Input>, _PurifiedInput = PurifiedModel<Input>> = (self: _DefaultInput, isChild?: boolean) => _PurifiedInput | null;
11
12
  export type PurifyFuncV2<Input, RelationKey extends string = never, _DefaultInput = DefaultOfSchema<Input, RelationKey>, _PurifiedInput = PurifiedModel<Input>> = (self: _DefaultInput, isChild?: boolean) => _PurifiedInput | null;
12
13
  export declare const makePurify: <I>(modelRef: ConstantModelRef<I>) => PurifyFunc<I>;
@@ -1,4 +1,5 @@
1
1
  import { CLIENT_VALUE, type Cls, DEFAULT_VALUE, type EnumInstance, FIELD_META, type ObjectAssign, type ObjectAssignKeyOfObjects, PURIFIED_VALUE, SERVER_VALUE } from "akanjs/base";
2
+ import { CascadePaths } from "./cascadePaths.d.ts";
2
3
  import { type ExtractFieldInfoObject, type FieldBuilder, type FieldInfoObject, type FieldInfoObjectToFieldObject, type FieldObject, type FieldResolver } from "./fieldInfo.d.ts";
3
4
  import { type PurifiedModel, type PurifyFunc } from "./purify.d.ts";
4
5
  import { TextFieldPaths } from "./textFieldPaths.d.ts";
@@ -34,6 +35,7 @@ export interface ConstantStatics<Schema = any, OwnSchema = Schema, OwnFieldObj e
34
35
  relations: Set<ConstantModelRef>;
35
36
  enums: Set<EnumInstance>;
36
37
  text: TextFieldPaths;
38
+ cascade: CascadePaths;
37
39
  _OptionalKey: OptionalKey;
38
40
  _RelationKey: RelationKey;
39
41
  _PrimitiveKey: PrimitiveKey;
@@ -60,6 +62,7 @@ export interface DatabaseConstantStatics<Schema = any, FieldObj extends FieldObj
60
62
  relations: Set<ConstantModelRef>;
61
63
  enums: Set<EnumInstance>;
62
64
  text: TextFieldPaths;
65
+ cascade: CascadePaths;
63
66
  _DatabaseSchema: {
64
67
  [K in keyof Schema]: K extends keyof FieldObj ? FieldObj[K]["fieldType"] extends "hidden" ? NonNullable<Schema[K]> : Schema[K] : Schema[K];
65
68
  };
@@ -71,6 +74,7 @@ export type ConstantModelRef<Schema = any, FieldObj extends FieldObject = FieldO
71
74
  relations: Set<ConstantModelRef>;
72
75
  enums: Set<EnumInstance>;
73
76
  text: TextFieldPaths;
77
+ cascade: CascadePaths;
74
78
  }>;
75
79
  export type DocumentConstantModelRef<Schema = any, FieldObj extends FieldObject = FieldObject, ModelType extends ConstantType = ConstantType, DatabaseSchema = DatabaseSchemaOf<Schema, never>> = Cls<Schema, {
76
80
  [FIELD_META]: FieldObj;
@@ -60,7 +60,7 @@ type AppliedBaseSliceFetchType<RefName extends string, Input, Full, _Capitalized
60
60
  } & {
61
61
  [K in `get${_CapitalizedRefName}Edit`]: (id: string, option?: FetchPolicy) => Promise<ServerEdit<RefName, Full>>;
62
62
  } & {
63
- [K in `add${_CapitalizedRefName}Files`]: (fileList: FileList, parentId?: string, option?: FetchPolicy) => Promise<ProtoFile[]>;
63
+ [K in `add${_CapitalizedRefName}Files`]: (fileList: FileList | File[], parentId?: string, option?: FetchPolicy) => Promise<ProtoFile[]>;
64
64
  } & {
65
65
  [K in `merge${_CapitalizedRefName}`]: (modelOrId: Full | string, data: Partial<_PurifiedInput>, option?: FetchPolicy) => Promise<Full>;
66
66
  };
@@ -1,7 +1,7 @@
1
- import type { ConstantModel } from "akanjs/constant";
1
+ import { type ConstantModel } from "akanjs/constant";
2
2
  import { type DatabaseModel } from "akanjs/document";
3
- import type { ServiceCls } from "akanjs/service";
3
+ import type { DatabaseService, ServiceCls } from "akanjs/service";
4
4
  export declare class ServiceResolver {
5
5
  #private;
6
- static resolveDatabaseService(constant: ConstantModel, database: DatabaseModel, srvRef: ServiceCls): ServiceCls;
6
+ static resolveDatabaseService(constant: ConstantModel, database: DatabaseModel, srvRef: ServiceCls, getService: (refName: string) => DatabaseService): ServiceCls;
7
7
  }
@@ -1,5 +1,5 @@
1
1
  import { type Dayjs, type EnumInstance, type PromiseOrObject } from "akanjs/base";
2
- import type { ConstantFieldType, ConstantFieldTypeInput, DocumentModel, FieldToValue, ParamFieldType, PlainTypeToFieldType, PurifiedModel } from "akanjs/constant";
2
+ import type { ConstantFieldType, ConstantFieldTypeInput, DocumentModel, FieldToValue, ParamFieldType, PlainTypeToFieldType, PurifiedModel, UploadableClientArg } from "akanjs/constant";
3
3
  import type { ServiceModel } from "akanjs/service";
4
4
  import type { InternalArgCls } from "./internalArg.d.ts";
5
5
  import type { ArgType, SignalOption, SrvMap } from "./types.d.ts";
@@ -45,7 +45,7 @@ export declare class EndpointInfo<ReqType extends EndpointType = EndpointType, S
45
45
  static getReturnInfo<Returns extends ConstantFieldTypeInput = ConstantFieldTypeInput, Nullable extends boolean = false>(modelRef: Returns, signalOption?: SignalOption<Returns, Nullable>): ReturnInfo<Returns, Nullable>;
46
46
  constructor(type: ReqType, returnRef: Returns, signalOption?: SignalOption<Returns, Nullable>);
47
47
  param<ArgName extends string, Arg extends ParamFieldType, _ClientArg = FieldToValue<Arg>, _ServerArg = DocumentModel<_ClientArg>>(name: string, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg], Returns, ClientReturns, ServerReturns, Nullable>;
48
- body<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], Optional extends true ? [...Args, arg?: _ClientArg | null] : [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg | (Optional extends true ? undefined : never)], Returns, ClientReturns, ServerReturns, Nullable>;
48
+ body<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = UploadableClientArg<PurifiedModel<_ArgType>>, _ServerArg = DocumentModel<_ArgType>>(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], Optional extends true ? [...Args, arg?: _ClientArg | null] : [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg | (Optional extends true ? undefined : never)], Returns, ClientReturns, ServerReturns, Nullable>;
49
49
  room<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: string, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg], Returns, ClientReturns, ServerReturns, Nullable>;
50
50
  msg<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: string, arg: Arg, option?: EndpointArgProps<Optional>): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], Optional extends true ? [...Args, arg?: _ClientArg | null] : [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg | (Optional extends true ? undefined : never)], Returns, ClientReturns, ServerReturns, Nullable>;
51
51
  search<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: string, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): EndpointInfo<ReqType, Srvs, [...ArgNames, ArgName], [...Args, arg?: _ClientArg | null], InternalArgs, [...ServerArgs, arg: _ServerArg | undefined], Returns, ClientReturns, ServerReturns, Nullable>;
@@ -51,7 +51,7 @@ export declare function slice<SrvModule extends ServiceModel, BuildSlice extends
51
51
  [""]: SliceInfo<SrvRefName<SrvModule>, _Input, _Full, _Light, _Insight, _Filter, SrvMap<SrvModule>, [
52
52
  "query"
53
53
  ], [
54
- _Query
54
+ query?: _Query | null
55
55
  ], [
56
56
  ], [
57
57
  _Query
@@ -1,5 +1,5 @@
1
1
  import type { Cls, PromiseOrObject } from "akanjs/base";
2
- import type { BaseInsight, BaseObject, ConstantFieldTypeInput, DocumentModel, FieldToValue, ParamFieldType, PlainTypeToFieldType, PurifiedModel, QueryOf } from "akanjs/constant";
2
+ import type { BaseInsight, BaseObject, ConstantFieldTypeInput, DocumentModel, FieldToValue, ParamFieldType, PlainTypeToFieldType, PurifiedModel, QueryOf, UploadableClientArg } from "akanjs/constant";
3
3
  import type { FilterCls, FilterInstance } from "akanjs/document";
4
4
  import type { ServiceModel } from "akanjs/service";
5
5
  import { type ArgInfo, type EndpointArgProps, type InternalArgInfo, type InternalArgProps } from "./endpointInfo.d.ts";
@@ -23,7 +23,7 @@ export declare class SliceInfo<RefName extends string = string, Input = any, Ful
23
23
  execFn: ((...args: [...ServerArgs, ...InternalArgs]) => QueryOf<DocumentModel<Full>>) | null;
24
24
  constructor(refName: RefName, input: Cls<Input>, full: Cls<Full>, light: Cls<Light>, insight: Cls<Insight>, filter: FilterCls<Filter>, signalOption?: SignalOption);
25
25
  param<ArgName extends string, Arg extends ParamFieldType, _ClientArg = FieldToValue<Arg>, _ServerArg = DocumentModel<_ClientArg>>(name: ArgName, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, [...ArgNames, ArgName], [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg]>;
26
- body<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, [...ArgNames, ArgName], Optional extends true ? [...Args, arg?: _ClientArg | null] : [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg | (Optional extends true ? undefined : never)]>;
26
+ body<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, Optional extends boolean = false, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = UploadableClientArg<PurifiedModel<_ArgType>>, _ServerArg = DocumentModel<_ArgType>>(name: ArgName, arg: Arg, option?: EndpointArgProps<Optional>): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, [...ArgNames, ArgName], Optional extends true ? [...Args, arg?: _ClientArg | null] : [...Args, arg: _ClientArg], InternalArgs, [...ServerArgs, arg: _ServerArg | (Optional extends true ? undefined : never)]>;
27
27
  search<ArgName extends string, ExplicitType = unknown, Arg extends ConstantFieldTypeInput = PlainTypeToFieldType<ExplicitType>, _ArgType = unknown extends ExplicitType ? FieldToValue<Arg> : ExplicitType, _ClientArg = PurifiedModel<_ArgType>, _ServerArg = DocumentModel<_ArgType>>(name: ArgName, arg: Arg, option?: Omit<EndpointArgProps, "nullable">): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, [...ArgNames, ArgName], [...Args, arg?: _ClientArg | null], InternalArgs, [...ServerArgs, arg: _ServerArg | undefined]>;
28
28
  with<ArgType, Optional extends boolean = false>(argRef: InternalArgCls<ArgType>, option?: InternalArgProps<Optional>): SliceInfo<RefName, Input, Full, Light, Insight, Filter, Srvs, ArgNames, Args, [...InternalArgs, arg: NonNullable<ArgType> | (Optional extends true ? null : never)], ServerArgs>;
29
29
  exec(query: (this: {
@@ -111,7 +111,7 @@ type ArrayFieldAddOrSubSetters<RefName extends string, _CapRef extends string, _
111
111
  }) => void;
112
112
  };
113
113
  type FormSetter<Full, RefName extends string, _CapitalizedRefName extends string = Capitalize<RefName>, _DefaultState = DefaultOf<Full>> = FieldFormSetter<_DefaultState, RefName, _CapitalizedRefName> & ArrayFieldAddSetters<RefName, _CapitalizedRefName, _DefaultState> & ArrayFieldSubSetters<RefName, _CapitalizedRefName, _DefaultState> & ArrayFieldAddOrSubSetters<RefName, _CapitalizedRefName, _DefaultState> & {
114
- [K in keyof _DefaultState as _DefaultState[K] extends (ProtoFile | null) | ProtoFile[] ? K extends string ? SetterKey<"upload", K, RefName, _CapitalizedRefName> : never : never]: (fileList: FileList, idx?: number) => Promise<void>;
114
+ [K in keyof _DefaultState as _DefaultState[K] extends (ProtoFile | null) | ProtoFile[] ? K extends string ? SetterKey<"upload", K, RefName, _CapitalizedRefName> : never : never]: (fileList: FileList | File[], idx?: number) => Promise<void>;
115
115
  } & {
116
116
  [K in `writeOn${_CapitalizedRefName}`]: (path: string | (string | number)[], value: any) => void;
117
117
  };
@@ -3,7 +3,7 @@ import { type DefaultOf } from "akanjs/constant";
3
3
  import type { ExtractSort, FilterInstance } from "akanjs/document";
4
4
  import type { SerializedSlice, SlceCnstCapitalizedRefName, SlceCnstDefault, SlceCnstFull, SlceCnstInsight, SlceCnstLight, SlceCnstRefName, SlceDbFilter, SlceDbSort, SliceCls } from "akanjs/signal";
5
5
  import type { StoreSliceArgs, StoreSliceMap, StoreSliceSuffixCap, Submit } from "./types.d.ts";
6
- export type SliceStateKey = "defaultModel" | "modelInsight" | "modelList" | "modelListLoading" | "modelInitList" | "modelInitAt" | "modelSelection" | "lastPageOfModel" | "pageOfModel" | "limitOfModel" | "queryArgsOfModel" | "sortOfModel";
6
+ export type SliceStateKey = "defaultModel" | "modelInsight" | "modelList" | "modelListLoading" | "modelInitList" | "modelInitAt" | "modelStaleAt" | "modelSelection" | "lastPageOfModel" | "pageOfModel" | "limitOfModel" | "queryArgsOfModel" | "sortOfModel";
7
7
  type _SliceMap<S extends SliceCls> = StoreSliceMap<S>;
8
8
  type _StateRefName<S extends SliceCls> = SlceCnstRefName<S>;
9
9
  type _StateCap<S extends SliceCls> = SlceCnstCapitalizedRefName<S>;
@@ -42,6 +42,8 @@ export type SliceState<RefName extends string, Suffix extends string, Full, Ligh
42
42
  [K in `${RefName}InitList${_CapitalizedSuffix}`]: DataList<Light>;
43
43
  } & {
44
44
  [K in `${RefName}InitAt${_CapitalizedSuffix}`]: Date;
45
+ } & {
46
+ [K in `${RefName}StaleAt${_CapitalizedSuffix}`]: Date;
45
47
  } & {
46
48
  [K in `${RefName}Selection${_CapitalizedSuffix}`]: DataList<Light>;
47
49
  } & {
@@ -64,7 +66,7 @@ type DefaultSliceStateFields<SlceCls extends SliceCls, _RefName extends string,
64
66
  } & {
65
67
  [Suffix in _Suffixes as `${_RefName}List${StoreSliceSuffixCap<SlceCls, Suffix>}` | `${_RefName}InitList${StoreSliceSuffixCap<SlceCls, Suffix>}` | `${_RefName}Selection${StoreSliceSuffixCap<SlceCls, Suffix>}`]: DataList<_Light>;
66
68
  } & {
67
- [Suffix in _Suffixes as `${_RefName}InitAt${StoreSliceSuffixCap<SlceCls, Suffix>}`]: Date;
69
+ [Suffix in _Suffixes as `${_RefName}InitAt${StoreSliceSuffixCap<SlceCls, Suffix>}` | `${_RefName}StaleAt${StoreSliceSuffixCap<SlceCls, Suffix>}`]: Date;
68
70
  } & {
69
71
  [Suffix in _Suffixes as `${_RefName}ListLoading${StoreSliceSuffixCap<SlceCls, Suffix>}`]: boolean;
70
72
  } & {
@@ -1,5 +1,5 @@
1
- import type { BaseObject, ConstantCls, DocumentModel } from "akanjs/constant";
1
+ import type { BaseObject, ConstantCls, DocumentModel, FieldObject } from "akanjs/constant";
2
2
  export type SampleOf<Model> = DocumentModel<{
3
3
  [K in keyof Model as Model[K] extends BaseObject ? never : K]: NonNullable<Model[K]>;
4
4
  }>;
5
- export declare const sampleOf: <Model>(modelRef: ConstantCls<Model>) => DocumentModel<{ [K in keyof Model as Model[K] extends BaseObject ? never : K]: NonNullable<Model[K]>; }>;
5
+ export declare const sampleOf: <Model, FieldObj extends FieldObject>(modelRef: ConstantCls<Model, FieldObj>) => DocumentModel<{ [K in keyof Model as Model[K] extends BaseObject ? never : K]: NonNullable<Model[K]>; }>;
@@ -18,6 +18,8 @@ interface DefaultProps<L extends {
18
18
  renderList?: (list: DataList<L>) => ReactNode;
19
19
  reverse?: boolean;
20
20
  pagination?: boolean;
21
+ /** Max age in ms of the cached slice data before the client refetches on mount; `0` always refetches. */
22
+ staleTime?: number;
21
23
  }
22
24
  interface UnitsProps<RefName extends string, Light extends {
23
25
  id: string;
@@ -26,5 +28,5 @@ interface UnitsProps<RefName extends string, Light extends {
26
28
  }
27
29
  export default function Units<RefName extends string, Light extends {
28
30
  id: string;
29
- }>({ containerRef, className, init, noDiv, from, to, loading, renderItem, renderList, renderEmpty, filter, sort, reverse, style, pagination, }: UnitsProps<RefName, Light>): import("react/jsx-runtime").JSX.Element;
31
+ }>({ containerRef, className, init, noDiv, from, to, loading, renderItem, renderList, renderEmpty, filter, sort, reverse, style, pagination, staleTime, }: UnitsProps<RefName, Light>): import("react/jsx-runtime").JSX.Element;
30
32
  export {};
package/ui/Load/Units.tsx CHANGED
@@ -28,6 +28,8 @@ interface DefaultProps<L extends { id: string }> {
28
28
  renderList?: (list: DataList<L>) => ReactNode;
29
29
  reverse?: boolean;
30
30
  pagination?: boolean;
31
+ /** Max age in ms of the cached slice data before the client refetches on mount; `0` always refetches. */
32
+ staleTime?: number;
31
33
  }
32
34
 
33
35
  interface UnitsProps<RefName extends string, Light extends { id: string }> extends DefaultProps<Light> {
@@ -60,6 +62,7 @@ function Render<RefName extends string, Light extends { id: string }>({
60
62
  sort = (a, b) => 1,
61
63
  reverse,
62
64
  pagination,
65
+ staleTime,
63
66
  }: RenderProps<RefName, Light>) {
64
67
  const loaded = useRef(false);
65
68
  const storeUse = st.use as { [key: string]: () => unknown };
@@ -75,6 +78,7 @@ function Render<RefName extends string, Light extends { id: string }>({
75
78
  modelInsight: `${modelName}Insight`,
76
79
  modelInitList: `${modelName}InitList`,
77
80
  modelInitAt: `${modelName}InitAt`,
81
+ modelStaleAt: `${modelName}StaleAt`,
78
82
  modelObjList: `${modelName}ObjList`,
79
83
  modelObjInsight: `${modelName}ObjInsight`,
80
84
  pageOfModel: `pageOf${ModelName}`,
@@ -84,12 +88,14 @@ function Render<RefName extends string, Light extends { id: string }>({
84
88
  sortOfModel: `sortOf${ModelName}`,
85
89
  setPageOfModel: `setPageOf${ModelName}`,
86
90
  addPageOfModel: `addPageOf${ModelName}`,
91
+ refreshModel: `refresh${ModelName}`,
87
92
  };
88
93
  const namesOfSlice = {
89
94
  modelList: sliceName.replace(names.model, names.modelList),
90
95
  modelListLoading: sliceName.replace(names.model, names.modelListLoading),
91
96
  modelInitList: sliceName.replace(names.model, names.modelInitList),
92
97
  modelInitAt: sliceName.replace(names.model, names.modelInitAt),
98
+ modelStaleAt: sliceName.replace(names.model, names.modelStaleAt),
93
99
  modelInsight: sliceName.replace(names.model, names.modelInsight),
94
100
  pageOfModel: sliceName.replace(names.model, names.pageOfModel),
95
101
  lastPageOfModel: sliceName.replace(names.model, names.lastPageOfModel),
@@ -98,6 +104,7 @@ function Render<RefName extends string, Light extends { id: string }>({
98
104
  sortOfModel: sliceName.replace(names.model, names.sortOfModel),
99
105
  setPageOfModel: sliceName.replace(names.model, names.setPageOfModel),
100
106
  addPageOfModel: sliceName.replace(names.model, names.addPageOfModel),
107
+ refreshModel: sliceName.replace(names.model, names.refreshModel),
101
108
  };
102
109
  const modelList = storeUse[namesOfSlice.modelList]() as DataList<Light>;
103
110
  const modelListLoading = storeUse[namesOfSlice.modelListLoading]() as string | boolean;
@@ -106,6 +113,7 @@ function Render<RefName extends string, Light extends { id: string }>({
106
113
  const initModelObjInsight = (init as any)[names.modelObjInsight] as BaseInsight;
107
114
  const initLimitOfModel = (init as any)[names.limitOfModel] as number;
108
115
  const initPageOfModel = (init as any)[names.pageOfModel] as number;
116
+ const modelStaleAt = storeUse[namesOfSlice.modelStaleAt]() as Date;
109
117
 
110
118
  const useCache =
111
119
  !modelListLoading &&
@@ -143,6 +151,13 @@ function Render<RefName extends string, Light extends { id: string }>({
143
151
  loaded.current = true;
144
152
  }, []);
145
153
 
154
+ useEffect(() => {
155
+ const staleThreshold = Math.max(modelStaleAt.getTime(), staleTime === undefined ? 0 : Date.now() - staleTime);
156
+ if (storeGet<Date>()[namesOfSlice.modelInitAt].getTime() >= staleThreshold) return;
157
+ if (storeGet<boolean>()[namesOfSlice.modelListLoading]) return;
158
+ void storeDo[namesOfSlice.refreshModel]({ invalidate: true });
159
+ }, [modelStaleAt]);
160
+
146
161
  const modelInsight = storeUse[namesOfSlice.modelInsight]() as BaseInsight;
147
162
  const limitOfModel = storeUse[namesOfSlice.limitOfModel]() as number;
148
163
  const pageOfModel = storeUse[namesOfSlice.pageOfModel]() as number;
@@ -237,6 +252,7 @@ export default function Units<RefName extends string, Light extends { id: string
237
252
  reverse,
238
253
  style,
239
254
  pagination = true,
255
+ staleTime,
240
256
  }: UnitsProps<RefName, Light>) {
241
257
  const props: UnitsProps<RefName, Light> = {
242
258
  containerRef,
@@ -254,6 +270,7 @@ export default function Units<RefName extends string, Light extends { id: string
254
270
  sort,
255
271
  reverse,
256
272
  pagination,
273
+ staleTime,
257
274
  };
258
275
 
259
276
  const { fulfilled, value: promiseInit } = useFetch(init);
package/ui/Signal/Arg.tsx CHANGED
@@ -351,7 +351,7 @@ const ArgUpload = ({ value, onChange }: ArgUploadProps) => {
351
351
  multiple
352
352
  className="file-input file-input-bordered w-full max-w-xs"
353
353
  onChange={(e: ChangeEvent<HTMLInputElement>) => {
354
- onChange(new Array(e.target.files?.length).fill(0).map((_, idx) => e.target.files?.[idx]) as any as FileList);
354
+ onChange(e.target.files);
355
355
  }}
356
356
  />
357
357
  );