@arcote.tech/arc-host 0.3.3 → 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.
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1024 -364
- package/dist/index.js.map +12 -8
- package/dist/src/connection-manager.d.ts +16 -1
- package/dist/src/connection-manager.d.ts.map +1 -1
- package/dist/src/context-handler.d.ts +3 -5
- package/dist/src/context-handler.d.ts.map +1 -1
- package/dist/src/create-server.d.ts +36 -0
- package/dist/src/create-server.d.ts.map +1 -0
- package/dist/src/cron-scheduler.d.ts +30 -0
- package/dist/src/cron-scheduler.d.ts.map +1 -0
- package/dist/src/event-auth.d.ts +6 -1
- package/dist/src/event-auth.d.ts.map +1 -1
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/middleware/http.d.ts +15 -0
- package/dist/src/middleware/http.d.ts.map +1 -0
- package/dist/src/middleware/index.d.ts +4 -0
- package/dist/src/middleware/index.d.ts.map +1 -0
- package/dist/src/middleware/types.d.ts +31 -0
- package/dist/src/middleware/types.d.ts.map +1 -0
- package/dist/src/middleware/ws.d.ts +9 -0
- package/dist/src/middleware/ws.d.ts.map +1 -0
- package/dist/src/types.d.ts +25 -4
- package/dist/src/types.d.ts.map +1 -1
- package/index.ts +2 -4
- package/package.json +2 -1
- package/src/connection-manager.ts +37 -7
- package/src/context-handler.ts +22 -23
- package/src/create-server.ts +213 -0
- package/src/cron-scheduler.ts +124 -0
- package/src/event-auth.ts +26 -1
- package/src/index.ts +39 -9
- package/src/middleware/http.ts +414 -0
- package/src/middleware/index.ts +27 -0
- package/src/middleware/types.ts +42 -0
- package/src/middleware/ws.ts +266 -0
- package/src/types.ts +22 -4
- package/dist/src/arc-host.d.ts +0 -75
- package/dist/src/arc-host.d.ts.map +0 -1
- package/src/arc-host.ts +0 -818
|
@@ -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
|
-
/**
|
|
40
|
-
|
|
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
|
};
|
package/dist/src/arc-host.d.ts
DELETED
|
@@ -1,75 +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
|
-
*/
|
|
51
|
-
private handleHttpCommand;
|
|
52
|
-
/**
|
|
53
|
-
* Handle HTTP query request
|
|
54
|
-
* Uses view.queryContext() to apply protections consistently with liveQuery
|
|
55
|
-
*/
|
|
56
|
-
private handleHttpQuery;
|
|
57
|
-
/**
|
|
58
|
-
* Handle HTTP stream (SSE) request for live queries
|
|
59
|
-
*/
|
|
60
|
-
private handleHttpStream;
|
|
61
|
-
/**
|
|
62
|
-
* Handle HTTP event sync request
|
|
63
|
-
*/
|
|
64
|
-
private handleHttpEventSync;
|
|
65
|
-
/**
|
|
66
|
-
* Handle HTTP route request
|
|
67
|
-
* Matches path against registered routes and executes handler
|
|
68
|
-
*/
|
|
69
|
-
private handleHttpRoute;
|
|
70
|
-
/**
|
|
71
|
-
* Stop the server
|
|
72
|
-
*/
|
|
73
|
-
stop(): void;
|
|
74
|
-
}
|
|
75
|
-
//# 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;;OAEG;YACW,iBAAiB;IAqC/B;;;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"}
|