@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.
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1024 -384
- 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 -81
- package/dist/src/arc-host.d.ts.map +0 -1
- package/src/arc-host.ts +0 -858
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import type { ArcRouteAny } from "@arcote.tech/arc";
|
|
2
|
+
import { ScopedModel } from "@arcote.tech/arc";
|
|
3
|
+
import type { ConnectionManager } from "../connection-manager";
|
|
4
|
+
import type { ContextHandler } from "../context-handler";
|
|
5
|
+
import type { ArcHttpHandler, ArcRequestContext } from "./types";
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Shared
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
async function parseCommandParams(req: Request): Promise<any> {
|
|
12
|
+
const contentType = req.headers.get("Content-Type") || "";
|
|
13
|
+
if (contentType.includes("multipart/form-data")) {
|
|
14
|
+
const formData = await req.formData();
|
|
15
|
+
const params: Record<string, any> = {};
|
|
16
|
+
for (const [key, value] of formData.entries()) {
|
|
17
|
+
if (
|
|
18
|
+
typeof value === "object" &&
|
|
19
|
+
value !== null &&
|
|
20
|
+
"name" in value &&
|
|
21
|
+
"size" in value
|
|
22
|
+
) {
|
|
23
|
+
params[key] = value;
|
|
24
|
+
} else {
|
|
25
|
+
try {
|
|
26
|
+
params[key] = JSON.parse(value as string);
|
|
27
|
+
} catch {
|
|
28
|
+
params[key] = value;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return params;
|
|
33
|
+
}
|
|
34
|
+
return await req.json();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Health
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
export function healthHandler(cm: ConnectionManager): ArcHttpHandler {
|
|
42
|
+
return (_req, url, ctx) => {
|
|
43
|
+
if (url.pathname !== "/health") return null;
|
|
44
|
+
return Response.json(
|
|
45
|
+
{ status: "ok", clients: cm.clientCount },
|
|
46
|
+
{ headers: ctx.corsHeaders },
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Command — POST /command/:name
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
export function commandHandler(ch: ContextHandler): ArcHttpHandler {
|
|
56
|
+
return async (req, url, ctx) => {
|
|
57
|
+
if (!url.pathname.startsWith("/command/") || req.method !== "POST")
|
|
58
|
+
return null;
|
|
59
|
+
|
|
60
|
+
const commandName = url.pathname.split("/command/")[1];
|
|
61
|
+
if (!commandName) {
|
|
62
|
+
return new Response("Invalid command path", {
|
|
63
|
+
status: 400,
|
|
64
|
+
headers: ctx.corsHeaders,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const params = await parseCommandParams(req);
|
|
70
|
+
const result = await ch.executeCommand(
|
|
71
|
+
commandName,
|
|
72
|
+
params,
|
|
73
|
+
ctx.rawToken,
|
|
74
|
+
);
|
|
75
|
+
return Response.json(result ?? { success: true }, {
|
|
76
|
+
headers: ctx.corsHeaders,
|
|
77
|
+
});
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error(`[ARC] Command '${commandName}' error:`, error);
|
|
80
|
+
return Response.json(
|
|
81
|
+
{ error: (error as Error).message },
|
|
82
|
+
{ status: 500, headers: ctx.corsHeaders },
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Query — POST /query/:viewName
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
export function queryHandler(ch: ContextHandler): ArcHttpHandler {
|
|
93
|
+
return async (req, url, ctx) => {
|
|
94
|
+
if (!url.pathname.startsWith("/query/") || req.method !== "POST")
|
|
95
|
+
return null;
|
|
96
|
+
|
|
97
|
+
const viewName = url.pathname.split("/query/")[1];
|
|
98
|
+
if (!viewName) {
|
|
99
|
+
return new Response("Invalid query path", {
|
|
100
|
+
status: 400,
|
|
101
|
+
headers: ctx.corsHeaders,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
try {
|
|
106
|
+
const params = await req.json();
|
|
107
|
+
const viewElement = ch.getModel().context.get(viewName) as any;
|
|
108
|
+
if (!viewElement?.queryContext) {
|
|
109
|
+
return Response.json(
|
|
110
|
+
{ error: "View not found" },
|
|
111
|
+
{ status: 404, headers: ctx.corsHeaders },
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const scoped = new ScopedModel(ch.getModel(), "request");
|
|
116
|
+
if (ctx.rawToken) scoped.setToken(ctx.rawToken);
|
|
117
|
+
const queryCtx = viewElement.queryContext(scoped.getAdapters());
|
|
118
|
+
const result = await queryCtx.find(params);
|
|
119
|
+
|
|
120
|
+
return Response.json(result, { headers: ctx.corsHeaders });
|
|
121
|
+
} catch (error) {
|
|
122
|
+
return Response.json(
|
|
123
|
+
{ error: (error as Error).message },
|
|
124
|
+
{ status: 500, headers: ctx.corsHeaders },
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// Stream — GET /stream/:viewName (SSE)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
interface StreamConnection {
|
|
135
|
+
id: string;
|
|
136
|
+
controller: ReadableStreamDefaultController<Uint8Array>;
|
|
137
|
+
unsubscribe: () => void;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let streamIdCounter = 0;
|
|
141
|
+
const streamConnections = new Map<string, StreamConnection>();
|
|
142
|
+
|
|
143
|
+
export function streamHandler(ch: ContextHandler): ArcHttpHandler {
|
|
144
|
+
return (_req, url, ctx) => {
|
|
145
|
+
if (!url.pathname.startsWith("/stream/") || _req.method !== "GET")
|
|
146
|
+
return null;
|
|
147
|
+
|
|
148
|
+
const viewName = url.pathname.split("/stream/")[1];
|
|
149
|
+
if (!viewName) {
|
|
150
|
+
return new Response("Invalid stream path", {
|
|
151
|
+
status: 400,
|
|
152
|
+
headers: ctx.corsHeaders,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const findOptions: any = {};
|
|
157
|
+
const whereParam = url.searchParams.get("where");
|
|
158
|
+
if (whereParam) {
|
|
159
|
+
try {
|
|
160
|
+
findOptions.where = JSON.parse(whereParam);
|
|
161
|
+
} catch {
|
|
162
|
+
return new Response("Invalid 'where' parameter", {
|
|
163
|
+
status: 400,
|
|
164
|
+
headers: ctx.corsHeaders,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const orderByParam = url.searchParams.get("orderBy");
|
|
169
|
+
if (orderByParam) {
|
|
170
|
+
try {
|
|
171
|
+
findOptions.orderBy = JSON.parse(orderByParam);
|
|
172
|
+
} catch {
|
|
173
|
+
return new Response("Invalid 'orderBy' parameter", {
|
|
174
|
+
status: 400,
|
|
175
|
+
headers: ctx.corsHeaders,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const limitParam = url.searchParams.get("limit");
|
|
180
|
+
if (limitParam) findOptions.limit = parseInt(limitParam, 10);
|
|
181
|
+
|
|
182
|
+
const streamId = `stream_${++streamIdCounter}_${Date.now()}`;
|
|
183
|
+
const rawToken = ctx.rawToken;
|
|
184
|
+
|
|
185
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
186
|
+
start(controller) {
|
|
187
|
+
const scoped = new ScopedModel(ch.getModel(), "stream");
|
|
188
|
+
if (rawToken) scoped.setToken(rawToken);
|
|
189
|
+
|
|
190
|
+
const descriptor = { element: viewName, method: "find", args: [findOptions] };
|
|
191
|
+
|
|
192
|
+
const sendData = async () => {
|
|
193
|
+
try {
|
|
194
|
+
const data = await scoped.callQuery(descriptor);
|
|
195
|
+
controller.enqueue(
|
|
196
|
+
new TextEncoder().encode(
|
|
197
|
+
`data: ${JSON.stringify({ type: "data", data })}\n\n`,
|
|
198
|
+
),
|
|
199
|
+
);
|
|
200
|
+
} catch {
|
|
201
|
+
unsubscribe();
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Initial data
|
|
206
|
+
sendData();
|
|
207
|
+
|
|
208
|
+
// Subscribe to changes
|
|
209
|
+
const unsubscribe = ch
|
|
210
|
+
.getEventPublisher()
|
|
211
|
+
.subscribe("*", () => sendData());
|
|
212
|
+
|
|
213
|
+
streamConnections.set(streamId, { id: streamId, controller, unsubscribe });
|
|
214
|
+
controller.enqueue(
|
|
215
|
+
new TextEncoder().encode(
|
|
216
|
+
`data: ${JSON.stringify({ type: "connected", streamId })}\n\n`,
|
|
217
|
+
),
|
|
218
|
+
);
|
|
219
|
+
},
|
|
220
|
+
cancel() {
|
|
221
|
+
const conn = streamConnections.get(streamId);
|
|
222
|
+
if (conn) {
|
|
223
|
+
conn.unsubscribe();
|
|
224
|
+
streamConnections.delete(streamId);
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
return new Response(stream, {
|
|
230
|
+
headers: {
|
|
231
|
+
...ctx.corsHeaders,
|
|
232
|
+
"Content-Type": "text/event-stream",
|
|
233
|
+
"Cache-Control": "no-cache",
|
|
234
|
+
Connection: "keep-alive",
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Event Sync — POST /sync/events
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
export function eventSyncHandler(ch: ContextHandler): ArcHttpHandler {
|
|
245
|
+
return async (req, url, ctx) => {
|
|
246
|
+
if (url.pathname !== "/sync/events" || req.method !== "POST") return null;
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const body = await req.json();
|
|
250
|
+
const events = body.events || [];
|
|
251
|
+
const persisted = await ch.persistEvents(
|
|
252
|
+
events.map((e: any) => ({
|
|
253
|
+
localId: e.localId,
|
|
254
|
+
type: e.type,
|
|
255
|
+
payload: e.payload,
|
|
256
|
+
createdAt: e.createdAt,
|
|
257
|
+
})),
|
|
258
|
+
"http-sync",
|
|
259
|
+
ctx.tokenPayload,
|
|
260
|
+
);
|
|
261
|
+
return Response.json(
|
|
262
|
+
{ success: true, syncedIds: persisted.map((e) => e.localId) },
|
|
263
|
+
{ headers: ctx.corsHeaders },
|
|
264
|
+
);
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return Response.json(
|
|
267
|
+
{ success: false, error: (error as Error).message },
|
|
268
|
+
{ status: 500, headers: ctx.corsHeaders },
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ---------------------------------------------------------------------------
|
|
275
|
+
// Route — /route/*
|
|
276
|
+
// ---------------------------------------------------------------------------
|
|
277
|
+
|
|
278
|
+
export function routeHandler(ch: ContextHandler): ArcHttpHandler {
|
|
279
|
+
return async (req, url, ctx) => {
|
|
280
|
+
if (!url.pathname.startsWith("/route/")) return null;
|
|
281
|
+
|
|
282
|
+
const method = req.method as "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
|
|
283
|
+
const context = ch.getModel().context;
|
|
284
|
+
let matchedRoute: ArcRouteAny | null = null;
|
|
285
|
+
let routeParams: Record<string, string> = {};
|
|
286
|
+
|
|
287
|
+
for (const element of context.elements) {
|
|
288
|
+
if (element && typeof (element as any).matchesPath === "function") {
|
|
289
|
+
const route = element as ArcRouteAny;
|
|
290
|
+
const match = route.matchesPath(url.pathname);
|
|
291
|
+
if (match.matches) {
|
|
292
|
+
matchedRoute = route;
|
|
293
|
+
routeParams = match.params;
|
|
294
|
+
break;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (!matchedRoute) {
|
|
300
|
+
return Response.json(
|
|
301
|
+
{ error: "Route not found" },
|
|
302
|
+
{ status: 404, headers: ctx.corsHeaders },
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const handler = matchedRoute.getHandler(method);
|
|
307
|
+
if (!handler) {
|
|
308
|
+
return Response.json(
|
|
309
|
+
{ error: `Method ${method} not allowed` },
|
|
310
|
+
{ status: 405, headers: ctx.corsHeaders },
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Protection checks
|
|
315
|
+
if (!matchedRoute.isPublic && matchedRoute.hasProtections) {
|
|
316
|
+
if (!ctx.tokenPayload) {
|
|
317
|
+
return Response.json(
|
|
318
|
+
{ error: "Unauthorized" },
|
|
319
|
+
{ status: 401, headers: ctx.corsHeaders },
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
let isAuthorized = false;
|
|
323
|
+
for (const protection of matchedRoute.protections) {
|
|
324
|
+
if (protection.token.name === ctx.tokenPayload.tokenType) {
|
|
325
|
+
const mockTokenInstance = {
|
|
326
|
+
params: ctx.tokenPayload.params,
|
|
327
|
+
getTokenDefinition: () => protection.token,
|
|
328
|
+
};
|
|
329
|
+
const allowed = await protection.check(mockTokenInstance as any);
|
|
330
|
+
if (allowed) {
|
|
331
|
+
isAuthorized = true;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (!isAuthorized) {
|
|
337
|
+
return Response.json(
|
|
338
|
+
{ error: "Forbidden" },
|
|
339
|
+
{ status: 403, headers: ctx.corsHeaders },
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
} else if (!matchedRoute.isPublic && !matchedRoute.hasProtections) {
|
|
343
|
+
if (!ctx.tokenPayload) {
|
|
344
|
+
return Response.json(
|
|
345
|
+
{ error: "Unauthorized" },
|
|
346
|
+
{ status: 401, headers: ctx.corsHeaders },
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Build route context with scoped auth
|
|
352
|
+
const scoped = new ScopedModel(ch.getModel(), "request");
|
|
353
|
+
if (ctx.rawToken) scoped.setToken(ctx.rawToken);
|
|
354
|
+
|
|
355
|
+
const authParams = ctx.tokenPayload
|
|
356
|
+
? {
|
|
357
|
+
params: ctx.tokenPayload.params,
|
|
358
|
+
tokenName: ctx.tokenPayload.tokenType,
|
|
359
|
+
}
|
|
360
|
+
: undefined;
|
|
361
|
+
|
|
362
|
+
const routeContext = matchedRoute.buildContext(
|
|
363
|
+
scoped.getAdapters(),
|
|
364
|
+
authParams,
|
|
365
|
+
);
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
const response = await handler(routeContext, req, routeParams, url);
|
|
369
|
+
const newHeaders = new Headers(response.headers);
|
|
370
|
+
for (const [key, value] of Object.entries(ctx.corsHeaders))
|
|
371
|
+
newHeaders.set(key, value);
|
|
372
|
+
const body =
|
|
373
|
+
response.status >= 300 && response.status < 400
|
|
374
|
+
? null
|
|
375
|
+
: response.body;
|
|
376
|
+
return new Response(body, {
|
|
377
|
+
status: response.status,
|
|
378
|
+
statusText: response.statusText,
|
|
379
|
+
headers: newHeaders,
|
|
380
|
+
});
|
|
381
|
+
} catch (error) {
|
|
382
|
+
return Response.json(
|
|
383
|
+
{ error: (error as Error).message },
|
|
384
|
+
{ status: 500, headers: ctx.corsHeaders },
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
// Convenience: all Arc HTTP handlers
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
|
|
394
|
+
export function arcHttpHandlers(
|
|
395
|
+
ch: ContextHandler,
|
|
396
|
+
cm: ConnectionManager,
|
|
397
|
+
): ArcHttpHandler[] {
|
|
398
|
+
return [
|
|
399
|
+
healthHandler(cm),
|
|
400
|
+
commandHandler(ch),
|
|
401
|
+
queryHandler(ch),
|
|
402
|
+
streamHandler(ch),
|
|
403
|
+
eventSyncHandler(ch),
|
|
404
|
+
routeHandler(ch),
|
|
405
|
+
];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Cleanup all active SSE stream connections.
|
|
410
|
+
*/
|
|
411
|
+
export function cleanupStreams(): void {
|
|
412
|
+
for (const conn of streamConnections.values()) conn.unsubscribe();
|
|
413
|
+
streamConnections.clear();
|
|
414
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type {
|
|
2
|
+
ArcHttpHandler,
|
|
3
|
+
ArcRequestContext,
|
|
4
|
+
ArcWsContext,
|
|
5
|
+
ArcWsHandler,
|
|
6
|
+
} from "./types";
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
arcHttpHandlers,
|
|
10
|
+
cleanupStreams,
|
|
11
|
+
commandHandler,
|
|
12
|
+
eventSyncHandler,
|
|
13
|
+
healthHandler,
|
|
14
|
+
queryHandler,
|
|
15
|
+
routeHandler,
|
|
16
|
+
streamHandler,
|
|
17
|
+
} from "./http";
|
|
18
|
+
|
|
19
|
+
export {
|
|
20
|
+
arcWsHandlers,
|
|
21
|
+
cleanupClientSubs,
|
|
22
|
+
executeCommandHandler,
|
|
23
|
+
requestSyncHandler,
|
|
24
|
+
scopeAuthHandler,
|
|
25
|
+
syncEventsHandler,
|
|
26
|
+
querySubscriptionHandler,
|
|
27
|
+
} from "./ws";
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { ConnectionManager } from "../connection-manager";
|
|
2
|
+
import type { ContextHandler } from "../context-handler";
|
|
3
|
+
import type { ConnectedClient, TokenPayload } from "../types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Per-request context passed to HTTP handlers.
|
|
7
|
+
* Created by createArcServer before running the handler chain.
|
|
8
|
+
*/
|
|
9
|
+
export interface ArcRequestContext {
|
|
10
|
+
rawToken: string | null;
|
|
11
|
+
tokenPayload: TokenPayload | null;
|
|
12
|
+
corsHeaders: Record<string, string>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* HTTP middleware handler.
|
|
17
|
+
* Return a Response to stop the chain, or null to pass to the next handler.
|
|
18
|
+
*/
|
|
19
|
+
export type ArcHttpHandler = (
|
|
20
|
+
req: Request,
|
|
21
|
+
url: URL,
|
|
22
|
+
ctx: ArcRequestContext,
|
|
23
|
+
) => Promise<Response | null> | Response | null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Server context shared by all WS handlers.
|
|
27
|
+
*/
|
|
28
|
+
export interface ArcWsContext {
|
|
29
|
+
contextHandler: ContextHandler;
|
|
30
|
+
connectionManager: ConnectionManager;
|
|
31
|
+
verifyToken: (token: string) => TokenPayload | null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* WebSocket message handler.
|
|
36
|
+
* Return true if handled, false to pass to the next handler.
|
|
37
|
+
*/
|
|
38
|
+
export type ArcWsHandler = (
|
|
39
|
+
client: ConnectedClient,
|
|
40
|
+
message: any,
|
|
41
|
+
ctx: ArcWsContext,
|
|
42
|
+
) => Promise<boolean>;
|