alepha 0.20.1 → 0.20.2

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 (232) hide show
  1. package/dist/api/files/index.js +2 -1
  2. package/dist/api/files/index.js.map +1 -1
  3. package/dist/api/jobs/index.browser.js +64 -148
  4. package/dist/api/jobs/index.browser.js.map +1 -1
  5. package/dist/api/jobs/index.d.ts +371 -573
  6. package/dist/api/jobs/index.d.ts.map +1 -1
  7. package/dist/api/jobs/index.js +605 -1012
  8. package/dist/api/jobs/index.js.map +1 -1
  9. package/dist/api/notifications/index.d.ts +78 -17
  10. package/dist/api/notifications/index.d.ts.map +1 -1
  11. package/dist/api/notifications/index.js +90 -23
  12. package/dist/api/notifications/index.js.map +1 -1
  13. package/dist/api/payments/index.d.ts +2 -1
  14. package/dist/api/payments/index.d.ts.map +1 -1
  15. package/dist/api/payments/index.js +4 -2
  16. package/dist/api/payments/index.js.map +1 -1
  17. package/dist/api/users/index.d.ts +34 -31
  18. package/dist/api/users/index.d.ts.map +1 -1
  19. package/dist/api/users/index.js +13 -7
  20. package/dist/api/users/index.js.map +1 -1
  21. package/dist/api/verifications/index.js +2 -1
  22. package/dist/api/verifications/index.js.map +1 -1
  23. package/dist/cli/core/index.d.ts +8 -34
  24. package/dist/cli/core/index.d.ts.map +1 -1
  25. package/dist/cli/core/index.js +43 -232
  26. package/dist/cli/core/index.js.map +1 -1
  27. package/dist/cli/platform/index.d.ts +36 -11
  28. package/dist/cli/platform/index.d.ts.map +1 -1
  29. package/dist/cli/platform/index.js +93 -27
  30. package/dist/cli/platform/index.js.map +1 -1
  31. package/dist/command/index.d.ts +1 -1
  32. package/dist/core/index.browser.js +6 -0
  33. package/dist/core/index.browser.js.map +1 -1
  34. package/dist/core/index.d.ts +6 -0
  35. package/dist/core/index.d.ts.map +1 -1
  36. package/dist/core/index.js +6 -0
  37. package/dist/core/index.js.map +1 -1
  38. package/dist/core/index.native.js +6 -0
  39. package/dist/core/index.native.js.map +1 -1
  40. package/dist/core/index.workerd.js +6 -0
  41. package/dist/core/index.workerd.js.map +1 -1
  42. package/dist/react/form/index.d.ts +60 -1
  43. package/dist/react/form/index.d.ts.map +1 -1
  44. package/dist/react/form/index.js +86 -1
  45. package/dist/react/form/index.js.map +1 -1
  46. package/dist/react/head/index.browser.js +16 -1
  47. package/dist/react/head/index.browser.js.map +1 -1
  48. package/dist/react/head/index.d.ts +6 -0
  49. package/dist/react/head/index.d.ts.map +1 -1
  50. package/dist/react/head/index.js +16 -1
  51. package/dist/react/head/index.js.map +1 -1
  52. package/dist/react/router/index.browser.js +0 -10
  53. package/dist/react/router/index.browser.js.map +1 -1
  54. package/dist/react/router/index.d.ts +35 -12
  55. package/dist/react/router/index.d.ts.map +1 -1
  56. package/dist/react/router/index.js +0 -10
  57. package/dist/react/router/index.js.map +1 -1
  58. package/dist/react/ui/index.d.ts +124 -0
  59. package/dist/react/ui/index.d.ts.map +1 -0
  60. package/dist/react/ui/index.js +206 -0
  61. package/dist/react/ui/index.js.map +1 -0
  62. package/dist/router/index.d.ts +13 -13
  63. package/dist/router/index.d.ts.map +1 -1
  64. package/dist/router/index.js +45 -32
  65. package/dist/router/index.js.map +1 -1
  66. package/dist/system/index.d.ts.map +1 -1
  67. package/dist/system/index.js +1 -0
  68. package/dist/system/index.js.map +1 -1
  69. package/dist/topic/core/index.js +1 -1
  70. package/dist/topic/core/index.js.map +1 -1
  71. package/package.json +6 -23
  72. package/src/api/files/jobs/FileJobs.ts +2 -1
  73. package/src/api/jobs/__tests__/$job.spec.ts +316 -2867
  74. package/src/api/jobs/controllers/AdminJobController.ts +29 -138
  75. package/src/api/jobs/entities/jobExecutionEntity.ts +27 -19
  76. package/src/api/jobs/index.browser.ts +5 -7
  77. package/src/api/jobs/index.ts +23 -51
  78. package/src/api/jobs/primitives/$job.ts +66 -58
  79. package/src/api/jobs/providers/JobProvider.ts +561 -566
  80. package/src/api/jobs/providers/JobQueueProvider.ts +18 -19
  81. package/src/api/jobs/schemas/jobConfigAtom.ts +20 -23
  82. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +3 -27
  83. package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +5 -7
  84. package/src/api/jobs/schemas/jobRegistrationSchema.ts +7 -4
  85. package/src/api/jobs/schemas/triggerJobSchema.ts +0 -1
  86. package/src/api/jobs/services/JobService.ts +90 -483
  87. package/src/api/notifications/controllers/AdminNotificationController.ts +19 -12
  88. package/src/api/notifications/index.ts +7 -4
  89. package/src/api/notifications/jobs/NotificationJobs.ts +83 -12
  90. package/src/api/payments/services/PaymentService.ts +4 -2
  91. package/src/api/users/__tests__/UserJobs.spec.ts +10 -49
  92. package/src/api/users/audits/UserAudits.ts +3 -1
  93. package/src/api/users/buckets/UserBuckets.ts +2 -1
  94. package/src/api/users/index.ts +1 -4
  95. package/src/api/users/jobs/UserJobs.ts +5 -4
  96. package/src/api/verifications/jobs/VerificationJobs.ts +2 -1
  97. package/src/cli/core/__tests__/init.spec.ts +1 -1
  98. package/src/cli/core/commands/init.ts +0 -12
  99. package/src/cli/core/services/PackageManagerUtils.ts +2 -9
  100. package/src/cli/core/services/ProjectScaffolder.ts +17 -65
  101. package/src/cli/core/templates/agentMd.ts +2 -8
  102. package/src/cli/core/templates/apiIndexTs.ts +4 -18
  103. package/src/cli/core/templates/mainCss.ts +1 -36
  104. package/src/cli/core/templates/vitestConfigTs.ts +17 -0
  105. package/src/cli/core/templates/webAppRouterTs.ts +2 -85
  106. package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +22 -71
  107. package/src/cli/platform/adapters/CloudflareAdapter.ts +12 -11
  108. package/src/cli/platform/atoms/platformOptions.ts +9 -0
  109. package/src/cli/platform/schemas/cloudflare.ts +3 -2
  110. package/src/cli/platform/services/CloudflareApi.ts +164 -25
  111. package/src/cli/platform/services/WranglerApi.ts +0 -17
  112. package/src/core/Alepha.ts +9 -0
  113. package/src/react/form/index.ts +2 -0
  114. package/src/react/form/services/parseField.ts +163 -0
  115. package/src/react/form/services/prettyName.ts +19 -0
  116. package/src/react/head/providers/BrowserHeadProvider.ts +31 -10
  117. package/src/react/router/primitives/$page.ts +35 -12
  118. package/src/react/ui/atoms/uiAtom.ts +28 -0
  119. package/src/react/ui/components/ColorScheme.tsx +36 -0
  120. package/src/react/ui/hooks/useColorMode.ts +49 -0
  121. package/src/react/ui/hooks/useSidebarState.ts +26 -0
  122. package/src/react/ui/hooks/useTheme.ts +22 -0
  123. package/src/react/ui/index.ts +35 -0
  124. package/src/react/ui/services/UiPersistence.ts +41 -0
  125. package/src/router/TemplatedPathParser.ts +50 -51
  126. package/src/router/__tests__/RouterProvider.spec.ts +62 -0
  127. package/src/router/__tests__/TemplatedPathParser.spec.ts +18 -0
  128. package/src/router/providers/RouterProvider.ts +10 -5
  129. package/src/system/providers/NodeShellProvider.ts +1 -0
  130. package/src/topic/core/providers/TopicProvider.ts +1 -1
  131. package/dist/api/invitations/index.d.ts +0 -790
  132. package/dist/api/invitations/index.d.ts.map +0 -1
  133. package/dist/api/invitations/index.js +0 -662
  134. package/dist/api/invitations/index.js.map +0 -1
  135. package/dist/api/issues/index.d.ts +0 -810
  136. package/dist/api/issues/index.d.ts.map +0 -1
  137. package/dist/api/issues/index.js +0 -444
  138. package/dist/api/issues/index.js.map +0 -1
  139. package/dist/api/subscriptions/index.d.ts +0 -1692
  140. package/dist/api/subscriptions/index.d.ts.map +0 -1
  141. package/dist/api/subscriptions/index.js +0 -1867
  142. package/dist/api/subscriptions/index.js.map +0 -1
  143. package/dist/api/workflows/index.browser.js +0 -246
  144. package/dist/api/workflows/index.browser.js.map +0 -1
  145. package/dist/api/workflows/index.d.ts +0 -1618
  146. package/dist/api/workflows/index.d.ts.map +0 -1
  147. package/dist/api/workflows/index.js +0 -1495
  148. package/dist/api/workflows/index.js.map +0 -1
  149. package/src/api/invitations/__tests__/InvitationService.spec.ts +0 -439
  150. package/src/api/invitations/controllers/AdminInvitationController.ts +0 -86
  151. package/src/api/invitations/controllers/InvitationController.ts +0 -84
  152. package/src/api/invitations/entities/invitations.ts +0 -33
  153. package/src/api/invitations/index.ts +0 -58
  154. package/src/api/invitations/jobs/InvitationJobs.ts +0 -37
  155. package/src/api/invitations/providers/InvitationProvider.ts +0 -45
  156. package/src/api/invitations/schemas/createInvitationSchema.ts +0 -12
  157. package/src/api/invitations/schemas/invitationConfigAtom.ts +0 -20
  158. package/src/api/invitations/schemas/invitationQuerySchema.ts +0 -15
  159. package/src/api/invitations/schemas/invitationResourceSchema.ts +0 -6
  160. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +0 -22
  161. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +0 -10
  162. package/src/api/invitations/services/InvitationService.ts +0 -556
  163. package/src/api/issues/__tests__/IssueService.spec.ts +0 -263
  164. package/src/api/issues/controllers/AdminIssueController.ts +0 -149
  165. package/src/api/issues/controllers/IssueController.ts +0 -44
  166. package/src/api/issues/entities/issues.ts +0 -49
  167. package/src/api/issues/index.ts +0 -50
  168. package/src/api/issues/schemas/createIssueSchema.ts +0 -13
  169. package/src/api/issues/schemas/issueConfigAtom.ts +0 -13
  170. package/src/api/issues/schemas/issueQuerySchema.ts +0 -18
  171. package/src/api/issues/schemas/issueResourceSchema.ts +0 -6
  172. package/src/api/issues/schemas/myIssueQuerySchema.ts +0 -10
  173. package/src/api/issues/schemas/updateIssueSchema.ts +0 -13
  174. package/src/api/issues/services/IssueService.ts +0 -264
  175. package/src/api/jobs/__tests__/$job-middleware.spec.ts +0 -126
  176. package/src/api/jobs/__tests__/JobService.spec.ts +0 -31
  177. package/src/api/jobs/entities/jobExecutionLogEntity.ts +0 -13
  178. package/src/api/jobs/schemas/jobActivitySchema.ts +0 -15
  179. package/src/api/jobs/schemas/jobCronInfoSchema.ts +0 -22
  180. package/src/api/jobs/schemas/jobExecutionDetailResourceSchema.ts +0 -20
  181. package/src/api/jobs/schemas/jobFailureSchema.ts +0 -9
  182. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +0 -14
  183. package/src/api/jobs/schemas/jobStatsSchema.ts +0 -14
  184. package/src/api/jobs/services/JobService-tests.ts +0 -157
  185. package/src/api/subscriptions/__tests__/BillingService.spec.ts +0 -218
  186. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +0 -278
  187. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +0 -212
  188. package/src/api/subscriptions/controllers/SubscriptionController.ts +0 -189
  189. package/src/api/subscriptions/entities/subscriptionEvents.ts +0 -54
  190. package/src/api/subscriptions/entities/subscriptions.ts +0 -68
  191. package/src/api/subscriptions/index.ts +0 -133
  192. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +0 -382
  193. package/src/api/subscriptions/middleware/$requireLimit.ts +0 -50
  194. package/src/api/subscriptions/middleware/$requirePlan.ts +0 -49
  195. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +0 -110
  196. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +0 -8
  197. package/src/api/subscriptions/schemas/changePlanSchema.ts +0 -9
  198. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +0 -11
  199. package/src/api/subscriptions/schemas/entitlementsSchema.ts +0 -21
  200. package/src/api/subscriptions/schemas/mrrSchema.ts +0 -13
  201. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +0 -71
  202. package/src/api/subscriptions/schemas/planResourceSchema.ts +0 -25
  203. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +0 -8
  204. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +0 -19
  205. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +0 -6
  206. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +0 -32
  207. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +0 -23
  208. package/src/api/subscriptions/services/BillingService.ts +0 -437
  209. package/src/api/subscriptions/services/SubscriptionConfig.ts +0 -56
  210. package/src/api/subscriptions/services/SubscriptionService.ts +0 -867
  211. package/src/api/subscriptions/services/UsageService.ts +0 -118
  212. package/src/api/workflows/__tests__/$workflow.spec.ts +0 -616
  213. package/src/api/workflows/controllers/AdminWorkflowController.ts +0 -191
  214. package/src/api/workflows/entities/workflowExecutions.ts +0 -74
  215. package/src/api/workflows/entities/workflowStepExecutions.ts +0 -74
  216. package/src/api/workflows/entities/workflowStepLogs.ts +0 -13
  217. package/src/api/workflows/index.browser.ts +0 -22
  218. package/src/api/workflows/index.ts +0 -115
  219. package/src/api/workflows/jobs/WorkflowJobs.ts +0 -77
  220. package/src/api/workflows/primitives/$workflow.ts +0 -202
  221. package/src/api/workflows/providers/WorkflowProvider.ts +0 -1284
  222. package/src/api/workflows/schemas/workflowActivitySchema.ts +0 -15
  223. package/src/api/workflows/schemas/workflowConfigAtom.ts +0 -51
  224. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +0 -18
  225. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +0 -26
  226. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +0 -30
  227. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +0 -26
  228. package/src/api/workflows/schemas/workflowStatsSchema.ts +0 -16
  229. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +0 -15
  230. package/src/api/workflows/services/WorkflowService.ts +0 -382
  231. package/src/cli/core/templates/apiAppSecurityTs.ts +0 -43
  232. package/src/cli/core/templates/webAdminDashboardTsx.ts +0 -17
@@ -0,0 +1,28 @@
1
+ import { $atom, type Static, t } from "alepha";
2
+
3
+ /**
4
+ * Persisted UI state — color mode, theme palette, sidebar collapsed state, etc.
5
+ *
6
+ * The atom is bound to a single `alepha-ui` cookie via {@link UiPersistence},
7
+ * so values survive page reloads and are available during SSR.
8
+ */
9
+ export const uiAtom = $atom({
10
+ name: "alepha.react.ui",
11
+ schema: t.object({
12
+ /** Color mode preference. `"system"` follows the OS-level setting. */
13
+ mode: t.enum(["light", "dark", "system"]),
14
+ /** Theme palette name. UI consumers map this to a CSS class on the root. */
15
+ theme: t.string(),
16
+ /** Sidebar UI state. */
17
+ sidebar: t.object({
18
+ collapsed: t.boolean(),
19
+ }),
20
+ }),
21
+ default: {
22
+ mode: "system",
23
+ theme: "default",
24
+ sidebar: { collapsed: false },
25
+ },
26
+ });
27
+
28
+ export type UiState = Static<typeof uiAtom.schema>;
@@ -0,0 +1,36 @@
1
+ import { useEffect } from "react";
2
+ import { useColorMode } from "../hooks/useColorMode.ts";
3
+ import { useTheme } from "../hooks/useTheme.ts";
4
+
5
+ /**
6
+ * Applies `class="dark"` and an optional theme palette class
7
+ * (`theme-<name>`) to the document root, syncing whenever the underlying
8
+ * atom mutates.
9
+ *
10
+ * Mount once near the root of your tree (typically inside the layout).
11
+ *
12
+ * @example
13
+ * <ColorScheme />
14
+ */
15
+ export function ColorScheme() {
16
+ const { resolved } = useColorMode();
17
+ const { theme } = useTheme();
18
+
19
+ useEffect(() => {
20
+ if (typeof document === "undefined") return;
21
+ document.documentElement.classList.toggle("dark", resolved === "dark");
22
+ }, [resolved]);
23
+
24
+ useEffect(() => {
25
+ if (typeof document === "undefined") return;
26
+ const root = document.documentElement;
27
+ const previous: string[] = [];
28
+ root.classList.forEach((cls) => {
29
+ if (cls.startsWith("theme-")) previous.push(cls);
30
+ });
31
+ for (const cls of previous) root.classList.remove(cls);
32
+ if (theme && theme !== "default") root.classList.add(`theme-${theme}`);
33
+ }, [theme]);
34
+
35
+ return null;
36
+ }
@@ -0,0 +1,49 @@
1
+ import { useStore } from "alepha/react";
2
+ import { useEffect, useState } from "react";
3
+ import { uiAtom } from "../atoms/uiAtom.ts";
4
+
5
+ export type ColorMode = "light" | "dark" | "system";
6
+ export type ResolvedColorMode = "light" | "dark";
7
+
8
+ /**
9
+ * Read and update the user's color-mode preference. `"system"` resolves to
10
+ * the OS preference and updates live as the OS toggles between light/dark.
11
+ *
12
+ * @example
13
+ * const { mode, setMode, resolved } = useColorMode();
14
+ * setMode("dark");
15
+ * document.documentElement.classList.toggle("dark", resolved === "dark");
16
+ */
17
+ export const useColorMode = () => {
18
+ const [state, set] = useStore(uiAtom);
19
+ const mode = (state?.mode ?? "system") as ColorMode;
20
+ const resolved = useResolvedColorMode(mode);
21
+
22
+ return {
23
+ mode,
24
+ resolved,
25
+ setMode: (next: ColorMode) => {
26
+ set({ ...(state ?? uiAtom.options.default!), mode: next });
27
+ },
28
+ };
29
+ };
30
+
31
+ const useResolvedColorMode = (mode: ColorMode): ResolvedColorMode => {
32
+ const [systemDark, setSystemDark] = useState<boolean>(() => {
33
+ if (typeof window === "undefined") return false;
34
+ return window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
35
+ });
36
+
37
+ useEffect(() => {
38
+ if (typeof window === "undefined") return;
39
+ const mq = window.matchMedia?.("(prefers-color-scheme: dark)");
40
+ if (!mq) return;
41
+ const onChange = (ev: MediaQueryListEvent) => setSystemDark(ev.matches);
42
+ mq.addEventListener("change", onChange);
43
+ return () => mq.removeEventListener("change", onChange);
44
+ }, []);
45
+
46
+ if (mode === "dark") return "dark";
47
+ if (mode === "light") return "light";
48
+ return systemDark ? "dark" : "light";
49
+ };
@@ -0,0 +1,26 @@
1
+ import { useStore } from "alepha/react";
2
+ import { uiAtom } from "../atoms/uiAtom.ts";
3
+
4
+ /**
5
+ * Read and update the sidebar collapsed state. The value is persisted via the
6
+ * `alepha-ui` cookie so it survives reloads and is available during SSR — no
7
+ * flash of expanded-then-collapsed when the user prefers a collapsed shell.
8
+ *
9
+ * @example
10
+ * const { collapsed, setCollapsed, toggle } = useSidebarState();
11
+ */
12
+ export const useSidebarState = () => {
13
+ const [state, set] = useStore(uiAtom);
14
+ const collapsed = state?.sidebar.collapsed ?? false;
15
+
16
+ const setCollapsed = (next: boolean) => {
17
+ const base = state ?? uiAtom.options.default!;
18
+ set({ ...base, sidebar: { ...base.sidebar, collapsed: next } });
19
+ };
20
+
21
+ return {
22
+ collapsed,
23
+ setCollapsed,
24
+ toggle: () => setCollapsed(!collapsed),
25
+ };
26
+ };
@@ -0,0 +1,22 @@
1
+ import { useStore } from "alepha/react";
2
+ import { uiAtom } from "../atoms/uiAtom.ts";
3
+
4
+ /**
5
+ * Read and update the active theme palette name. UI consumers typically map
6
+ * the value to a class on the document root (e.g. `theme-claude`).
7
+ *
8
+ * @example
9
+ * const { theme, setTheme } = useTheme();
10
+ * setTheme("claude");
11
+ */
12
+ export const useTheme = () => {
13
+ const [state, set] = useStore(uiAtom);
14
+ const theme = state?.theme ?? "default";
15
+
16
+ return {
17
+ theme,
18
+ setTheme: (next: string) => {
19
+ set({ ...(state ?? uiAtom.options.default!), theme: next });
20
+ },
21
+ };
22
+ };
@@ -0,0 +1,35 @@
1
+ import { $module } from "alepha";
2
+ import type { UiState } from "./atoms/uiAtom.ts";
3
+ import { UiPersistence } from "./services/UiPersistence.ts";
4
+
5
+ // ---------------------------------------------------------------------------------------------------------------------
6
+
7
+ export * from "./atoms/uiAtom.ts";
8
+ export * from "./components/ColorScheme.tsx";
9
+ export * from "./hooks/useColorMode.ts";
10
+ export * from "./hooks/useSidebarState.ts";
11
+ export * from "./hooks/useTheme.ts";
12
+ export * from "./services/UiPersistence.ts";
13
+
14
+ // ---------------------------------------------------------------------------------------------------------------------
15
+
16
+ declare module "alepha" {
17
+ export interface State {
18
+ "alepha.react.ui": UiState;
19
+ }
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------------------------------------------------
23
+
24
+ /**
25
+ * Persisted UI state: color mode, theme palette, sidebar collapsed state.
26
+ *
27
+ * Backed by an `alepha-ui` cookie so preferences survive reloads and are
28
+ * available during SSR (no flash of wrong theme).
29
+ *
30
+ * @module alepha.react.ui
31
+ */
32
+ export const AlephaReactUi = $module({
33
+ name: "alepha.react.ui",
34
+ services: [UiPersistence],
35
+ });
@@ -0,0 +1,41 @@
1
+ import { $head } from "alepha/react/head";
2
+ import { $cookie } from "alepha/server/cookies";
3
+ import { uiAtom } from "../atoms/uiAtom.ts";
4
+
5
+ /**
6
+ * Inline `<script>` rendered by SSR into the document `<head>`. Runs
7
+ * synchronously before any CSS or React hydration: reads the `alepha-ui`
8
+ * cookie, resolves `mode === "system"` via `prefers-color-scheme`, and
9
+ * applies `class="dark"` (and optional `theme-<name>`) on `<html>`.
10
+ *
11
+ * This is what kills the flash-of-wrong-theme (FOUC) you'd otherwise get
12
+ * with React-effect-based theming. Failures are swallowed silently — at
13
+ * worst the page paints in light mode for one frame.
14
+ */
15
+ const colorSchemeBoot = `(function(){try{var m=document.cookie.match(/(?:^|;\\s*)alepha-ui=([^;]+)/);var s=m?JSON.parse(decodeURIComponent(m[1])):{};var mode=s.mode||"system";var dark=mode==="dark"||(mode==="system"&&window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches);var r=document.documentElement;if(dark)r.classList.add("dark");if(s.theme&&s.theme!=="default")r.classList.add("theme-"+s.theme);}catch(e){}})();`;
16
+
17
+ /**
18
+ * Binds the {@link uiAtom} to an `alepha-ui` cookie + injects an inline
19
+ * boot script into the SSR head to prevent FOUC on first paint.
20
+ *
21
+ * Reading flow: on app boot the cookie is parsed and pushed into the atom
22
+ * (via the `key` option on `$cookie`). Writing flow: every time the atom
23
+ * mutates, the cookie is rewritten — a single `useStore(uiAtom)` call is
24
+ * enough to persist UI preferences across reloads.
25
+ *
26
+ * Persists for 365 days; SameSite=lax so it travels on top-level navigation
27
+ * but not on cross-origin requests.
28
+ */
29
+ export class UiPersistence {
30
+ ui = $cookie({
31
+ name: "alepha-ui",
32
+ key: uiAtom.key,
33
+ schema: uiAtom.schema,
34
+ ttl: [365, "days"],
35
+ sameSite: "lax",
36
+ });
37
+
38
+ head = $head({
39
+ script: [{ content: colorSchemeBoot }],
40
+ });
41
+ }
@@ -1,3 +1,5 @@
1
+ import { AlephaError } from "alepha";
2
+
1
3
  /**
2
4
  * Parses and manipulates templated paths with `{param}` placeholders.
3
5
  *
@@ -23,26 +25,25 @@
23
25
  export class TemplatedPathParser {
24
26
  protected static readonly PARAM_REGEX = /\{([^}]+)\}/g;
25
27
 
26
- protected readonly template: string;
27
- protected readonly separator: string;
28
+ public readonly template: string;
29
+ public readonly separator: string;
30
+ public readonly paramNames: readonly string[];
31
+ public readonly hasParams: boolean;
32
+ protected readonly extractRegex: RegExp | null;
28
33
 
29
34
  constructor(template: string, separator = "/") {
35
+ if (separator.length !== 1) {
36
+ throw new AlephaError(
37
+ `TemplatedPathParser separator must be a single character, got '${separator}'`,
38
+ );
39
+ }
30
40
  this.template = template;
31
41
  this.separator = separator;
32
- }
33
-
34
- /**
35
- * Returns true if the template contains at least one `{param}` placeholder.
36
- */
37
- get hasParams(): boolean {
38
- return /\{[^}]+\}/.test(this.template);
39
- }
40
-
41
- /**
42
- * Returns an ordered list of parameter names found in the template.
43
- */
44
- get paramNames(): string[] {
45
- return [...this.template.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
42
+ this.paramNames = [
43
+ ...template.matchAll(TemplatedPathParser.PARAM_REGEX),
44
+ ].map((m) => m[1]);
45
+ this.hasParams = this.paramNames.length > 0;
46
+ this.extractRegex = this.hasParams ? this.buildExtractRegex() : null;
46
47
  }
47
48
 
48
49
  /**
@@ -51,50 +52,31 @@ export class TemplatedPathParser {
51
52
  */
52
53
  interpolate(params: Record<string, string>): string {
53
54
  return this.template.replace(
54
- /\{([^}]+)\}/g,
55
+ TemplatedPathParser.PARAM_REGEX,
55
56
  (_, name: string) => params[name] ?? `{${name}}`,
56
57
  );
57
58
  }
58
59
 
59
60
  /**
60
61
  * Extracts parameter values from a concrete path by matching it against
61
- * the template structure. Returns an empty object when the template has
62
- * no parameters.
62
+ * the template structure.
63
+ *
64
+ * Returns `null` when the path does not match the template.
65
+ * Returns `{}` when the template has no parameters and the path matches.
63
66
  */
64
- extract(path: string): Record<string, string> {
65
- const names = this.paramNames;
66
- if (names.length === 0) {
67
- return {};
67
+ extract(path: string): Record<string, string> | null {
68
+ if (!this.extractRegex) {
69
+ return path === this.template ? {} : null;
68
70
  }
69
71
 
70
- const escapedSeparator = this.separator.replace(
71
- /[.*+?^${}()|[\]\\]/g,
72
- "\\$&",
73
- );
74
-
75
- // Build a regex from the template: escape literal parts, replace {param} with a capture group
76
- const regexSource = this.template
77
- .replace(/[.*+?^${}()|[\]\\]/g, (char) => {
78
- // Allow { and } through so we can then replace them as param groups
79
- if (char === "{" || char === "}") {
80
- return char;
81
- }
82
- return `\\${char}`;
83
- })
84
- // After escaping literal chars, replace {name} patterns with capture groups.
85
- // The separator is already escaped above, so we match anything that is not the separator.
86
- .replace(/\{([^}]+)\}/g, `([^${escapedSeparator}]+)`);
87
-
88
- const regex = new RegExp(`^${regexSource}$`);
89
- const match = regex.exec(path);
90
-
72
+ const match = this.extractRegex.exec(path);
91
73
  if (!match) {
92
- return {};
74
+ return null;
93
75
  }
94
76
 
95
77
  const result: Record<string, string> = {};
96
- for (let i = 0; i < names.length; i++) {
97
- result[names[i]] = match[i + 1];
78
+ for (let i = 0; i < this.paramNames.length; i++) {
79
+ result[this.paramNames[i]] = match[i + 1];
98
80
  }
99
81
  return result;
100
82
  }
@@ -104,7 +86,7 @@ export class TemplatedPathParser {
104
86
  * wildcard string. Defaults to `"+"` (MQTT-style).
105
87
  */
106
88
  wildcardize(wildcard = "+"): string {
107
- return this.template.replace(/\{[^}]+\}/g, wildcard);
89
+ return this.template.replace(TemplatedPathParser.PARAM_REGEX, wildcard);
108
90
  }
109
91
 
110
92
  /**
@@ -113,16 +95,33 @@ export class TemplatedPathParser {
113
95
  */
114
96
  normalize(path: string): string {
115
97
  const sep = this.separator;
116
- const escapedSep = sep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
98
+ const escapedSep = this.escapeRegex(sep);
117
99
 
118
- // Replace consecutive separators with a single one
119
100
  let result = path.replace(new RegExp(`${escapedSep}{2,}`, "g"), sep);
120
101
 
121
- // Strip trailing separator, but preserve a lone separator
122
102
  if (result.endsWith(sep) && result.length > sep.length) {
123
103
  result = result.slice(0, -sep.length);
124
104
  }
125
105
 
126
106
  return result;
127
107
  }
108
+
109
+ protected buildExtractRegex(): RegExp {
110
+ const escapedSeparator = this.escapeRegex(this.separator);
111
+
112
+ const regexSource = this.template
113
+ .replace(/[.*+?^${}()|[\]\\]/g, (char) => {
114
+ if (char === "{" || char === "}") {
115
+ return char;
116
+ }
117
+ return `\\${char}`;
118
+ })
119
+ .replace(/\{[^}]+\}/g, `([^${escapedSeparator}]+)`);
120
+
121
+ return new RegExp(`^${regexSource}$`);
122
+ }
123
+
124
+ protected escapeRegex(s: string): string {
125
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
126
+ }
128
127
  }
@@ -177,6 +177,68 @@ describe("RouterProvider", () => {
177
177
  });
178
178
  });
179
179
 
180
+ it("should return a fresh params object on each match (no shared mutation)", ({
181
+ expect,
182
+ }) => {
183
+ const { add, router } = playground();
184
+ add("/users/:id", "user");
185
+
186
+ const a = router.match("/users/42");
187
+ const b = router.match("/users/42");
188
+ expect(a.params).not.toBe(b.params);
189
+ a.params!.injected = "x";
190
+ expect(b.params).toEqual({ id: "42" });
191
+ });
192
+
193
+ it("should cache by pathname only (ignoring query string)", ({ expect }) => {
194
+ const { add, router } = playground();
195
+ add("/search", "search");
196
+
197
+ router.match("/search?q=1");
198
+ router.match("/search?q=2");
199
+ router.match("/search?q=3");
200
+ // cache is protected; assert via Map size on the subclass
201
+ expect(
202
+ (router as unknown as { cache: Map<string, unknown> }).cache.size,
203
+ ).toBe(1);
204
+ });
205
+
206
+ it("should cap the cache size to avoid unbounded growth", ({ expect }) => {
207
+ const { add, router } = playground();
208
+ add("/*", "catchall");
209
+
210
+ (router as unknown as { maxCacheSize: number }).maxCacheSize = 5;
211
+ for (let i = 0; i < 20; i++) {
212
+ router.match(`/x/${i}`);
213
+ }
214
+ const size = (router as unknown as { cache: Map<string, unknown> }).cache
215
+ .size;
216
+ expect(size).toBeLessThanOrEqual(5);
217
+ });
218
+
219
+ it("should invalidate the cache when a new route is pushed", ({ expect }) => {
220
+ const { add, router } = playground();
221
+ const m1 = router.match("/new");
222
+ expect(m1.route).toBeUndefined();
223
+
224
+ add("/new", "new-route");
225
+ const m2 = router.match("/new");
226
+ expect(m2.route?.name).toBe("new-route");
227
+ });
228
+
229
+ it("should handle three-way conflicting param names at the same level", ({
230
+ expect,
231
+ }) => {
232
+ const { add, match } = playground();
233
+ add("/u/:a", "a-route");
234
+ add("/u/:b/x", "b-route");
235
+ add("/u/:c/y", "c-route");
236
+
237
+ expect(match("/u/1")).toEqual({ name: "a-route", params: { a: "1" } });
238
+ expect(match("/u/1/x")).toEqual({ name: "b-route", params: { b: "1" } });
239
+ expect(match("/u/1/y")).toEqual({ name: "c-route", params: { c: "1" } });
240
+ });
241
+
180
242
  it("should handle routes with different param names", ({ expect }) => {
181
243
  const { add, match } = playground();
182
244
  add("/users/:id", "home");
@@ -120,6 +120,24 @@ describe("TemplatedPathParser", () => {
120
120
  deviceId: "device-1",
121
121
  });
122
122
  });
123
+
124
+ it("returns null when the path does not match the template", () => {
125
+ const parser = new TemplatedPathParser("/users/{id}");
126
+ expect(parser.extract("/posts/42")).toBeNull();
127
+ expect(parser.extract("/users/42/extra")).toBeNull();
128
+ });
129
+
130
+ it("returns null for a parameterless template when path differs", () => {
131
+ const parser = new TemplatedPathParser("/users/profile");
132
+ expect(parser.extract("/users/other")).toBeNull();
133
+ });
134
+ });
135
+
136
+ describe("constructor validation", () => {
137
+ it("throws when separator is not a single character", () => {
138
+ expect(() => new TemplatedPathParser("foo", "::")).toThrow();
139
+ expect(() => new TemplatedPathParser("foo", "")).toThrow();
140
+ });
123
141
  });
124
142
 
125
143
  describe("wildcardize", () => {
@@ -5,14 +5,18 @@ export abstract class RouterProvider<T extends Route = Route> {
5
5
 
6
6
  protected tree: Tree<T> = { children: {} };
7
7
  protected cache = new Map<string, RouteMatch<T>>();
8
+ protected maxCacheSize = 10_000;
8
9
 
9
10
  public match(path: string): RouteMatch<T> {
10
- if (this.cache.has(path)) {
11
- return this.cache.get(path)!;
11
+ const pathname = path.split("?", 1)[0];
12
+ const hit = this.cache.get(pathname);
13
+ if (hit) {
14
+ return { route: hit.route, params: { ...hit.params } };
12
15
  }
13
- const result = this.mapParams(this.createRouteMatch(path));
14
- this.cache.set(path, result);
15
- return result;
16
+ const result = this.mapParams(this.createRouteMatch(pathname));
17
+ if (this.cache.size >= this.maxCacheSize) this.cache.clear();
18
+ this.cache.set(pathname, result);
19
+ return { route: result.route, params: { ...result.params } };
16
20
  }
17
21
 
18
22
  protected test(path: string): void {
@@ -25,6 +29,7 @@ export abstract class RouterProvider<T extends Route = Route> {
25
29
  const path = route.path.replaceAll("//", "/");
26
30
 
27
31
  this.test(path);
32
+ this.cache.clear();
28
33
 
29
34
  const parts = this.createParts(path);
30
35
 
@@ -115,6 +115,7 @@ export class NodeShellProvider implements ShellProvider {
115
115
  command,
116
116
  {
117
117
  cwd: options.cwd,
118
+ maxBuffer: 50 * 1024 * 1024,
118
119
  env: {
119
120
  ...process.env,
120
121
  LOG_FORMAT: "pretty",
@@ -112,7 +112,7 @@ export abstract class TopicProvider {
112
112
  const parsed = this.parseMessage<T>(schema, message);
113
113
 
114
114
  if (parser.hasParams && receivedTopic) {
115
- const params = parser.extract(receivedTopic);
115
+ const params = parser.extract(receivedTopic) ?? {};
116
116
  await handler({ ...parsed, params } as TopicMessage<T>);
117
117
  } else {
118
118
  await handler(parsed);