@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,266 @@
1
+ import { ScopedModel } from "@arcote.tech/arc";
2
+ import { filterEventsForTokens } from "../event-auth";
3
+ import type { ConnectedClient } from "../types";
4
+ import type { ArcWsContext, ArcWsHandler } from "./types";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // View subscription tracking (per client)
8
+ // ---------------------------------------------------------------------------
9
+
10
+ const clientViewSubs = new Map<string, Map<string, () => void>>();
11
+
12
+ export function cleanupClientSubs(clientId: string): void {
13
+ const subs = clientViewSubs.get(clientId);
14
+ if (subs) {
15
+ for (const unsub of subs.values()) unsub();
16
+ clientViewSubs.delete(clientId);
17
+ }
18
+ }
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // scope:auth
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export function scopeAuthHandler(): ArcWsHandler {
25
+ return async (client, message, ctx) => {
26
+ if (message.type !== "scope:auth") return false;
27
+ const decoded = ctx.verifyToken(message.token);
28
+ if (decoded) {
29
+ ctx.connectionManager.setScopeToken(
30
+ client.id,
31
+ message.scope,
32
+ decoded,
33
+ message.token,
34
+ );
35
+ }
36
+ return true;
37
+ };
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // sync-events — persist + broadcast
42
+ // ---------------------------------------------------------------------------
43
+
44
+ export function syncEventsHandler(): ArcWsHandler {
45
+ return async (client, message, ctx) => {
46
+ if (message.type !== "sync-events") return false;
47
+
48
+ const allTokens = ctx.connectionManager.getAllScopeTokens(client.id);
49
+ const token = allTokens.length > 0 ? allTokens[0] : null;
50
+
51
+ const persisted = await ctx.contextHandler.persistEvents(
52
+ message.events,
53
+ client.id,
54
+ token,
55
+ );
56
+ if (persisted.length === 0) return true;
57
+
58
+ // Broadcast to other authorized clients
59
+ for (const c of ctx.connectionManager.getAllClients()) {
60
+ if (c.id === client.id) continue;
61
+ const clientTokens = ctx.connectionManager.getAllScopeTokens(c.id);
62
+ const authorized = filterEventsForTokens(
63
+ clientTokens,
64
+ persisted,
65
+ ctx.contextHandler.getEventDefinitions(),
66
+ );
67
+ if (authorized.length > 0) {
68
+ ctx.connectionManager.sendToClient(c.id, {
69
+ type: "events",
70
+ events: authorized,
71
+ });
72
+ }
73
+ }
74
+
75
+ const last = persisted[persisted.length - 1];
76
+ ctx.connectionManager.updateLastSyncedEventId(client.id, last.hostId);
77
+ ctx.connectionManager.sendToClient(client.id, {
78
+ type: "sync-complete",
79
+ lastHostEventId: last.hostId,
80
+ });
81
+ return true;
82
+ };
83
+ }
84
+
85
+ // ---------------------------------------------------------------------------
86
+ // request-sync — initial sync or catch-up
87
+ // ---------------------------------------------------------------------------
88
+
89
+ export function requestSyncHandler(): ArcWsHandler {
90
+ return async (client, message, ctx) => {
91
+ if (message.type !== "request-sync") return false;
92
+
93
+ const allTokens = ctx.connectionManager.getAllScopeTokens(client.id);
94
+ const token = allTokens.length > 0 ? allTokens[0] : null;
95
+ const events = await ctx.contextHandler.getEventsSince(
96
+ message.lastHostEventId,
97
+ token,
98
+ );
99
+
100
+ ctx.connectionManager.sendToClient(client.id, {
101
+ type: "events",
102
+ events,
103
+ });
104
+
105
+ if (events.length > 0) {
106
+ const last = events[events.length - 1];
107
+ ctx.connectionManager.updateLastSyncedEventId(client.id, last.hostId);
108
+ ctx.connectionManager.sendToClient(client.id, {
109
+ type: "sync-complete",
110
+ lastHostEventId: last.hostId,
111
+ });
112
+ } else {
113
+ ctx.connectionManager.sendToClient(client.id, {
114
+ type: "sync-complete",
115
+ lastHostEventId: message.lastHostEventId || "",
116
+ });
117
+ }
118
+ return true;
119
+ };
120
+ }
121
+
122
+ // ---------------------------------------------------------------------------
123
+ // execute-command
124
+ // ---------------------------------------------------------------------------
125
+
126
+ export function executeCommandHandler(): ArcWsHandler {
127
+ return async (client, message, ctx) => {
128
+ if (message.type !== "execute-command") return false;
129
+
130
+ let rawToken: string | null = null;
131
+ if (client.scopeTokens.size > 0) {
132
+ rawToken = client.scopeTokens.values().next().value!.raw;
133
+ }
134
+
135
+ try {
136
+ const result = await ctx.contextHandler.executeCommand(
137
+ message.commandName,
138
+ message.params,
139
+ rawToken,
140
+ );
141
+ ctx.connectionManager.sendToClient(client.id, {
142
+ type: "command-result",
143
+ requestId: message.requestId,
144
+ result,
145
+ });
146
+ } catch (error) {
147
+ ctx.connectionManager.sendToClient(client.id, {
148
+ type: "command-result",
149
+ requestId: message.requestId,
150
+ error: (error as Error).message,
151
+ });
152
+ }
153
+ return true;
154
+ };
155
+ }
156
+
157
+ // ---------------------------------------------------------------------------
158
+ // subscribe-view / unsubscribe-view — with scope-aware token selection
159
+ // ---------------------------------------------------------------------------
160
+
161
+ export function querySubscriptionHandler(): ArcWsHandler {
162
+ return async (client, message, ctx) => {
163
+ if (message.type === "subscribe-query") {
164
+ const { subscriptionId, descriptor, scope } = message;
165
+
166
+ // Pick the token matching the requested scope
167
+ const scopeToken = scope ? client.scopeTokens.get(scope) : null;
168
+ let rawToken = scopeToken?.raw ?? null;
169
+
170
+ // Fallback to first available token if no scope specified
171
+ if (!rawToken && client.scopeTokens.size > 0) {
172
+ rawToken = client.scopeTokens.values().next().value!.raw;
173
+ }
174
+
175
+ // Per-request scoped model with the right token
176
+ const scoped = new ScopedModel(ctx.contextHandler.getModel(), scope ?? "default");
177
+ if (rawToken) scoped.setToken(rawToken);
178
+
179
+ // Cache last result to avoid re-sending unchanged data
180
+ let lastResultJson = "";
181
+
182
+ const sendData = async () => {
183
+ try {
184
+ const data = await scoped.callQuery(descriptor);
185
+ const json = JSON.stringify(data ?? null);
186
+ if (json === lastResultJson) return; // No change — skip
187
+ lastResultJson = json;
188
+ ctx.connectionManager.sendToClient(client.id, {
189
+ type: "query-data",
190
+ subscriptionId,
191
+ data: data ?? null,
192
+ } as any);
193
+ } catch (err) {
194
+ console.error(`[Arc] Query subscription error:`, err);
195
+ }
196
+ };
197
+
198
+ // Initial data
199
+ sendData();
200
+
201
+ // Subscribe to SPECIFIC event types this element handles (not wildcard)
202
+ const element = ctx.contextHandler.getModel().context.get(descriptor.element);
203
+ const eventTypes = (element as any)?.getElements?.()?.map((e: any) => e.name) ?? [];
204
+
205
+ let debounceTimer: ReturnType<typeof setTimeout> | undefined;
206
+ const debouncedSend = () => {
207
+ if (debounceTimer) clearTimeout(debounceTimer);
208
+ debounceTimer = setTimeout(() => sendData(), 50);
209
+ };
210
+
211
+ const unsubscribes: (() => void)[] = [];
212
+ if (eventTypes.length > 0) {
213
+ for (const eventType of eventTypes) {
214
+ unsubscribes.push(
215
+ ctx.contextHandler.getEventPublisher().subscribe(eventType, debouncedSend),
216
+ );
217
+ }
218
+ } else {
219
+ // Fallback to wildcard if no event types declared
220
+ unsubscribes.push(
221
+ ctx.contextHandler.getEventPublisher().subscribe("*", debouncedSend),
222
+ );
223
+ }
224
+
225
+ const cleanup = () => {
226
+ if (debounceTimer) clearTimeout(debounceTimer);
227
+ for (const unsub of unsubscribes) unsub();
228
+ };
229
+
230
+ // Track for cleanup on disconnect
231
+ if (!clientViewSubs.has(client.id)) {
232
+ clientViewSubs.set(client.id, new Map());
233
+ }
234
+ clientViewSubs.get(client.id)!.set(subscriptionId, cleanup);
235
+ return true;
236
+ }
237
+
238
+ if (message.type === "unsubscribe-query") {
239
+ const subs = clientViewSubs.get(client.id);
240
+ if (subs) {
241
+ const unsub = subs.get(message.subscriptionId);
242
+ if (unsub) {
243
+ unsub();
244
+ subs.delete(message.subscriptionId);
245
+ }
246
+ }
247
+ return true;
248
+ }
249
+
250
+ return false;
251
+ };
252
+ }
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // Convenience: all Arc WS handlers
256
+ // ---------------------------------------------------------------------------
257
+
258
+ export function arcWsHandlers(): ArcWsHandler[] {
259
+ return [
260
+ scopeAuthHandler(),
261
+ syncEventsHandler(),
262
+ requestSyncHandler(),
263
+ executeCommandHandler(),
264
+ querySubscriptionHandler(),
265
+ ];
266
+ }
package/src/types.ts CHANGED
@@ -36,10 +36,8 @@ export interface TokenPayload {
36
36
  export interface ConnectedClient {
37
37
  /** Unique client ID */
38
38
  id: string;
39
- /** Token payload (decoded) */
40
- token: TokenPayload | null;
41
- /** Raw JWT token string */
42
- rawToken: string | null;
39
+ /** Scope tokens: Map<scope, { decoded, raw }> */
40
+ scopeTokens: Map<string, { decoded: TokenPayload; raw: string }>;
43
41
  /** Last synced host event ID */
44
42
  lastHostEventId: string | null;
45
43
  /** WebSocket instance */
@@ -86,6 +84,21 @@ export type ClientToHostMessage =
86
84
  commandName: string;
87
85
  params: any;
88
86
  requestId: string;
87
+ }
88
+ | {
89
+ type: "scope:auth";
90
+ scope: string;
91
+ token: string;
92
+ }
93
+ | {
94
+ type: "subscribe-query";
95
+ subscriptionId: string;
96
+ descriptor: { element: string; method: string; args: any[] };
97
+ scope?: string;
98
+ }
99
+ | {
100
+ type: "unsubscribe-query";
101
+ subscriptionId: string;
89
102
  };
90
103
 
91
104
  /**
@@ -109,4 +122,9 @@ export type HostToClientMessage =
109
122
  | {
110
123
  type: "error";
111
124
  message: string;
125
+ }
126
+ | {
127
+ type: "query-data";
128
+ subscriptionId: string;
129
+ data: any[];
112
130
  };
@@ -1,81 +0,0 @@
1
- import type { ArcHostConfig } from "./types";
2
- /**
3
- * Arc Host - WebSocket server for real-time event sync
4
- */
5
- export declare class ArcHost {
6
- private config;
7
- private server;
8
- private connectionManager;
9
- private contextHandler;
10
- private streamConnections;
11
- private jwtSecret;
12
- private port;
13
- private streamIdCounter;
14
- constructor(config: ArcHostConfig);
15
- /**
16
- * Start the host server
17
- */
18
- start(): Promise<void>;
19
- /**
20
- * Verify JWT token
21
- * Supports both standard JWT (jsonwebtoken) and Arc's custom JWT format
22
- */
23
- private verifyToken;
24
- /**
25
- * Handle WebSocket message
26
- */
27
- private handleMessage;
28
- /**
29
- * Handle sync-events message
30
- */
31
- private handleSyncEvents;
32
- /**
33
- * Handle request-sync message (initial sync or catch-up)
34
- */
35
- private handleRequestSync;
36
- /**
37
- * Handle execute-command message
38
- */
39
- private handleExecuteCommand;
40
- /**
41
- * Send error to WebSocket
42
- */
43
- private sendError;
44
- /**
45
- * Setup Bun server
46
- */
47
- private setupServer;
48
- /**
49
- * Handle HTTP command request
50
- * Supports both JSON and FormData (for file uploads)
51
- */
52
- private handleHttpCommand;
53
- /**
54
- * Parse command parameters from request body
55
- * Handles both JSON and FormData (multipart/form-data for file uploads)
56
- */
57
- private parseCommandParams;
58
- /**
59
- * Handle HTTP query request
60
- * Uses view.queryContext() to apply protections consistently with liveQuery
61
- */
62
- private handleHttpQuery;
63
- /**
64
- * Handle HTTP stream (SSE) request for live queries
65
- */
66
- private handleHttpStream;
67
- /**
68
- * Handle HTTP event sync request
69
- */
70
- private handleHttpEventSync;
71
- /**
72
- * Handle HTTP route request
73
- * Matches path against registered routes and executes handler
74
- */
75
- private handleHttpRoute;
76
- /**
77
- * Stop the server
78
- */
79
- stop(): void;
80
- }
81
- //# sourceMappingURL=arc-host.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"arc-host.d.ts","sourceRoot":"","sources":["../../src/arc-host.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EACV,aAAa,EAId,MAAM,SAAS,CAAC;AAiBjB;;GAEG;AACH,qBAAa,OAAO;IASN,OAAO,CAAC,MAAM;IAR1B,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,iBAAiB,CAA2B;IACpD,OAAO,CAAC,cAAc,CAAkB;IACxC,OAAO,CAAC,iBAAiB,CAAuC;IAChE,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,eAAe,CAAK;gBAER,MAAM,EAAE,aAAa;IAQzC;;OAEG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAS5B;;;OAGG;IACH,OAAO,CAAC,WAAW;IAuCnB;;OAEG;YACW,aAAa;IA4B3B;;OAEG;YACW,gBAAgB;IAgD9B;;OAEG;YACW,iBAAiB;IAkC/B;;OAEG;YACW,oBAAoB;IA0BlC;;OAEG;IACH,OAAO,CAAC,SAAS;IAIjB;;OAEG;IACH,OAAO,CAAC,WAAW;IA6JnB;;;OAGG;YACW,iBAAiB;IAqC/B;;;OAGG;YACW,kBAAkB;IAmChC;;;OAGG;YACW,eAAe;IAoD7B;;OAEG;IACH,OAAO,CAAC,gBAAgB;IA+GxB;;OAEG;YACW,mBAAmB;IA4CjC;;;OAGG;YACW,eAAe;IA6H7B;;OAEG;IACH,IAAI,IAAI,IAAI;CASb"}