akanjs 3.0.0-alpha.2 → 3.0.0-alpha.4

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 (76) hide show
  1. package/base/symbols.ts +0 -1
  2. package/client/cn.ts +7 -0
  3. package/client/csrTypes.ts +2 -0
  4. package/client/frameDebug.ts +3 -2
  5. package/constant/cascadePaths.ts +8 -79
  6. package/document/database.ts +1 -2
  7. package/document/databaseRegistry.ts +2 -2
  8. package/document/into.ts +6 -10
  9. package/package.json +1 -1
  10. package/server/SSR_MEMORY_DIAGNOSIS.md +2 -19
  11. package/server/cachePolicy.ts +12 -114
  12. package/server/di/diLifecycle.ts +10 -8
  13. package/server/processMetricsCollector.ts +0 -4
  14. package/server/resolver/database.resolver.ts +3 -23
  15. package/server/resolver/index.ts +0 -1
  16. package/server/resolver/service.resolver.ts +25 -9
  17. package/server/rscWorker.tsx +4 -32
  18. package/server/rscWorkerHost.ts +12 -61
  19. package/server/ssrFromRscRenderer.tsx +2 -2
  20. package/server/webRouter.ts +96 -9
  21. package/service/ipcTypes.ts +0 -25
  22. package/service/predefinedAdaptor/database.adaptor.ts +3 -3
  23. package/service/serve.ts +1 -5
  24. package/service/types.ts +0 -2
  25. package/types/base/symbols.d.ts +0 -1
  26. package/types/client/cn.d.ts +5 -0
  27. package/types/client/csrTypes.d.ts +2 -0
  28. package/types/constant/cascadePaths.d.ts +4 -22
  29. package/types/document/database.d.ts +1 -2
  30. package/types/document/databaseRegistry.d.ts +2 -2
  31. package/types/document/into.d.ts +5 -6
  32. package/types/server/cachePolicy.d.ts +2 -33
  33. package/types/server/resolver/database.resolver.d.ts +2 -6
  34. package/types/server/resolver/index.d.ts +0 -1
  35. package/types/server/resolver/service.resolver.d.ts +3 -3
  36. package/types/server/rscWorkerHost.d.ts +0 -13
  37. package/types/service/ipcTypes.d.ts +0 -24
  38. package/types/service/predefinedAdaptor/database.adaptor.d.ts +2 -2
  39. package/types/service/types.d.ts +1 -2
  40. package/types/ui/Layout/BottomInset.d.ts +2 -1
  41. package/types/ui/Layout/index.d.ts +1 -1
  42. package/types/ui/Signal/style.d.ts +1 -1
  43. package/types/ui/UiOverride/context.d.ts +12 -2
  44. package/types/ui/recipe/badgeRecipe.d.ts +7 -3
  45. package/types/ui/recipe/buttonRecipe.d.ts +7 -3
  46. package/types/ui/recipe/inputRecipe.d.ts +5 -1
  47. package/types/webkit/useCsrValues.d.ts +3 -3
  48. package/types/webkit/useFrameRuntime.d.ts +6 -1
  49. package/ui/Badge.tsx +2 -2
  50. package/ui/Button.tsx +3 -1
  51. package/ui/Constant/Doc.tsx +1 -1
  52. package/ui/Data/ListContainer.tsx +1 -1
  53. package/ui/DatePicker.tsx +4 -3
  54. package/ui/DraggableList.tsx +3 -1
  55. package/ui/Dropdown.tsx +4 -2
  56. package/ui/Field.tsx +17 -9
  57. package/ui/Input.tsx +7 -5
  58. package/ui/Layout/BottomInset.tsx +6 -1
  59. package/ui/Loading/ProgressBar.tsx +8 -1
  60. package/ui/Menu.tsx +7 -8
  61. package/ui/Model/SureToRemove.tsx +2 -1
  62. package/ui/ObjectId.tsx +3 -4
  63. package/ui/Pagination.tsx +3 -4
  64. package/ui/Popconfirm.tsx +5 -7
  65. package/ui/Select.tsx +1 -1
  66. package/ui/System/CSR.tsx +6 -2
  67. package/ui/ToggleSelect.tsx +4 -3
  68. package/ui/UiOverride/context.ts +12 -2
  69. package/ui/recipe/badgeRecipe.ts +22 -3
  70. package/ui/recipe/buttonRecipe.ts +51 -2
  71. package/ui/recipe/factory.ts +2 -2
  72. package/ui/recipe/inputRecipe.ts +18 -4
  73. package/webkit/useCsrValues.ts +121 -4
  74. package/webkit/useFrameRuntime.ts +42 -12
  75. package/server/resolver/CascadeRunner.ts +0 -201
  76. package/types/server/resolver/CascadeRunner.d.ts +0 -13
package/base/symbols.ts CHANGED
@@ -3,7 +3,6 @@ export const SLICE_META = Symbol.for("akan.slice");
3
3
  export const FILTER_META = Symbol.for("akan.filter");
4
4
  export const LOADER_META = Symbol.for("akan.loader");
5
5
  export const INJECT_META = Symbol.for("akan.inject");
6
- export const LIBS_REMOVE_HOOK = Symbol.for("akan.service.libsRemoveHook");
7
6
  export const ENDPOINT_META = Symbol.for("akan.endpoint");
8
7
  export const ENDPOINT_DICT_SHAPE: unique symbol = Symbol.for("akan.endpoint.dictShape") as never;
9
8
  export const SLICE_DICT_SHAPE: unique symbol = Symbol.for("akan.slice.dictShape") as never;
package/client/cn.ts CHANGED
@@ -38,10 +38,17 @@ export const colorTokens = [
38
38
  "ring",
39
39
  ];
40
40
 
41
+ /**
42
+ * Akan's semantic radius tokens (`--radius-box` / `--radius-field` in ui/styles.css). Without them
43
+ * `cn("rounded-field", "rounded-full")` keeps both classes and stylesheet order decides the winner.
44
+ */
45
+ export const radiusTokens = ["box", "field"];
46
+
41
47
  const twMerge = extendTailwindMerge({
42
48
  extend: {
43
49
  theme: {
44
50
  color: colorTokens,
51
+ radius: radiusTokens,
45
52
  },
46
53
  },
47
54
  });
@@ -455,6 +455,7 @@ export interface FrameLayoutState {
455
455
  keyboard: KeyboardFrameState;
456
456
  contentViewport: FrameContentViewportState;
457
457
  keyboardAccessory: KeyboardAccessoryFrameState;
458
+ contentAnchor?: "bottom";
458
459
  platformProfile: FramePlatformProfile;
459
460
  zIndex: FrameLayerZIndex;
460
461
  pageStateByPath: Map<string, PageState>;
@@ -463,6 +464,7 @@ export interface FrameSlotRegistration {
463
464
  scope?: FrameSlotScope;
464
465
  type: FrameSlotType;
465
466
  role?: FrameSlotRole;
467
+ contentAnchor?: "bottom";
466
468
  height?: number;
467
469
  estimatedHeight?: number;
468
470
  source?: "navbar" | "topInset" | "bottomInset" | "bottomTab" | (string & {});
@@ -20,9 +20,10 @@ const isFrameDebugEnabled = () => {
20
20
  export function debugFrame(event: string, payload: DebugPayload = {}) {
21
21
  if (!isFrameDebugEnabled()) return;
22
22
  debugSeq += 1;
23
- console.info(`[akan:frame:${debugSessionId}:${debugSeq}] ${event}`, {
23
+ const details = {
24
24
  href: window.location.href,
25
25
  now: Math.round(performance.now()),
26
26
  ...payload,
27
- });
27
+ };
28
+ console.info(`[akan:frame:${debugSessionId}:${debugSeq}] ${event}`, details, JSON.stringify(details));
28
29
  }
@@ -1,103 +1,32 @@
1
- import { type Cls, PrimitiveRegistry, type PrimitiveScalar } from "akanjs/base";
2
1
  import type { ConstantField, FieldObject } from "./fieldInfo";
3
2
  import type { ConstantModelRef } from "./via";
4
3
 
5
- /**
6
- * Which end of a relation goes away with the other. `removeRef` removes what the field points at when this
7
- * document is removed; `removeWith` removes this document when what the field points at is removed. The two
8
- * read identically on a relation field, so the value has to name the direction — a mistake here is a data loss.
9
- */
10
- export const cascadeActions = ["removeRef", "removeWith"] as const;
4
+ /** What happens to the documents a relation field points at when the owner is removed. */
5
+ export const cascadeActions = ["remove"] as const;
11
6
  export type CascadeAction = (typeof cascadeActions)[number];
12
7
 
13
- /** How a `removeWith` field names the owner whose removal takes this document with it. */
14
- export interface CascadeWithPath {
15
- readonly key: string;
16
- /** Set when the field is a relation. Resolved to a refName later: the owner may not be registered yet. */
17
- readonly modelRef: ConstantModelRef | null;
18
- /** Set when the field declares `ref`, which names the owner at declaration. */
19
- readonly refName: string | null;
20
- /** Set when the field declares `refPath`: the sibling field holding the owner's refName. */
21
- readonly typeKey: string | null;
22
- /** The refNames `typeKey` may hold. Empty unless the field is polymorphic. */
23
- readonly typeValues: readonly string[];
24
- }
25
-
26
- const idNames = new Set(["ID", "String"]);
27
-
28
8
  export class CascadePaths {
29
- /** Field key → the model its ids point at, removed when this document is. */
30
- readonly removeRef = new Map<string, ConstantModelRef>();
31
- /** Field key → the owner whose removal removes this document. */
32
- readonly removeWith = new Map<string, CascadeWithPath>();
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>();
33
11
 
34
12
  collect(fieldMap: FieldObject) {
35
13
  for (const [key, field] of Object.entries(fieldMap)) {
36
14
  if (!field.cascade) continue;
37
- this.#assertKnownAction(key, field.cascade);
38
- if (field.cascade === "removeRef") this.removeRef.set(key, this.#readOwnedRelation(key, field));
39
- else this.removeWith.set(key, this.#readOwnerPath(key, field, fieldMap));
15
+ this.#assertCascadable(key, field.cascade, field);
16
+ this.remove.set(key, field.modelRef);
40
17
  }
41
18
  return this;
42
19
  }
43
20
 
44
- #assertKnownAction(key: string, action: CascadeAction) {
45
-
21
+ #assertCascadable(key: string, action: CascadeAction, field: ConstantField) {
22
+
46
23
  if (!cascadeActions.includes(action)) {
47
24
  throw new Error(`Cascade field "${key}" declares cascade: "${action}", which is not one of ${cascadeActions}`);
48
25
  }
49
- }
50
-
51
- #readOwnedRelation(key: string, field: ConstantField) {
52
26
 
53
27
  if (!field.isClass || field.isScalar) {
54
28
  throw new Error(`Cascade field "${key}" is not a model reference and has no document to remove`);
55
29
  }
56
30
  if (field.arrDepth > 1) throw new Error(`Cascade field "${key}" is a nested array and cannot cascade`);
57
- return field.modelRef;
58
- }
59
-
60
- #readOwnerPath(key: string, field: ConstantField, fieldMap: FieldObject): CascadeWithPath {
61
-
62
- if (field.arrDepth > 0) throw new Error(`Cascade field "${key}" is an array and names more than one owner`);
63
- if (field.isMap) throw new Error(`Cascade field "${key}" is a Map and names no owner`);
64
- if (field.refPath) return this.#readPolymorphicOwner(key, field, fieldMap);
65
- if (field.ref) {
66
- this.#assertHoldsId(key, field);
67
- return { key, modelRef: null, refName: field.ref, typeKey: null, typeValues: [] };
68
- }
69
- if (field.isClass && !field.isScalar) {
70
- return { key, modelRef: field.modelRef, refName: null, typeKey: null, typeValues: [] };
71
- }
72
- throw new Error(
73
- `Cascade field "${key}" declares cascade: "removeWith" but names no owner; make it a model reference, ` +
74
- `or add ref: "<model>" / refPath: "<typeField>"`,
75
- );
76
- }
77
-
78
- #readPolymorphicOwner(key: string, field: ConstantField, fieldMap: FieldObject): CascadeWithPath {
79
- if (field.ref) throw new Error(`Cascade field "${key}" declares both ref and refPath; keep one`);
80
- this.#assertHoldsId(key, field);
81
- const typeKey = field.refPath as string;
82
- const typeField = fieldMap[typeKey];
83
- if (!typeField) throw new Error(`Cascade field "${key}" declares refPath: "${typeKey}", which is not a field`);
84
-
85
- if (!typeField.enum) {
86
- throw new Error(
87
- `Cascade field "${key}" declares refPath: "${typeKey}", which must be an enumOf(...) naming the owner ` +
88
- `refNames it may hold`,
89
- );
90
- }
91
- const typeValues = typeField.enum.values.map((value) => String(value));
92
- return { key, modelRef: null, refName: null, typeKey, typeValues };
93
- }
94
-
95
- #assertHoldsId(key: string, field: ConstantField) {
96
- const modelRef = field.modelRef as unknown as Cls;
97
- const refName = PrimitiveRegistry.has(modelRef)
98
- ? PrimitiveRegistry.getName(modelRef as unknown as typeof PrimitiveScalar)
99
- : null;
100
- if (refName && idNames.has(refName)) return;
101
- throw new Error(`Cascade field "${key}" declares ref or refPath and must hold an ID`);
102
31
  }
103
32
  }
@@ -4,7 +4,7 @@ import type { DocumentModel, QueryOf } from "akanjs/constant";
4
4
  import type { CacheAdaptor, CacheSetOptions } from "akanjs/service";
5
5
  import type { DataLoader } from "./dataLoader";
6
6
  import type { ExtractQuery, ExtractSort, FilterInstance } from "./filterMeta";
7
- import type { CRUDEventType, Mdl, SaveEventType, UpdateResult } from "./into";
7
+ import type { CRUDEventType, Mdl, SaveEventType } from "./into";
8
8
  import type { DataInputOf, FindQueryOption, ListQueryOption } from "./types";
9
9
 
10
10
  export class CacheDatabase<T = unknown> {
@@ -105,7 +105,6 @@ type DatabaseModelWithQuerySort<
105
105
  __create: (data: _DataInput) => Promise<Doc>;
106
106
  __update: (id: string, data: Partial<Doc>) => Promise<Doc>;
107
107
  __remove: (id: string) => Promise<Doc>;
108
- __removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
109
108
  __list(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
110
109
  __listIds(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
111
110
  __find(query: _QueryOfDoc, queryOption?: _FindQueryOption): Promise<Doc | null>;
@@ -19,7 +19,7 @@ export interface DatabaseModel<
19
19
  model: ModelCls<Model>;
20
20
  filter: FilterCls<Filter>;
21
21
  obj: ConstantCls<Obj>;
22
- insight: DatabaseCls<Insight>;
22
+ insight: ConstantCls<Insight>;
23
23
  _Input: Input;
24
24
  _Doc: Doc;
25
25
  _Model: Model;
@@ -92,7 +92,7 @@ export class DatabaseRegistry {
92
92
  doc: DatabaseCls<Doc>,
93
93
  model: ModelCls<Model>,
94
94
  obj: ConstantCls<Obj>,
95
- insight: DatabaseCls<Insight>,
95
+ insight: ConstantCls<Insight>,
96
96
  filter: FilterCls<Filter>,
97
97
  ): DatabaseModel<T, Input, Doc, Model, Obj, Insight, Filter, _Query, _Sort> {
98
98
  const dbInfo = {
package/document/into.ts CHANGED
@@ -85,15 +85,14 @@ export type Mdl<
85
85
  options?: DocumentUpdateOptions,
86
86
  ): Promise<UpdateResult>;
87
87
  updateMany(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>): Promise<UpdateResult>;
88
- removeMany(query: _RawQuery): Promise<UpdateResult>;
88
+ deleteMany(query: _RawQuery): Promise<UpdateResult>;
89
89
  bulkWrite(operations: BulkWriteOperation<Raw, _RawDoc, _RawQuery>[]): Promise<UpdateResult>;
90
90
  };
91
91
 
92
- interface IntoConstantModel<T extends string, _CapitalizedRefName extends string, Raw, Insight> {
92
+ interface IntoConstantModel<T extends string, _CapitalizedRefName extends string, Raw> {
93
93
  refName: T;
94
94
  _CapitalizedRefName: _CapitalizedRefName;
95
95
  _Full: Raw;
96
- _Insight: Insight;
97
96
  }
98
97
  type NoInferType<T> = [T][T extends unknown ? 0 : never];
99
98
  type IntoModelActions<
@@ -101,8 +100,6 @@ type IntoModelActions<
101
100
  _CapitalizedRefName extends string,
102
101
  Doc,
103
102
  Raw,
104
-
105
- Insight,
106
103
  _Query,
107
104
  _Sort,
108
105
  _QueryOfDoc = QueryOf<Doc>,
@@ -124,14 +121,13 @@ type IntoModelActions<
124
121
  [K in `update${_CapitalizedRefName}`]: (id: string, data: Partial<Doc>) => Promise<Doc>;
125
122
  } & {
126
123
  [K in `remove${_CapitalizedRefName}`]: (id: string) => Promise<Doc>;
127
- } & QueryMethodPart<_Query, _Sort, Raw, Doc, DocumentModel<Insight>, unknown, unknown, _QueryOfDoc>;
124
+ } & QueryMethodPart<_Query, _Sort, Raw, Doc, unknown, unknown, unknown, _QueryOfDoc>;
128
125
 
129
126
  export const into = <
130
127
  Doc,
131
128
  FilterRef extends FilterCls,
132
129
  T extends string,
133
130
  Raw,
134
- Insight,
135
131
  AddDbModels extends ModelCls[],
136
132
  _CapitalizedRefName extends string,
137
133
  _QueryOfDoc = QueryOf<Doc>,
@@ -141,11 +137,11 @@ export const into = <
141
137
  >(
142
138
  docRef: Cls<Doc>,
143
139
  filterRef: FilterRef,
144
- cnst: IntoConstantModel<T, _CapitalizedRefName, Raw, Insight>,
140
+ cnst: IntoConstantModel<T, _CapitalizedRefName, Raw>,
145
141
  loaderBuilder: _LoaderBuilder,
146
142
  ...addMdls: [...AddDbModels]
147
143
  ): ModelCls<
148
- IntoModelActions<T, _CapitalizedRefName, Doc, Raw, Insight, _Query, _Sort, _QueryOfDoc>,
144
+ IntoModelActions<T, _CapitalizedRefName, Doc, Raw, _Query, _Sort, _QueryOfDoc>,
149
145
  ReturnType<_LoaderBuilder>
150
146
  > => {
151
147
  const loaderInfoMap = loaderBuilder(makeLoaderBuilder<Doc>());
@@ -166,7 +162,7 @@ export const into = <
166
162
  });
167
163
  });
168
164
  return DefaultModel as unknown as ModelCls<
169
- IntoModelActions<T, _CapitalizedRefName, Doc, Raw, Insight, _Query, _Sort, _QueryOfDoc>,
165
+ IntoModelActions<T, _CapitalizedRefName, Doc, Raw, _Query, _Sort, _QueryOfDoc>,
170
166
  ReturnType<_LoaderBuilder>
171
167
  >;
172
168
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "akanjs",
3
- "version": "3.0.0-alpha.2",
3
+ "version": "3.0.0-alpha.4",
4
4
  "sourceType": "module",
5
5
  "type": "module",
6
6
  "publishConfig": {
@@ -23,31 +23,14 @@ Read snapshots from:
23
23
  curl http://localhost:8080/_akan/app/metrics
24
24
  ```
25
25
 
26
- A replica and its RSC worker are separate processes and are reported separately. **`rssBytes` is the
27
- replica's own; the worker's is `rscWorkerRssBytes`.** Sum them for what the pod pays.
28
-
29
26
  Key fields:
30
27
 
31
- - `rssBytes`, `heapUsedBytes`, `jscHeapSizeBytes`: the replica. Distinguish RSS-only native retention from JS
32
- heap retention.
33
- - `rscWorkerRssBytes`, `rscWorkerHeapUsedBytes`, `rscWorkerJscHeapSizeBytes`, `rscWorkerJscExtraMemorySizeBytes`:
34
- the same for the worker. `jscExtra` is off-heap, mostly typed-array backing stores.
28
+ - `rssBytes`, `heapUsedBytes`, `jscHeapSizeBytes`: distinguish RSS-only native retention from JS heap retention.
35
29
  - `rscRenderCount`, `rscInFlightRenderCount`: detect request lifecycle leaks.
36
30
  - `rscLoadedRouteModuleCount`, `rscRouteModuleCacheHits`, `rscRouteModuleCacheMisses`: detect route module warm-up.
37
- - `ssrChunkRegistrySize`, `ssrChunkLoadCount`: full-document SSR client chunk loading. **`…RegistrySize` counts
38
- keys, not bytes** — evicting does not unload the module, so it can never fall on its own.
39
- - `httpHtmlCacheEntries` / `httpHtmlCacheBytes`, `rscResultCacheEntries` / `rscResultCacheBytes`,
40
- `rscPatchResultCacheEntries` / `rscPatchResultCacheBytes`: what each cache actually holds. Entry count alone
41
- says nothing when entries span three orders of magnitude.
31
+ - `ssrChunkRegistrySize`, `ssrChunkLoadCount`: detect full-document SSR client chunk retention.
42
32
  - `httpFullSsrCount`, `httpRscNavigationCount`, `httpStaticAssetCount`, `httpImageCount`: separate request kinds.
43
33
 
44
- Two things measured on `apps/akan` that shape how to read all of the above:
45
-
46
- - **Growth converges; it is not a leak.** Ten passes over the same routes plateau by roughly the sixth, with a
47
- flat JS heap throughout. Three passes is not enough to tell a plateau from a ratchet.
48
- - **Freeing JS objects does not lower RSS.** Emptying both result caches returns their bytes to the heap and
49
- leaves RSS unchanged. Only not allocating, or restarting the process, reduces what the pod pays.
50
-
51
34
  ## Scenarios
52
35
 
53
36
  ### Same Route Repeated
@@ -234,62 +234,20 @@ export function shouldInvalidateRouteCacheEntry(
234
234
  return false;
235
235
  }
236
236
 
237
- export interface LruTtlCacheOptions<T> {
238
- /**
239
- * Measures one entry's payload. Entry count alone says nothing about a cache whose entries span
240
- * three orders of magnitude, and a byte ceiling needs a running total to enforce. The default
241
- * reports 0 rather than guessing, so `byteSize` stays honest about not knowing.
242
- */
243
- sizeOf?: (value: T) => number;
244
- /** Total payload ceiling. 0 leaves the cache bounded only by `maxEntries`. */
245
- maxBytes?: number;
246
- /** An entry over this is not stored at all, rather than evicting everything else to fit it. */
247
- maxEntryBytes?: number;
248
- /**
249
- * Cadence of the idle sweep. Without one a filled cache never shrinks: an entry is dropped only
250
- * when its own key is fetched after expiry or when a write evicts it, so a pod that stops
251
- * serving holds its peak forever — measured at 100 entries / 21.4 MiB still resident 310s after
252
- * the last request, with a 30s TTL. 0 disables it.
253
- */
254
- sweepIntervalMs?: number;
255
- }
256
-
257
237
  export class LruTtlCache<T> {
258
- readonly #entries = new Map<string, { value: T; expiresAt: number; byteLength: number }>();
259
- #byteLength = 0;
260
- #sweepTimer: ReturnType<typeof setInterval> | null = null;
261
- readonly #sizeOf: (value: T) => number;
262
- readonly #maxBytes: number;
263
- readonly #maxEntryBytes: number;
264
-
265
- constructor(
266
- readonly maxEntries = 100,
267
- options: LruTtlCacheOptions<T> = {},
268
- ) {
269
- this.#sizeOf = options.sizeOf ?? (() => 0);
270
- this.#maxBytes = options.maxBytes ?? 0;
271
- this.#maxEntryBytes = options.maxEntryBytes ?? 0;
272
- const sweepIntervalMs = options.sweepIntervalMs ?? 0;
273
- if (sweepIntervalMs > 0) {
274
- this.#sweepTimer = setInterval(() => this.sweepExpired(), sweepIntervalMs);
275
-
276
- (this.#sweepTimer as { unref?: () => void }).unref?.();
277
- }
278
- }
238
+ readonly #entries = new Map<string, { value: T; expiresAt: number }>();
239
+
240
+ constructor(readonly maxEntries = 100) {}
279
241
 
280
242
  get size(): number {
281
243
  return this.#entries.size;
282
244
  }
283
245
 
284
- get byteSize(): number {
285
- return this.#byteLength;
286
- }
287
-
288
246
  get(key: string): T | null {
289
247
  const entry = this.#entries.get(key);
290
248
  if (!entry) return null;
291
249
  if (entry.expiresAt <= Date.now()) {
292
- this.#remove(key);
250
+ this.#entries.delete(key);
293
251
  return null;
294
252
  }
295
253
  this.#entries.delete(key);
@@ -297,56 +255,26 @@ export class LruTtlCache<T> {
297
255
  return entry.value;
298
256
  }
299
257
 
300
- /** Returns whether the entry was stored; a payload over `maxEntryBytes` is rejected. */
301
- set(key: string, value: T, ttlSeconds: number): boolean {
302
- this.#remove(key);
303
- const byteLength = LruTtlCache.#measure(this.#sizeOf, value);
304
- if (this.#maxEntryBytes > 0 && byteLength > this.#maxEntryBytes) return false;
305
- this.sweepExpired();
258
+ set(key: string, value: T, ttlSeconds: number): void {
259
+ this.#entries.delete(key);
306
260
  const maxEntries = this.maxEntries > 0 ? this.maxEntries : 100;
307
261
  while (this.#entries.size >= maxEntries) {
308
- if (!this.#removeOldest()) break;
309
- }
310
- while (this.#maxBytes > 0 && this.#entries.size > 0 && this.#byteLength + byteLength > this.#maxBytes) {
311
- if (!this.#removeOldest()) break;
262
+ const oldest = this.#entries.keys().next().value;
263
+ if (!oldest) break;
264
+ this.#entries.delete(oldest);
312
265
  }
313
- this.#entries.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000, byteLength });
314
- this.#byteLength += byteLength;
315
- return true;
316
- }
317
-
318
- /**
319
- * Drops every expired entry. Deliberately a full scan rather than a walk from the oldest that
320
- * stops at the first live entry: insertion order is *LRU* order because `get` reinserts, and TTLs
321
- * differ per entry, so expiry is not monotonic in map order and an early break would leave
322
- * expired entries behind. The map is bounded by `maxEntries`, so the scan is cheap.
323
- */
324
- sweepExpired(now = Date.now()): number {
325
- let removed = 0;
326
- for (const [key, entry] of this.#entries) {
327
- if (entry.expiresAt > now) continue;
328
- this.#remove(key);
329
- removed += 1;
330
- }
331
- return removed;
332
- }
333
-
334
- /** Stops the idle sweep. The cache stays usable; only the timer goes away. */
335
- dispose(): void {
336
- if (!this.#sweepTimer) return;
337
- clearInterval(this.#sweepTimer);
338
- this.#sweepTimer = null;
266
+ this.#entries.set(key, { value, expiresAt: Date.now() + ttlSeconds * 1000 });
339
267
  }
340
268
 
341
269
  delete(key: string): boolean {
342
- return this.#remove(key);
270
+ return this.#entries.delete(key);
343
271
  }
344
272
 
345
273
  invalidate(predicate: (key: string, value: T) => boolean): number {
346
274
  let count = 0;
347
275
  for (const [key, entry] of this.#entries) {
348
276
  if (!predicate(key, entry.value)) continue;
349
- this.#remove(key);
277
+ this.#entries.delete(key);
350
278
  count += 1;
351
279
  }
352
280
  return count;
@@ -354,35 +282,5 @@ export class LruTtlCache<T> {
354
282
 
355
283
  clear(): void {
356
284
  this.#entries.clear();
357
- this.#byteLength = 0;
358
- }
359
-
360
- static parseByteCeiling(value: string | undefined | null, fallback = 0): number {
361
- const parsed = Number.parseInt(value ?? "", 10);
362
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
363
- }
364
-
365
- #remove(key: string): boolean {
366
- const entry = this.#entries.get(key);
367
- if (!entry) return false;
368
- this.#entries.delete(key);
369
- this.#byteLength -= entry.byteLength;
370
- return true;
371
- }
372
-
373
- #removeOldest(): boolean {
374
- const oldest = this.#entries.keys().next().value;
375
- if (!oldest) return false;
376
- return this.#remove(oldest);
377
- }
378
-
379
- /** A measurement must never fail a cache write, and a bad measurement must never skew the total. */
380
- static #measure<T>(sizeOf: (value: T) => number, value: T): number {
381
- try {
382
- const measured = sizeOf(value);
383
- return Number.isFinite(measured) && measured > 0 ? measured : 0;
384
- } catch {
385
- return 0;
386
- }
387
285
  }
388
286
  }
@@ -21,7 +21,7 @@ import { SignalRegistry } from "../../signal/signalRegistry";
21
21
  import type { AkanLib, DatabaseModule, ScalarModule, ServiceModule } from "../akanLib";
22
22
  import { createDefaultAkanOption } from "../akanOption";
23
23
  import type { WebProxyRegistration } from "../proxy";
24
- import { CascadeRunner, DatabaseResolver, ServiceResolver, SignalResolver } from "../resolver";
24
+ import { DatabaseResolver, ServiceResolver, SignalResolver } from "../resolver";
25
25
  import type { SignalRoutes, WebsocketRoutes } from "../types";
26
26
  import { getPredefinedAdaptor, predefinedAdaptorRole } from "./predefinedAdaptor";
27
27
  import { collectAdaptors, resolveAdaptorHierarchy } from "./resolveAdaptorHierarchy";
@@ -63,7 +63,6 @@ export class DiLifecycle {
63
63
  readonly disabledModules = new Map<string, string>();
64
64
  readonly #predefinedAdaptor;
65
65
  readonly #predefinedAdaptorRole = predefinedAdaptorRole;
66
- readonly #cascade = new CascadeRunner();
67
66
 
68
67
  /** Read-only view of the resolved module maps, for tooling that needs to describe the container. */
69
68
  get modules(): {
@@ -123,9 +122,8 @@ export class DiLifecycle {
123
122
  this.#service.set(refName, module as ServiceModule);
124
123
  });
125
124
  this.#database.forEach((mod) => {
126
- const { adaptor, schema } = DatabaseResolver.resolveDatabase(mod.constant, mod.database);
127
- this.#adaptor.set(adaptor.refName, adaptor);
128
- this.#cascade.register(mod.constant, schema, mod.service.srv);
125
+ const databaseAdaptor = DatabaseResolver.resolveDatabase(mod.constant, mod.database);
126
+ this.#adaptor.set(databaseAdaptor.refName, databaseAdaptor);
129
127
  });
130
128
  const services = [
131
129
  ...[...this.#service.values()].map((mod) => mod.service.srv),
@@ -459,7 +457,13 @@ export class DiLifecycle {
459
457
  if (serviceCls.type === "database") {
460
458
  const databaseModule = this.#database.get(serviceCls.refName);
461
459
  if (!databaseModule) throw new Error(`Database "${serviceCls.refName}" is not registered`);
462
- ServiceResolver.resolveDatabaseService(databaseModule.database, serviceCls, this.#cascade);
460
+ ServiceResolver.resolveDatabaseService(
461
+ databaseModule.constant,
462
+ databaseModule.database,
463
+ serviceCls,
464
+
465
+ (refName) => this.getService(refName),
466
+ );
463
467
  }
464
468
  const service = new serviceCls();
465
469
  await InjectInfo.resolveInjection(service, serviceCls, this.registry, this.#env);
@@ -472,8 +476,6 @@ export class DiLifecycle {
472
476
  })),
473
477
  );
474
478
  }
475
-
476
- this.#cascade.seal((refName) => this.getService(refName));
477
479
  }
478
480
 
479
481
  async #initializeInternal() {
@@ -138,10 +138,6 @@ export class ProcessMetricsCollector {
138
138
  ...(metrics.jscHeapSizeBytes !== undefined
139
139
  ? [`jscHeap=${ProcessMetricsCollector.formatBytes(metrics.jscHeapSizeBytes)}`]
140
140
  : []),
141
-
142
- ...(metrics.rscWorkerRssBytes !== undefined
143
- ? [`rscWorkerRss=${ProcessMetricsCollector.formatBytes(metrics.rscWorkerRssBytes)}`]
144
- : []),
145
141
  ...(metrics.eventLoopLagMeanMs !== undefined
146
142
  ? [`elLag=${metrics.eventLoopLagMeanMs}/${metrics.eventLoopLagP99Ms ?? 0}/${metrics.eventLoopLagMaxMs ?? 0}ms`]
147
143
  : []),
@@ -47,11 +47,7 @@ const timedQuery = async <T>(fn: () => Promise<T>): Promise<T> => {
47
47
  };
48
48
 
49
49
  export class DatabaseResolver {
50
- /** Returns the schema alongside the adaptor: the cascade planner reads its `remove` hooks to pick a strategy. */
51
- static resolveDatabase(
52
- constant: ConstantModel,
53
- database: DatabaseModel,
54
- ): { adaptor: AdaptorCls<DatabaseInstance>; schema: DocumentSchema } {
50
+ static resolveDatabase(constant: ConstantModel, database: DatabaseModel): AdaptorCls<DatabaseInstance> {
55
51
  const [modelName, className]: [string, string] = [database.refName, capitalize(database.refName)];
56
52
 
57
53
  const resolveSort = (sortKey?: string | null) =>
@@ -89,16 +85,6 @@ export class DatabaseResolver {
89
85
  schema.index(fields);
90
86
  }
91
87
 
92
- for (const path of constant.full.cascade.removeWith.values()) {
93
- const fields = path.typeKey
94
- ? { removedAt: 1, [path.typeKey]: 1, [path.key]: 1 }
95
- : { removedAt: 1, [path.key]: 1 };
96
- const key = Object.keys(fields).join(",");
97
- if (indexedSortFieldKeys.has(key)) continue;
98
- indexedSortFieldKeys.add(key);
99
- schema.index(fields as { [key: string]: 1 | -1 });
100
- }
101
-
102
88
  class DatabaseModelInstance extends adapt(`${modelName}Model`, ({ plug }) => ({
103
89
  __database: plug(DatabaseAdaptorRole, (database) => database),
104
90
  __cache: plug(CacheAdaptorRole, (cache) => new CacheDatabase(modelName, cache)),
@@ -236,7 +222,7 @@ export class DatabaseResolver {
236
222
  updateOne: (query: QueryOf<any>, update: DocumentUpdateInput, options?: { upsert?: boolean }) =>
237
223
  store.updateOneByQuery(query, update, options),
238
224
  updateMany: (query: QueryOf<any>, update: DocumentUpdateInput) => store.updateManyByQuery(query, update),
239
- removeMany: (query: QueryOf<any>) => store.removeManyByQuery(query),
225
+ deleteMany: (query: QueryOf<any>) => store.deleteManyByQuery(query),
240
226
  bulkWrite: (
241
227
  operations: { updateOne: { filter: QueryOf<any>; update: DocumentUpdateInput; upsert?: boolean } }[],
242
228
  ) => store.bulkWrite(operations),
@@ -336,9 +322,6 @@ export class DatabaseResolver {
336
322
  async __remove(id: string) {
337
323
  return await this.__store.remove(id);
338
324
  }
339
- async __removeMany(query: QueryOf<any>) {
340
- return await timedQuery(() => this.__store.removeManyByQuery(query));
341
- }
342
325
  async [`remove${className}`](id: string) {
343
326
  return this.__remove(id);
344
327
  }
@@ -406,9 +389,6 @@ export class DatabaseResolver {
406
389
  });
407
390
  });
408
391
  applyMixins(DatabaseModelInstance, [database.model]);
409
- return {
410
- adaptor: DatabaseModelInstance as unknown as AdaptorCls<DatabaseInstance<any, any, any, any, any, any>>,
411
- schema,
412
- };
392
+ return DatabaseModelInstance as unknown as AdaptorCls<DatabaseInstance<any, any, any, any, any, any>>;
413
393
  }
414
394
  }
@@ -1,4 +1,3 @@
1
- export * from "./CascadeRunner";
2
1
  export * from "./database.resolver";
3
2
  export * from "./service.resolver";
4
3
  export * from "./signal.resolver";