alepha 0.20.6 → 0.20.8
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/AGENTS.md +0 -1
- package/CLAUDE.md +0 -1
- package/assets/agents-template.md +0 -1
- package/dist/api/audits/index.browser.js +1 -0
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.d.ts +370 -355
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +1 -0
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.browser.js +1 -0
- package/dist/api/files/index.browser.js.map +1 -1
- package/dist/api/files/index.d.ts +179 -170
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +1 -0
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +7 -0
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +259 -250
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +21 -3
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +198 -192
- package/dist/api/keys/index.d.ts.map +1 -1
- package/dist/api/keys/index.js +1 -0
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +246 -245
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/organizations/index.d.ts +100 -97
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/parameters/index.d.ts +323 -320
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/payments/index.d.ts +431 -376
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js +202 -87
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/subscriptions/index.d.ts +1695 -0
- package/dist/api/subscriptions/index.d.ts.map +1 -0
- package/dist/api/subscriptions/index.js +1919 -0
- package/dist/api/subscriptions/index.js.map +1 -0
- package/dist/api/users/index.d.ts +857 -841
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/verifications/index.d.ts +128 -127
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/bucket/index.d.ts +3 -2
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/cache/core/index.d.ts +114 -4
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +181 -15
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js +181 -15
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/cache/database/index.d.ts +20 -19
- package/dist/cache/database/index.d.ts.map +1 -1
- package/dist/cache/redis/index.d.ts +3 -2
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cli/core/index.d.ts +116 -132
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +75 -7
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.d.ts +3 -2
- package/dist/cli/devtools/index.d.ts.map +1 -1
- package/dist/cli/platform/index.d.ts +346 -290
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +105 -6
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.d.ts +12 -11
- package/dist/cli/vendor/index.d.ts.map +1 -1
- package/dist/command/index.d.ts +5 -4
- package/dist/command/index.d.ts.map +1 -1
- package/dist/core/index.browser.js +1 -1
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +119 -118
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +1 -1
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +1 -1
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.d.ts +3 -2
- package/dist/crypto/index.d.ts.map +1 -1
- package/dist/email/core/index.d.ts +3 -2
- package/dist/email/core/index.d.ts.map +1 -1
- package/dist/email/smtp/index.d.ts +7 -6
- package/dist/email/smtp/index.d.ts.map +1 -1
- package/dist/lock/core/index.d.ts +5 -4
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/logger/index.d.ts +10 -9
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/mcp/index.d.ts +9 -8
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +9 -3
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +31 -10
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +33 -14
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +31 -10
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +6 -5
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/queue/core/index.d.ts +5 -4
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/redis/index.d.ts +3 -2
- package/dist/queue/redis/index.d.ts.map +1 -1
- package/dist/react/form/index.d.ts +5 -0
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +6 -4
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/i18n/index.d.ts +2 -1
- package/dist/react/i18n/index.d.ts.map +1 -1
- package/dist/react/router/index.d.ts +206 -205
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/ui/index.d.ts +11 -11
- package/dist/react/ui/index.d.ts.map +1 -1
- package/dist/scheduler/index.d.ts +3 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/security/index.browser.js +29 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +82 -35
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +56 -3
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +163 -158
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +16 -4
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/core/index.d.ts +35 -34
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/cors/index.d.ts +7 -6
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/health/index.d.ts +16 -15
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/links/index.d.ts +51 -50
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.d.ts +6 -5
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/swagger/index.d.ts +2 -1
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/topic/redis/index.d.ts +3 -2
- package/dist/topic/redis/index.d.ts.map +1 -1
- package/package.json +16 -32
- package/src/api/audits/entities/audits.ts +1 -0
- package/src/api/files/entities/files.ts +1 -0
- package/src/api/jobs/__tests__/$job.spec.ts +92 -40
- package/src/api/jobs/entities/jobExecutionEntity.ts +1 -0
- package/src/api/jobs/providers/JobProvider.ts +20 -5
- package/src/api/jobs/schemas/jobConfigAtom.ts +5 -0
- package/src/api/keys/entities/apiKeyEntity.ts +1 -0
- package/src/api/payments/controllers/MockCheckoutController.ts +146 -0
- package/src/api/payments/index.ts +3 -0
- package/src/api/payments/providers/MemoryPaymentProvider.ts +9 -4
- package/src/api/payments/providers/PaymentProvider.ts +25 -9
- package/src/api/payments/services/PaymentService.ts +3 -0
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +218 -0
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +278 -0
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +212 -0
- package/src/api/subscriptions/controllers/SubscriptionController.ts +189 -0
- package/src/api/subscriptions/entities/subscriptionEvents.ts +54 -0
- package/src/api/subscriptions/entities/subscriptions.ts +68 -0
- package/src/api/subscriptions/index.ts +133 -0
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +382 -0
- package/src/api/subscriptions/middleware/$requireLimit.ts +50 -0
- package/src/api/subscriptions/middleware/$requirePlan.ts +49 -0
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +110 -0
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +8 -0
- package/src/api/subscriptions/schemas/changePlanSchema.ts +9 -0
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +11 -0
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +21 -0
- package/src/api/subscriptions/schemas/mrrSchema.ts +13 -0
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +71 -0
- package/src/api/subscriptions/schemas/planResourceSchema.ts +25 -0
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +8 -0
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +19 -0
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +6 -0
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +32 -0
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +23 -0
- package/src/api/subscriptions/services/BillingService.ts +437 -0
- package/src/api/subscriptions/services/SubscriptionConfig.ts +56 -0
- package/src/api/subscriptions/services/SubscriptionService.ts +867 -0
- package/src/api/subscriptions/services/UsageService.ts +118 -0
- package/src/cache/core/__tests__/$cache.memory.spec.ts +450 -0
- package/src/cache/core/__tests__/$cache.swr.spec.ts +394 -0
- package/src/cache/core/index.ts +16 -0
- package/src/cache/core/primitives/$cache.ts +347 -21
- package/src/cli/core/tasks/BuildCloudflareTask.ts +16 -0
- package/src/cli/core/templates/agentMd.ts +39 -4
- package/src/cli/core/templates/biomeJson.ts +25 -1
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +2 -2
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +117 -0
- package/src/cli/platform/adapters/CloudflareAdapter.ts +104 -7
- package/src/cli/platform/atoms/platformOptions.ts +13 -0
- package/src/cli/platform/schemas/platform.ts +1 -0
- package/src/cli/platform/services/CloudflareApi.ts +61 -0
- package/src/cli/platform/services/PlatformOrchestrator.ts +9 -4
- package/src/core/__tests__/$module.spec.ts +2 -2
- package/src/core/primitives/$module.ts +4 -4
- package/src/mcp/providers/McpServerProvider.ts +1 -1
- package/src/orm/core/providers/DatabaseTypeProvider.ts +9 -3
- package/src/orm/core/providers/drivers/DatabaseProvider.ts +1 -1
- package/src/orm/core/schemas/insertSchema.ts +10 -2
- package/src/orm/core/services/Repository.ts +27 -7
- package/src/react/form/hooks/useFormState.ts +8 -1
- package/src/react/form/index.ts +10 -1
- package/src/react/form/services/FormModel.ts +9 -3
- package/src/security/atoms/currentTenantAtom.ts +34 -0
- package/src/security/index.browser.ts +1 -0
- package/src/security/index.ts +12 -1
- package/src/security/primitives/$issuer.ts +17 -1
- package/src/security/providers/SecurityProvider.ts +37 -0
- package/src/server/auth/__tests__/validateRedirectUri.spec.ts +78 -0
- package/src/server/auth/providers/ServerAuthProvider.ts +21 -5
- package/tsconfig.base.json +2 -1
- package/dist/react/websocket/index.d.ts +0 -117
- package/dist/react/websocket/index.d.ts.map +0 -1
- package/dist/react/websocket/index.js +0 -108
- package/dist/react/websocket/index.js.map +0 -1
- package/dist/websocket/index.browser.js +0 -848
- package/dist/websocket/index.browser.js.map +0 -1
- package/dist/websocket/index.d.ts +0 -876
- package/dist/websocket/index.d.ts.map +0 -1
- package/dist/websocket/index.js +0 -1185
- package/dist/websocket/index.js.map +0 -1
- package/src/react/websocket/hooks/useRoom.tsx +0 -251
- package/src/react/websocket/index.ts +0 -7
- package/src/websocket/__tests__/$channel.spec.ts +0 -30
- package/src/websocket/__tests__/$websocket-new.spec.ts +0 -195
- package/src/websocket/__tests__/RoomManager.spec.ts +0 -146
- package/src/websocket/__tests__/websocket-integration.spec.ts +0 -951
- package/src/websocket/errors/WebSocketError.ts +0 -34
- package/src/websocket/index.browser.ts +0 -25
- package/src/websocket/index.shared.ts +0 -8
- package/src/websocket/index.ts +0 -85
- package/src/websocket/interfaces/WebSocketInterfaces.ts +0 -252
- package/src/websocket/primitives/$channel.ts +0 -131
- package/src/websocket/primitives/$websocket.ts +0 -107
- package/src/websocket/providers/NodeWebSocketServerProvider.ts +0 -617
- package/src/websocket/providers/WebSocketServerProvider.ts +0 -56
- package/src/websocket/services/RoomManager.ts +0 -160
- package/src/websocket/services/WebSocketClient.ts +0 -642
- package/src/websocket/services/WebSocketTopicService.ts +0 -108
|
@@ -1,642 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
$env,
|
|
3
|
-
$inject,
|
|
4
|
-
Alepha,
|
|
5
|
-
AlephaError,
|
|
6
|
-
SchemaValidator,
|
|
7
|
-
type Static,
|
|
8
|
-
t,
|
|
9
|
-
} from "alepha";
|
|
10
|
-
import { $logger } from "alepha/logger";
|
|
11
|
-
import type { ChannelPrimitive, TWSObject } from "../primitives/$channel.ts";
|
|
12
|
-
|
|
13
|
-
const envSchema = t.object({
|
|
14
|
-
WEBSOCKET_URL: t.text({
|
|
15
|
-
default: "",
|
|
16
|
-
description:
|
|
17
|
-
"WebSocket server URL (e.g., ws://localhost:3001). Leave empty to auto-detect.",
|
|
18
|
-
}),
|
|
19
|
-
WEBSOCKET_RECONNECT_INTERVAL: t.integer({
|
|
20
|
-
default: 3000,
|
|
21
|
-
description: "Reconnection interval in milliseconds",
|
|
22
|
-
}),
|
|
23
|
-
WEBSOCKET_MAX_RECONNECT_ATTEMPTS: t.integer({
|
|
24
|
-
default: 10,
|
|
25
|
-
description:
|
|
26
|
-
"Maximum number of reconnection attempts. Set to -1 for infinite.",
|
|
27
|
-
}),
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
declare module "alepha" {
|
|
31
|
-
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Room subscription
|
|
36
|
-
*/
|
|
37
|
-
interface RoomSubscription<TClient extends TWSObject> {
|
|
38
|
-
roomId: string;
|
|
39
|
-
handler: (message: Static<TClient>) => void;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* WebSocket channel connection
|
|
44
|
-
*
|
|
45
|
-
* Manages a single WebSocket connection to a channel with multiple room subscriptions.
|
|
46
|
-
* One connection can handle multiple rooms on the same channel.
|
|
47
|
-
*/
|
|
48
|
-
export class WebSocketChannelConnection<
|
|
49
|
-
TClient extends TWSObject,
|
|
50
|
-
TServer extends TWSObject,
|
|
51
|
-
> {
|
|
52
|
-
protected readonly alepha = $inject(Alepha);
|
|
53
|
-
protected readonly schemaValidator = $inject(SchemaValidator);
|
|
54
|
-
protected readonly log = $logger();
|
|
55
|
-
protected ws?: WebSocket;
|
|
56
|
-
protected reconnectAttempts = 0;
|
|
57
|
-
protected reconnectTimer?: number;
|
|
58
|
-
protected static readonly MAX_QUEUE_SIZE = 1000;
|
|
59
|
-
protected messageQueue: Array<{ roomId: string; message: Static<TServer> }> =
|
|
60
|
-
[];
|
|
61
|
-
|
|
62
|
-
// Room subscriptions: Map<roomId, handler>
|
|
63
|
-
protected subscriptions = new Map<
|
|
64
|
-
string,
|
|
65
|
-
(message: Static<TClient>) => void
|
|
66
|
-
>();
|
|
67
|
-
|
|
68
|
-
// Connection state
|
|
69
|
-
public isConnected = false;
|
|
70
|
-
public isConnecting = false;
|
|
71
|
-
public isError = false;
|
|
72
|
-
public error?: Error;
|
|
73
|
-
protected connectPromise?: Promise<void>;
|
|
74
|
-
|
|
75
|
-
// Connection callbacks
|
|
76
|
-
protected onConnectCallbacks = new Set<() => void>();
|
|
77
|
-
protected onDisconnectCallbacks = new Set<() => void>();
|
|
78
|
-
protected onErrorCallbacks = new Set<(error: Error) => void>();
|
|
79
|
-
|
|
80
|
-
constructor(
|
|
81
|
-
protected readonly channel: ChannelPrimitive<TClient, TServer>,
|
|
82
|
-
protected readonly options: {
|
|
83
|
-
url?: string;
|
|
84
|
-
autoReconnect?: boolean;
|
|
85
|
-
reconnectInterval?: number;
|
|
86
|
-
maxReconnectAttempts?: number;
|
|
87
|
-
},
|
|
88
|
-
protected readonly env: Static<typeof envSchema>,
|
|
89
|
-
) {}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Build WebSocket URL
|
|
93
|
-
*/
|
|
94
|
-
protected buildUrl(): string {
|
|
95
|
-
this.log.trace("Building WebSocket URL", {
|
|
96
|
-
hasCustomUrl: !!this.options.url,
|
|
97
|
-
channelPath: this.channel.options.path,
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
if (this.options.url) {
|
|
101
|
-
this.log.debug("Using custom WebSocket URL", { url: this.options.url });
|
|
102
|
-
return this.options.url;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
// Auto-detect URL from current location (browser only)
|
|
106
|
-
if (typeof window !== "undefined") {
|
|
107
|
-
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
|
|
108
|
-
const host = window.location.host;
|
|
109
|
-
const path = this.channel.options.path;
|
|
110
|
-
// Send all room IDs as query params
|
|
111
|
-
const roomIds = Array.from(this.subscriptions.keys());
|
|
112
|
-
const roomParam =
|
|
113
|
-
roomIds.length > 0 ? `?roomIds=${roomIds.join(",")}` : "";
|
|
114
|
-
const url = `${protocol}//${host}${path}${roomParam}`;
|
|
115
|
-
this.log.debug("Auto-detected WebSocket URL", { url, roomIds });
|
|
116
|
-
return url;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Fallback to env URL
|
|
120
|
-
const url = `${this.env.WEBSOCKET_URL}${this.channel.options.path}`;
|
|
121
|
-
this.log.debug("Using env WebSocket URL", { url });
|
|
122
|
-
return url;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Subscribe to a room on this channel
|
|
127
|
-
*/
|
|
128
|
-
public subscribe(
|
|
129
|
-
roomId: string,
|
|
130
|
-
handler: (message: Static<TClient>) => void,
|
|
131
|
-
callbacks?: {
|
|
132
|
-
onConnect?: () => void;
|
|
133
|
-
onDisconnect?: () => void;
|
|
134
|
-
onError?: (error: Error) => void;
|
|
135
|
-
},
|
|
136
|
-
): () => void {
|
|
137
|
-
this.log.debug("Subscribing to room", {
|
|
138
|
-
roomId,
|
|
139
|
-
channelPath: this.channel.options.path,
|
|
140
|
-
existingSubscriptions: this.subscriptions.size,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// Add subscription
|
|
144
|
-
this.subscriptions.set(roomId, handler);
|
|
145
|
-
|
|
146
|
-
// Add callbacks
|
|
147
|
-
if (callbacks?.onConnect) this.onConnectCallbacks.add(callbacks.onConnect);
|
|
148
|
-
if (callbacks?.onDisconnect)
|
|
149
|
-
this.onDisconnectCallbacks.add(callbacks.onDisconnect);
|
|
150
|
-
if (callbacks?.onError) this.onErrorCallbacks.add(callbacks.onError);
|
|
151
|
-
|
|
152
|
-
// Connect or reconnect to include the new room in the URL
|
|
153
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
154
|
-
this.log.trace("No active connection, initiating connect");
|
|
155
|
-
this.connect().catch((error) => {
|
|
156
|
-
this.log.error("Failed to connect:", error);
|
|
157
|
-
});
|
|
158
|
-
} else {
|
|
159
|
-
this.log.trace("Reconnecting to include new room subscription", {
|
|
160
|
-
roomId,
|
|
161
|
-
});
|
|
162
|
-
this.reconnect();
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Return unsubscribe function
|
|
166
|
-
return () => {
|
|
167
|
-
this.log.debug("Unsubscribing from room", { roomId });
|
|
168
|
-
this.subscriptions.delete(roomId);
|
|
169
|
-
if (callbacks?.onConnect)
|
|
170
|
-
this.onConnectCallbacks.delete(callbacks.onConnect);
|
|
171
|
-
if (callbacks?.onDisconnect)
|
|
172
|
-
this.onDisconnectCallbacks.delete(callbacks.onDisconnect);
|
|
173
|
-
if (callbacks?.onError) this.onErrorCallbacks.delete(callbacks.onError);
|
|
174
|
-
|
|
175
|
-
// Disconnect if no more subscriptions
|
|
176
|
-
if (this.subscriptions.size === 0) {
|
|
177
|
-
this.log.debug("No more subscriptions, disconnecting");
|
|
178
|
-
this.disconnect();
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Connect to WebSocket server
|
|
185
|
-
*/
|
|
186
|
-
protected async connect(): Promise<void> {
|
|
187
|
-
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
188
|
-
this.log.trace("Already connected, skipping connect");
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
if (this.connectPromise) {
|
|
193
|
-
this.log.trace("Connection already in progress, reusing promise");
|
|
194
|
-
return this.connectPromise;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
this.isConnecting = true;
|
|
198
|
-
this.isError = false;
|
|
199
|
-
this.error = undefined;
|
|
200
|
-
|
|
201
|
-
const url = this.buildUrl();
|
|
202
|
-
this.log.info("Connecting to WebSocket server", { url });
|
|
203
|
-
|
|
204
|
-
this.connectPromise = new Promise<void>((resolve, reject) => {
|
|
205
|
-
try {
|
|
206
|
-
const ws = new WebSocket(url);
|
|
207
|
-
this.ws = ws;
|
|
208
|
-
|
|
209
|
-
ws.onopen = () => {
|
|
210
|
-
this.isConnected = true;
|
|
211
|
-
this.isConnecting = false;
|
|
212
|
-
this.isError = false;
|
|
213
|
-
this.error = undefined;
|
|
214
|
-
this.reconnectAttempts = 0;
|
|
215
|
-
|
|
216
|
-
this.log.info("WebSocket connected", {
|
|
217
|
-
channelPath: this.channel.options.path,
|
|
218
|
-
rooms: Array.from(this.subscriptions.keys()),
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
// Flush queued messages
|
|
222
|
-
if (this.messageQueue.length > 0) {
|
|
223
|
-
this.log.debug("Flushing queued messages", {
|
|
224
|
-
count: this.messageQueue.length,
|
|
225
|
-
});
|
|
226
|
-
}
|
|
227
|
-
while (this.messageQueue.length > 0) {
|
|
228
|
-
const msg = this.messageQueue.shift();
|
|
229
|
-
if (msg) {
|
|
230
|
-
this.log.trace("Sending queued message", { roomId: msg.roomId });
|
|
231
|
-
ws.send(
|
|
232
|
-
JSON.stringify({
|
|
233
|
-
roomId: msg.roomId,
|
|
234
|
-
message: msg.message,
|
|
235
|
-
}),
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// Call all connect callbacks
|
|
241
|
-
for (const callback of this.onConnectCallbacks) {
|
|
242
|
-
callback();
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
resolve();
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
ws.onmessage = (event) => {
|
|
249
|
-
this.log.trace("Message received", {
|
|
250
|
-
dataLength: event.data?.length,
|
|
251
|
-
});
|
|
252
|
-
this.handleMessage(event.data);
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
ws.onclose = (event) => {
|
|
256
|
-
this.isConnected = false;
|
|
257
|
-
this.isConnecting = false;
|
|
258
|
-
this.ws = undefined;
|
|
259
|
-
|
|
260
|
-
this.log.info("WebSocket disconnected", {
|
|
261
|
-
code: event.code,
|
|
262
|
-
reason: event.reason,
|
|
263
|
-
wasClean: event.wasClean,
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
// Call all disconnect callbacks
|
|
267
|
-
for (const callback of this.onDisconnectCallbacks) {
|
|
268
|
-
callback();
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
// Attempt reconnection
|
|
272
|
-
if (this.options.autoReconnect !== false) {
|
|
273
|
-
this.scheduleReconnect();
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
ws.onerror = () => {
|
|
278
|
-
const err = new Error("WebSocket connection error");
|
|
279
|
-
this.isError = true;
|
|
280
|
-
this.error = err;
|
|
281
|
-
this.isConnecting = false;
|
|
282
|
-
|
|
283
|
-
this.log.error("WebSocket error", { url });
|
|
284
|
-
|
|
285
|
-
// Call all error callbacks
|
|
286
|
-
for (const callback of this.onErrorCallbacks) {
|
|
287
|
-
callback(err);
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
reject(err);
|
|
291
|
-
};
|
|
292
|
-
} catch (err) {
|
|
293
|
-
const error =
|
|
294
|
-
err instanceof Error ? err : new Error("Connection failed");
|
|
295
|
-
this.isError = true;
|
|
296
|
-
this.error = error;
|
|
297
|
-
this.isConnecting = false;
|
|
298
|
-
|
|
299
|
-
this.log.error("Failed to create WebSocket", { error: error.message });
|
|
300
|
-
|
|
301
|
-
// Call all error callbacks
|
|
302
|
-
for (const callback of this.onErrorCallbacks) {
|
|
303
|
-
callback(error);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
reject(error);
|
|
307
|
-
}
|
|
308
|
-
}).finally(() => {
|
|
309
|
-
this.connectPromise = undefined;
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
return this.connectPromise;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
/**
|
|
316
|
-
* Handle incoming message
|
|
317
|
-
*/
|
|
318
|
-
protected handleMessage(data: string): void {
|
|
319
|
-
try {
|
|
320
|
-
const parsed = JSON.parse(data);
|
|
321
|
-
this.log.trace("Parsed incoming message", { parsed });
|
|
322
|
-
|
|
323
|
-
// Validate incoming message against schema
|
|
324
|
-
const inSchema = this.channel.options.schema.in;
|
|
325
|
-
this.alepha.codec.validate(inSchema, parsed);
|
|
326
|
-
|
|
327
|
-
this.log.debug("Dispatching message to handlers", {
|
|
328
|
-
handlerCount: this.subscriptions.size,
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
// Extract roomId from message if present (server should send it back)
|
|
332
|
-
// For now, broadcast to all subscribed rooms
|
|
333
|
-
// TODO: Server should include roomId in response
|
|
334
|
-
for (const handler of this.subscriptions.values()) {
|
|
335
|
-
handler(parsed as Static<TClient>);
|
|
336
|
-
}
|
|
337
|
-
} catch (err) {
|
|
338
|
-
this.log.error("Error handling message:", err);
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
/**
|
|
343
|
-
* Send message to a specific room
|
|
344
|
-
*/
|
|
345
|
-
public async send(roomId: string, message: Static<TServer>): Promise<void> {
|
|
346
|
-
this.log.trace("Sending message", { roomId, message });
|
|
347
|
-
|
|
348
|
-
// Validate outgoing message against schema
|
|
349
|
-
const outSchema = this.channel.options.schema.out;
|
|
350
|
-
try {
|
|
351
|
-
this.schemaValidator.validate(outSchema, message, {
|
|
352
|
-
trim: false,
|
|
353
|
-
nullToUndefined: false,
|
|
354
|
-
deleteUndefined: false,
|
|
355
|
-
});
|
|
356
|
-
} catch (err) {
|
|
357
|
-
this.log.warn("Message validation failed", { error: err });
|
|
358
|
-
throw new AlephaError(
|
|
359
|
-
`Message validation failed: ${(err as Error).message}`,
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
364
|
-
if (
|
|
365
|
-
this.messageQueue.length >= WebSocketChannelConnection.MAX_QUEUE_SIZE
|
|
366
|
-
) {
|
|
367
|
-
this.log.warn("Message queue full, dropping oldest message", {
|
|
368
|
-
roomId,
|
|
369
|
-
queueSize: this.messageQueue.length,
|
|
370
|
-
});
|
|
371
|
-
this.messageQueue.shift();
|
|
372
|
-
}
|
|
373
|
-
this.log.debug("Connection not ready, queuing message", {
|
|
374
|
-
roomId,
|
|
375
|
-
queueSize: this.messageQueue.length + 1,
|
|
376
|
-
});
|
|
377
|
-
this.messageQueue.push({ roomId, message });
|
|
378
|
-
return;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
this.log.debug("Sending message to server", { roomId });
|
|
382
|
-
this.ws.send(
|
|
383
|
-
JSON.stringify({
|
|
384
|
-
roomId,
|
|
385
|
-
message,
|
|
386
|
-
}),
|
|
387
|
-
);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Schedule reconnection
|
|
392
|
-
*/
|
|
393
|
-
protected scheduleReconnect(): void {
|
|
394
|
-
// Clear any pending reconnect timer
|
|
395
|
-
if (this.reconnectTimer) {
|
|
396
|
-
clearTimeout(this.reconnectTimer);
|
|
397
|
-
this.reconnectTimer = undefined;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const maxAttempts =
|
|
401
|
-
this.options.maxReconnectAttempts ??
|
|
402
|
-
this.env.WEBSOCKET_MAX_RECONNECT_ATTEMPTS ??
|
|
403
|
-
10;
|
|
404
|
-
const reconnectInterval =
|
|
405
|
-
this.options.reconnectInterval ??
|
|
406
|
-
this.env.WEBSOCKET_RECONNECT_INTERVAL ??
|
|
407
|
-
3000;
|
|
408
|
-
|
|
409
|
-
if (maxAttempts !== -1 && this.reconnectAttempts >= maxAttempts) {
|
|
410
|
-
this.log.warn("Max reconnection attempts reached", {
|
|
411
|
-
attempts: this.reconnectAttempts,
|
|
412
|
-
maxAttempts,
|
|
413
|
-
});
|
|
414
|
-
return;
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
this.reconnectAttempts++;
|
|
418
|
-
|
|
419
|
-
this.log.debug("Scheduling reconnection", {
|
|
420
|
-
attempt: this.reconnectAttempts,
|
|
421
|
-
maxAttempts,
|
|
422
|
-
intervalMs: reconnectInterval,
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
this.reconnectTimer = window.setTimeout(() => {
|
|
426
|
-
this.log.info("Reconnecting...", {
|
|
427
|
-
attempt: this.reconnectAttempts,
|
|
428
|
-
maxAttempts,
|
|
429
|
-
});
|
|
430
|
-
this.connect().catch((error) => {
|
|
431
|
-
this.log.error("Reconnection failed:", error);
|
|
432
|
-
});
|
|
433
|
-
}, reconnectInterval);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Disconnect from server
|
|
438
|
-
*/
|
|
439
|
-
public disconnect(): void {
|
|
440
|
-
this.log.debug("Disconnecting", {
|
|
441
|
-
hasTimer: !!this.reconnectTimer,
|
|
442
|
-
hasConnection: !!this.ws,
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
if (this.reconnectTimer) {
|
|
446
|
-
clearTimeout(this.reconnectTimer);
|
|
447
|
-
this.reconnectTimer = undefined;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (this.ws) {
|
|
451
|
-
this.ws.close();
|
|
452
|
-
this.ws = undefined;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
this.isConnected = false;
|
|
456
|
-
this.isConnecting = false;
|
|
457
|
-
this.connectPromise = undefined;
|
|
458
|
-
|
|
459
|
-
this.log.info("Disconnected");
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
/**
|
|
463
|
-
* Reconnect manually
|
|
464
|
-
*/
|
|
465
|
-
public reconnect(): void {
|
|
466
|
-
this.log.info("Manual reconnect requested");
|
|
467
|
-
this.disconnect();
|
|
468
|
-
this.connect().catch((error) => {
|
|
469
|
-
this.log.error("Manual reconnection failed:", error);
|
|
470
|
-
});
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Check if subscribed to a room
|
|
475
|
-
*/
|
|
476
|
-
public hasRoom(roomId: string): boolean {
|
|
477
|
-
return this.subscriptions.has(roomId);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
/**
|
|
481
|
-
* Get all subscribed rooms
|
|
482
|
-
*/
|
|
483
|
-
public getRooms(): string[] {
|
|
484
|
-
return Array.from(this.subscriptions.keys());
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
/**
|
|
489
|
-
* WebSocket Client Service
|
|
490
|
-
*
|
|
491
|
-
* Manages WebSocket connections from the client side (browser).
|
|
492
|
-
* One connection per channel, multiple rooms per connection.
|
|
493
|
-
*/
|
|
494
|
-
export class WebSocketClient {
|
|
495
|
-
protected readonly log = $logger();
|
|
496
|
-
protected readonly alepha = $inject(Alepha);
|
|
497
|
-
protected readonly env = $env(envSchema);
|
|
498
|
-
|
|
499
|
-
// Map<channelPath, connection>
|
|
500
|
-
protected connections = new Map<
|
|
501
|
-
string,
|
|
502
|
-
WebSocketChannelConnection<any, any>
|
|
503
|
-
>();
|
|
504
|
-
|
|
505
|
-
/**
|
|
506
|
-
* Subscribe to a room on a channel
|
|
507
|
-
*/
|
|
508
|
-
public subscribe<TClient extends TWSObject, TServer extends TWSObject>(
|
|
509
|
-
roomId: string,
|
|
510
|
-
channel: ChannelPrimitive<TClient, TServer>,
|
|
511
|
-
handler: (message: Static<TClient>) => void,
|
|
512
|
-
options: {
|
|
513
|
-
url?: string;
|
|
514
|
-
autoReconnect?: boolean;
|
|
515
|
-
reconnectInterval?: number;
|
|
516
|
-
maxReconnectAttempts?: number;
|
|
517
|
-
onConnect?: () => void;
|
|
518
|
-
onDisconnect?: () => void;
|
|
519
|
-
onError?: (error: Error) => void;
|
|
520
|
-
} = {},
|
|
521
|
-
): () => void {
|
|
522
|
-
const channelPath = channel.options.path;
|
|
523
|
-
|
|
524
|
-
this.log.debug("WebSocketClient.subscribe", {
|
|
525
|
-
roomId,
|
|
526
|
-
channelPath,
|
|
527
|
-
existingConnections: this.connections.size,
|
|
528
|
-
});
|
|
529
|
-
|
|
530
|
-
// Get or create connection for this channel
|
|
531
|
-
let connection = this.connections.get(
|
|
532
|
-
channelPath,
|
|
533
|
-
) as WebSocketChannelConnection<TClient, TServer>;
|
|
534
|
-
|
|
535
|
-
if (!connection) {
|
|
536
|
-
this.log.debug("Creating new connection for channel", { channelPath });
|
|
537
|
-
connection = this.alepha.inject(WebSocketChannelConnection, {
|
|
538
|
-
lifetime: "transient",
|
|
539
|
-
args: [
|
|
540
|
-
channel,
|
|
541
|
-
{
|
|
542
|
-
url: options.url,
|
|
543
|
-
autoReconnect: options.autoReconnect,
|
|
544
|
-
reconnectInterval: options.reconnectInterval,
|
|
545
|
-
maxReconnectAttempts: options.maxReconnectAttempts,
|
|
546
|
-
},
|
|
547
|
-
this.env,
|
|
548
|
-
],
|
|
549
|
-
}) as WebSocketChannelConnection<any, any>;
|
|
550
|
-
|
|
551
|
-
this.connections.set(channelPath, connection);
|
|
552
|
-
} else {
|
|
553
|
-
this.log.trace("Reusing existing connection for channel", {
|
|
554
|
-
channelPath,
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
// Subscribe to the room on this connection
|
|
559
|
-
const unsubscribe = connection.subscribe(roomId, handler, {
|
|
560
|
-
onConnect: options.onConnect,
|
|
561
|
-
onDisconnect: options.onDisconnect,
|
|
562
|
-
onError: options.onError,
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
// Return unsubscribe function
|
|
566
|
-
return () => {
|
|
567
|
-
this.log.debug("WebSocketClient.unsubscribe", { roomId, channelPath });
|
|
568
|
-
unsubscribe();
|
|
569
|
-
|
|
570
|
-
// Clean up connection if no more rooms
|
|
571
|
-
if (connection.getRooms().length === 0) {
|
|
572
|
-
this.log.debug("Removing connection for channel (no more rooms)", {
|
|
573
|
-
channelPath,
|
|
574
|
-
});
|
|
575
|
-
this.connections.delete(channelPath);
|
|
576
|
-
}
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* Send message to a room on a channel
|
|
582
|
-
*/
|
|
583
|
-
public async send<TClient extends TWSObject, TServer extends TWSObject>(
|
|
584
|
-
roomId: string,
|
|
585
|
-
channel: ChannelPrimitive<TClient, TServer>,
|
|
586
|
-
message: Static<TServer>,
|
|
587
|
-
): Promise<void> {
|
|
588
|
-
const channelPath = channel.options.path;
|
|
589
|
-
|
|
590
|
-
this.log.trace("WebSocketClient.send", { roomId, channelPath });
|
|
591
|
-
|
|
592
|
-
const connection = this.connections.get(
|
|
593
|
-
channelPath,
|
|
594
|
-
) as WebSocketChannelConnection<TClient, TServer>;
|
|
595
|
-
|
|
596
|
-
if (!connection) {
|
|
597
|
-
this.log.warn("Attempted to send on unsubscribed channel", {
|
|
598
|
-
channelPath,
|
|
599
|
-
});
|
|
600
|
-
throw new AlephaError(
|
|
601
|
-
`Not subscribed to channel ${channelPath}. Subscribe first before sending messages.`,
|
|
602
|
-
);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
await connection.send(roomId, message);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
/**
|
|
609
|
-
* Get connection for a channel
|
|
610
|
-
*/
|
|
611
|
-
public getConnection<TClient extends TWSObject, TServer extends TWSObject>(
|
|
612
|
-
channel: ChannelPrimitive<TClient, TServer>,
|
|
613
|
-
): WebSocketChannelConnection<TClient, TServer> | undefined {
|
|
614
|
-
const channelPath = channel.options.path;
|
|
615
|
-
const connection = this.connections.get(channelPath) as
|
|
616
|
-
| WebSocketChannelConnection<TClient, TServer>
|
|
617
|
-
| undefined;
|
|
618
|
-
|
|
619
|
-
this.log.trace("WebSocketClient.getConnection", {
|
|
620
|
-
channelPath,
|
|
621
|
-
found: !!connection,
|
|
622
|
-
});
|
|
623
|
-
|
|
624
|
-
return connection;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
/**
|
|
628
|
-
* Disconnect all connections
|
|
629
|
-
*/
|
|
630
|
-
public disconnectAll(): void {
|
|
631
|
-
this.log.info("Disconnecting all connections", {
|
|
632
|
-
count: this.connections.size,
|
|
633
|
-
});
|
|
634
|
-
|
|
635
|
-
for (const connection of this.connections.values()) {
|
|
636
|
-
connection.disconnect();
|
|
637
|
-
}
|
|
638
|
-
this.connections.clear();
|
|
639
|
-
|
|
640
|
-
this.log.debug("All connections disconnected");
|
|
641
|
-
}
|
|
642
|
-
}
|