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
@@ -1,6 +1,6 @@
1
1
  import type { PromiseOrObject } from "akanjs/base";
2
2
  import { capitalize } from "akanjs/common";
3
- import type { 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,
@@ -15,10 +15,13 @@ import {
15
15
  type SaveEventType,
16
16
  } from "akanjs/document";
17
17
  import type { DatabaseService, ServiceCls } from "akanjs/service";
18
- import type { CascadeRunner } from "./CascadeRunner";
19
18
 
20
19
  export class ServiceResolver {
21
- static #getDefaultDbServiceMethods(refName: string, className: string, cascade: CascadeRunner) {
20
+ static #getDefaultDbServiceMethods(
21
+ className: string,
22
+ cascades: [string, string][],
23
+ getService: (refName: string) => DatabaseService,
24
+ ) {
22
25
  const dbServiceMethods = {
23
26
  async __get(this: DatabaseService, id: string) {
24
27
  return await this.__databaseModel.__get(id);
@@ -96,24 +99,37 @@ export class ServiceResolver {
96
99
  return this.__update(id, data);
97
100
  },
98
101
  async __remove(this: DatabaseService, id: string): Promise<Doc> {
102
+
103
+ const targets = cascades.map(([key, refName]) => [key, getService(refName)] as const);
99
104
  await this.__libsPreRemove(id);
100
105
  const doc = await this.__databaseModel.__remove(id);
101
106
  const removed = await this.__libsPostRemove(doc);
102
- await cascade.run(refName, removed as Record<string, unknown>);
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
+ }
103
113
  return removed;
104
114
  },
105
115
  async [`remove${className}`](this: DatabaseService, id: string): Promise<Doc> {
106
116
  return this.__remove(id);
107
117
  },
108
- async __removeMany(this: DatabaseService, query: QueryOf<any>) {
109
- return await this.__databaseModel.__removeMany(query);
110
- },
111
118
  };
112
119
  return dbServiceMethods;
113
120
  }
114
- static resolveDatabaseService(database: DatabaseModel, srvRef: ServiceCls, cascade: CascadeRunner): ServiceCls {
121
+ static resolveDatabaseService(
122
+ constant: ConstantModel,
123
+ database: DatabaseModel,
124
+ srvRef: ServiceCls,
125
+ getService: (refName: string) => DatabaseService,
126
+ ): ServiceCls {
115
127
  const className = capitalize(database.refName);
116
- Object.assign(srvRef.prototype, ServiceResolver.#getDefaultDbServiceMethods(database.refName, className, cascade));
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));
117
133
  const getQueryDataFromKey = (queryKey: string, args: any): { query: any; queryOption: any } => {
118
134
  const lastArg = args.at(-1);
119
135
  const hasQueryOption =
@@ -24,7 +24,6 @@ import { renderToReadableStream } from "react-server-dom-webpack/server.node";
24
24
  import type { ClientManifest } from "./artifact";
25
25
  import {
26
26
  LruTtlCache,
27
- type LruTtlCacheOptions,
28
27
  parsePositiveInt,
29
28
  type RouteCacheEntry,
30
29
  type RouteCacheInvalidation,
@@ -123,9 +122,6 @@ interface FlightRenderResult {
123
122
  cancelled: boolean;
124
123
  }
125
124
 
126
- /** Well above any sensible TTL: this reclaims a cache nobody is reading, not tracks expiry closely. */
127
- const RESULT_CACHE_SWEEP_INTERVAL_MS = 60_000;
128
-
129
125
  function hashRscTraceCacheKey(cacheKey: string): string {
130
126
  let hash = 5381;
131
127
  for (let index = 0; index < cacheKey.length; index += 1) hash = (hash * 33) ^ cacheKey.charCodeAt(index);
@@ -201,14 +197,11 @@ export class RscRenderer {
201
197
  pagesBundleBuildId: 0,
202
198
  };
203
199
  readonly #routeStats = new Map<string, RouteRenderStats>();
204
-
205
200
  #resultCache = new LruTtlCache<CachedRscResult>(
206
201
  parsePositiveInt(process.env.AKAN_RSC_RESULT_CACHE_MAX_ENTRIES) ?? 100,
207
- RscRenderer.#resultCacheOptions(),
208
202
  );
209
203
  #patchResultCache = new LruTtlCache<CachedRscResult>(
210
204
  parsePositiveInt(process.env.AKAN_RSC_RESULT_CACHE_MAX_ENTRIES) ?? 100,
211
- RscRenderer.#resultCacheOptions(),
212
205
  );
213
206
  readonly #activeRenderReaders = new Map<string, ReadableStreamDefaultReader<Uint8Array>>();
214
207
  readonly #cancelledRenderRequests = new Set<string>();
@@ -825,11 +818,7 @@ export class RscRenderer {
825
818
  rscLoadedRouteModuleKeys: routeStats.loadedModuleKeys,
826
819
  rscTopRoutesByRenderCount: this.#topRoutes((route) => route.count),
827
820
  rscTopRoutesByFlightBytes: this.#topRoutes((route) => route.flightBytes),
828
-
829
- rscResultCacheEntries: this.#resultCache.size,
830
- rscResultCacheBytes: this.#resultCache.byteSize,
831
- rscPatchResultCacheEntries: this.#patchResultCache.size,
832
- rscPatchResultCacheBytes: this.#patchResultCache.byteSize,
821
+ rscResultCacheEntries: this.#resultCache.size + this.#patchResultCache.size,
833
822
  rscResultCacheHits: this.#resultCacheHits,
834
823
  rscResultCacheMisses: this.#resultCacheMisses,
835
824
  rscResultCacheBypass: this.#resultCacheBypass,
@@ -910,7 +899,7 @@ export class RscRenderer {
910
899
  });
911
900
  };
912
901
  try {
913
- for (;;) {
902
+ while (true) {
914
903
  if (options.requestId && this.#cancelledRenderRequests.has(options.requestId)) {
915
904
  await reader.cancel();
916
905
  return { chunks, bytes, chunksCount, control: null, lateControlSent, cancelled: true };
@@ -1136,15 +1125,6 @@ export class RscRenderer {
1136
1125
  return decision;
1137
1126
  }
1138
1127
 
1139
- static #resultCacheOptions(): LruTtlCacheOptions<CachedRscResult> {
1140
- return {
1141
- sizeOf: (result) => result.bytes,
1142
- maxBytes: LruTtlCache.parseByteCeiling(process.env.AKAN_RSC_RESULT_CACHE_MAX_BYTES),
1143
- maxEntryBytes: LruTtlCache.parseByteCeiling(process.env.AKAN_RSC_RESULT_CACHE_MAX_BODY_BYTES),
1144
- sweepIntervalMs: RESULT_CACHE_SWEEP_INTERVAL_MS,
1145
- };
1146
- }
1147
-
1148
1128
  #getCachedResult(cacheKey: string): CachedRscResult | null {
1149
1129
  const cached = this.#resultCache.get(cacheKey);
1150
1130
  if (!cached) {
@@ -1166,19 +1146,11 @@ export class RscRenderer {
1166
1146
  }
1167
1147
 
1168
1148
  #setCachedResult(cacheKey: string, result: CachedRscResult, ttl: number): void {
1169
- this.#logRejectedStore("full", result, this.#resultCache.set(cacheKey, result, ttl));
1149
+ this.#resultCache.set(cacheKey, result, ttl);
1170
1150
  }
1171
1151
 
1172
1152
  #setCachedPatchResult(cacheKey: string, result: CachedRscResult, ttl: number): void {
1173
- this.#logRejectedStore("patch", result, this.#patchResultCache.set(cacheKey, result, ttl));
1174
- }
1175
-
1176
- /** A silently dropped store looks identical to a cache miss; say which route is too big to cache. */
1177
- #logRejectedStore(kind: string, result: CachedRscResult, stored: boolean): void {
1178
- if (stored) return;
1179
- this.#logger.verbose(
1180
- `${kind} result cache store skipped pathname=${result.pathname} bytes=${result.bytes} reason=body-too-large`,
1181
- );
1153
+ this.#patchResultCache.set(cacheKey, result, ttl);
1182
1154
  }
1183
1155
 
1184
1156
  #runWithRequest<T>(request: Request, fn: () => Promise<T>): Promise<T> {
@@ -89,66 +89,6 @@ export function createRscWorkerInvalidateCacheMessage(
89
89
  };
90
90
  }
91
91
 
92
- /**
93
- * The RSC worker samples its own process with the same `ProcessMetricsCollector.collect` its host
94
- * replica uses, so its report carries `pid` / `rssBytes` / `role` / … under the identical names.
95
- * `AkanServer` spreads the host's report into its own `collect({ role, ...webRouter.getMetrics() })`
96
- * and `collect` spreads `extra` last (`processMetricsCollector.ts:122`) — so passing these through
97
- * unprefixed silently overwrote the replica's own process fields, and the replica's RSS was reported
98
- * nowhere. Rename them onto `rscWorker*`; the `rsc*` render counters, which only the worker
99
- * produces, pass through untouched.
100
- *
101
- * XXX A new process-level field in `ProcessMetricsCollector.collect` must be added here too, or it
102
- * starts shadowing the replica again.
103
- */
104
- export function projectRscWorkerProcessMetrics(metrics: AkanMetricsReport): AkanMetricsReport {
105
- const {
106
- role: _role,
107
- pid: _pid,
108
- trace: _trace,
109
- reportedAt,
110
- rssBytes,
111
- heapTotalBytes,
112
- heapUsedBytes,
113
- externalBytes,
114
- arrayBuffersBytes,
115
- cpuUserMicros,
116
- cpuSystemMicros,
117
- maxRssKb,
118
- jscHeapSizeBytes,
119
- jscHeapCapacityBytes,
120
- jscExtraMemorySizeBytes,
121
- jscObjectCount,
122
- jscProtectedObjectCount,
123
- eventLoopLagMeanMs,
124
- eventLoopLagP99Ms,
125
- eventLoopLagMaxMs,
126
- gcDurationMs,
127
- ...renderMetrics
128
- } = metrics;
129
- return {
130
- ...renderMetrics,
131
- rscWorkerReportedAt: reportedAt,
132
- rscWorkerRssBytes: rssBytes,
133
- rscWorkerHeapTotalBytes: heapTotalBytes,
134
- rscWorkerHeapUsedBytes: heapUsedBytes,
135
- rscWorkerExternalBytes: externalBytes,
136
- rscWorkerArrayBuffersBytes: arrayBuffersBytes,
137
- rscWorkerCpuUserMicros: cpuUserMicros,
138
- rscWorkerCpuSystemMicros: cpuSystemMicros,
139
- rscWorkerMaxRssKb: maxRssKb,
140
- rscWorkerJscHeapSizeBytes: jscHeapSizeBytes,
141
- rscWorkerJscHeapCapacityBytes: jscHeapCapacityBytes,
142
- rscWorkerJscExtraMemorySizeBytes: jscExtraMemorySizeBytes,
143
- rscWorkerJscObjectCount: jscObjectCount,
144
- rscWorkerJscProtectedObjectCount: jscProtectedObjectCount,
145
- rscWorkerEventLoopLagMeanMs: eventLoopLagMeanMs,
146
- rscWorkerEventLoopLagP99Ms: eventLoopLagP99Ms,
147
- rscWorkerEventLoopLagMaxMs: eventLoopLagMaxMs,
148
- rscWorkerGcDurationMs: gcDurationMs,
149
- };
150
- }
151
-
152
92
  export function createRscHostRenderStream(input: {
153
93
  setPending: (pending: RscPending) => void;
154
94
  deletePending: () => void;
@@ -524,7 +464,18 @@ export class RscWorker {
524
464
  }
525
465
 
526
466
  getMetrics(): AkanMetricsReport {
527
- return Object.assign(projectRscWorkerProcessMetrics(this.#lastWorkerMetrics), {
467
+ const {
468
+ rscWorkerPid: _rscWorkerPid,
469
+ rscWorkerStatus: _rscWorkerStatus,
470
+ rscWorkerRestartCount: _rscWorkerRestartCount,
471
+ rscWorkerRecycleCount: _rscWorkerRecycleCount,
472
+ rscWorkerLastRecycleReason: _rscWorkerLastRecycleReason,
473
+ rscPendingRenderCount: _rscPendingRenderCount,
474
+ rscQueuedSendCount: _rscQueuedSendCount,
475
+ rscHostPendingChunkOverflowCount: _rscHostPendingChunkOverflowCount,
476
+ ...workerMetrics
477
+ } = this.#lastWorkerMetrics;
478
+ return Object.assign(workerMetrics, {
528
479
  rscWorkerPid: this.#proc.pid,
529
480
  rscWorkerStatus: this.#status,
530
481
  rscWorkerRestartCount: this.#restartCount,
@@ -411,7 +411,7 @@ export function interleaveRscScriptsWithHtml(
411
411
  const pumpRscScripts = async () => {
412
412
  rscReader = rscClientStream.getReader();
413
413
  try {
414
- for (;;) {
414
+ while (true) {
415
415
  const { value, done } = await rscReader.read();
416
416
  if (done || errored) break;
417
417
  await waitForRscQueueDrain();
@@ -446,7 +446,7 @@ export function interleaveRscScriptsWithHtml(
446
446
 
447
447
  htmlReader = htmlStream.getReader();
448
448
  try {
449
- for (;;) {
449
+ while (true) {
450
450
  const { value, done } = await htmlReader.read();
451
451
  if (done || errored) break;
452
452
  controller.enqueue(value);
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { pathToFileURL } from "node:url";
3
4
  import { getEnv } from "akanjs/base";
4
5
  import {
5
6
  type AkanI18nConfig,
@@ -56,10 +57,20 @@ import type { BaseBuildArtifact, HttpRoutes, RenderState } from "./types";
56
57
 
57
58
  const CLIENT_CLOSED_REQUEST_STATUS = 499;
58
59
  export const DEFAULT_HTML_RESULT_CACHE_MAX_BODY_BYTES = 2 * 1024 * 1024;
59
- /** Well above any sensible TTL: this only has to reclaim a cache nobody is reading, not track expiry closely. */
60
- const ROUTE_CACHE_SWEEP_INTERVAL_MS = 60_000;
61
60
  const APPLE_APP_SITE_ASSOCIATION_PATH = "/.well-known/apple-app-site-association";
62
61
  const ANDROID_ASSET_LINKS_PATH = "/.well-known/assetlinks.json";
62
+ const FIREBASE_MESSAGING_SW_PATH = "/firebase-messaging-sw.js";
63
+ const FIREBASE_WEB_SDK_VERSION = "12.13.0";
64
+
65
+ interface FirebaseClientEnvConfig {
66
+ apiKey: string;
67
+ authDomain?: string;
68
+ projectId: string;
69
+ storageBucket?: string;
70
+ messagingSenderId: string;
71
+ appId: string;
72
+ vapidKey?: string;
73
+ }
63
74
 
64
75
  export function createRscRedirectResponse(
65
76
  location: string,
@@ -288,11 +299,6 @@ export class WebRouter {
288
299
  };
289
300
  readonly #htmlCache = new LruTtlCache<CachedHtmlResult>(
290
301
  parsePositiveInt(process.env.AKAN_HTML_RESULT_CACHE_MAX_ENTRIES) ?? 100,
291
- {
292
- sizeOf: (result) => result.html.length,
293
- maxBytes: LruTtlCache.parseByteCeiling(process.env.AKAN_HTML_RESULT_CACHE_MAX_BYTES),
294
- sweepIntervalMs: ROUTE_CACHE_SWEEP_INTERVAL_MS,
295
- },
296
302
  );
297
303
  #htmlCacheHits = 0;
298
304
  #htmlCacheMisses = 0;
@@ -486,6 +492,16 @@ export class WebRouter {
486
492
  WebRouter.#deepLinkAssociationResponse(ANDROID_ASSET_LINKS_PATH, this.#artifact, {
487
493
  cacheControl: this.#prodMode ? "public, max-age=3600" : "no-store",
488
494
  }) ?? new Response("Not Found", { status: 404 }),
495
+ [FIREBASE_MESSAGING_SW_PATH]: async () => {
496
+ this.#requestStats.staticAsset += 1;
497
+ const firebaseConfig = await WebRouter.#resolveFirebaseClientConfig();
498
+ return new Response(WebRouter.#createFirebaseMessagingServiceWorker(firebaseConfig), {
499
+ headers: {
500
+ "Content-Type": "application/javascript; charset=utf-8",
501
+ "Cache-Control": "no-store",
502
+ },
503
+ });
504
+ },
489
505
  "/*": async (req) => {
490
506
  const url = new URL(req.url);
491
507
  if (WebRouter.#isImageOptimizerPath(url.pathname)) {
@@ -676,7 +692,6 @@ export class WebRouter {
676
692
  this.#devHmr?.dispose();
677
693
  this.#devHmr = null;
678
694
  this.#builderRpc = null;
679
- this.#htmlCache.dispose();
680
695
  this.#rsc.kill();
681
696
  this.#hub = null;
682
697
  }
@@ -694,7 +709,6 @@ export class WebRouter {
694
709
  httpCsrCount: this.#requestStats.csr,
695
710
  httpImageCount: this.#requestStats.image,
696
711
  httpHtmlCacheEntries: this.#htmlCache.size,
697
- httpHtmlCacheBytes: this.#htmlCache.byteSize,
698
712
  httpHtmlCacheHits: this.#htmlCacheHits,
699
713
  httpHtmlCacheMisses: this.#htmlCacheMisses,
700
714
  httpHtmlCacheBypass: this.#htmlCacheBypass,
@@ -994,6 +1008,79 @@ export class WebRouter {
994
1008
  return process.env.AKAN_APP_DIR ?? path.dirname(Bun.main);
995
1009
  }
996
1010
 
1011
+ static async #resolveFirebaseClientConfig(): Promise<FirebaseClientEnvConfig | null> {
1012
+ const envPath = path.join(WebRouter.#resolveAppDir(), "env", "env.client.ts");
1013
+ if (!fs.existsSync(envPath)) return null;
1014
+ try {
1015
+ const envUrl = pathToFileURL(envPath);
1016
+ envUrl.searchParams.set("t", String(Date.now()));
1017
+ const envModule = (await import(envUrl.href)) as { env?: { firebase?: unknown } };
1018
+ return WebRouter.#normalizeFirebaseClientConfig(envModule.env?.firebase);
1019
+ } catch {
1020
+ return null;
1021
+ }
1022
+ }
1023
+
1024
+ static #normalizeFirebaseClientConfig(config: unknown): FirebaseClientEnvConfig | null {
1025
+ if (!config || typeof config !== "object") return null;
1026
+ const value = config as Partial<Record<keyof FirebaseClientEnvConfig, unknown>>;
1027
+ if (
1028
+ typeof value.apiKey !== "string" ||
1029
+ typeof value.projectId !== "string" ||
1030
+ typeof value.messagingSenderId !== "string" ||
1031
+ typeof value.appId !== "string"
1032
+ ) {
1033
+ return null;
1034
+ }
1035
+ return {
1036
+ apiKey: value.apiKey,
1037
+ ...(typeof value.authDomain === "string" ? { authDomain: value.authDomain } : {}),
1038
+ projectId: value.projectId,
1039
+ ...(typeof value.storageBucket === "string" ? { storageBucket: value.storageBucket } : {}),
1040
+ messagingSenderId: value.messagingSenderId,
1041
+ appId: value.appId,
1042
+ };
1043
+ }
1044
+
1045
+ static #createFirebaseMessagingServiceWorker(config: FirebaseClientEnvConfig | null): string {
1046
+ const configJson = JSON.stringify(config);
1047
+ return `/* Generated by Akan.js. Do not edit. */
1048
+ const firebaseConfig = ${configJson};
1049
+
1050
+ if (firebaseConfig) {
1051
+ importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-app-compat.js");
1052
+ importScripts("https://www.gstatic.com/firebasejs/${FIREBASE_WEB_SDK_VERSION}/firebase-messaging-compat.js");
1053
+
1054
+ firebase.initializeApp(firebaseConfig);
1055
+ const messaging = firebase.messaging();
1056
+
1057
+ const notificationUrl = (payload) =>
1058
+ payload?.data?.url || payload?.fcmOptions?.link || payload?.notification?.click_action;
1059
+
1060
+ messaging.onBackgroundMessage((payload) => {
1061
+ const title = payload?.notification?.title || "";
1062
+ const options = {
1063
+ body: payload?.notification?.body,
1064
+ icon: payload?.notification?.icon,
1065
+ image: payload?.notification?.image,
1066
+ data: {
1067
+ url: notificationUrl(payload),
1068
+ FCM_MSG: payload,
1069
+ },
1070
+ };
1071
+ self.registration.showNotification(title, options);
1072
+ });
1073
+ }
1074
+
1075
+ self.addEventListener("notificationclick", (event) => {
1076
+ const url = event.notification?.data?.url || event.notification?.data?.FCM_MSG?.data?.url;
1077
+ event.notification?.close();
1078
+ if (!url) return;
1079
+ event.waitUntil(clients.openWindow(url));
1080
+ });
1081
+ `;
1082
+ }
1083
+
997
1084
  static #normalizeArtifact(artifact: BaseBuildArtifact, artifactDir: string): BaseBuildArtifact {
998
1085
  const normalizedArtifactDir = path.resolve(artifactDir);
999
1086
  const pagesBundlePath = WebRouter.#resolveArtifactPath(artifact.pagesBundlePath, normalizedArtifactDir);
@@ -55,26 +55,6 @@ export interface AkanMetricsReport {
55
55
  rscWorkerRestartCount?: number;
56
56
  rscWorkerRecycleCount?: number;
57
57
  rscWorkerLastRecycleReason?: string;
58
-
59
- rscWorkerReportedAt?: number;
60
- rscWorkerRssBytes?: number;
61
- rscWorkerHeapTotalBytes?: number;
62
- rscWorkerHeapUsedBytes?: number;
63
- rscWorkerExternalBytes?: number;
64
- rscWorkerArrayBuffersBytes?: number;
65
- rscWorkerCpuUserMicros?: number;
66
- rscWorkerCpuSystemMicros?: number;
67
- rscWorkerMaxRssKb?: number;
68
- rscWorkerJscHeapSizeBytes?: number;
69
- rscWorkerJscHeapCapacityBytes?: number;
70
- /** Off-heap bytes JSC attributes to JS objects — typed-array backing stores, i.e. cached Flight chunks. */
71
- rscWorkerJscExtraMemorySizeBytes?: number;
72
- rscWorkerJscObjectCount?: number;
73
- rscWorkerJscProtectedObjectCount?: number;
74
- rscWorkerEventLoopLagMeanMs?: number;
75
- rscWorkerEventLoopLagP99Ms?: number;
76
- rscWorkerEventLoopLagMaxMs?: number;
77
- rscWorkerGcDurationMs?: number;
78
58
  rscPendingRenderCount?: number;
79
59
  rscQueuedSendCount?: number;
80
60
  rscHostPendingChunkOverflowCount?: number;
@@ -100,13 +80,9 @@ export interface AkanMetricsReport {
100
80
  rscLastRenderLoadedModuleDelta?: number;
101
81
  rscLastRenderLoadedModules?: string[];
102
82
  rscResultCacheEntries?: number;
103
- rscResultCacheBytes?: number;
104
- rscPatchResultCacheEntries?: number;
105
- rscPatchResultCacheBytes?: number;
106
83
  rscResultCacheHits?: number;
107
84
  rscResultCacheMisses?: number;
108
85
  rscResultCacheBypass?: number;
109
- /** Keys the SSR chunk registry tracks — NOT a memory bound; the modules stay in Bun's ESM registry. */
110
86
  ssrChunkRegistrySize?: number;
111
87
  ssrChunkLoadCount?: number;
112
88
  ssrChunkCacheHitCount?: number;
@@ -117,7 +93,6 @@ export interface AkanMetricsReport {
117
93
  httpCsrCount?: number;
118
94
  httpImageCount?: number;
119
95
  httpHtmlCacheEntries?: number;
120
- httpHtmlCacheBytes?: number;
121
96
  httpHtmlCacheHits?: number;
122
97
  httpHtmlCacheMisses?: number;
123
98
  httpHtmlCacheBypass?: number;
@@ -95,7 +95,7 @@ export interface DocumentStore {
95
95
  query: DocumentQuery,
96
96
  update: DocumentUpdateInput,
97
97
  ): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number }>;
98
- removeManyByQuery(
98
+ deleteManyByQuery(
99
99
  query: DocumentQuery,
100
100
  ): Promise<{ acknowledged: boolean; matchedCount: number; modifiedCount: number }>;
101
101
  bulkWrite(
@@ -1081,8 +1081,8 @@ export class SqlDocumentStore {
1081
1081
  return { acknowledged: true, matchedCount: changes, modifiedCount: changes };
1082
1082
  }
1083
1083
 
1084
- async removeManyByQuery(query: DocumentQuery) {
1085
-
1084
+ async deleteManyByQuery(query: DocumentQuery) {
1085
+
1086
1086
  return this.updateManyByQuery(query, { removedAt: dayjs() });
1087
1087
  }
1088
1088
 
package/service/serve.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Cls, INJECT_META, LIBS_REMOVE_HOOK } from "akanjs/base";
1
+ import { type Cls, INJECT_META } from "akanjs/base";
2
2
  import { applyMixins, capitalize, Logger, lowerlize } from "akanjs/common";
3
3
  import type { DatabaseModel } from "akanjs/document";
4
4
 
@@ -146,10 +146,6 @@ export function serve(
146
146
  const postUpdateFns = extSrvs.map((srv) => srv.prototype._postUpdate);
147
147
  const preRemoveFns = extSrvs.map((srv) => srv.prototype._preRemove);
148
148
  const postRemoveFns = extSrvs.map((srv) => srv.prototype._postRemove);
149
-
150
- Object.assign(srvRef, {
151
- [LIBS_REMOVE_HOOK]: preRemoveFns.some(Boolean) || postRemoveFns.some(Boolean),
152
- });
153
149
  Object.assign(srvRef.prototype, {
154
150
  async __libsPreCreate(this: DatabaseService, data: DatabaseServiceData) {
155
151
  let result = data;
package/service/types.ts CHANGED
@@ -11,7 +11,6 @@ import type {
11
11
  ListQueryOption,
12
12
  QueryMethodPart,
13
13
  SaveEventType,
14
- UpdateResult,
15
14
  } from "akanjs/document";
16
15
 
17
16
  type ServiceMixinOmitKey =
@@ -102,7 +101,6 @@ export type DatabaseService<
102
101
  __create: (data: _DataInputOfDoc) => Promise<Doc>;
103
102
  __update: (id: string, data: Partial<Doc>) => Promise<Doc>;
104
103
  __remove: (id: string) => Promise<Doc>;
105
- __removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
106
104
  __list(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
107
105
  __listIds(query?: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
108
106
  __find(query?: _QueryOfDoc, queryOption?: _FindQueryOption): Promise<Doc | null>;
@@ -3,7 +3,6 @@ export declare const SLICE_META: unique symbol;
3
3
  export declare const FILTER_META: unique symbol;
4
4
  export declare const LOADER_META: unique symbol;
5
5
  export declare const INJECT_META: unique symbol;
6
- export declare const LIBS_REMOVE_HOOK: unique symbol;
7
6
  export declare const ENDPOINT_META: unique symbol;
8
7
  export declare const ENDPOINT_DICT_SHAPE: unique symbol;
9
8
  export declare const SLICE_DICT_SHAPE: unique symbol;
@@ -6,6 +6,11 @@ import { type ClassNameValue } from "tailwind-merge";
6
6
  * `cn("bg-primary", "bg-open")` correctly resolves to `"bg-open"` instead of keeping both.
7
7
  */
8
8
  export declare const colorTokens: string[];
9
+ /**
10
+ * Akan's semantic radius tokens (`--radius-box` / `--radius-field` in ui/styles.css). Without them
11
+ * `cn("rounded-field", "rounded-full")` keeps both classes and stylesheet order decides the winner.
12
+ */
13
+ export declare const radiusTokens: string[];
9
14
  /** The one class-combining function: joins strings/arrays/conditional parts (`cond && "x"`) and
10
15
  * resolves Tailwind conflicts with a shared tailwind-merge instance that knows Akan's semantic
11
16
  * color tokens. clsx-style object syntax (`{ x: cond }`) is not supported — write `cond && "x"`. */
@@ -428,6 +428,7 @@ export interface FrameLayoutState {
428
428
  keyboard: KeyboardFrameState;
429
429
  contentViewport: FrameContentViewportState;
430
430
  keyboardAccessory: KeyboardAccessoryFrameState;
431
+ contentAnchor?: "bottom";
431
432
  platformProfile: FramePlatformProfile;
432
433
  zIndex: FrameLayerZIndex;
433
434
  pageStateByPath: Map<string, PageState>;
@@ -436,6 +437,7 @@ export interface FrameSlotRegistration {
436
437
  scope?: FrameSlotScope;
437
438
  type: FrameSlotType;
438
439
  role?: FrameSlotRole;
440
+ contentAnchor?: "bottom";
439
441
  height?: number;
440
442
  estimatedHeight?: number;
441
443
  source?: "navbar" | "topInset" | "bottomInset" | "bottomTab" | (string & {});
@@ -1,29 +1,11 @@
1
1
  import type { FieldObject } from "./fieldInfo.d.ts";
2
2
  import type { ConstantModelRef } from "./via.d.ts";
3
- /**
4
- * Which end of a relation goes away with the other. `removeRef` removes what the field points at when this
5
- * document is removed; `removeWith` removes this document when what the field points at is removed. The two
6
- * read identically on a relation field, so the value has to name the direction — a mistake here is a data loss.
7
- */
8
- export declare const cascadeActions: readonly ["removeRef", "removeWith"];
3
+ /** What happens to the documents a relation field points at when the owner is removed. */
4
+ export declare const cascadeActions: readonly ["remove"];
9
5
  export type CascadeAction = (typeof cascadeActions)[number];
10
- /** How a `removeWith` field names the owner whose removal takes this document with it. */
11
- export interface CascadeWithPath {
12
- readonly key: string;
13
- /** Set when the field is a relation. Resolved to a refName later: the owner may not be registered yet. */
14
- readonly modelRef: ConstantModelRef | null;
15
- /** Set when the field declares `ref`, which names the owner at declaration. */
16
- readonly refName: string | null;
17
- /** Set when the field declares `refPath`: the sibling field holding the owner's refName. */
18
- readonly typeKey: string | null;
19
- /** The refNames `typeKey` may hold. Empty unless the field is polymorphic. */
20
- readonly typeValues: readonly string[];
21
- }
22
6
  export declare class CascadePaths {
23
7
  #private;
24
- /** Field key → the model its ids point at, removed when this document is. */
25
- readonly removeRef: Map<string, ConstantModelRef>;
26
- /** Field key → the owner whose removal removes this document. */
27
- readonly removeWith: Map<string, CascadeWithPath>;
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>;
28
10
  collect(fieldMap: FieldObject): this;
29
11
  }
@@ -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.d.ts";
6
6
  import type { ExtractQuery, ExtractSort, FilterInstance } from "./filterMeta.d.ts";
7
- import type { CRUDEventType, Mdl, SaveEventType, UpdateResult } from "./into.d.ts";
7
+ import type { CRUDEventType, Mdl, SaveEventType } from "./into.d.ts";
8
8
  import type { DataInputOf, FindQueryOption, ListQueryOption } from "./types.d.ts";
9
9
  export declare class CacheDatabase<T = unknown> {
10
10
  private readonly refName;
@@ -57,7 +57,6 @@ type DatabaseModelWithQuerySort<T extends string, Input, Doc, Obj, Insight, Quer
57
57
  __create: (data: _DataInput) => Promise<Doc>;
58
58
  __update: (id: string, data: Partial<Doc>) => Promise<Doc>;
59
59
  __remove: (id: string) => Promise<Doc>;
60
- __removeMany: (query: _QueryOfDoc) => Promise<UpdateResult>;
61
60
  __list(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<Doc[]>;
62
61
  __listIds(query: _QueryOfDoc, queryOption?: _ListQueryOption): Promise<string[]>;
63
62
  __find(query: _QueryOfDoc, queryOption?: _FindQueryOption): Promise<Doc | null>;
@@ -8,7 +8,7 @@ export interface DatabaseModel<T extends string = string, Input = any, Doc = any
8
8
  model: ModelCls<Model>;
9
9
  filter: FilterCls<Filter>;
10
10
  obj: ConstantCls<Obj>;
11
- insight: DatabaseCls<Insight>;
11
+ insight: ConstantCls<Insight>;
12
12
  _Input: Input;
13
13
  _Doc: Doc;
14
14
  _Model: Model;
@@ -32,6 +32,6 @@ export declare class DatabaseRegistry {
32
32
  static getScalar<AllowEmpty extends boolean = false>(refName: string, { allowEmpty }?: {
33
33
  allowEmpty?: AllowEmpty;
34
34
  }): AllowEmpty extends true ? DatabaseCls | undefined : DatabaseCls;
35
- static buildModel<T extends string, Input, Doc, Model, Obj, Insight, Filter extends FilterInstance, _Query extends ExtractQuery<Filter> = ExtractQuery<Filter>, _Sort extends ExtractSort<Filter> = ExtractSort<Filter>>(refName: T, input: DatabaseCls<Input>, doc: DatabaseCls<Doc>, model: ModelCls<Model>, obj: ConstantCls<Obj>, insight: DatabaseCls<Insight>, filter: FilterCls<Filter>): DatabaseModel<T, Input, Doc, Model, Obj, Insight, Filter, _Query, _Sort>;
35
+ static buildModel<T extends string, Input, Doc, Model, Obj, Insight, Filter extends FilterInstance, _Query extends ExtractQuery<Filter> = ExtractQuery<Filter>, _Sort extends ExtractSort<Filter> = ExtractSort<Filter>>(refName: T, input: DatabaseCls<Input>, doc: DatabaseCls<Doc>, model: ModelCls<Model>, obj: ConstantCls<Obj>, insight: ConstantCls<Insight>, filter: FilterCls<Filter>): DatabaseModel<T, Input, Doc, Model, Obj, Insight, Filter, _Query, _Sort>;
36
36
  static buildScalar<T extends string, Model>(refName: T, Model: DatabaseCls<Model>): DatabaseCls<Model>;
37
37
  }
@@ -60,17 +60,16 @@ export type Mdl<Doc, Raw, _RawDoc = DocumentModel<Raw>, _RawQuery extends Docume
60
60
  exists(query: _RawQuery): Promise<string | null>;
61
61
  updateOne(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>, options?: DocumentUpdateOptions): Promise<UpdateResult>;
62
62
  updateMany(query: _RawQuery, update: DocumentUpdateInput<_RawDoc>): Promise<UpdateResult>;
63
- removeMany(query: _RawQuery): Promise<UpdateResult>;
63
+ deleteMany(query: _RawQuery): Promise<UpdateResult>;
64
64
  bulkWrite(operations: BulkWriteOperation<Raw, _RawDoc, _RawQuery>[]): Promise<UpdateResult>;
65
65
  };
66
- interface IntoConstantModel<T extends string, _CapitalizedRefName extends string, Raw, Insight> {
66
+ interface IntoConstantModel<T extends string, _CapitalizedRefName extends string, Raw> {
67
67
  refName: T;
68
68
  _CapitalizedRefName: _CapitalizedRefName;
69
69
  _Full: Raw;
70
- _Insight: Insight;
71
70
  }
72
71
  type NoInferType<T> = [T][T extends unknown ? 0 : never];
73
- type IntoModelActions<T extends string, _CapitalizedRefName extends string, Doc, Raw, Insight, _Query, _Sort, _QueryOfDoc = QueryOf<Doc>> = {
72
+ type IntoModelActions<T extends string, _CapitalizedRefName extends string, Doc, Raw, _Query, _Sort, _QueryOfDoc = QueryOf<Doc>> = {
74
73
  [key in _CapitalizedRefName]: Mdl<Doc, Raw>;
75
74
  } & {
76
75
  [key in `${Uncapitalize<_CapitalizedRefName>}Loader`]: DataLoader<string, Doc, string>;
@@ -88,6 +87,6 @@ type IntoModelActions<T extends string, _CapitalizedRefName extends string, Doc,
88
87
  [K in `update${_CapitalizedRefName}`]: (id: string, data: Partial<Doc>) => Promise<Doc>;
89
88
  } & {
90
89
  [K in `remove${_CapitalizedRefName}`]: (id: string) => Promise<Doc>;
91
- } & QueryMethodPart<_Query, _Sort, Raw, Doc, DocumentModel<Insight>, unknown, unknown, _QueryOfDoc>;
92
- export declare const into: <Doc, FilterRef extends FilterCls, T extends string, Raw, Insight, AddDbModels extends ModelCls[], _CapitalizedRefName extends string, _QueryOfDoc = QueryOf<Doc>, _Query = FilterQueryOf<FilterRef>, _Sort = FilterSortOf<FilterRef>, _LoaderBuilder extends LoaderBuilder<NoInferType<Doc>> = LoaderBuilder<Doc>>(docRef: Cls<Doc>, filterRef: FilterRef, cnst: IntoConstantModel<T, _CapitalizedRefName, Raw, Insight>, loaderBuilder: _LoaderBuilder, ...addMdls: [...AddDbModels]) => ModelCls<IntoModelActions<T, _CapitalizedRefName, Doc, Raw, Insight, _Query, _Sort, _QueryOfDoc>, ReturnType<_LoaderBuilder>>;
90
+ } & QueryMethodPart<_Query, _Sort, Raw, Doc, unknown, unknown, unknown, _QueryOfDoc>;
91
+ export declare const into: <Doc, FilterRef extends FilterCls, T extends string, Raw, AddDbModels extends ModelCls[], _CapitalizedRefName extends string, _QueryOfDoc = QueryOf<Doc>, _Query = FilterQueryOf<FilterRef>, _Sort = FilterSortOf<FilterRef>, _LoaderBuilder extends LoaderBuilder<NoInferType<Doc>> = LoaderBuilder<Doc>>(docRef: Cls<Doc>, filterRef: FilterRef, cnst: IntoConstantModel<T, _CapitalizedRefName, Raw>, loaderBuilder: _LoaderBuilder, ...addMdls: [...AddDbModels]) => ModelCls<IntoModelActions<T, _CapitalizedRefName, Doc, Raw, _Query, _Sort, _QueryOfDoc>, ReturnType<_LoaderBuilder>>;
93
92
  export {};