@arcote.tech/arc-host 0.3.4 → 0.4.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 (42) hide show
  1. package/dist/index.d.ts +1 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1024 -384
  4. package/dist/index.js.map +12 -8
  5. package/dist/src/connection-manager.d.ts +16 -1
  6. package/dist/src/connection-manager.d.ts.map +1 -1
  7. package/dist/src/context-handler.d.ts +3 -5
  8. package/dist/src/context-handler.d.ts.map +1 -1
  9. package/dist/src/create-server.d.ts +36 -0
  10. package/dist/src/create-server.d.ts.map +1 -0
  11. package/dist/src/cron-scheduler.d.ts +30 -0
  12. package/dist/src/cron-scheduler.d.ts.map +1 -0
  13. package/dist/src/event-auth.d.ts +6 -1
  14. package/dist/src/event-auth.d.ts.map +1 -1
  15. package/dist/src/index.d.ts +6 -2
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/middleware/http.d.ts +15 -0
  18. package/dist/src/middleware/http.d.ts.map +1 -0
  19. package/dist/src/middleware/index.d.ts +4 -0
  20. package/dist/src/middleware/index.d.ts.map +1 -0
  21. package/dist/src/middleware/types.d.ts +31 -0
  22. package/dist/src/middleware/types.d.ts.map +1 -0
  23. package/dist/src/middleware/ws.d.ts +9 -0
  24. package/dist/src/middleware/ws.d.ts.map +1 -0
  25. package/dist/src/types.d.ts +25 -4
  26. package/dist/src/types.d.ts.map +1 -1
  27. package/index.ts +2 -4
  28. package/package.json +2 -1
  29. package/src/connection-manager.ts +37 -7
  30. package/src/context-handler.ts +22 -23
  31. package/src/create-server.ts +213 -0
  32. package/src/cron-scheduler.ts +124 -0
  33. package/src/event-auth.ts +26 -1
  34. package/src/index.ts +39 -9
  35. package/src/middleware/http.ts +414 -0
  36. package/src/middleware/index.ts +27 -0
  37. package/src/middleware/types.ts +42 -0
  38. package/src/middleware/ws.ts +266 -0
  39. package/src/types.ts +22 -4
  40. package/dist/src/arc-host.d.ts +0 -81
  41. package/dist/src/arc-host.d.ts.map +0 -1
  42. package/src/arc-host.ts +0 -858
@@ -0,0 +1,213 @@
1
+ import type { ArcContextAny, DatabaseAdapter } from "@arcote.tech/arc";
2
+ import type { Server } from "bun";
3
+ import jwt from "jsonwebtoken";
4
+ import { ConnectionManager } from "./connection-manager";
5
+ import { ContextHandler } from "./context-handler";
6
+ import { CronScheduler } from "./cron-scheduler";
7
+ import {
8
+ arcHttpHandlers,
9
+ arcWsHandlers,
10
+ cleanupClientSubs,
11
+ cleanupStreams,
12
+ } from "./middleware";
13
+ import type {
14
+ ArcHttpHandler,
15
+ ArcRequestContext,
16
+ ArcWsContext,
17
+ ArcWsHandler,
18
+ } from "./middleware/types";
19
+ import type { TokenPayload } from "./types";
20
+
21
+ type WebSocketData = { clientId: string };
22
+
23
+ export interface ArcServerConfig {
24
+ context: ArcContextAny;
25
+ dbAdapterFactory: (ctx: any) => Promise<DatabaseAdapter>;
26
+ httpHandlers?: ArcHttpHandler[];
27
+ wsHandlers?: ArcWsHandler[];
28
+ port?: number;
29
+ jwtSecret?: string;
30
+ /** Extra callback when a WS client disconnects (e.g. to clean up platform state) */
31
+ onWsClose?: (clientId: string) => void;
32
+ }
33
+
34
+ export interface ArcServer {
35
+ server: Server<WebSocketData>;
36
+ contextHandler: ContextHandler;
37
+ connectionManager: ConnectionManager;
38
+ cronScheduler: CronScheduler;
39
+ stop: () => void;
40
+ }
41
+
42
+ /**
43
+ * Create an Arc server with composable HTTP + WS middleware.
44
+ *
45
+ * By default includes all Arc handlers (command, query, stream, route, eventSync, health,
46
+ * scope:auth, sync-events, request-sync, execute-command, view subscriptions).
47
+ * Platform servers add their own handlers on top.
48
+ */
49
+ export async function createArcServer(
50
+ config: ArcServerConfig,
51
+ ): Promise<ArcServer> {
52
+ const jwtSecret =
53
+ config.jwtSecret ||
54
+ process.env.JWT_SECRET ||
55
+ "arc-host-secret-change-in-production";
56
+ const port = config.port || 5005;
57
+
58
+ // Init context handler
59
+ const dbAdapter = config.dbAdapterFactory(config.context);
60
+ const contextHandler = new ContextHandler(config.context, dbAdapter);
61
+ await contextHandler.init();
62
+
63
+ // Start cron scheduler
64
+ const cronScheduler = new CronScheduler(contextHandler);
65
+ cronScheduler.start();
66
+
67
+ const connectionManager = new ConnectionManager();
68
+
69
+ const corsHeaders = {
70
+ "Access-Control-Allow-Origin": "*",
71
+ "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
72
+ "Access-Control-Allow-Headers":
73
+ "Content-Type, Authorization, X-Arc-Scope",
74
+ };
75
+
76
+ function verifyToken(token: string): TokenPayload | null {
77
+ try {
78
+ const decoded = jwt.verify(token, jwtSecret) as any;
79
+ if (decoded.tokenName && !decoded.tokenType) {
80
+ return {
81
+ tokenType: decoded.tokenName,
82
+ params: decoded.params || {},
83
+ iat: decoded.iat,
84
+ exp: decoded.exp,
85
+ };
86
+ }
87
+ return decoded as TokenPayload;
88
+ } catch {
89
+ try {
90
+ const parts = token.split(".");
91
+ if (parts.length !== 3) return null;
92
+ const payload = JSON.parse(atob(parts[1]));
93
+ return {
94
+ tokenType: payload.tokenName,
95
+ params: payload.params || {},
96
+ iat: payload.iat,
97
+ exp: payload.exp,
98
+ };
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ }
104
+
105
+ // Build handler chains — Arc defaults + user-provided extras
106
+ const defaultHttp = arcHttpHandlers(contextHandler, connectionManager);
107
+ const defaultWs = arcWsHandlers();
108
+
109
+ const httpHandlers = config.httpHandlers
110
+ ? [...defaultHttp, ...config.httpHandlers]
111
+ : defaultHttp;
112
+
113
+ const wsHandlers = config.wsHandlers
114
+ ? [...defaultWs, ...config.wsHandlers]
115
+ : defaultWs;
116
+
117
+ const wsCtx: ArcWsContext = {
118
+ contextHandler,
119
+ connectionManager,
120
+ verifyToken,
121
+ };
122
+
123
+ const server = Bun.serve<WebSocketData>({
124
+ port,
125
+ idleTimeout: 255,
126
+
127
+ async fetch(req, server) {
128
+ const url = new URL(req.url);
129
+
130
+ // CORS preflight
131
+ if (req.method === "OPTIONS") {
132
+ return new Response(null, { headers: corsHeaders });
133
+ }
134
+
135
+ // Token extraction
136
+ const authHeader = req.headers.get("Authorization");
137
+ const rawToken =
138
+ authHeader?.replace("Bearer ", "") ||
139
+ url.searchParams.get("token");
140
+ const tokenPayload = rawToken ? verifyToken(rawToken) : null;
141
+
142
+ // WebSocket upgrade
143
+ if (
144
+ url.pathname === "/ws" &&
145
+ req.headers.get("Upgrade") === "websocket"
146
+ ) {
147
+ if (server.upgrade(req, { data: { clientId: "" } })) return undefined;
148
+ return new Response("WebSocket upgrade failed", {
149
+ status: 500,
150
+ headers: corsHeaders,
151
+ });
152
+ }
153
+
154
+ // Run HTTP handler chain
155
+ const reqCtx: ArcRequestContext = {
156
+ rawToken,
157
+ tokenPayload,
158
+ corsHeaders,
159
+ };
160
+ for (const handler of httpHandlers) {
161
+ const response = await handler(req, url, reqCtx);
162
+ if (response) return response;
163
+ }
164
+
165
+ return new Response("Not Found", {
166
+ status: 404,
167
+ headers: corsHeaders,
168
+ });
169
+ },
170
+
171
+ websocket: {
172
+ open(ws) {
173
+ connectionManager.addClient(ws as any);
174
+ },
175
+ async message(ws, messageStr) {
176
+ const client = connectionManager.getClientByWs(ws as any);
177
+ if (!client) return;
178
+
179
+ try {
180
+ const message = JSON.parse(messageStr as string);
181
+ for (const handler of wsHandlers) {
182
+ const handled = await handler(client, message, wsCtx);
183
+ if (handled) break;
184
+ }
185
+ } catch (error) {
186
+ console.error("Failed to parse WS message:", error);
187
+ }
188
+ },
189
+ close(ws) {
190
+ const client = connectionManager.getClientByWs(ws as any);
191
+ if (client) {
192
+ cleanupClientSubs(client.id);
193
+ config.onWsClose?.(client.id);
194
+ connectionManager.removeClient(client.id);
195
+ }
196
+ },
197
+ },
198
+ });
199
+
200
+ console.log(`Arc Server running on http://localhost:${port}`);
201
+
202
+ return {
203
+ server,
204
+ contextHandler,
205
+ connectionManager,
206
+ cronScheduler,
207
+ stop: () => {
208
+ cronScheduler.stop();
209
+ cleanupStreams();
210
+ server.stop();
211
+ },
212
+ };
213
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * CronScheduler
3
+ *
4
+ * Discovers aggregate cron methods from context elements and schedules them
5
+ * using node-cron. For each cron trigger, queries all instances of the aggregate
6
+ * and executes the command for each instance.
7
+ */
8
+
9
+ import type { ArcContextAny, AggregateCronMethodEntry } from "@arcote.tech/arc";
10
+ import { Cron } from "croner";
11
+ import type { ContextHandler } from "./context-handler";
12
+
13
+ interface CronJob {
14
+ entry: AggregateCronMethodEntry;
15
+ task: Cron;
16
+ }
17
+
18
+ export class CronScheduler {
19
+ private jobs: CronJob[] = [];
20
+ private contextHandler: ContextHandler;
21
+
22
+ constructor(contextHandler: ContextHandler) {
23
+ this.contextHandler = contextHandler;
24
+ }
25
+
26
+ /**
27
+ * Discover cron methods from all aggregates in the context and start scheduling.
28
+ */
29
+ start(): void {
30
+ const context = this.contextHandler.context;
31
+ const cronEntries = this.discoverCronMethods(context);
32
+
33
+ if (cronEntries.length === 0) return;
34
+
35
+ console.log(
36
+ `[ARC:Cron] Discovered ${cronEntries.length} cron method(s):`,
37
+ );
38
+
39
+ for (const entry of cronEntries) {
40
+ console.log(
41
+ `[ARC:Cron] ${entry.aggregateName}.${entry.methodName} → "${entry.cronExpression}"`,
42
+ );
43
+
44
+ const task = new Cron(entry.cronExpression, async () => {
45
+ await this.executeCronMethod(entry);
46
+ });
47
+
48
+ this.jobs.push({ entry, task });
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Stop all cron jobs.
54
+ */
55
+ stop(): void {
56
+ for (const job of this.jobs) {
57
+ job.task.stop();
58
+ }
59
+ this.jobs = [];
60
+ console.log("[ARC:Cron] All cron jobs stopped.");
61
+ }
62
+
63
+ /**
64
+ * Extract cron method entries from all aggregate context elements.
65
+ */
66
+ private discoverCronMethods(
67
+ context: ArcContextAny,
68
+ ): AggregateCronMethodEntry[] {
69
+ const entries: AggregateCronMethodEntry[] = [];
70
+
71
+ for (const element of context.elements) {
72
+ const ctor = (element as any).ctor;
73
+ if (ctor?.__aggregateCronMethods) {
74
+ entries.push(...ctor.__aggregateCronMethods);
75
+ }
76
+ }
77
+
78
+ return entries;
79
+ }
80
+
81
+ /**
82
+ * Execute a cron method for all instances of the aggregate.
83
+ */
84
+ private async executeCronMethod(
85
+ entry: AggregateCronMethodEntry,
86
+ ): Promise<void> {
87
+ const commandName = `${entry.aggregateName}.${entry.methodName}`;
88
+
89
+ try {
90
+ // Query all instances of this aggregate
91
+ const dataStorage = this.contextHandler.getDataStorage();
92
+ const store = dataStorage.getStore<any>(entry.aggregateName);
93
+ const instances = await store.find({});
94
+
95
+ if (instances.length === 0) {
96
+ console.log(
97
+ `[ARC:Cron] ${commandName}: no instances found, skipping.`,
98
+ );
99
+ return;
100
+ }
101
+
102
+ console.log(
103
+ `[ARC:Cron] ${commandName}: executing for ${instances.length} instance(s)...`,
104
+ );
105
+
106
+ for (const instance of instances) {
107
+ try {
108
+ await this.contextHandler.executeCommand(
109
+ commandName,
110
+ { _id: instance._id },
111
+ null,
112
+ );
113
+ } catch (error) {
114
+ console.error(
115
+ `[ARC:Cron] ${commandName} failed for instance ${instance._id}:`,
116
+ error,
117
+ );
118
+ }
119
+ }
120
+ } catch (error) {
121
+ console.error(`[ARC:Cron] ${commandName} failed:`, error);
122
+ }
123
+ }
124
+ }
package/src/event-auth.ts CHANGED
@@ -109,7 +109,7 @@ function checkConditionsMatch(
109
109
  }
110
110
 
111
111
  /**
112
- * Filter events that a token can receive
112
+ * Filter events that a single token can receive
113
113
  */
114
114
  export function filterEventsForToken(
115
115
  token: TokenPayload | null,
@@ -125,3 +125,28 @@ export function filterEventsForToken(
125
125
  return canTokenReceiveEvent(token, eventDef, eventInstance);
126
126
  });
127
127
  }
128
+
129
+ /**
130
+ * Filter events visible to ANY of the provided tokens.
131
+ * Used for multi-scope clients where an event visible to any scope should be sent.
132
+ */
133
+ export function filterEventsForTokens(
134
+ tokens: TokenPayload[],
135
+ events: SyncableEvent[],
136
+ eventDefinitions: Map<string, ArcEventAny>,
137
+ ): SyncableEvent[] {
138
+ if (tokens.length === 0) {
139
+ // No tokens — only public events
140
+ return filterEventsForToken(null, events, eventDefinitions);
141
+ }
142
+
143
+ return events.filter((eventInstance) => {
144
+ const eventDef = eventDefinitions.get(eventInstance.type);
145
+ if (!eventDef) return false;
146
+
147
+ // Event is visible if ANY token can receive it
148
+ return tokens.some((token) =>
149
+ canTokenReceiveEvent(token, eventDef, eventInstance),
150
+ );
151
+ });
152
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,44 @@
1
- // Main exports
2
- export { ArcHost } from "./arc-host";
1
+ // Server
2
+ export { createArcServer } from "./create-server";
3
+ export type { ArcServer, ArcServerConfig } from "./create-server";
4
+
5
+ // Middleware (composable handlers)
6
+ export {
7
+ arcHttpHandlers,
8
+ arcWsHandlers,
9
+ cleanupClientSubs,
10
+ cleanupStreams,
11
+ commandHandler,
12
+ eventSyncHandler,
13
+ executeCommandHandler,
14
+ healthHandler,
15
+ queryHandler,
16
+ requestSyncHandler,
17
+ routeHandler,
18
+ scopeAuthHandler,
19
+ streamHandler,
20
+ syncEventsHandler,
21
+ querySubscriptionHandler,
22
+ } from "./middleware";
23
+ export type {
24
+ ArcHttpHandler,
25
+ ArcRequestContext,
26
+ ArcWsContext,
27
+ ArcWsHandler,
28
+ } from "./middleware";
29
+
30
+ // Core (unchanged)
3
31
  export { ConnectionManager } from "./connection-manager";
4
32
  export { ContextHandler } from "./context-handler";
33
+ export { CronScheduler } from "./cron-scheduler";
34
+
35
+ // Auth utilities
36
+ export {
37
+ canTokenEmitEvent,
38
+ canTokenReceiveEvent,
39
+ filterEventsForToken,
40
+ filterEventsForTokens,
41
+ } from "./event-auth";
5
42
 
6
43
  // Types
7
44
  export type {
@@ -12,10 +49,3 @@ export type {
12
49
  SyncableEvent,
13
50
  TokenPayload,
14
51
  } from "./types";
15
-
16
- // Auth utilities
17
- export {
18
- canTokenEmitEvent,
19
- canTokenReceiveEvent,
20
- filterEventsForToken,
21
- } from "./event-auth";