@akanjs/devkit 3.0.0-beta.9 → 3.0.1

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 (346) hide show
  1. package/CHANGELOG.md +1819 -0
  2. package/DEV_RUNTIME_KNOBS.md +44 -26
  3. package/README.ko.md +21 -7
  4. package/README.md +17 -8
  5. package/abstractDoc.test.ts +6 -15
  6. package/abstractDoc.ts +6 -8
  7. package/agentsIndex.test.ts +4 -7
  8. package/agentsIndex.ts +6 -22
  9. package/akanApp/BackendImportGraph.test.ts +8 -22
  10. package/akanApp/BackendImportGraph.ts +1 -20
  11. package/akanApp/akanApp.host.test.ts +56 -3
  12. package/akanApp/akanApp.host.ts +218 -278
  13. package/akanApp/devBootLatch.test.ts +39 -0
  14. package/akanApp/devBootLatch.ts +39 -0
  15. package/akanApp/devHostPolicy.test.ts +81 -35
  16. package/akanApp/devHostPolicy.ts +41 -102
  17. package/akanConfig/akanBin.test.ts +62 -0
  18. package/akanConfig/akanBin.ts +78 -0
  19. package/akanConfig/akanConfig.test.ts +439 -141
  20. package/akanConfig/akanConfig.ts +359 -275
  21. package/akanConfig/index.ts +1 -0
  22. package/akanConfig/types.ts +22 -8
  23. package/akanContext.ts +110 -160
  24. package/akanMcpContract.ts +28 -73
  25. package/appSelectionMemory.ts +1 -4
  26. package/applicationBuildReporter.ts +37 -5
  27. package/applicationBuildRunner.test.ts +41 -5
  28. package/applicationBuildRunner.ts +71 -63
  29. package/applicationTestPreload.ts +3 -12
  30. package/artifact/implicitRootLayout.test.ts +34 -31
  31. package/artifact/implicitRootLayout.ts +60 -107
  32. package/artifact/routeSeedIndex.test.ts +5 -18
  33. package/artifact/routeSeedIndex.ts +11 -44
  34. package/biome.base.json +39 -2
  35. package/biomeBase.ts +2 -5
  36. package/cloud/cloudApi.test.ts +202 -0
  37. package/cloud/cloudApi.ts +77 -39
  38. package/cloud/configLock.test.ts +64 -0
  39. package/cloud/configLock.ts +96 -0
  40. package/cloud/constants.ts +26 -5
  41. package/cloud/globalConfig.ts +71 -32
  42. package/codeAgent/agent/AkanCodeServices.ts +97 -0
  43. package/codeAgent/agent/AkanContextFiles.ts +94 -0
  44. package/codeAgent/agent/CodeAgent.ts +551 -0
  45. package/codeAgent/agent/CodeAgentAsks.ts +66 -0
  46. package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
  47. package/codeAgent/agent/CodeAgentGate.ts +93 -0
  48. package/codeAgent/agent/CodeAgentProxy.ts +82 -0
  49. package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
  50. package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
  51. package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
  52. package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
  53. package/codeAgent/agent/CodeAgentUi.ts +47 -0
  54. package/codeAgent/agent/CodeMailbox.ts +148 -0
  55. package/codeAgent/agent/CodeSessionFork.ts +56 -0
  56. package/codeAgent/agent/CodeSessionIndex.ts +78 -0
  57. package/codeAgent/agent/SubagentPool.ts +214 -0
  58. package/codeAgent/agent/akanCodeModel.ts +47 -0
  59. package/codeAgent/agent/akanCodePaths.ts +31 -0
  60. package/codeAgent/agent/akanContextFiles.test.ts +114 -0
  61. package/codeAgent/agent/akanSystemPrompt.ts +46 -0
  62. package/codeAgent/agent/codeAgent.test.ts +178 -0
  63. package/codeAgent/agent/codeAgentContract.test.ts +523 -0
  64. package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
  65. package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
  66. package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
  67. package/codeAgent/agent/codeMailbox.test.ts +76 -0
  68. package/codeAgent/agent/codeSessionFork.test.ts +58 -0
  69. package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
  70. package/codeAgent/agent/consoleToStderr.ts +8 -0
  71. package/codeAgent/agent/jsonLines.ts +29 -0
  72. package/codeAgent/agent/stringArg.ts +5 -0
  73. package/codeAgent/feedback/DevLogFeedback.ts +82 -0
  74. package/codeAgent/feedback/PreviewChrome.ts +82 -0
  75. package/codeAgent/feedback/PreviewFeedback.ts +46 -0
  76. package/codeAgent/feedback/PreviewView.ts +95 -0
  77. package/codeAgent/feedback/TurnFeedback.ts +98 -0
  78. package/codeAgent/feedback/VerifyFeedback.ts +40 -0
  79. package/codeAgent/feedback/previewChrome.test.ts +73 -0
  80. package/codeAgent/index.ts +34 -0
  81. package/codeAgent/skills/akan-code/SKILL.md +130 -0
  82. package/codeAgent/skills/akan-debug/SKILL.md +42 -0
  83. package/codeAgent/skills/akan-deps/SKILL.md +50 -0
  84. package/codeAgent/skills/akan-err/SKILL.md +39 -0
  85. package/codeAgent/skills/akan-module/SKILL.md +45 -0
  86. package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
  87. package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
  88. package/codeAgent/skills/akan-service/SKILL.md +34 -0
  89. package/codeAgent/skills/akan-store/SKILL.md +67 -0
  90. package/codeAgent/skills/akan-ui/SKILL.md +63 -0
  91. package/codeAgent/skills/akan-validate/SKILL.md +44 -0
  92. package/codeAgent/tools/AkanCli.ts +50 -0
  93. package/codeAgent/tools/AkanCodePlugins.ts +136 -0
  94. package/codeAgent/tools/AkanEditScope.ts +48 -0
  95. package/codeAgent/tools/AkanEnvKeys.ts +50 -0
  96. package/codeAgent/tools/AkanToolPack.ts +92 -0
  97. package/codeAgent/tools/AkanVerifier.ts +48 -0
  98. package/codeAgent/tools/AskToolPack.ts +99 -0
  99. package/codeAgent/tools/MailToolPack.ts +76 -0
  100. package/codeAgent/tools/McpClient.ts +187 -0
  101. package/codeAgent/tools/McpOAuth.ts +233 -0
  102. package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
  103. package/codeAgent/tools/McpServerConfig.ts +129 -0
  104. package/codeAgent/tools/McpSignIn.ts +158 -0
  105. package/codeAgent/tools/McpTokenStore.ts +81 -0
  106. package/codeAgent/tools/McpToolPack.ts +199 -0
  107. package/codeAgent/tools/SessionToolPack.ts +91 -0
  108. package/codeAgent/tools/WebToolPack.ts +147 -0
  109. package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
  110. package/codeAgent/tools/askToolPack.test.ts +116 -0
  111. package/codeAgent/tools/mcpOAuth.test.ts +612 -0
  112. package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
  113. package/codegenLock.test.ts +4 -13
  114. package/codegenLock.ts +3 -17
  115. package/commandDecorators/argMeta.ts +1 -5
  116. package/commandDecorators/camelToKebabCase.ts +1 -0
  117. package/commandDecorators/command.ts +46 -102
  118. package/commandDecorators/commandBuilder.ts +30 -61
  119. package/commandDecorators/commandDecorators.test.ts +2 -10
  120. package/commandDecorators/dependencyBuilder.ts +10 -1
  121. package/commandDecorators/helpFormatter.ts +68 -145
  122. package/commandDecorators/index.ts +0 -2
  123. package/commandDecorators/targetMeta.ts +9 -7
  124. package/createTunnel.ts +7 -14
  125. package/csrE2e/csrE2eHarness.fixture.ts +506 -0
  126. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  127. package/csrE2e/csrPageStack.e2e.test.ts +265 -0
  128. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  129. package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
  130. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  131. package/csrE2e/registryVendor.e2e.test.ts +221 -0
  132. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  133. package/dependencyScanner.test.ts +4 -16
  134. package/dependencyScanner.ts +31 -80
  135. package/devkitUtils.test.ts +48 -20
  136. package/executors.linkFallback.test.ts +103 -0
  137. package/executors.test.ts +176 -106
  138. package/executors.ts +212 -498
  139. package/fileEditor.ts +9 -37
  140. package/{getDirname.test.ts → fileSys.test.ts} +5 -3
  141. package/fileSys.ts +22 -9
  142. package/formSetterScanner.ts +3 -16
  143. package/frontendBuild/allRoutesBuilder.ts +7 -21
  144. package/frontendBuild/autoImportSync.test.ts +27 -38
  145. package/frontendBuild/autoImportSync.ts +31 -65
  146. package/frontendBuild/buildRouteClient.test.ts +123 -37
  147. package/frontendBuild/bundleDefine.test.ts +12 -0
  148. package/frontendBuild/bundleDefine.ts +16 -0
  149. package/frontendBuild/clientBuildTypes.ts +5 -36
  150. package/frontendBuild/clientEntriesBundler.ts +9 -40
  151. package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
  152. package/frontendBuild/clientEntryDiscovery.ts +122 -110
  153. package/frontendBuild/csrArtifactBuilder.ts +9 -31
  154. package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
  155. package/frontendBuild/csrDevArtifactWriter.ts +307 -0
  156. package/frontendBuild/csrDevBundler.test.ts +66 -0
  157. package/frontendBuild/csrDevBundler.ts +95 -0
  158. package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
  159. package/frontendBuild/csrDevFactoryCheck.ts +39 -0
  160. package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
  161. package/frontendBuild/csrDevModuleCompiler.ts +422 -0
  162. package/frontendBuild/csrDevPatcher.test.ts +572 -0
  163. package/frontendBuild/csrDevPatcher.ts +548 -0
  164. package/frontendBuild/csrDevPaths.ts +145 -0
  165. package/frontendBuild/csrDevResolver.test.ts +239 -0
  166. package/frontendBuild/csrDevResolver.ts +363 -0
  167. package/frontendBuild/csrDevTypes.ts +61 -0
  168. package/frontendBuild/csrEntryFiles.ts +24 -0
  169. package/frontendBuild/cssCandidateCache.test.ts +0 -6
  170. package/frontendBuild/cssCandidateCache.ts +6 -41
  171. package/frontendBuild/cssCompiler.ts +31 -82
  172. package/frontendBuild/cssImportResolver.ts +6 -19
  173. package/frontendBuild/devChangePlanner.ts +7 -10
  174. package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
  175. package/frontendBuild/devGeneratedIndexSync.ts +1 -4
  176. package/frontendBuild/devRegistryBundler.ts +192 -0
  177. package/frontendBuild/fontOptimizer.test.ts +6 -14
  178. package/frontendBuild/fontOptimizer.ts +15 -72
  179. package/frontendBuild/fontPruner.test.ts +5 -10
  180. package/frontendBuild/fontPruner.ts +5 -20
  181. package/frontendBuild/frontendBuild.test.ts +57 -70
  182. package/frontendBuild/hmrChangeClassifier.ts +2 -2
  183. package/frontendBuild/hmrWatcher.test.ts +59 -32
  184. package/frontendBuild/hmrWatcher.ts +37 -74
  185. package/frontendBuild/index.ts +4 -0
  186. package/frontendBuild/pagesBundleBuilder.ts +28 -30
  187. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  188. package/frontendBuild/precompressArtifacts.ts +5 -18
  189. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  190. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  191. package/frontendBuild/routeClientBuilder.ts +100 -62
  192. package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
  193. package/frontendBuild/serverGraphFile.test.ts +77 -0
  194. package/frontendBuild/serverGraphFile.ts +93 -0
  195. package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
  196. package/frontendBuild/sourceMtimeIndex.ts +19 -130
  197. package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
  198. package/frontendBuild/ssrDevBundler.ts +85 -0
  199. package/frontendBuild/styleContract.ts +1 -8
  200. package/frontendBuild/themeValidator.ts +4 -26
  201. package/frontendBuild/watchRootResolver.test.ts +23 -42
  202. package/frontendBuild/watchRootResolver.ts +5 -27
  203. package/getRelatedCnsts.ts +12 -92
  204. package/incrementalBuilder/buildBatch.proc.ts +147 -63
  205. package/incrementalBuilder/buildBatchProtocol.ts +13 -38
  206. package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
  207. package/incrementalBuilder/buildBatchRunner.ts +26 -38
  208. package/incrementalBuilder/builderChannel.test.ts +24 -49
  209. package/incrementalBuilder/builderChannel.ts +6 -37
  210. package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
  211. package/incrementalBuilder/builderRequestRouter.ts +5 -24
  212. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  213. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  214. package/incrementalBuilder/devWatchBatch.test.ts +21 -24
  215. package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
  216. package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
  217. package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
  218. package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
  219. package/index.ts +5 -19
  220. package/integration/devResourceProbe.ts +50 -56
  221. package/integration/devStability.integration.test.ts +111 -202
  222. package/integration/devStabilityHarness.test.ts +13 -28
  223. package/integration/devStabilityHarness.ts +67 -330
  224. package/integration/ssrMemoryProbe.ts +41 -167
  225. package/libSource.test.ts +4 -15
  226. package/libSource.ts +19 -31
  227. package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
  228. package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
  229. package/lint/__fixtures__/no-static-in-object-light-model/bad.ts +43 -0
  230. package/lint/__fixtures__/no-static-in-object-light-model/fixture.json +1 -0
  231. package/lint/__fixtures__/no-static-in-object-light-model/good.ts +42 -0
  232. package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
  233. package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
  234. package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
  235. package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
  236. package/lint/gritRules.test.ts +8 -24
  237. package/lint/no-deep-internal-import.grit +2 -2
  238. package/lint/no-document-cookie.grit +29 -0
  239. package/lint/no-redeclare-predefined-endpoint.grit +2 -1
  240. package/lint/no-static-in-object-light-model.grit +27 -0
  241. package/lint/no-web-only-api-outside-webkit.grit +44 -0
  242. package/lint/no-web-storage.grit +30 -0
  243. package/linter.test.ts +1 -2
  244. package/linter.ts +26 -116
  245. package/mobile/desktopBin.test.ts +182 -0
  246. package/mobile/desktopBin.ts +144 -0
  247. package/mobile/desktopServerStage.test.ts +252 -0
  248. package/mobile/desktopServerStage.ts +178 -0
  249. package/mobile/index.ts +9 -0
  250. package/mobile/mobileTarget.test.ts +57 -8
  251. package/mobile/mobileTarget.ts +45 -26
  252. package/mobile/nativeApi.test.ts +30 -0
  253. package/mobile/nativeApi.ts +40 -0
  254. package/mobile/nativeApp.test.ts +184 -0
  255. package/mobile/nativeApp.ts +454 -0
  256. package/mobile/nativeAppLine.test.ts +69 -0
  257. package/mobile/nativeAppLine.ts +71 -0
  258. package/mobile/nativeConfig.test.ts +438 -0
  259. package/mobile/nativeConfig.ts +294 -0
  260. package/mobile/nativePluginFolders.test.ts +81 -0
  261. package/mobile/nativePluginFolders.ts +60 -0
  262. package/mobile/nativeWebDir.test.ts +80 -0
  263. package/mobile/nativeWebDir.ts +66 -0
  264. package/mobile/placeholderAppId.test.ts +13 -0
  265. package/mobile/placeholderAppId.ts +16 -0
  266. package/mobile/usageDescriptions.test.ts +27 -0
  267. package/mobile/usageDescriptions.ts +24 -0
  268. package/package.json +11 -11
  269. package/packageExports.test.ts +3 -18
  270. package/packageExportsMap.ts +3 -19
  271. package/piDependencyPins.test.ts +46 -0
  272. package/platformTest/BunTestReport.test.ts +60 -0
  273. package/platformTest/BunTestReport.ts +65 -0
  274. package/platformTest/IdleWatchedProcess.test.ts +56 -0
  275. package/platformTest/IdleWatchedProcess.ts +73 -0
  276. package/platformTest/LinuxDockerTarget.ts +105 -0
  277. package/platformTest/PlatformTestRun.test.ts +64 -0
  278. package/platformTest/PlatformTestRun.ts +141 -0
  279. package/platformTest/PlatformTestTarget.ts +142 -0
  280. package/platformTest/TestSnapshot.test.ts +72 -0
  281. package/platformTest/TestSnapshot.ts +103 -0
  282. package/platformTest/WindowsSshTarget.ts +151 -0
  283. package/prompter.ts +3 -13
  284. package/qualityScanner.test.ts +9 -26
  285. package/qualityScanner.ts +200 -286
  286. package/recipeScanner.test.ts +7 -12
  287. package/recipeScanner.ts +9 -38
  288. package/repoIdentity.ts +2 -9
  289. package/routeSourceValidator.test.ts +0 -1
  290. package/routeSourceValidator.ts +7 -42
  291. package/scanInfo.ts +91 -225
  292. package/selectModel.ts +1 -2
  293. package/semver.ts +3 -5
  294. package/slicePlanner.test.ts +6 -24
  295. package/slicePlanner.ts +11 -32
  296. package/spinner.test.ts +3 -8
  297. package/spinner.ts +4 -16
  298. package/sshTunnel.ts +1 -6
  299. package/ssrScanner.test.ts +1 -6
  300. package/ssrScanner.ts +103 -113
  301. package/stripAnsi.ts +3 -0
  302. package/subspace.test.ts +19 -26
  303. package/subspace.ts +48 -115
  304. package/subspaceConfig.ts +3 -11
  305. package/testHelpers.ts +194 -0
  306. package/transforms/asyncDefaultExportDetector.ts +3 -11
  307. package/transforms/barrelAnalyzer.ts +43 -135
  308. package/transforms/barrelImportsPlugin.ts +38 -175
  309. package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
  310. package/transforms/externalizeFrameworkPlugin.ts +13 -87
  311. package/transforms/moduleSyntax.ts +32 -0
  312. package/transforms/rscUseClientTransform.ts +42 -20
  313. package/transforms/serverEnvPlugin.test.ts +103 -0
  314. package/transforms/serverEnvPlugin.ts +41 -0
  315. package/transforms/transforms.test.ts +166 -44
  316. package/transforms/tsconfigPackageResolver.test.ts +6 -24
  317. package/transforms/useClientBundlePlugin.ts +6 -23
  318. package/typeChecker.ts +7 -63
  319. package/types.ts +2 -0
  320. package/uploadRelease.ts +10 -25
  321. package/workflow/artifacts.ts +18 -0
  322. package/workflow/executor.test.ts +1 -12
  323. package/workflow/executor.ts +101 -110
  324. package/workflow/index.ts +0 -1
  325. package/workflow/moduleIndex.test.ts +4 -15
  326. package/workflow/moduleIndex.ts +9 -49
  327. package/workflow/plan.ts +18 -25
  328. package/workflow/render.ts +80 -96
  329. package/workflow/rolloutGate.test.ts +1 -1
  330. package/workflow/source.ts +52 -69
  331. package/workflow/sourceAst.ts +50 -0
  332. package/workspaceLayout.test.ts +18 -3
  333. package/workspaceLayout.ts +22 -23
  334. package/abstractCompactor.ts +0 -112
  335. package/aiEditor.test.ts +0 -60
  336. package/aiEditor.ts +0 -387
  337. package/applicationReleasePackager.ts +0 -209
  338. package/capacitorApp.test.ts +0 -372
  339. package/capacitorApp.ts +0 -1648
  340. package/commandDecorators/commandMeta.ts +0 -7
  341. package/commandDecorators/types.ts +0 -10
  342. package/getDirname.ts +0 -4
  343. package/guideline.ts +0 -19
  344. package/local/optimize-resource/ipcprobe/child.ts +0 -1
  345. package/local/optimize-resource/ipcprobe/parent.ts +0 -11
  346. package/workflow/primitive.ts +0 -59
@@ -1,17 +1,11 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { DevResourceProbe, type DevResourceProc } from "./devResourceProbe";
4
5
 
5
6
  export type SsrProcRole = "gateway" | "replica" | "rsc" | "other";
6
7
 
7
- export interface SsrProc {
8
- pid: number;
9
- ppid: number;
10
- rssMb: number;
11
- cpuSec: number;
12
- role: SsrProcRole;
13
- command: string;
14
- }
8
+ export type SsrProc = DevResourceProc<SsrProcRole>;
15
9
 
16
10
  export interface SsrCgroupSample {
17
11
  currentMb: number;
@@ -35,12 +29,7 @@ export interface SsrCacheSample {
35
29
  heap: SsrHeapSample;
36
30
  }
37
31
 
38
- /**
39
- * RSS minus JS heap is what a module graph costs beyond its objects — compiled code, module
40
- * records, native allocator retention — so the split is what separates "the app retains objects"
41
- * from "the runtime retains code". `jscExtra` is JSC's off-heap attribution, which is where
42
- * typed-array backing stores (cached Flight chunks) land.
43
- */
32
+ // `jscExtra` is JSC's off-heap attribution, where typed-array backing stores (cached Flight chunks) land.
44
33
  export interface SsrHeapSample {
45
34
  replicaHeapUsedMb: number;
46
35
  replicaJscHeapMb: number;
@@ -52,63 +41,30 @@ export interface SsrHeapSample {
52
41
 
53
42
  export interface SsrMemoryProbeOptions {
54
43
  appName: string;
55
- /** Built app directory — `dist/apps/<app>` unless the build was relocated. */
56
44
  distDir: string;
57
45
  port: number;
58
46
  basePaths: string;
59
47
  repoName: string;
60
48
  serveDomain: string;
61
49
  scenarios: number[];
62
- /** Repeat count for scenario 1 (same route) and per-route passes in scenario 6. */
63
50
  repeats: number;
64
51
  concurrencies: number[];
65
52
  idleSeconds: number;
66
- /** Report cadence forced on the children; also bounds how long a sample waits for a fresh one. */
67
53
  metricsIntervalMs: number;
68
- /** Disables both result caches at boot — the second half of scenario 3. */
69
54
  cacheOff: boolean;
70
- /**
71
- * Forces `Bun.gc(true)` before every metrics sample. Required for the s7 ratchet test: without
72
- * it, growth across passes cannot be told apart from garbage that has not been collected yet.
73
- */
74
55
  gcOnReport: boolean;
75
- /** s7: how many times to walk the whole route list. */
76
56
  passes: number;
77
- /** s8: cumulative distinct-route counts to sample the per-route slope at. */
78
57
  sweep: number[];
79
58
  logPath: string;
80
59
  }
81
60
 
82
- /**
83
- * Production process-tree RSS and cache occupancy — probe §0.3 of
84
- * `local/reduce-ssr-ram/01-measurement-harness.md`, and the source of the phase 0 results table.
85
- *
86
- * bun pkgs/@akanjs/devkit/integration/ssrMemoryProbe.ts <app> [--scenarios=1,2,5] [--cache=off]
87
- *
88
- * The production tree is gateway → replica × N → rsc worker × N, with no builder; `DevResourceProbe`
89
- * measures the dev tree instead and the two are not interchangeable.
90
- *
91
- * Two things this exists to get right:
92
- *
93
- * - **Replica and worker RSS are reported separately.** They are separate processes and the worker
94
- * holds a private copy of the pages bundle, so one summed number hides which of the two grows.
95
- * - **On Linux it samples the cgroup's `anon` / `file` split, not just RSS.** Whether a cache is
96
- * anonymous heap (an OOM contributor) or page cache (evictable) is the entire question phase 2
97
- * asks; RSS cannot answer it, and neither can macOS, where the file half does not exist in the
98
- * same form. Read a macOS run as directional only.
99
- */
61
+ // Usage: bun pkgs/@akanjs/devkit/integration/ssrMemoryProbe.ts <app> [--scenarios=1,2,5] [--cache=off]
62
+ // The cgroup `anon`/`file` split exists only on Linux; read a macOS run as directional.
100
63
  export class SsrMemoryProbe {
101
64
  static readonly #columns: SsrProcRole[] = ["gateway", "replica", "rsc"];
102
65
 
103
66
  static parseArgs(argv: string[]): SsrMemoryProbeOptions {
104
- const args = new Map(
105
- argv.slice(1).map((arg) => {
106
- const [key, value] = arg.replace(/^--/, "").split("=");
107
- return [key ?? "", value ?? "1"];
108
- }),
109
- );
110
- const appName = argv[0] ?? "akan";
111
- const num = (key: string, fallback: number) => Number(args.get(key) ?? fallback);
67
+ const { appName, args, num } = DevResourceProbe.parseArgMap(argv);
112
68
  const list = (key: string, fallback: number[]) =>
113
69
  args.has(key)
114
70
  ? (args.get(key) ?? "")
@@ -155,10 +111,7 @@ export class SsrMemoryProbe {
155
111
  NODE_ENV: "production",
156
112
  USE_AKANJS_PKGS: "true",
157
113
  AKAN_PUBLIC_APP_NAME: appName,
158
- // `getEnv()` requires these one at a time, and a missing one fails the *replica* while the
159
- // gateway stays up: the process tree still looks like a healthy three, and only the child's
160
- // `ready` flag and the 503s give it away. Both are why `#waitForReady` checks child
161
- // readiness rather than the gateway's own 200.
114
+ // A missing one fails the replica while the gateway stays up and keeps answering.
162
115
  AKAN_PUBLIC_REPO_NAME: process.env.AKAN_PUBLIC_REPO_NAME ?? repoName,
163
116
  AKAN_PUBLIC_SERVE_DOMAIN: process.env.AKAN_PUBLIC_SERVE_DOMAIN ?? serveDomain,
164
117
  AKAN_PUBLIC_ENV: "local",
@@ -181,7 +134,7 @@ export class SsrMemoryProbe {
181
134
  try {
182
135
  await this.#measure();
183
136
  } finally {
184
- await this.#cleanup(gateway);
137
+ await DevResourceProbe.killTree(gateway, () => this.#sampleTree());
185
138
  }
186
139
  }
187
140
 
@@ -221,11 +174,7 @@ export class SsrMemoryProbe {
221
174
  for (const concurrency of concurrencies) await this.#run(`s6 concurrency=${concurrency}`, routes, concurrency);
222
175
  }
223
176
 
224
- // s7 — ratchet vs working set. Every pass renders the same routes, so pass 1 pays for module
225
- // evaluation and later passes pay for nothing new. Flat later passes mean the memory is a
226
- // working set and only a smaller bundle or fewer replicas reduces it; continued growth means
227
- // per-render retention, which is a bug worth more than any ceiling. Run with `--cache=off --gc`
228
- // or it measures cache hits and uncollected garbage instead.
177
+ // s7: flat passes after the first mean a working set; continued growth means per-render retention.
229
178
  if (scenarios.includes(7)) {
230
179
  if (!this.#options.cacheOff)
231
180
  console.info("[probe] WARNING s7 without --cache=off measures cache hits from pass 2 on");
@@ -235,8 +184,7 @@ export class SsrMemoryProbe {
235
184
  await this.#run(`s7 pass ${pass}/${this.#options.passes}`, routes, 1);
236
185
  }
237
186
 
238
- // s8 — per-route slope vs fixed intercept. Cumulative: each step adds routes the process has
239
- // not seen, so the RSS series against distinct-route count separates the two.
187
+ // s8: cumulative, so RSS against distinct-route count separates the per-route slope from the intercept.
240
188
  if (scenarios.includes(8)) {
241
189
  for (const count of this.#options.sweep) {
242
190
  const subset = routes.slice(0, Math.min(count, routes.length));
@@ -255,11 +203,7 @@ export class SsrMemoryProbe {
255
203
  }
256
204
  }
257
205
 
258
- /**
259
- * A scenario is only a measurement if its requests succeeded. A gateway whose replica died still
260
- * answers — with 503s, instantly — and the resulting RSS row looks like a legitimately cheap one.
261
- * Fail loudly rather than publish that number.
262
- */
206
+ // A dead replica still answers instant 503s, which would read as a legitimately cheap row.
263
207
  async #run(label: string, urls: string[], concurrency: number): Promise<void> {
264
208
  const started = Date.now();
265
209
  const { ok, failed, statuses } = await this.#browse(urls, concurrency);
@@ -307,11 +251,7 @@ export class SsrMemoryProbe {
307
251
  return `${(bytes / 1024 / 1024).toFixed(1)}MiB`;
308
252
  }
309
253
 
310
- /**
311
- * Reads the container's own accounting. `memory.current` counts page cache, so a disk-backed
312
- * cache still shows up here — but under `file`, which the kernel reclaims under pressure instead
313
- * of OOM-killing. That split is what phase 2 has to move. Returns null off cgroup v2.
314
- */
254
+ // cgroup v2 only. `file` is page cache the kernel reclaims under pressure; `anon` is what gets OOM-killed.
315
255
  static readCgroupSample(): SsrCgroupSample | null {
316
256
  try {
317
257
  const current = Number.parseInt(fs.readFileSync("/sys/fs/cgroup/memory.current", "utf8").trim(), 10);
@@ -328,22 +268,14 @@ export class SsrMemoryProbe {
328
268
  }
329
269
  }
330
270
 
331
- /**
332
- * Waits for a metrics report sampled *after* `since` before reading the cache columns.
333
- *
334
- * The counters ride a periodic IPC report, so reading immediately after a browse returns the
335
- * state from before it — and the two-hop path (worker → replica → gateway) means the `rsc*` and
336
- * `rscWorker*` fields can be a further interval behind. Without this wait every row silently
337
- * attributes one scenario's cache growth to the previous scenario.
338
- */
271
+ // Counters ride a periodic two-hop IPC report (worker → replica → gateway), so wait until both hops have
272
+ // re-reported after `since`, or one scenario's growth lands in the next row.
339
273
  async #sampleCaches(since: number): Promise<SsrCacheSample | null> {
340
274
  const deadline = Date.now() + this.#options.metricsIntervalMs * 3 + 5_000;
341
275
  let children: Array<Record<string, number>> = [];
342
276
  let fresh = false;
343
277
  while (Date.now() < deadline) {
344
278
  children = await this.#fetchChildMetrics();
345
- // Both hops must have re-reported: `reportedAt` is the replica's own sample time, and
346
- // `rscWorkerReportedAt` the worker's as of the replica's last read of it.
347
279
  fresh =
348
280
  children.length > 0 &&
349
281
  children.every(
@@ -354,32 +286,30 @@ export class SsrMemoryProbe {
354
286
  }
355
287
  if (children.length === 0) return null;
356
288
  if (!fresh) console.info(`${"".padEnd(24)} WARNING cache columns are stale — no fresh report within the wait`);
357
- {
358
- const sum = (key: string) => children.reduce((total, metrics) => total + Number(metrics[key] ?? 0), 0);
359
- const mb = (key: string) => sum(key) / 1024 / 1024;
360
- return {
361
- replicaRssMb: sum("rssBytes") / 1024 / 1024,
362
- rscWorkerRssMb: sum("rscWorkerRssBytes") / 1024 / 1024,
363
- htmlEntries: sum("httpHtmlCacheEntries"),
364
- htmlBytes: sum("httpHtmlCacheBytes"),
365
- rscEntries: sum("rscResultCacheEntries"),
366
- rscBytes: sum("rscResultCacheBytes"),
367
- patchEntries: sum("rscPatchResultCacheEntries"),
368
- patchBytes: sum("rscPatchResultCacheBytes"),
369
- ssrChunkKeys: sum("ssrChunkRegistrySize"),
370
- loadedRouteModules: sum("rscLoadedRouteModuleCount"),
371
- fullSsr: sum("httpFullSsrCount"),
372
- rscNavigation: sum("httpRscNavigationCount"),
373
- heap: {
374
- replicaHeapUsedMb: mb("heapUsedBytes"),
375
- replicaJscHeapMb: mb("jscHeapSizeBytes"),
376
- replicaJscExtraMb: mb("jscExtraMemorySizeBytes"),
377
- workerHeapUsedMb: mb("rscWorkerHeapUsedBytes"),
378
- workerJscHeapMb: mb("rscWorkerJscHeapSizeBytes"),
379
- workerJscExtraMb: mb("rscWorkerJscExtraMemorySizeBytes"),
380
- },
381
- };
382
- }
289
+ const sum = (key: string) => children.reduce((total, metrics) => total + Number(metrics[key] ?? 0), 0);
290
+ const mb = (key: string) => sum(key) / 1024 / 1024;
291
+ return {
292
+ replicaRssMb: mb("rssBytes"),
293
+ rscWorkerRssMb: mb("rscWorkerRssBytes"),
294
+ htmlEntries: sum("httpHtmlCacheEntries"),
295
+ htmlBytes: sum("httpHtmlCacheBytes"),
296
+ rscEntries: sum("rscResultCacheEntries"),
297
+ rscBytes: sum("rscResultCacheBytes"),
298
+ patchEntries: sum("rscPatchResultCacheEntries"),
299
+ patchBytes: sum("rscPatchResultCacheBytes"),
300
+ ssrChunkKeys: sum("ssrChunkRegistrySize"),
301
+ loadedRouteModules: sum("rscLoadedRouteModuleCount"),
302
+ fullSsr: sum("httpFullSsrCount"),
303
+ rscNavigation: sum("httpRscNavigationCount"),
304
+ heap: {
305
+ replicaHeapUsedMb: mb("heapUsedBytes"),
306
+ replicaJscHeapMb: mb("jscHeapSizeBytes"),
307
+ replicaJscExtraMb: mb("jscExtraMemorySizeBytes"),
308
+ workerHeapUsedMb: mb("rscWorkerHeapUsedBytes"),
309
+ workerJscHeapMb: mb("rscWorkerJscHeapSizeBytes"),
310
+ workerJscExtraMb: mb("rscWorkerJscExtraMemorySizeBytes"),
311
+ },
312
+ };
383
313
  }
384
314
 
385
315
  async #fetchChildMetrics(): Promise<Array<Record<string, number>>> {
@@ -402,49 +332,10 @@ export class SsrMemoryProbe {
402
332
  return "other";
403
333
  }
404
334
 
405
- /** One `ps`, then walk from the gateway pid to a fixpoint. */
406
- async #sampleTree(): Promise<SsrProc[]> {
407
- const proc = Bun.spawn(["ps", "-eo", "pid=,ppid=,rss=,time=,command="], { stdout: "pipe" });
408
- const text = await new Response(proc.stdout).text();
409
- await proc.exited;
410
- const all = text
411
- .split("\n")
412
- .map((line) => line.trim())
413
- .filter(Boolean)
414
- .map((line) => {
415
- const match = /^(\d+)\s+(\d+)\s+(\d+)\s+([\d:.]+)\s+(.*)$/.exec(line);
416
- if (!match) return null;
417
- const [, pid, ppid, rss, time, command] = match;
418
- const parts = (time ?? "0:0").split(":");
419
- const cpuSec =
420
- parts.length === 3
421
- ? Number(parts[0]) * 3600 + Number(parts[1]) * 60 + Number(parts[2])
422
- : Number(parts[0] ?? 0) * 60 + Number(parts[1] ?? 0);
423
- return {
424
- pid: Number(pid),
425
- ppid: Number(ppid),
426
- rssMb: Number(rss) / 1024,
427
- cpuSec,
428
- role: this.#roleOf(command ?? ""),
429
- command: command ?? "",
430
- } satisfies SsrProc;
431
- })
432
- .filter((proc): proc is SsrProc => proc !== null);
433
- const kept = new Map<number, SsrProc>();
434
- const root = all.find((proc) => proc.pid === this.#gatewayPid);
435
- if (root) kept.set(this.#gatewayPid, root);
436
- for (let pass = 0; pass < 12; pass++) {
437
- const before = kept.size;
438
- for (const proc of all) if (kept.has(proc.ppid) && !kept.has(proc.pid)) kept.set(proc.pid, proc);
439
- if (kept.size === before) break;
440
- }
441
- return [...kept.values()].filter((proc) => proc.role !== "other" || proc.pid === this.#gatewayPid);
335
+ #sampleTree(): Promise<SsrProc[]> {
336
+ return DevResourceProbe.sampleTree(this.#gatewayPid, (command) => this.#roleOf(command));
442
337
  }
443
338
 
444
- /**
445
- * Concrete urls from the built route seed index. `:lang` takes the default locale; any route with
446
- * another `:param` is skipped because it needs a real id to render.
447
- */
448
339
  async #staticRoutes(): Promise<string[]> {
449
340
  const artifactDir = path.join(this.#options.distDir, ".akan", "artifact");
450
341
  const seed = (await Bun.file(path.join(artifactDir, "route-seed-index.json")).json()) as {
@@ -495,11 +386,7 @@ export class SsrMemoryProbe {
495
386
  return { ok, failed, statuses };
496
387
  }
497
388
 
498
- /**
499
- * Waits for a **child** to report ready, not for the gateway to answer. The gateway binds and
500
- * serves `/health` happily while every replica is in a crash-restart loop, so a 200 here proves
501
- * nothing about whether anything can render.
502
- */
389
+ // The gateway answers `/health` while every replica crash-loops, so wait for a ready child instead.
503
390
  async #waitForReady(timeoutMs: number): Promise<boolean> {
504
391
  const deadline = Date.now() + timeoutMs;
505
392
  let lastError = "";
@@ -524,19 +411,6 @@ export class SsrMemoryProbe {
524
411
  if (lastError) console.info(`[probe] no child became ready; last child error: ${lastError}`);
525
412
  return false;
526
413
  }
527
-
528
- async #cleanup(gateway: { kill: (signal: NodeJS.Signals) => void }): Promise<void> {
529
- for (const proc of (await this.#sampleTree()).reverse()) {
530
- try {
531
- process.kill(proc.pid, "SIGKILL");
532
- } catch {}
533
- }
534
- try {
535
- gateway.kill("SIGKILL");
536
- } catch {}
537
- await Bun.sleep(1_000);
538
- console.info(`[probe] survivors after kill: ${(await this.#sampleTree()).length}`);
539
- }
540
414
  }
541
415
 
542
416
  if (import.meta.main) await new SsrMemoryProbe(SsrMemoryProbe.parseArgs(process.argv.slice(2))).run();
package/libSource.test.ts CHANGED
@@ -1,25 +1,14 @@
1
- import { afterEach, describe, expect, test } from "bun:test";
2
- import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
3
- import os from "node:os";
1
+ import { describe, expect, test } from "bun:test";
4
2
  import path from "node:path";
5
3
  import { Executor, LibExecutor, WorkspaceExecutor } from "./executors";
6
4
  import { formatLibStatuses, LibSource } from "./libSource";
5
+ import { tempDirs, writeText as write } from "./testHelpers";
7
6
 
8
- const tempRoots: string[] = [];
9
-
10
- afterEach(async () => {
11
- await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
12
- });
13
-
14
- const write = async (filePath: string, content: string) => {
15
- await mkdir(path.dirname(filePath), { recursive: true });
16
- await writeFile(filePath, content);
17
- };
7
+ const makeTempRoot = tempDirs("akan-libsource-");
18
8
 
19
9
  // `LibExecutor.from` memoises by name, so each fixture needs a name no other test has used.
20
10
  const makeLib = async (libName: string) => {
21
- const root = await mkdtemp(path.join(os.tmpdir(), "akan-libsource-"));
22
- tempRoots.push(root);
11
+ const root = await makeTempRoot();
23
12
  await write(
24
13
  path.join(root, "package.json"),
25
14
  '{ "name": "workspace", "version": "0.0.1", "description": "workspace" }\n',
package/libSource.ts CHANGED
@@ -20,20 +20,10 @@ export interface LibStatus {
20
20
  hash: string;
21
21
  }
22
22
 
23
- /**
24
- * A library's origin, recorded in its own `package.json` under an `akan.source` key.
25
- *
26
- * The key rides `package.json` rather than a file of its own because every akan write to a library's
27
- * manifest is a spread of the existing object (`LibExecutor.syncPackageJson`), so an unknown top-level
28
- * key survives — while a new root file would have to be added to `libRootAllowedFiles` before
29
- * `akan sync` stopped rejecting it.
30
- */
23
+ // In package.json, not a file of its own: every akan manifest write spreads the existing object, so the key survives.
31
24
  export class LibSource {
32
25
  static readonly manifestKey = "akan";
33
- /**
34
- * Left out of the hash. `env/` holds per-deployment values that belong to the workspace the library
35
- * was installed into, not to the origin, so an env edit is not drift.
36
- */
26
+ // `env/` holds the installing workspace's per-deployment values, so an env edit is not drift.
37
27
  static readonly unhashedDirs = ["env"];
38
28
 
39
29
  #lib: LibExecutor;
@@ -50,33 +40,35 @@ export class LibSource {
50
40
  return !LibSource.unhashedDirs.includes(relative.split("/")[0] ?? "");
51
41
  }
52
42
 
53
- /** `package.json` cannot hash the stamp it carries, so the key comes off before hashing. */
54
- async #hashableContent(file: string) {
55
- const content = await this.#lib.workspace.readFile(file);
56
- if (file !== `${this.#prefix}package.json`) return content;
57
- const manifest = JSON.parse(content) as PackageJson;
43
+ static unstamped(manifestContent: string) {
44
+ const manifest = JSON.parse(manifestContent) as PackageJson;
58
45
  delete manifest[LibSource.manifestKey];
59
46
  return JSON.stringify(manifest);
60
47
  }
61
48
 
62
- /**
63
- * Hash over the library's own files. Untracked files count: a freshly copied library is not committed
64
- * yet, and its hash has to be the same one a later `status` recomputes.
65
- */
66
- async computeHash() {
67
- const files = (await this.#lib.workspace.listGitFiles([`libs/${this.#lib.name}`], { untracked: true })).filter(
68
- (file) => this.#isHashed(file),
69
- );
49
+ //* `package.json` cannot hash the stamp it carries, so the key comes off before hashing.
50
+ static async hashFiles(files: string[], manifestFile: string, read: (file: string) => Promise<string>) {
70
51
  const hasher = new Bun.CryptoHasher("sha256");
71
52
  for (const file of files) {
53
+ const content = await read(file);
72
54
  hasher.update(file);
73
55
  hasher.update("\0");
74
- hasher.update(await this.#hashableContent(file));
56
+ hasher.update(file === manifestFile ? LibSource.unstamped(content) : content);
75
57
  hasher.update("\0");
76
58
  }
77
59
  return hasher.digest("hex").slice(0, 32);
78
60
  }
79
61
 
62
+ // Untracked files count: a freshly copied library is not committed yet.
63
+ async computeHash() {
64
+ const files = (await this.#lib.workspace.listGitFiles([`libs/${this.#lib.name}`], { untracked: true })).filter(
65
+ (file) => this.#isHashed(file),
66
+ );
67
+ return await LibSource.hashFiles(files, `${this.#prefix}package.json`, (file) =>
68
+ this.#lib.workspace.readFile(file),
69
+ );
70
+ }
71
+
80
72
  async read(): Promise<LibSourceStamp | null> {
81
73
  const manifest = await this.#lib.getPackageJson();
82
74
  const akan = manifest[LibSource.manifestKey] as { source?: LibSourceStamp } | undefined;
@@ -91,11 +83,7 @@ export class LibSource {
91
83
  return stamp;
92
84
  }
93
85
 
94
- /**
95
- * Writes the stamp only when it would change. `syncedAt` moves on every write, so an unconditional
96
- * write leaves the manifest dirty and defeats an idempotent caller — the hash is computed with the
97
- * stamp removed, so comparing it first is sound.
98
- */
86
+ // `syncedAt` moves on every write, so an unchanged stamp is left alone (the hash excludes the stamp itself).
99
87
  async syncStamp({ origin, sha }: Pick<LibSourceStamp, "origin" | "sha">) {
100
88
  const [current, hash] = await Promise.all([this.read(), this.computeHash()]);
101
89
  if (current?.origin === origin && current.sha === sha && current.hash === hash)
@@ -0,0 +1,4 @@
1
+ export const readTheme = () => document.cookie; // @flag
2
+ export const writeTheme = (value: string) => { document.cookie = `theme=${value}`; }; // @flag
3
+ export const throughWindow = () => window.document.cookie; // @flag
4
+ export const throughGlobalThis = () => globalThis.document.cookie; // @flag
@@ -0,0 +1,7 @@
1
+ import { getCookie, setCookie } from "akanjs/client";
2
+ export const readTheme = () => getCookie("theme"); // @ok
3
+ export const writeTheme = (value: string) => setCookie("theme", value); // @ok
4
+ export const otherDocumentFields = () => document.title; // @ok
5
+ export const aFieldNamedCookie = (req: { cookie: string }) => req.cookie; // @ok
6
+ export const aLiteral = "document.cookie is empty on app://"; // @ok
7
+ export const Docs = () => <p>getCookie reads the request, document.cookie the browser.</p>; // @ok
@@ -0,0 +1,43 @@
1
+ import { enumOf, ID, Int } from "akanjs/base";
2
+ import { via } from "akanjs/constant";
3
+
4
+ export class QuizInput extends via((field) => ({
5
+ userId: field(ID, { ref: "user", immutable: true }),
6
+ })) {}
7
+
8
+ export class QuizObject extends via(QuizInput, (field) => ({
9
+ correctNum: field(Int, { default: 0, min: 0 }),
10
+ })) {
11
+ static maxNum = 4; // @flag
12
+ static readonly minNum = 1; // @flag
13
+ private static secretNum = 2; // @flag
14
+ static { // @flag
15
+ QuizObject.maxNum = 5;
16
+ }
17
+ }
18
+
19
+ export class LightQuiz extends via(
20
+ QuizObject,
21
+ ["userId", "correctNum"] as const,
22
+ (resolve) => ({}),
23
+ ) {
24
+ static choiceNum = 4; // @flag
25
+ static xpOfCorrect(comboNum: number) { // @flag
26
+ return comboNum * 10;
27
+ }
28
+ static async loadAll() { // @flag
29
+ return [];
30
+ }
31
+ static get defaultNum() { // @flag
32
+ return 1;
33
+ }
34
+ isPerfect() {
35
+ return this.correctNum === 4;
36
+ }
37
+ }
38
+
39
+ export class LightRenamed extends via(QuizObject, ["userId"] as const, (resolve) => ({})) {
40
+ static sizeNum = 1; // @flag
41
+ }
42
+
43
+ export class Quiz extends via(QuizObject, LightQuiz, (resolve) => ({})) {}
@@ -0,0 +1 @@
1
+ { "path": "lib/quiz/quiz.constant.ts" }
@@ -0,0 +1,42 @@
1
+ import { ID, Int } from "akanjs/base";
2
+ import { via } from "akanjs/constant";
3
+
4
+ export class QuizInput extends via((field) => ({
5
+ userId: field(ID, { ref: "user", immutable: true }),
6
+ })) {
7
+ static minSize = 4; // @ok
8
+ }
9
+
10
+ export class QuizObject extends via(QuizInput, (field) => ({
11
+ correctNum: field(Int, { default: 0, min: 0 }),
12
+ })) {}
13
+
14
+ export class LightQuiz extends via(QuizObject, ["userId", "correctNum"] as const, (resolve) => ({})) {
15
+ isPerfect() { // @ok
16
+ return this.correctNum === 4;
17
+ }
18
+ staticNum = 1; // @ok
19
+ }
20
+
21
+ export class Quiz extends via(QuizObject, LightQuiz, (resolve) => ({})) {
22
+ static choiceNum = 4; // @ok
23
+ static xpOfCorrect(comboNum: number) { // @ok
24
+ return comboNum * 10;
25
+ }
26
+ }
27
+
28
+ export class QuizInsight extends via(Quiz, (field) => ({})) {
29
+ static emptyNum = 0; // @ok
30
+ }
31
+
32
+ export class Coordinate extends via((field) => ({
33
+ lat: field(Int),
34
+ })) {
35
+ static getDistanceKm(a: Coordinate, b: Coordinate) { // @ok
36
+ return a.lat - b.lat;
37
+ }
38
+ }
39
+
40
+ export class ObjectRegistry {
41
+ static instance = new ObjectRegistry(); // @ok
42
+ }
@@ -0,0 +1,7 @@
1
+ export const share = (url: string) => navigator.share({ url }); // @flag
2
+ export const canShare = () => typeof navigator.canShare === "function"; // @flag
3
+ export const worker = () => navigator.serviceWorker.register("/sw.js"); // @flag
4
+ export const ask = () => Notification.requestPermission(); // @flag
5
+ export const permission = () => Notification.permission; // @flag
6
+ export const locate = () => navigator.geolocation.getCurrentPosition(() => undefined); // @flag
7
+ export const buzz = () => navigator.vibrate(50); // @flag
@@ -0,0 +1,11 @@
1
+ import { haptics } from "akanjs/client/native";
2
+ import { useGeoLocation } from "akanjs/webkit";
3
+ export const buzz = () => haptics.impact(); // @ok
4
+ export const locate = () => useGeoLocation(); // @ok
5
+ export const languages = () => navigator.languages; // @ok
6
+ export const clipboard = (text: string) => navigator.clipboard.writeText(text); // @ok
7
+ export const aField = (payload: { share: boolean }) => payload.share; // @ok
8
+ export const aLiteral = "navigator.share is missing on Android"; // @ok
9
+ export const Docs = () => <p>navigator.share is missing on Android.</p>; // @ok
10
+ export const ModelZone = () => <Notification.Zone.Initialize />; // @ok
11
+ export const model = (cnst: { Notification: unknown }) => cnst.Notification; // @ok
@@ -0,0 +1,6 @@
1
+ export const readDraft = () => localStorage.getItem("draft"); // @flag
2
+ export const writeDraft = (value: string) => localStorage.setItem("draft", value); // @flag
3
+ export const readTab = () => sessionStorage.getItem("tab"); // @flag
4
+ export const throughWindow = () => window.localStorage.getItem("draft"); // @flag
5
+ export const throughGlobalThis = () => globalThis.sessionStorage.clear(); // @flag
6
+ export const guarded = () => (typeof localStorage === "undefined" ? null : 1); // @flag
@@ -0,0 +1,7 @@
1
+ import { secretStorage, storage } from "akanjs/client";
2
+ export const readDraft = () => storage.getItem("draft"); // @ok
3
+ export const keepToken = (jwt: string) => secretStorage.setItem("jwt", jwt); // @ok
4
+ export const aFieldWithTheName = (cache: { localStorage: string }) => cache.localStorage; // @ok
5
+ export const anObjectKey = { sessionStorage: false }; // @ok
6
+ export const aLiteral = "localStorage works in the shells"; // @ok
7
+ export const Docs = () => <p>window.localStorage and sessionStorage work in the shells.</p>; // @ok