arcway 0.1.0

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 (274) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +711 -0
  3. package/client/env.js +55 -0
  4. package/client/fetcher.js +50 -0
  5. package/client/graphql.js +35 -0
  6. package/client/head.js +140 -0
  7. package/client/hooks/use-api.js +80 -0
  8. package/client/hooks/use-debounce.js +12 -0
  9. package/client/hooks/use-form.js +86 -0
  10. package/client/hooks/use-graphql.js +30 -0
  11. package/client/hooks/use-interval.js +12 -0
  12. package/client/hooks/use-mutation.js +27 -0
  13. package/client/hooks/use-query.js +45 -0
  14. package/client/hooks/web/use-click-outside.js +22 -0
  15. package/client/hooks/web/use-local-storage.js +42 -0
  16. package/client/index.js +62 -0
  17. package/client/page-loader.js +155 -0
  18. package/client/provider.js +53 -0
  19. package/client/query.js +13 -0
  20. package/client/router.jsx +303 -0
  21. package/client/ui/accordion.jsx +65 -0
  22. package/client/ui/accordion.stories.jsx +48 -0
  23. package/client/ui/alert-dialog.jsx +122 -0
  24. package/client/ui/alert-dialog.stories.jsx +44 -0
  25. package/client/ui/alert.jsx +52 -0
  26. package/client/ui/alert.stories.jsx +31 -0
  27. package/client/ui/app-shell.jsx +39 -0
  28. package/client/ui/app-shell.stories.jsx +51 -0
  29. package/client/ui/aspect-ratio.jsx +6 -0
  30. package/client/ui/aspect-ratio.stories.jsx +69 -0
  31. package/client/ui/avatar.jsx +78 -0
  32. package/client/ui/avatar.stories.jsx +62 -0
  33. package/client/ui/badge.jsx +34 -0
  34. package/client/ui/badge.stories.js +32 -0
  35. package/client/ui/breadcrumb.jsx +86 -0
  36. package/client/ui/breadcrumb.stories.jsx +43 -0
  37. package/client/ui/button-group.jsx +58 -0
  38. package/client/ui/button-group.stories.jsx +67 -0
  39. package/client/ui/button.jsx +46 -0
  40. package/client/ui/button.stories.js +72 -0
  41. package/client/ui/calendar.jsx +172 -0
  42. package/client/ui/card.jsx +57 -0
  43. package/client/ui/card.stories.jsx +33 -0
  44. package/client/ui/carousel.jsx +167 -0
  45. package/client/ui/chart.jsx +244 -0
  46. package/client/ui/checkbox.jsx +24 -0
  47. package/client/ui/checkbox.stories.js +33 -0
  48. package/client/ui/collapsible.jsx +12 -0
  49. package/client/ui/collapsible.stories.jsx +42 -0
  50. package/client/ui/combobox.jsx +223 -0
  51. package/client/ui/command.jsx +128 -0
  52. package/client/ui/context-menu.jsx +170 -0
  53. package/client/ui/context-menu.stories.jsx +35 -0
  54. package/client/ui/dialog.jsx +109 -0
  55. package/client/ui/dialog.stories.jsx +37 -0
  56. package/client/ui/direction.jsx +9 -0
  57. package/client/ui/drawer.jsx +87 -0
  58. package/client/ui/dropdown-menu.jsx +172 -0
  59. package/client/ui/dropdown-menu.stories.jsx +34 -0
  60. package/client/ui/empty.jsx +76 -0
  61. package/client/ui/empty.stories.jsx +64 -0
  62. package/client/ui/field.jsx +174 -0
  63. package/client/ui/field.stories.jsx +118 -0
  64. package/client/ui/form.jsx +17 -0
  65. package/client/ui/hooks/use-mobile.js +16 -0
  66. package/client/ui/hover-card.jsx +26 -0
  67. package/client/ui/hover-card.stories.jsx +28 -0
  68. package/client/ui/index.js +649 -0
  69. package/client/ui/input-group.jsx +116 -0
  70. package/client/ui/input-group.stories.jsx +65 -0
  71. package/client/ui/input-otp.jsx +62 -0
  72. package/client/ui/input.jsx +16 -0
  73. package/client/ui/input.stories.js +27 -0
  74. package/client/ui/item.jsx +155 -0
  75. package/client/ui/item.stories.jsx +118 -0
  76. package/client/ui/kbd.jsx +24 -0
  77. package/client/ui/kbd.stories.jsx +32 -0
  78. package/client/ui/label.jsx +16 -0
  79. package/client/ui/label.stories.js +25 -0
  80. package/client/ui/lib/utils.js +6 -0
  81. package/client/ui/main-content.jsx +30 -0
  82. package/client/ui/menubar.jsx +189 -0
  83. package/client/ui/menubar.stories.jsx +43 -0
  84. package/client/ui/native-select.jsx +34 -0
  85. package/client/ui/native-select.stories.jsx +67 -0
  86. package/client/ui/navigation-menu.jsx +120 -0
  87. package/client/ui/navigation-menu.stories.jsx +45 -0
  88. package/client/ui/pagination.jsx +92 -0
  89. package/client/ui/pagination.stories.jsx +52 -0
  90. package/client/ui/panel.jsx +66 -0
  91. package/client/ui/popover.jsx +54 -0
  92. package/client/ui/popover.stories.jsx +27 -0
  93. package/client/ui/progress.jsx +19 -0
  94. package/client/ui/progress.stories.js +34 -0
  95. package/client/ui/radio-group.jsx +33 -0
  96. package/client/ui/radio-group.stories.jsx +49 -0
  97. package/client/ui/resizable.jsx +33 -0
  98. package/client/ui/scroll-area.jsx +41 -0
  99. package/client/ui/scroll-area.stories.jsx +43 -0
  100. package/client/ui/select.jsx +145 -0
  101. package/client/ui/select.stories.jsx +80 -0
  102. package/client/ui/separator.jsx +18 -0
  103. package/client/ui/separator.stories.jsx +37 -0
  104. package/client/ui/sheet.jsx +95 -0
  105. package/client/ui/sheet.stories.jsx +56 -0
  106. package/client/ui/sidebar.jsx +544 -0
  107. package/client/ui/skeleton.jsx +8 -0
  108. package/client/ui/skeleton.stories.js +23 -0
  109. package/client/ui/slider.jsx +41 -0
  110. package/client/ui/slider.stories.js +31 -0
  111. package/client/ui/sonner.jsx +37 -0
  112. package/client/ui/spinner.jsx +14 -0
  113. package/client/ui/spinner.stories.js +16 -0
  114. package/client/ui/style-mira.css +1316 -0
  115. package/client/ui/switch.jsx +22 -0
  116. package/client/ui/switch.stories.js +44 -0
  117. package/client/ui/table.jsx +33 -0
  118. package/client/ui/table.stories.jsx +42 -0
  119. package/client/ui/tabs.jsx +63 -0
  120. package/client/ui/tabs.stories.jsx +45 -0
  121. package/client/ui/textarea.jsx +15 -0
  122. package/client/ui/textarea.stories.js +33 -0
  123. package/client/ui/theme.css +459 -0
  124. package/client/ui/toggle-group.jsx +62 -0
  125. package/client/ui/toggle-group.stories.jsx +68 -0
  126. package/client/ui/toggle.jsx +34 -0
  127. package/client/ui/toggle.stories.js +46 -0
  128. package/client/ui/tooltip.jsx +37 -0
  129. package/client/ui/tooltip.stories.jsx +32 -0
  130. package/client/ui/use-transition.js +35 -0
  131. package/client/ws.js +132 -0
  132. package/package.json +134 -0
  133. package/server/bin/cli.js +42 -0
  134. package/server/bin/commands/build.js +23 -0
  135. package/server/bin/commands/dev.js +57 -0
  136. package/server/bin/commands/docs.js +30 -0
  137. package/server/bin/commands/graphql-schema.js +32 -0
  138. package/server/bin/commands/lint.js +35 -0
  139. package/server/bin/commands/mcp.js +26 -0
  140. package/server/bin/commands/migrate.js +82 -0
  141. package/server/bin/commands/schema.js +41 -0
  142. package/server/bin/commands/seed.js +36 -0
  143. package/server/bin/commands/start.js +31 -0
  144. package/server/bin/commands/test.js +20 -0
  145. package/server/bin/solo.js +4 -0
  146. package/server/boot/index.js +150 -0
  147. package/server/boot.js +2 -0
  148. package/server/build.js +23 -0
  149. package/server/cache/drivers/memory.js +23 -0
  150. package/server/cache/drivers/redis.js +28 -0
  151. package/server/cache/index.js +69 -0
  152. package/server/config/loader.js +89 -0
  153. package/server/config/modules/api.js +17 -0
  154. package/server/config/modules/build.js +9 -0
  155. package/server/config/modules/cache.js +10 -0
  156. package/server/config/modules/database.js +29 -0
  157. package/server/config/modules/events.js +15 -0
  158. package/server/config/modules/files.js +15 -0
  159. package/server/config/modules/jobs.js +20 -0
  160. package/server/config/modules/logger.js +9 -0
  161. package/server/config/modules/mail.js +11 -0
  162. package/server/config/modules/mcp.js +9 -0
  163. package/server/config/modules/pages.js +20 -0
  164. package/server/config/modules/queue.js +10 -0
  165. package/server/config/modules/redis.js +9 -0
  166. package/server/config/modules/server.js +30 -0
  167. package/server/config/modules/session.js +9 -0
  168. package/server/config/modules/websocket.js +11 -0
  169. package/server/constants.js +67 -0
  170. package/server/context.js +15 -0
  171. package/server/db/index.js +87 -0
  172. package/server/db/schema/drivers/mysql.js +28 -0
  173. package/server/db/schema/drivers/pg.js +34 -0
  174. package/server/db/schema/drivers/sqlite.js +22 -0
  175. package/server/db/schema/index.js +78 -0
  176. package/server/db/seeds.js +22 -0
  177. package/server/discovery.js +67 -0
  178. package/server/docs/openapi.js +153 -0
  179. package/server/env.js +17 -0
  180. package/server/events/drivers/memory.js +45 -0
  181. package/server/events/drivers/redis.js +64 -0
  182. package/server/events/handler.js +67 -0
  183. package/server/events/index.js +35 -0
  184. package/server/events/pattern.js +5 -0
  185. package/server/files/drivers/local.js +83 -0
  186. package/server/files/drivers/s3.js +113 -0
  187. package/server/files/index.js +57 -0
  188. package/server/filewatcher/index.js +156 -0
  189. package/server/glob.js +6 -0
  190. package/server/graphql/discovery.js +70 -0
  191. package/server/graphql/handler.js +41 -0
  192. package/server/graphql/index.js +13 -0
  193. package/server/graphql/loaders.js +19 -0
  194. package/server/graphql/merge.js +48 -0
  195. package/server/graphql/subscriptions.js +43 -0
  196. package/server/health.js +34 -0
  197. package/server/helpers.js +9 -0
  198. package/server/index.js +55 -0
  199. package/server/internals.js +139 -0
  200. package/server/jobs/cron.js +10 -0
  201. package/server/jobs/drivers/knex-queue.js +207 -0
  202. package/server/jobs/drivers/lease.js +148 -0
  203. package/server/jobs/drivers/memory-queue.js +134 -0
  204. package/server/jobs/queue.js +27 -0
  205. package/server/jobs/runner.js +197 -0
  206. package/server/jobs/throughput.js +63 -0
  207. package/server/lib/vault/encrypt.js +40 -0
  208. package/server/lib/vault/ids.js +9 -0
  209. package/server/lib/vault/index.js +14 -0
  210. package/server/lib/vault/jwt.js +55 -0
  211. package/server/lib/vault/password.js +10 -0
  212. package/server/lint/boundaries.js +77 -0
  213. package/server/logger/index.js +130 -0
  214. package/server/mail/drivers/console.js +31 -0
  215. package/server/mail/drivers/smtp.js +34 -0
  216. package/server/mail/imap.js +105 -0
  217. package/server/mail/inbound-store.js +58 -0
  218. package/server/mail/inbound.js +79 -0
  219. package/server/mail/index.js +112 -0
  220. package/server/mcp/debug-api.js +137 -0
  221. package/server/mcp/helpers.js +30 -0
  222. package/server/mcp/index.js +77 -0
  223. package/server/mcp/runtime.js +7 -0
  224. package/server/mcp/server.js +19 -0
  225. package/server/mcp/tools/debugging.js +133 -0
  226. package/server/mcp/tools/introspection.js +87 -0
  227. package/server/middlewares/cors.js +30 -0
  228. package/server/middlewares/index.js +3 -0
  229. package/server/middlewares/require-session.js +15 -0
  230. package/server/module-loader.js +9 -0
  231. package/server/pages/build-client.js +187 -0
  232. package/server/pages/build-css.js +47 -0
  233. package/server/pages/build-manifest.js +55 -0
  234. package/server/pages/build-plugins.js +75 -0
  235. package/server/pages/build-server.js +115 -0
  236. package/server/pages/build.js +116 -0
  237. package/server/pages/discovery.js +120 -0
  238. package/server/pages/fonts.js +128 -0
  239. package/server/pages/handler.js +276 -0
  240. package/server/pages/hmr.js +176 -0
  241. package/server/pages/pages-router.js +78 -0
  242. package/server/pages/ssr.js +276 -0
  243. package/server/pages/static.js +92 -0
  244. package/server/pages/watcher.js +90 -0
  245. package/server/queue/drivers/knex.js +67 -0
  246. package/server/queue/drivers/redis.js +91 -0
  247. package/server/queue/index.js +61 -0
  248. package/server/rate-limit/consume.js +21 -0
  249. package/server/rate-limit/drivers/memory.js +24 -0
  250. package/server/rate-limit/drivers/redis.js +32 -0
  251. package/server/rate-limit/index.js +33 -0
  252. package/server/redis/index.js +67 -0
  253. package/server/ring-buffer.js +44 -0
  254. package/server/route.js +4 -0
  255. package/server/router/api-router.js +317 -0
  256. package/server/router/cors.js +31 -0
  257. package/server/router/middleware.js +91 -0
  258. package/server/router/routes.js +132 -0
  259. package/server/server.js +35 -0
  260. package/server/session/helpers.js +21 -0
  261. package/server/session/index.js +89 -0
  262. package/server/static/index.js +36 -0
  263. package/server/system-jobs/index.js +50 -0
  264. package/server/system-routes/index.js +84 -0
  265. package/server/testing/index.js +263 -0
  266. package/server/validation.js +41 -0
  267. package/server/watcher.js +34 -0
  268. package/server/web-server.js +231 -0
  269. package/server/ws/discovery.js +54 -0
  270. package/server/ws/index.js +14 -0
  271. package/server/ws/realtime.js +318 -0
  272. package/server/ws/registry.js +17 -0
  273. package/server/ws/server.js +152 -0
  274. package/server/ws/ws-router.js +335 -0
@@ -0,0 +1,70 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { loadModule } from '../module-loader.js';
4
+ async function discoverGraphQL(rootDir) {
5
+ const graphqlDir = path.join(rootDir, 'graphql');
6
+ if (!fs.existsSync(graphqlDir)) return [];
7
+ const entry = {};
8
+ let hasContent = false;
9
+ const schemaPath = findFile(graphqlDir, 'schema');
10
+ if (schemaPath) {
11
+ let module;
12
+ try {
13
+ module = await loadModule(schemaPath, import.meta.url);
14
+ } catch (err) {
15
+ throw new Error(`Failed to load GraphQL schema at ${schemaPath}: ${err}`);
16
+ }
17
+ const schema = module.default;
18
+ if (typeof schema !== 'string') {
19
+ throw new Error(
20
+ `GraphQL schema must default-export an SDL string. Got ${typeof schema} (${schemaPath})`,
21
+ );
22
+ }
23
+ entry.schema = schema;
24
+ hasContent = true;
25
+ }
26
+ const resolversPath = findFile(graphqlDir, 'resolvers');
27
+ if (resolversPath) {
28
+ let module;
29
+ try {
30
+ module = await loadModule(resolversPath, import.meta.url);
31
+ } catch (err) {
32
+ throw new Error(`Failed to load GraphQL resolvers at ${resolversPath}: ${err}`);
33
+ }
34
+ const resolvers = module.default;
35
+ if (!resolvers || typeof resolvers !== 'object' || Array.isArray(resolvers)) {
36
+ throw new Error(
37
+ `GraphQL resolvers must default-export an object. Got ${typeof resolvers} (${resolversPath})`,
38
+ );
39
+ }
40
+ entry.resolvers = resolvers;
41
+ hasContent = true;
42
+ }
43
+ const loadersPath = findFile(graphqlDir, 'loaders');
44
+ if (loadersPath) {
45
+ let module;
46
+ try {
47
+ module = await loadModule(loadersPath, import.meta.url);
48
+ } catch (err) {
49
+ throw new Error(`Failed to load GraphQL loaders at ${loadersPath}: ${err}`);
50
+ }
51
+ const loaderFactory = module.default;
52
+ if (typeof loaderFactory !== 'function') {
53
+ throw new Error(
54
+ `GraphQL loaders must default-export a function. Got ${typeof loaderFactory} (${loadersPath})`,
55
+ );
56
+ }
57
+ entry.loaderFactory = loaderFactory;
58
+ hasContent = true;
59
+ }
60
+ if (hasContent) {
61
+ return [entry];
62
+ }
63
+ return [];
64
+ }
65
+ function findFile(dir, name) {
66
+ const jsPath = path.join(dir, `${name}.js`);
67
+ if (fs.existsSync(jsPath)) return jsPath;
68
+ return null;
69
+ }
70
+ export { discoverGraphQL };
@@ -0,0 +1,41 @@
1
+ import { createSchema, createYoga } from 'graphql-yoga';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mergeGraphQLSchemas, mergeGraphQLResolvers } from './merge.js';
4
+ import { createLoaderFactory } from './loaders.js';
5
+ import { parseCookiesFromHeader, resolveSession } from '../session/helpers.js';
6
+ function createGraphQLHandler(options) {
7
+ const { domains, graphiql, session: sessionConfig, log } = options;
8
+ const typeDefs = mergeGraphQLSchemas(domains);
9
+ const resolvers = mergeGraphQLResolvers(domains);
10
+ const makeLoaders = createLoaderFactory(domains);
11
+ const schema = createSchema({ typeDefs, resolvers });
12
+ const yoga = createYoga({
13
+ schema,
14
+ graphqlEndpoint: '/graphql',
15
+ graphiql: graphiql ?? false,
16
+ // Disable yoga's own CORS — Arcway handles CORS at the framework level
17
+ cors: false,
18
+ context: async ({ request }) => {
19
+ const loaders = makeLoaders();
20
+ const requestId = request.headers.get('x-request-id') ?? randomUUID();
21
+ const headers = {};
22
+ request.headers.forEach((value, key) => {
23
+ headers[key] = value;
24
+ });
25
+ const cookies = parseCookiesFromHeader(request.headers.get('cookie') ?? '');
26
+ const session = await resolveSession(cookies, sessionConfig);
27
+ return { loaders, session, requestId, headers, cookies };
28
+ },
29
+ logging: log
30
+ ? {
31
+ debug: (...args) => log.info(String(args[0])),
32
+ info: (...args) => log.info(String(args[0])),
33
+ warn: (...args) => log.warn(String(args[0])),
34
+ error: (...args) => log.error(String(args[0])),
35
+ }
36
+ : false,
37
+ });
38
+ const handler = yoga;
39
+ return { handler, schema, session: sessionConfig };
40
+ }
41
+ export { createGraphQLHandler };
@@ -0,0 +1,13 @@
1
+ import { discoverGraphQL } from './discovery.js';
2
+ import { mergeGraphQLSchemas, mergeGraphQLResolvers } from './merge.js';
3
+ import { createLoaderFactory } from './loaders.js';
4
+ import { createGraphQLHandler } from './handler.js';
5
+ import { attachGraphQLSubscriptions } from './subscriptions.js';
6
+ export {
7
+ attachGraphQLSubscriptions,
8
+ createGraphQLHandler,
9
+ createLoaderFactory,
10
+ discoverGraphQL,
11
+ mergeGraphQLResolvers,
12
+ mergeGraphQLSchemas,
13
+ };
@@ -0,0 +1,19 @@
1
+ import DataLoader from 'dataloader';
2
+ function createLoaderFactory(entries) {
3
+ const factories = [];
4
+ for (const entry of entries) {
5
+ if (!entry.loaderFactory) continue;
6
+ factories.push(entry.loaderFactory);
7
+ }
8
+ return () => {
9
+ const loaders = {};
10
+ for (const factory of factories) {
11
+ const batchFns = factory();
12
+ for (const [name, batchFn] of Object.entries(batchFns)) {
13
+ loaders[name] = new DataLoader(batchFn);
14
+ }
15
+ }
16
+ return loaders;
17
+ };
18
+ }
19
+ export { createLoaderFactory };
@@ -0,0 +1,48 @@
1
+ const BASE_SCHEMA = `
2
+ type Query {
3
+ _system_health: Boolean
4
+ }
5
+ type Mutation {
6
+ _system_noop: Boolean
7
+ }
8
+ `;
9
+ const BASE_RESOLVERS = {
10
+ Query: {
11
+ _system_health: () => true,
12
+ },
13
+ Mutation: {
14
+ _system_noop: () => true,
15
+ },
16
+ };
17
+ function mergeGraphQLSchemas(entries) {
18
+ const parts = [BASE_SCHEMA];
19
+ for (const entry of entries) {
20
+ if (entry.schema) {
21
+ parts.push(entry.schema);
22
+ }
23
+ }
24
+ return parts.join('\n\n');
25
+ }
26
+ function mergeGraphQLResolvers(entries) {
27
+ const merged = {};
28
+ for (const [typeName, fields] of Object.entries(BASE_RESOLVERS)) {
29
+ merged[typeName] = { ...fields };
30
+ }
31
+ for (const entry of entries) {
32
+ if (!entry.resolvers) continue;
33
+ for (const [typeName, fields] of Object.entries(entry.resolvers)) {
34
+ if (!merged[typeName]) {
35
+ merged[typeName] = {};
36
+ }
37
+ if (typeof fields === 'object' && fields !== null && !Array.isArray(fields)) {
38
+ for (const [fieldName, resolver] of Object.entries(fields)) {
39
+ merged[typeName][fieldName] = resolver;
40
+ }
41
+ } else {
42
+ merged[typeName] = fields;
43
+ }
44
+ }
45
+ }
46
+ return merged;
47
+ }
48
+ export { mergeGraphQLResolvers, mergeGraphQLSchemas };
@@ -0,0 +1,43 @@
1
+ import { WebSocketServer } from 'ws';
2
+ import { useServer } from 'graphql-ws/use/ws';
3
+ import { parseCookiesFromHeader, resolveSession, flattenHeaders } from '../session/helpers.js';
4
+ function attachGraphQLSubscriptions(options) {
5
+ const { server, schema, session: sessionConfig, log } = options;
6
+ const wss = new WebSocketServer({ noServer: true });
7
+ server.on('upgrade', (req, socket, head) => {
8
+ const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
9
+ if (url.pathname !== '/graphql') return;
10
+ const protocol = req.headers['sec-websocket-protocol'];
11
+ if (!protocol?.includes('graphql-transport-ws')) return;
12
+ wss.handleUpgrade(req, socket, head, (ws) => {
13
+ wss.emit('connection', ws, req);
14
+ });
15
+ });
16
+ const serverCleanup = useServer(
17
+ {
18
+ schema,
19
+ context: async (ctx) => {
20
+ const req = ctx.extra.request;
21
+ const headers = flattenHeaders(req.headers);
22
+ const cookies = parseCookiesFromHeader(req.headers.cookie ?? '');
23
+ const session = await resolveSession(cookies, sessionConfig);
24
+ return { session, headers, cookies };
25
+ },
26
+ onConnect: () => {
27
+ log?.info('GraphQL subscription client connected');
28
+ },
29
+ onDisconnect: () => {
30
+ log?.info('GraphQL subscription client disconnected');
31
+ },
32
+ },
33
+ wss,
34
+ );
35
+ return {
36
+ wss,
37
+ close: async () => {
38
+ await serverCleanup.dispose();
39
+ wss.close();
40
+ },
41
+ };
42
+ }
43
+ export { attachGraphQLSubscriptions };
@@ -0,0 +1,34 @@
1
+ import { toErrorMessage } from './helpers.js';
2
+
3
+ async function checkHealth(deps) {
4
+ const components = {};
5
+ const dbStart = Date.now();
6
+ try {
7
+ await deps.db.raw('SELECT 1');
8
+ components.database = { status: 'ok', responseMs: Date.now() - dbStart };
9
+ } catch (err) {
10
+ components.database = {
11
+ status: 'error',
12
+ responseMs: Date.now() - dbStart,
13
+ error: toErrorMessage(err),
14
+ };
15
+ }
16
+ if (deps.redisClients) {
17
+ for (const { name, client } of deps.redisClients) {
18
+ const start = Date.now();
19
+ try {
20
+ await client.ping();
21
+ components[name] = { status: 'ok', responseMs: Date.now() - start };
22
+ } catch (err) {
23
+ components[name] = {
24
+ status: 'error',
25
+ responseMs: Date.now() - start,
26
+ error: toErrorMessage(err),
27
+ };
28
+ }
29
+ }
30
+ }
31
+ const allOk = Object.values(components).every((c) => c.status === 'ok');
32
+ return { status: allOk ? 'ok' : 'degraded', components };
33
+ }
34
+ export { checkHealth };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Extract a human-readable message from an unknown caught value.
3
+ * Replaces the repeated `err instanceof Error ? err.message : String(err)` pattern.
4
+ */
5
+ function toErrorMessage(err) {
6
+ return err instanceof Error ? err.message : String(err);
7
+ }
8
+
9
+ export { toErrorMessage };
@@ -0,0 +1,55 @@
1
+ import { type } from 'arktype';
2
+ import { buildContext } from './context.js';
3
+ import * as vault from './lib/vault/index.js';
4
+ import { defineRoute } from './route.js';
5
+ import boot from './boot.js';
6
+ import {
7
+ Arcway,
8
+ Solo,
9
+ createTestContext,
10
+ createEventStub,
11
+ createQueueStub,
12
+ createCacheStub,
13
+ createFilesStub,
14
+ createMailStub,
15
+ createLoggerStub,
16
+ } from './testing/index.js';
17
+ import {
18
+ createRateLimitMiddleware,
19
+ MemoryRateLimitStore,
20
+ RedisRateLimitStore,
21
+ } from './rate-limit/index.js';
22
+ import { wsSendToSocket, wsBroadcastToPath } from './ws/registry.js';
23
+ import {
24
+ SYSTEM_JOB_DOMAIN,
25
+ addSystemJobEntry,
26
+ registerSystemJobs,
27
+ validateNoSystemJobCollision,
28
+ getApplicableSystemJobs,
29
+ } from './system-jobs/index.js';
30
+ export {
31
+ MemoryRateLimitStore,
32
+ RedisRateLimitStore,
33
+ Arcway,
34
+ SYSTEM_JOB_DOMAIN,
35
+ Solo,
36
+ addSystemJobEntry,
37
+ boot,
38
+ buildContext,
39
+ createCacheStub,
40
+ createEventStub,
41
+ createFilesStub,
42
+ createLoggerStub,
43
+ createMailStub,
44
+ createQueueStub,
45
+ createRateLimitMiddleware,
46
+ createTestContext,
47
+ defineRoute,
48
+ getApplicableSystemJobs,
49
+ registerSystemJobs,
50
+ type,
51
+ validateNoSystemJobCollision,
52
+ vault,
53
+ wsBroadcastToPath,
54
+ wsSendToSocket,
55
+ };
@@ -0,0 +1,139 @@
1
+ export * from './constants.js';
2
+ import { loadEnvFiles } from './env.js';
3
+ import { makeConfig } from './config/loader.js';
4
+ import { createDB, MigrationSource } from './db/index.js';
5
+ import { runSeeds } from './db/seeds.js';
6
+ import MemoryTransport from './events/drivers/memory.js';
7
+ import RedisTransport from './events/drivers/redis.js';
8
+ import { EventHandler, listenerPathToEvent } from './events/handler.js';
9
+ import JobDispatcher from './jobs/drivers/memory-queue.js';
10
+ import { discoverJobs, JobRunner } from './jobs/runner.js';
11
+ import { parseCron, matchesCron } from './jobs/cron.js';
12
+ import Queue from './queue/index.js';
13
+ import KnexQueueDriver from './queue/drivers/knex.js';
14
+ import RedisQueueDriver from './queue/drivers/redis.js';
15
+ import Cache from './cache/index.js';
16
+ import MemoryCacheDriver from './cache/drivers/memory.js';
17
+ import RedisCacheDriver from './cache/drivers/redis.js';
18
+ import Files from './files/index.js';
19
+ import LocalFileDriver from './files/drivers/local.js';
20
+ import S3FileDriver from './files/drivers/s3.js';
21
+ import { generateOpenAPISpec, typeToOpenAPISchema, toOpenAPIPath } from './docs/openapi.js';
22
+ import { checkServerClientBoundaries, extractImports } from './lint/boundaries.js';
23
+ import { filePathToPattern, compilePattern, discoverRoutes, matchRoute } from './router/routes.js';
24
+ import { ApiRouter } from './router/api-router.js';
25
+ import WebServer from './web-server.js';
26
+ import {
27
+ discoverMiddleware,
28
+ getMiddlewareForRoute,
29
+ buildMiddlewareChain,
30
+ } from './router/middleware.js';
31
+ import { buildCorsHeaders, buildPreflightHeaders } from './router/cors.js';
32
+ import {
33
+ resolveSessionConfig,
34
+ unsealSession,
35
+ sealSession,
36
+ buildSessionSetCookie,
37
+ buildSessionClearCookie,
38
+ } from './session/index.js';
39
+ import { checkHealth } from './health.js';
40
+ import MemoryRateLimitStore from './rate-limit/drivers/memory.js';
41
+ import { createRateLimitMiddleware } from './rate-limit/index.js';
42
+ import { createHttpServer, listen, closeServer } from './server.js';
43
+ import { startWatcher } from './watcher.js';
44
+ import { build } from './build.js';
45
+ import {
46
+ discoverPages,
47
+ discoverLayouts,
48
+ discoverErrorPages,
49
+ resolveLayoutChain,
50
+ matchPage,
51
+ } from './pages/discovery.js';
52
+ import { buildPages, patternToFileName } from './pages/build.js';
53
+ import { createPagesHandler } from './pages/handler.js';
54
+ import { createPagesWatcher } from './pages/watcher.js';
55
+ import {
56
+ discoverGraphQL,
57
+ mergeGraphQLSchemas,
58
+ mergeGraphQLResolvers,
59
+ createLoaderFactory,
60
+ createGraphQLHandler,
61
+ } from './graphql/index.js';
62
+ import {
63
+ discoverWebSocketHandlers,
64
+ mergeWebSocketHandlers,
65
+ createWebSocketServer,
66
+ } from './ws/index.js';
67
+ import { attachGraphQLSubscriptions } from './graphql/subscriptions.js';
68
+ export {
69
+ ApiRouter,
70
+ Cache,
71
+ makeConfig,
72
+ Files,
73
+ KnexQueueDriver,
74
+ LocalFileDriver,
75
+ MemoryCacheDriver,
76
+ JobDispatcher,
77
+ MemoryRateLimitStore,
78
+ MemoryTransport,
79
+ MigrationSource,
80
+ RedisCacheDriver,
81
+ RedisQueueDriver,
82
+ RedisTransport,
83
+ Queue,
84
+ S3FileDriver,
85
+ attachGraphQLSubscriptions,
86
+ build,
87
+ buildCorsHeaders,
88
+ buildMiddlewareChain,
89
+ buildPages,
90
+ buildPreflightHeaders,
91
+ buildSessionClearCookie,
92
+ buildSessionSetCookie,
93
+ checkHealth,
94
+ checkServerClientBoundaries,
95
+ closeServer,
96
+ compilePattern,
97
+ createGraphQLHandler,
98
+ createHttpServer,
99
+ JobRunner,
100
+ createDB,
101
+ createLoaderFactory,
102
+ createPagesHandler,
103
+ createPagesWatcher,
104
+ createRateLimitMiddleware,
105
+ createWebSocketServer,
106
+ discoverJobs,
107
+ EventHandler,
108
+ discoverErrorPages,
109
+ discoverGraphQL,
110
+ discoverLayouts,
111
+ discoverMiddleware,
112
+ discoverPages,
113
+ discoverRoutes,
114
+ discoverWebSocketHandlers,
115
+ extractImports,
116
+ filePathToPattern,
117
+ generateOpenAPISpec,
118
+ getMiddlewareForRoute,
119
+ listen,
120
+ listenerPathToEvent,
121
+ loadEnvFiles,
122
+ matchPage,
123
+ matchRoute,
124
+ matchesCron,
125
+ mergeGraphQLResolvers,
126
+ mergeGraphQLSchemas,
127
+ mergeWebSocketHandlers,
128
+ parseCron,
129
+ patternToFileName,
130
+ resolveLayoutChain,
131
+ resolveSessionConfig,
132
+ runSeeds,
133
+ sealSession,
134
+ startWatcher,
135
+ toOpenAPIPath,
136
+ typeToOpenAPISchema,
137
+ unsealSession,
138
+ WebServer,
139
+ };
@@ -0,0 +1,10 @@
1
+ import { CronExpressionParser } from 'cron-parser';
2
+ function parseCron(expression) {
3
+ return CronExpressionParser.parse(expression);
4
+ }
5
+ function matchesCron(parsed, date) {
6
+ const normalized = new Date(date);
7
+ normalized.setSeconds(0, 0);
8
+ return parsed.includesDate(normalized);
9
+ }
10
+ export { matchesCron, parseCron };
@@ -0,0 +1,207 @@
1
+ import { buildContext } from '../../context.js';
2
+ import { validateEnqueue, toError, calculateBackoff } from '../queue.js';
3
+ import { checkDbThroughput } from '../throughput.js';
4
+ import LeaseManager from './lease.js';
5
+ const STALE_JOB_TIMEOUT_MS = 5 * 60 * 1e3;
6
+ class KnexJobQueue {
7
+ db;
8
+ tableName;
9
+ backoffMs;
10
+ registered = new Map();
11
+ _size = 0;
12
+ leaseManager;
13
+ constructor(db, options) {
14
+ this.db = db;
15
+ this.tableName = options?.tableName ?? 'arcway_jobs';
16
+ this.backoffMs = options?.backoffMs ?? 1000;
17
+ this.leaseManager = new LeaseManager(db, { tableName: options?.leaseTableName });
18
+ }
19
+ /** Create the jobs table if it doesn't exist. Must be called before use. */
20
+ async init() {
21
+ const exists = await this.db.schema.hasTable(this.tableName);
22
+ if (!exists) {
23
+ await this.db.schema.createTable(this.tableName, (table) => {
24
+ table.increments('id').primary();
25
+ table.string('qualified_name').notNullable().index();
26
+ table.string('domain').notNullable();
27
+ table.string('job_name').notNullable();
28
+ table.text('payload').notNullable();
29
+ table.integer('max_retries').notNullable().defaultTo(0);
30
+ table.integer('attempt').notNullable().defaultTo(0);
31
+ table.bigInteger('run_at').notNullable();
32
+ table.string('status').notNullable().defaultTo('pending').index();
33
+ table.timestamp('created_at').defaultTo(this.db.fn.now());
34
+ table.timestamp('updated_at').defaultTo(this.db.fn.now());
35
+ });
36
+ }
37
+ await this.leaseManager.init();
38
+ await this.syncSize();
39
+ }
40
+ register(domain, definition, store) {
41
+ const qualifiedName = `${domain}/${definition.name}`;
42
+ this.registered.set(qualifiedName, { domain, definition, store });
43
+ }
44
+ async enqueue(qualifiedName, payload, options) {
45
+ const { reg, validatedPayload, maxRetries, delay } = validateEnqueue(
46
+ qualifiedName,
47
+ payload,
48
+ this.registered,
49
+ options,
50
+ );
51
+ await this.db(this.tableName).insert({
52
+ qualified_name: qualifiedName,
53
+ domain: reg.domain,
54
+ job_name: reg.definition.name,
55
+ payload: JSON.stringify(validatedPayload),
56
+ max_retries: maxRetries,
57
+ attempt: 0,
58
+ run_at: Date.now() + delay,
59
+ status: 'pending',
60
+ });
61
+ this._size++;
62
+ }
63
+ async process() {
64
+ const now = Date.now();
65
+ await this._recoverStaleJobs(now);
66
+ const jobs = await this._claimReadyJobs(now);
67
+
68
+ const results = [];
69
+ for (const row of jobs) {
70
+ const result = await this._executeJob(row);
71
+ if (result) results.push(result);
72
+ }
73
+ return results;
74
+ }
75
+
76
+ async _recoverStaleJobs(now) {
77
+ const staleCutoff = new Date(now - STALE_JOB_TIMEOUT_MS).toISOString();
78
+ await this.db(this.tableName)
79
+ .where('status', 'running')
80
+ .andWhere('updated_at', '<', staleCutoff)
81
+ .update({ status: 'pending', updated_at: new Date().toISOString() });
82
+ }
83
+
84
+ async _claimReadyJobs(now) {
85
+ return this.db.transaction(async (trx) => {
86
+ const readyRows = await trx(this.tableName)
87
+ .where('status', 'pending')
88
+ .andWhere('run_at', '<=', now)
89
+ .orderBy('id', 'asc')
90
+ .select('id', 'qualified_name');
91
+ if (readyRows.length === 0) return [];
92
+
93
+ const throttledTypes = new Set();
94
+ const allowedIds = [];
95
+ for (const row of readyRows) {
96
+ const qualifiedName = row.qualified_name;
97
+ if (throttledTypes.has(qualifiedName)) continue;
98
+ const reg = this.registered.get(qualifiedName);
99
+ if (reg?.definition.throughput) {
100
+ const allowed = await checkDbThroughput(
101
+ trx,
102
+ this.tableName,
103
+ qualifiedName,
104
+ reg.definition.throughput,
105
+ );
106
+ if (!allowed) {
107
+ throttledTypes.add(qualifiedName);
108
+ continue;
109
+ }
110
+ }
111
+ allowedIds.push(row.id);
112
+ }
113
+ if (allowedIds.length === 0) return [];
114
+
115
+ await trx(this.tableName)
116
+ .whereIn('id', allowedIds)
117
+ .update({ status: 'running', updated_at: new Date().toISOString() });
118
+ return trx(this.tableName).whereIn('id', allowedIds).select('*');
119
+ });
120
+ }
121
+
122
+ async _executeJob(row) {
123
+ const qualifiedName = row.qualified_name;
124
+ const reg = this.registered.get(qualifiedName);
125
+ if (!reg) {
126
+ await this.db(this.tableName).where('id', row.id).update({ status: 'pending' });
127
+ return null;
128
+ }
129
+
130
+ const jobId = row.id;
131
+ const maxConcurrency = reg.definition.maxConcurrency;
132
+
133
+ if (maxConcurrency !== undefined) {
134
+ const acquired = await this.leaseManager.acquire(qualifiedName, jobId, maxConcurrency);
135
+ if (!acquired) {
136
+ await this.db(this.tableName)
137
+ .where('id', jobId)
138
+ .update({ status: 'pending', updated_at: new Date().toISOString() });
139
+ return null;
140
+ }
141
+ this.leaseManager.startHeartbeat(jobId);
142
+ }
143
+
144
+ const attempt = row.attempt + 1;
145
+ const maxAttempts = row.max_retries + 1;
146
+ const payload = JSON.parse(row.payload);
147
+
148
+ try {
149
+ console.log(`[job] ${qualifiedName} attempt ${attempt}/${maxAttempts}`);
150
+ const ctx = buildContext(reg.store, { payload });
151
+ await reg.definition.handler(ctx);
152
+ console.log(`[job] ${qualifiedName} completed`);
153
+ await this._updateJob(jobId, { status: 'completed', attempt });
154
+ this._size--;
155
+ return { id: String(jobId), status: 'completed', attempts: attempt };
156
+ } catch (err) {
157
+ const error = toError(err);
158
+ console.error(
159
+ `[job] ${qualifiedName} attempt ${attempt}/${maxAttempts} failed:`,
160
+ error.message,
161
+ );
162
+
163
+ if (attempt < maxAttempts) {
164
+ const backoffMs = calculateBackoff(this.backoffMs, attempt);
165
+ console.log(
166
+ `[job] ${qualifiedName} retrying in ${backoffMs}ms (attempt ${attempt + 1}/${maxAttempts})`,
167
+ );
168
+ await this._updateJob(jobId, {
169
+ status: 'pending',
170
+ attempt,
171
+ run_at: Date.now() + backoffMs,
172
+ });
173
+ return null;
174
+ }
175
+
176
+ await this._updateJob(jobId, { status: 'failed', attempt });
177
+ this._size--;
178
+ return { id: String(jobId), status: 'failed', error, attempts: attempt };
179
+ } finally {
180
+ if (maxConcurrency !== undefined) {
181
+ await this.leaseManager.release(jobId, qualifiedName);
182
+ }
183
+ }
184
+ }
185
+
186
+ async _updateJob(jobId, fields) {
187
+ await this.db(this.tableName)
188
+ .where('id', jobId)
189
+ .update({ ...fields, updated_at: new Date().toISOString() });
190
+ }
191
+ get size() {
192
+ return this._size;
193
+ }
194
+ /** Release all leases held by this runner (call during graceful shutdown). */
195
+ async shutdown() {
196
+ await this.leaseManager.releaseAll();
197
+ }
198
+ /** Sync the in-memory size counter with the database. */
199
+ async syncSize() {
200
+ const result = await this.db(this.tableName)
201
+ .where('status', 'pending')
202
+ .count('id as count')
203
+ .first();
204
+ this._size = Number(result?.count ?? 0);
205
+ }
206
+ }
207
+ export default KnexJobQueue;