@bruch/max-client 0.1.0

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 (174) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +168 -0
  3. package/UPSTREAM.md +10 -0
  4. package/dist/api/account.d.ts +24 -0
  5. package/dist/api/account.d.ts.map +1 -0
  6. package/dist/api/auth.d.ts +57 -0
  7. package/dist/api/auth.d.ts.map +1 -0
  8. package/dist/api/bots.d.ts +8 -0
  9. package/dist/api/bots.d.ts.map +1 -0
  10. package/dist/api/chats.d.ts +41 -0
  11. package/dist/api/chats.d.ts.map +1 -0
  12. package/dist/api/facade.d.ts +19 -0
  13. package/dist/api/facade.d.ts.map +1 -0
  14. package/dist/api/index.d.ts +10 -0
  15. package/dist/api/index.d.ts.map +1 -0
  16. package/dist/api/messages.d.ts +35 -0
  17. package/dist/api/messages.d.ts.map +1 -0
  18. package/dist/api/response.d.ts +7 -0
  19. package/dist/api/response.d.ts.map +1 -0
  20. package/dist/api/runtime.d.ts +19 -0
  21. package/dist/api/runtime.d.ts.map +1 -0
  22. package/dist/api/uploads.d.ts +23 -0
  23. package/dist/api/uploads.d.ts.map +1 -0
  24. package/dist/api/users.d.ts +25 -0
  25. package/dist/api/users.d.ts.map +1 -0
  26. package/dist/app.d.ts +55 -0
  27. package/dist/app.d.ts.map +1 -0
  28. package/dist/auth/flows.d.ts +27 -0
  29. package/dist/auth/flows.d.ts.map +1 -0
  30. package/dist/auth/index.d.ts +3 -0
  31. package/dist/auth/index.d.ts.map +1 -0
  32. package/dist/auth/index.js +109 -0
  33. package/dist/auth/index.js.map +11 -0
  34. package/dist/auth/providers.d.ts +11 -0
  35. package/dist/auth/providers.d.ts.map +1 -0
  36. package/dist/client.d.ts +139 -0
  37. package/dist/client.d.ts.map +1 -0
  38. package/dist/config.d.ts +80 -0
  39. package/dist/config.d.ts.map +1 -0
  40. package/dist/connection/connection.d.ts +25 -0
  41. package/dist/connection/connection.d.ts.map +1 -0
  42. package/dist/connection/index.d.ts +4 -0
  43. package/dist/connection/index.d.ts.map +1 -0
  44. package/dist/connection/pending.d.ts +9 -0
  45. package/dist/connection/pending.d.ts.map +1 -0
  46. package/dist/connection/readers.d.ts +17 -0
  47. package/dist/connection/readers.d.ts.map +1 -0
  48. package/dist/dispatch/dispatcher.d.ts +16 -0
  49. package/dist/dispatch/dispatcher.d.ts.map +1 -0
  50. package/dist/dispatch/index.d.ts +3 -0
  51. package/dist/dispatch/index.d.ts.map +1 -0
  52. package/dist/dispatch/router.d.ts +64 -0
  53. package/dist/dispatch/router.d.ts.map +1 -0
  54. package/dist/errors.d.ts +24 -0
  55. package/dist/errors.d.ts.map +1 -0
  56. package/dist/files/file.d.ts +29 -0
  57. package/dist/files/file.d.ts.map +1 -0
  58. package/dist/files/index.d.ts +2 -0
  59. package/dist/files/index.d.ts.map +1 -0
  60. package/dist/files/index.js +95 -0
  61. package/dist/files/index.js.map +10 -0
  62. package/dist/formatting/index.d.ts +2 -0
  63. package/dist/formatting/index.d.ts.map +1 -0
  64. package/dist/formatting/markdown.d.ts +13 -0
  65. package/dist/formatting/markdown.d.ts.map +1 -0
  66. package/dist/index.d.ts +15 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +3967 -0
  69. package/dist/index.js.map +48 -0
  70. package/dist/logging.d.ts +10 -0
  71. package/dist/logging.d.ts.map +1 -0
  72. package/dist/protocol/compression.d.ts +8 -0
  73. package/dist/protocol/compression.d.ts.map +1 -0
  74. package/dist/protocol/enums.d.ts +175 -0
  75. package/dist/protocol/enums.d.ts.map +1 -0
  76. package/dist/protocol/index.d.ts +6 -0
  77. package/dist/protocol/index.d.ts.map +1 -0
  78. package/dist/protocol/index.js +438 -0
  79. package/dist/protocol/index.js.map +13 -0
  80. package/dist/protocol/tcp.d.ts +35 -0
  81. package/dist/protocol/tcp.d.ts.map +1 -0
  82. package/dist/protocol/types.d.ts +38 -0
  83. package/dist/protocol/types.d.ts.map +1 -0
  84. package/dist/protocol/websocket.d.ts +7 -0
  85. package/dist/protocol/websocket.d.ts.map +1 -0
  86. package/dist/session/index.d.ts +3 -0
  87. package/dist/session/index.d.ts.map +1 -0
  88. package/dist/session/index.js +179 -0
  89. package/dist/session/index.js.map +12 -0
  90. package/dist/session/store.d.ts +18 -0
  91. package/dist/session/store.d.ts.map +1 -0
  92. package/dist/session/types.d.ts +29 -0
  93. package/dist/session/types.d.ts.map +1 -0
  94. package/dist/telemetry/index.d.ts +4 -0
  95. package/dist/telemetry/index.d.ts.map +1 -0
  96. package/dist/telemetry/index.js +831 -0
  97. package/dist/telemetry/index.js.map +17 -0
  98. package/dist/telemetry/navigation.d.ts +41 -0
  99. package/dist/telemetry/navigation.d.ts.map +1 -0
  100. package/dist/telemetry/payloads.d.ts +30 -0
  101. package/dist/telemetry/payloads.d.ts.map +1 -0
  102. package/dist/telemetry/service.d.ts +53 -0
  103. package/dist/telemetry/service.d.ts.map +1 -0
  104. package/dist/transport/base.d.ts +18 -0
  105. package/dist/transport/base.d.ts.map +1 -0
  106. package/dist/transport/index.d.ts +4 -0
  107. package/dist/transport/index.d.ts.map +1 -0
  108. package/dist/transport/tcp.d.ts +28 -0
  109. package/dist/transport/tcp.d.ts.map +1 -0
  110. package/dist/transport/websocket.d.ts +14 -0
  111. package/dist/transport/websocket.d.ts.map +1 -0
  112. package/dist/types/attachments.d.ts +18 -0
  113. package/dist/types/attachments.d.ts.map +1 -0
  114. package/dist/types/index.d.ts +4 -0
  115. package/dist/types/index.d.ts.map +1 -0
  116. package/dist/types/index.js +376 -0
  117. package/dist/types/index.js.map +12 -0
  118. package/dist/types/models.d.ts +210 -0
  119. package/dist/types/models.d.ts.map +1 -0
  120. package/dist/types/utils.d.ts +10 -0
  121. package/dist/types/utils.d.ts.map +1 -0
  122. package/package.json +95 -0
  123. package/src/api/account.ts +93 -0
  124. package/src/api/auth.ts +312 -0
  125. package/src/api/bots.ts +21 -0
  126. package/src/api/chats.ts +234 -0
  127. package/src/api/facade.ts +28 -0
  128. package/src/api/index.ts +9 -0
  129. package/src/api/messages.ts +230 -0
  130. package/src/api/response.ts +24 -0
  131. package/src/api/runtime.ts +27 -0
  132. package/src/api/uploads.ts +91 -0
  133. package/src/api/users.ts +68 -0
  134. package/src/app.ts +271 -0
  135. package/src/auth/flows.ts +103 -0
  136. package/src/auth/index.ts +2 -0
  137. package/src/auth/providers.ts +30 -0
  138. package/src/client.ts +506 -0
  139. package/src/config.ts +161 -0
  140. package/src/connection/connection.ts +93 -0
  141. package/src/connection/index.ts +3 -0
  142. package/src/connection/pending.ts +49 -0
  143. package/src/connection/readers.ts +33 -0
  144. package/src/dispatch/dispatcher.ts +194 -0
  145. package/src/dispatch/index.ts +2 -0
  146. package/src/dispatch/router.ts +149 -0
  147. package/src/errors.ts +41 -0
  148. package/src/files/file.ts +92 -0
  149. package/src/files/index.ts +1 -0
  150. package/src/formatting/index.ts +1 -0
  151. package/src/formatting/markdown.ts +123 -0
  152. package/src/index.ts +15 -0
  153. package/src/logging.ts +67 -0
  154. package/src/protocol/compression.ts +69 -0
  155. package/src/protocol/enums.ts +175 -0
  156. package/src/protocol/index.ts +5 -0
  157. package/src/protocol/tcp.ts +139 -0
  158. package/src/protocol/types.ts +45 -0
  159. package/src/protocol/websocket.ts +47 -0
  160. package/src/session/index.ts +2 -0
  161. package/src/session/store.ts +154 -0
  162. package/src/session/types.ts +45 -0
  163. package/src/telemetry/index.ts +3 -0
  164. package/src/telemetry/navigation.ts +160 -0
  165. package/src/telemetry/payloads.ts +84 -0
  166. package/src/telemetry/service.ts +194 -0
  167. package/src/transport/base.ts +43 -0
  168. package/src/transport/index.ts +3 -0
  169. package/src/transport/tcp.ts +296 -0
  170. package/src/transport/websocket.ts +63 -0
  171. package/src/types/attachments.ts +24 -0
  172. package/src/types/index.ts +3 -0
  173. package/src/types/models.ts +461 -0
  174. package/src/types/utils.ts +45 -0
@@ -0,0 +1,93 @@
1
+ import {
2
+ Command,
3
+ type InboundFrame,
4
+ type OutboundFrame,
5
+ type Protocol,
6
+ } from "../protocol/index.ts";
7
+ import { ConnectionError } from "../errors.ts";
8
+ import type { Transport } from "../transport/index.ts";
9
+ import { PendingRequests } from "./pending.ts";
10
+ import type { Reader } from "./readers.ts";
11
+
12
+ export class ConnectionManager<Raw extends string | Uint8Array> {
13
+ readonly requests = new PendingRequests();
14
+ onEvent: ((frame: InboundFrame) => void | Promise<void>) | null = null;
15
+ onClose: ((error: Error | null) => void) | null = null;
16
+ private sequence = -1;
17
+ private receiveTask: Promise<void> | null = null;
18
+ private abortController: AbortController | null = null;
19
+
20
+ constructor(
21
+ readonly reader: Reader<Raw>,
22
+ readonly transport: Transport<Raw>,
23
+ readonly protocol: Protocol<Raw>,
24
+ ) {}
25
+
26
+ get isOpen(): boolean {
27
+ return this.transport.connected;
28
+ }
29
+
30
+ async open(): Promise<void> {
31
+ if (this.isOpen) return;
32
+ await this.transport.connect();
33
+ this.abortController = new AbortController();
34
+ this.receiveTask = this.receiveLoop(this.abortController.signal);
35
+ }
36
+
37
+ async close(): Promise<void> {
38
+ this.abortController?.abort();
39
+ this.abortController = null;
40
+ this.requests.cancelAll();
41
+ await this.transport.close();
42
+ try {
43
+ await this.receiveTask;
44
+ } catch {
45
+ // Closing intentionally interrupts recv().
46
+ }
47
+ this.receiveTask = null;
48
+ }
49
+
50
+ async send(frame: OutboundFrame): Promise<void> {
51
+ if (!this.isOpen) throw new ConnectionError("Connection is not open");
52
+ await this.transport.send(this.protocol.encode(frame));
53
+ }
54
+
55
+ async request(frame: OutboundFrame, timeoutSeconds?: number): Promise<InboundFrame> {
56
+ const response = this.requests.create(frame.seq, timeoutSeconds);
57
+ try {
58
+ await this.send(frame);
59
+ return await response;
60
+ } catch (error) {
61
+ this.requests.discard(frame.seq);
62
+ throw error;
63
+ }
64
+ }
65
+
66
+ async waitClosed(): Promise<void> {
67
+ await this.receiveTask;
68
+ }
69
+
70
+ nextSequence(): number {
71
+ this.sequence = (this.sequence + 1) & 0xffff;
72
+ return this.sequence;
73
+ }
74
+
75
+ private async receiveLoop(signal: AbortSignal): Promise<void> {
76
+ try {
77
+ while (!signal.aborted) {
78
+ const raw = await this.reader.read();
79
+ const frame = this.protocol.decode(raw);
80
+ if ((frame.cmd === Command.RESPONSE || frame.cmd === Command.ERROR) && frame.seq !== null) {
81
+ this.requests.resolve(frame.seq, frame);
82
+ }
83
+ if (this.onEvent) void Promise.resolve(this.onEvent(frame));
84
+ }
85
+ } catch (error) {
86
+ if (signal.aborted) return;
87
+ const failure = error instanceof Error ? error : new ConnectionError(String(error));
88
+ this.requests.cancelAll(failure);
89
+ this.onClose?.(failure);
90
+ throw failure;
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./connection.ts";
2
+ export * from "./pending.ts";
3
+ export * from "./readers.ts";
@@ -0,0 +1,49 @@
1
+ import type { InboundFrame } from "../protocol/index.ts";
2
+ import { ConnectionError, TimeoutError } from "../errors.ts";
3
+
4
+ interface PendingRequest {
5
+ resolve(frame: InboundFrame): void;
6
+ reject(reason: unknown): void;
7
+ timer: ReturnType<typeof setTimeout> | null;
8
+ }
9
+
10
+ export class PendingRequests {
11
+ private readonly requests = new Map<number, PendingRequest>();
12
+
13
+ create(sequence: number, timeoutSeconds?: number): Promise<InboundFrame> {
14
+ if (this.requests.has(sequence)) throw new Error(`Duplicate request sequence: ${sequence}`);
15
+ return new Promise<InboundFrame>((resolve, reject) => {
16
+ const request: PendingRequest = { resolve, reject, timer: null };
17
+ if (timeoutSeconds !== undefined) {
18
+ request.timer = setTimeout(() => {
19
+ this.requests.delete(sequence);
20
+ reject(new TimeoutError(`Request ${sequence} timed out`));
21
+ }, timeoutSeconds * 1000);
22
+ }
23
+ this.requests.set(sequence, request);
24
+ });
25
+ }
26
+
27
+ resolve(sequence: number, frame: InboundFrame): boolean {
28
+ const request = this.requests.get(sequence);
29
+ if (!request) return false;
30
+ this.requests.delete(sequence);
31
+ if (request.timer) clearTimeout(request.timer);
32
+ request.resolve(frame);
33
+ return true;
34
+ }
35
+
36
+ discard(sequence: number): void {
37
+ const request = this.requests.get(sequence);
38
+ if (request?.timer) clearTimeout(request.timer);
39
+ this.requests.delete(sequence);
40
+ }
41
+
42
+ cancelAll(reason: Error = new ConnectionError("Connection closed")): void {
43
+ for (const request of this.requests.values()) {
44
+ if (request.timer) clearTimeout(request.timer);
45
+ request.reject(reason);
46
+ }
47
+ this.requests.clear();
48
+ }
49
+ }
@@ -0,0 +1,33 @@
1
+ import { TCP_HEADER_SIZE, TcpPacketFramer } from "../protocol/index.ts";
2
+ import { ConnectionError } from "../errors.ts";
3
+ import type { Transport } from "../transport/index.ts";
4
+
5
+ export interface Reader<Raw extends string | Uint8Array> {
6
+ read(): Promise<Raw>;
7
+ }
8
+
9
+ export class TcpReader implements Reader<Uint8Array> {
10
+ constructor(
11
+ private readonly transport: Transport<Uint8Array>,
12
+ private readonly framer = new TcpPacketFramer(),
13
+ ) {}
14
+
15
+ async read(): Promise<Uint8Array> {
16
+ const header = await this.transport.recv(TCP_HEADER_SIZE);
17
+ const payloadLength = this.framer.unpackPayloadLength(header);
18
+ if (payloadLength === null) throw new ConnectionError("Incomplete TCP header");
19
+ if (payloadLength === 0) return header;
20
+ const payload = await this.transport.recv(payloadLength);
21
+ const result = new Uint8Array(header.byteLength + payload.byteLength);
22
+ result.set(header);
23
+ result.set(payload, header.byteLength);
24
+ return result;
25
+ }
26
+ }
27
+
28
+ export class WsReader implements Reader<string> {
29
+ constructor(private readonly transport: Transport<string>) {}
30
+ async read(): Promise<string> {
31
+ return await this.transport.recv();
32
+ }
33
+ }
@@ -0,0 +1,194 @@
1
+ import { Command, Opcode, type InboundFrame } from "../protocol/index.ts";
2
+ import {
3
+ Chat,
4
+ Message,
5
+ asArray,
6
+ asBoolean,
7
+ asNumber,
8
+ asRecord,
9
+ optionalMaxId,
10
+ parseReactionInfo,
11
+ toMaxId,
12
+ type MessageDeleteEvent,
13
+ type MessageReadEvent,
14
+ type PresenceEvent,
15
+ type ReactionUpdateEvent,
16
+ type TypingEvent,
17
+ } from "../types/index.ts";
18
+ import { EventType, type EventMap, type Router } from "./router.ts";
19
+
20
+ export class Dispatcher<Client> {
21
+ private client: Client | null = null;
22
+
23
+ constructor(readonly rootRouter: Router<Client>) {}
24
+
25
+ bindClient(client: Client): void {
26
+ this.client = client;
27
+ }
28
+
29
+ async emitStart(): Promise<void> {
30
+ const client = this.requireClient();
31
+ for (const router of this.routers()) {
32
+ for (const handler of router.startHandlers) {
33
+ try {
34
+ await handler(client);
35
+ } catch (error) {
36
+ await this.emitError(error, "start", null, router);
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ async emitDisconnect(error: Error, reconnect: boolean, delay: number): Promise<void> {
43
+ for (const router of this.routers()) {
44
+ for (const handler of router.disconnectHandlers) {
45
+ try {
46
+ await handler(error, reconnect, delay);
47
+ } catch {
48
+ // Disconnect handlers cannot make reconnect fail.
49
+ }
50
+ }
51
+ }
52
+ }
53
+
54
+ async dispatch(frame: InboundFrame): Promise<void> {
55
+ await this.dispatchEvent("raw", frame);
56
+ if (frame.cmd !== Command.REQUEST || !frame.payload) return;
57
+ const resolved = resolveEvent(frame);
58
+ if (resolved) await this.dispatchEvent(resolved.type, resolved.event);
59
+ }
60
+
61
+ private async dispatchEvent<Key extends keyof EventMap>(
62
+ type: Key,
63
+ event: EventMap[Key],
64
+ ): Promise<void> {
65
+ const client = this.requireClient();
66
+ for (const router of this.routers()) {
67
+ const entries = router.handlers.get(type) ?? [];
68
+ for (const rawEntry of entries) {
69
+ const entry = rawEntry as {
70
+ handler(event: EventMap[Key], client: Client): void | Promise<void>;
71
+ filters: Array<(event: EventMap[Key]) => boolean | Promise<boolean>>;
72
+ };
73
+ try {
74
+ let matches = true;
75
+ for (const filter of entry.filters) {
76
+ if (!(await filter(event))) {
77
+ matches = false;
78
+ break;
79
+ }
80
+ }
81
+ if (matches) await entry.handler(event, client);
82
+ } catch (error) {
83
+ await this.emitError(error, type, event, router);
84
+ }
85
+ }
86
+ }
87
+ }
88
+
89
+ private async emitError(
90
+ error: unknown,
91
+ eventType: keyof EventMap | "start",
92
+ event: unknown,
93
+ source: Router<Client>,
94
+ ): Promise<void> {
95
+ const failure = error instanceof Error ? error : new Error(String(error));
96
+ const client = this.requireClient();
97
+ let handled = false;
98
+ for (const router of this.routers()) {
99
+ for (const handler of router.errorHandlers) {
100
+ handled = true;
101
+ await handler(failure, { client, eventType, event, router: source });
102
+ }
103
+ }
104
+ if (!handled) throw failure;
105
+ }
106
+
107
+ private *routers(router = this.rootRouter): Generator<Router<Client>> {
108
+ yield router;
109
+ for (const child of router.children) yield* this.routers(child);
110
+ }
111
+
112
+ private requireClient(): Client {
113
+ if (!this.client) throw new Error("Dispatcher has no bound client");
114
+ return this.client;
115
+ }
116
+ }
117
+
118
+ function resolveEvent(
119
+ frame: InboundFrame,
120
+ ): { type: keyof EventMap; event: EventMap[keyof EventMap] } | null {
121
+ const payload = frame.payload as Record<string, unknown>;
122
+ if (frame.opcode === Opcode.NOTIF_MESSAGE || frame.opcode === Opcode.MSG_EDIT) {
123
+ const messagePayload = payload.message ?? payload;
124
+ const message = Message.fromPayload(messagePayload);
125
+ if (message.status === "REMOVED")
126
+ return { type: EventType.MESSAGE_DELETE, event: deleteFromMessage(message, payload) };
127
+ return {
128
+ type: message.status === "EDITED" ? EventType.MESSAGE_EDIT : EventType.MESSAGE_NEW,
129
+ event: message,
130
+ };
131
+ }
132
+ if (frame.opcode === Opcode.NOTIF_CHAT) {
133
+ return { type: EventType.CHAT_UPDATE, event: Chat.fromPayload(payload.chat ?? payload) };
134
+ }
135
+ if (frame.opcode === Opcode.NOTIF_MSG_DELETE) {
136
+ const chat = payload.chat ? asRecord(payload.chat) : null;
137
+ const event: MessageDeleteEvent = {
138
+ chatId: toMaxId(payload.chatId ?? chat?.id, "delete.chatId"),
139
+ messageIds: asArray(payload.messageIds).map((id) => toMaxId(id)),
140
+ chat: chat ? Chat.fromPayload(chat) : null,
141
+ ttl: asBoolean(payload.ttl),
142
+ };
143
+ return { type: EventType.MESSAGE_DELETE, event };
144
+ }
145
+ if (frame.opcode === Opcode.NOTIF_TYPING) {
146
+ const event: TypingEvent = {
147
+ chatId: toMaxId(payload.chatId),
148
+ userId: toMaxId(payload.userId),
149
+ typing: asBoolean(payload.typing, true),
150
+ };
151
+ return { type: EventType.TYPING, event };
152
+ }
153
+ if (frame.opcode === Opcode.NOTIF_MARK) {
154
+ const event: MessageReadEvent = {
155
+ chatId: toMaxId(payload.chatId),
156
+ userId: optionalMaxId(payload.userId),
157
+ mark: asNumber(payload.mark),
158
+ };
159
+ return { type: EventType.MESSAGE_READ, event };
160
+ }
161
+ if (frame.opcode === Opcode.NOTIF_PRESENCE) {
162
+ const presence = payload.presence ? asRecord(payload.presence) : payload;
163
+ const event: PresenceEvent = {
164
+ userId: toMaxId(payload.userId),
165
+ online: asBoolean(presence.online ?? presence.status === "ONLINE"),
166
+ lastSeen: presence.lastSeen === undefined ? null : asNumber(presence.lastSeen),
167
+ raw: presence,
168
+ };
169
+ return { type: EventType.PRESENCE, event };
170
+ }
171
+ if (frame.opcode === Opcode.NOTIF_MSG_REACTIONS_CHANGED) {
172
+ const event: ReactionUpdateEvent = {
173
+ chatId: toMaxId(payload.chatId),
174
+ messageId: toMaxId(payload.messageId),
175
+ reactionInfo: parseReactionInfo({
176
+ totalCount: payload.totalCount,
177
+ counters: payload.counters,
178
+ yourReaction: payload.yourReaction,
179
+ }),
180
+ };
181
+ return { type: EventType.REACTION_UPDATE, event };
182
+ }
183
+ return null;
184
+ }
185
+
186
+ function deleteFromMessage(message: Message, payload: Record<string, unknown>): MessageDeleteEvent {
187
+ if (message.chatId === null) throw new TypeError("Removed message has no chatId");
188
+ return {
189
+ chatId: message.chatId,
190
+ messageIds: [message.id],
191
+ message,
192
+ ttl: asBoolean(payload.ttl),
193
+ };
194
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./dispatcher.ts";
2
+ export * from "./router.ts";
@@ -0,0 +1,149 @@
1
+ import type { InboundFrame } from "../protocol/index.ts";
2
+ import type {
3
+ Chat,
4
+ Message,
5
+ MessageDeleteEvent,
6
+ MessageReadEvent,
7
+ PresenceEvent,
8
+ ReactionUpdateEvent,
9
+ TypingEvent,
10
+ } from "../types/index.ts";
11
+
12
+ export const EventType = {
13
+ MESSAGE_NEW: "messageNew",
14
+ MESSAGE_EDIT: "messageEdit",
15
+ MESSAGE_DELETE: "messageDelete",
16
+ MESSAGE_READ: "messageRead",
17
+ TYPING: "typing",
18
+ PRESENCE: "presence",
19
+ REACTION_UPDATE: "reactionUpdate",
20
+ CHAT_UPDATE: "chatUpdate",
21
+ RAW: "raw",
22
+ } as const;
23
+ export type EventType = (typeof EventType)[keyof typeof EventType];
24
+
25
+ export interface EventMap {
26
+ messageNew: Message;
27
+ messageEdit: Message;
28
+ messageDelete: MessageDeleteEvent;
29
+ messageRead: MessageReadEvent;
30
+ typing: TypingEvent;
31
+ presence: PresenceEvent;
32
+ reactionUpdate: ReactionUpdateEvent;
33
+ chatUpdate: Chat;
34
+ raw: InboundFrame;
35
+ }
36
+
37
+ export type MaybePromise<T> = T | Promise<T>;
38
+ export type Handler<Event, Client> = (event: Event, client: Client) => MaybePromise<void>;
39
+ export type Filter<Event> = (event: Event) => MaybePromise<boolean>;
40
+ export type StartHandler<Client> = (client: Client) => MaybePromise<void>;
41
+ export type DisconnectHandler = (
42
+ error: Error,
43
+ reconnect: boolean,
44
+ delay: number,
45
+ ) => MaybePromise<void>;
46
+
47
+ export interface ErrorContext<Client> {
48
+ client: Client;
49
+ eventType: EventType | "start";
50
+ event: unknown;
51
+ router: Router<Client>;
52
+ }
53
+ export type ErrorHandler<Client> = (
54
+ error: Error,
55
+ context: ErrorContext<Client>,
56
+ ) => MaybePromise<void>;
57
+
58
+ interface HandlerEntry<Event, Client> {
59
+ handler: Handler<Event, Client>;
60
+ filters: Filter<Event>[];
61
+ }
62
+
63
+ export class Router<Client> {
64
+ readonly children: Router<Client>[] = [];
65
+ readonly handlers = new Map<EventType, HandlerEntry<unknown, Client>[]>();
66
+ readonly startHandlers: StartHandler<Client>[] = [];
67
+ readonly errorHandlers: ErrorHandler<Client>[] = [];
68
+ readonly disconnectHandlers: DisconnectHandler[] = [];
69
+
70
+ includeRouter(router: Router<Client>): () => void {
71
+ this.children.push(router);
72
+ return () => {
73
+ const index = this.children.indexOf(router);
74
+ if (index >= 0) this.children.splice(index, 1);
75
+ };
76
+ }
77
+
78
+ on<Key extends keyof EventMap>(
79
+ event: Key,
80
+ handler: Handler<EventMap[Key], Client>,
81
+ ...filters: Filter<EventMap[Key]>[]
82
+ ): () => void {
83
+ const entry: HandlerEntry<EventMap[Key], Client> = { handler, filters };
84
+ const entries = (this.handlers.get(event) ?? []) as HandlerEntry<EventMap[Key], Client>[];
85
+ entries.push(entry);
86
+ this.handlers.set(event, entries as HandlerEntry<unknown, Client>[]);
87
+ return () => {
88
+ const index = entries.indexOf(entry);
89
+ if (index >= 0) entries.splice(index, 1);
90
+ };
91
+ }
92
+
93
+ onStart(handler: StartHandler<Client>): () => void {
94
+ this.startHandlers.push(handler);
95
+ return () => remove(this.startHandlers, handler);
96
+ }
97
+ onError(handler: ErrorHandler<Client>): () => void {
98
+ this.errorHandlers.push(handler);
99
+ return () => remove(this.errorHandlers, handler);
100
+ }
101
+ onDisconnect(handler: DisconnectHandler): () => void {
102
+ this.disconnectHandlers.push(handler);
103
+ return () => remove(this.disconnectHandlers, handler);
104
+ }
105
+ onMessage(handler: Handler<Message, Client>, ...filters: Filter<Message>[]): () => void {
106
+ return this.on("messageNew", handler, ...filters);
107
+ }
108
+ onMessageEdit(handler: Handler<Message, Client>, ...filters: Filter<Message>[]): () => void {
109
+ return this.on("messageEdit", handler, ...filters);
110
+ }
111
+ onMessageDelete(
112
+ handler: Handler<MessageDeleteEvent, Client>,
113
+ ...filters: Filter<MessageDeleteEvent>[]
114
+ ): () => void {
115
+ return this.on("messageDelete", handler, ...filters);
116
+ }
117
+ onMessageRead(
118
+ handler: Handler<MessageReadEvent, Client>,
119
+ ...filters: Filter<MessageReadEvent>[]
120
+ ): () => void {
121
+ return this.on("messageRead", handler, ...filters);
122
+ }
123
+ onTyping(handler: Handler<TypingEvent, Client>, ...filters: Filter<TypingEvent>[]): () => void {
124
+ return this.on("typing", handler, ...filters);
125
+ }
126
+ onPresence(
127
+ handler: Handler<PresenceEvent, Client>,
128
+ ...filters: Filter<PresenceEvent>[]
129
+ ): () => void {
130
+ return this.on("presence", handler, ...filters);
131
+ }
132
+ onReactionUpdate(
133
+ handler: Handler<ReactionUpdateEvent, Client>,
134
+ ...filters: Filter<ReactionUpdateEvent>[]
135
+ ): () => void {
136
+ return this.on("reactionUpdate", handler, ...filters);
137
+ }
138
+ onChatUpdate(handler: Handler<Chat, Client>, ...filters: Filter<Chat>[]): () => void {
139
+ return this.on("chatUpdate", handler, ...filters);
140
+ }
141
+ onRaw(handler: Handler<InboundFrame, Client>, ...filters: Filter<InboundFrame>[]): () => void {
142
+ return this.on("raw", handler, ...filters);
143
+ }
144
+ }
145
+
146
+ function remove<T>(values: T[], value: T): void {
147
+ const index = values.indexOf(value);
148
+ if (index >= 0) values.splice(index, 1);
149
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,41 @@
1
+ export class TsMaxError extends Error {
2
+ constructor(message: string, options?: ErrorOptions) {
3
+ super(message, options);
4
+ this.name = new.target.name;
5
+ }
6
+ }
7
+
8
+ export class UploadError extends TsMaxError {}
9
+ export class ConnectionError extends TsMaxError {}
10
+ export class TimeoutError extends TsMaxError {}
11
+
12
+ export class ApiError extends TsMaxError {
13
+ readonly payload: Record<string, unknown>;
14
+
15
+ constructor(
16
+ readonly opcode: number,
17
+ options: {
18
+ error?: string | null;
19
+ title?: string | null;
20
+ message?: string | null;
21
+ localizedMessage?: string | null;
22
+ payload?: Record<string, unknown> | null;
23
+ } = {},
24
+ ) {
25
+ const parts: string[] = [];
26
+ if (options.localizedMessage) parts.push(options.localizedMessage);
27
+ if (options.message && options.message !== options.localizedMessage)
28
+ parts.push(options.message);
29
+ if (options.title) parts.push(`(${options.title})`);
30
+ if (options.error) parts.push(`[${options.error}]`);
31
+ super(parts.join(" ") || "API request failed");
32
+ this.payload = options.payload ?? {};
33
+ this.error = options.error ?? null;
34
+ this.title = options.title ?? null;
35
+ this.localizedMessage = options.localizedMessage ?? null;
36
+ }
37
+
38
+ readonly error: string | null;
39
+ readonly title: string | null;
40
+ readonly localizedMessage: string | null;
41
+ }
@@ -0,0 +1,92 @@
1
+ import { basename, extname } from "node:path";
2
+
3
+ export interface FileSource {
4
+ raw?: Uint8Array;
5
+ path?: string;
6
+ url?: string;
7
+ name?: string;
8
+ }
9
+
10
+ export class MaxFile {
11
+ readonly raw: Uint8Array | null;
12
+ readonly path: string | null;
13
+ readonly url: string | null;
14
+ readonly name: string;
15
+
16
+ constructor(source: FileSource) {
17
+ const sources = [source.raw, source.path, source.url].filter((value) => value !== undefined);
18
+ if (sources.length !== 1)
19
+ throw new TypeError("Exactly one of raw, path, or url must be provided");
20
+ this.raw = source.raw ?? null;
21
+ this.path = source.path ?? null;
22
+ this.url = source.url ?? null;
23
+ this.name = source.name ?? inferName(source.path, source.url);
24
+ if (!this.name) throw new TypeError("A name is required for raw file data");
25
+ }
26
+
27
+ async read(): Promise<Uint8Array> {
28
+ if (this.raw) return this.raw;
29
+ if (this.path) return new Uint8Array(await Bun.file(this.path).arrayBuffer());
30
+ if (this.url) {
31
+ const response = await fetch(this.url);
32
+ if (!response.ok) throw new Error(`Failed to download ${this.url}: ${response.status}`);
33
+ return new Uint8Array(await response.arrayBuffer());
34
+ }
35
+ throw new Error("File has no source");
36
+ }
37
+
38
+ async size(): Promise<number> {
39
+ if (this.raw) return this.raw.byteLength;
40
+ if (this.path) return Bun.file(this.path).size;
41
+ if (this.url) {
42
+ const response = await fetch(this.url, { method: "HEAD" });
43
+ if (!response.ok) throw new Error(`Failed to inspect ${this.url}: ${response.status}`);
44
+ const length = response.headers.get("content-length");
45
+ if (!length) return (await this.read()).byteLength;
46
+ return Number(length);
47
+ }
48
+ throw new Error("File has no source");
49
+ }
50
+
51
+ async *chunks(chunkSize: number): AsyncGenerator<Uint8Array> {
52
+ if (!Number.isInteger(chunkSize) || chunkSize <= 0)
53
+ throw new RangeError("chunkSize must be positive");
54
+ const data = await this.read();
55
+ for (let offset = 0; offset < data.byteLength; offset += chunkSize) {
56
+ yield data.slice(offset, offset + chunkSize);
57
+ }
58
+ }
59
+ }
60
+
61
+ export class File extends MaxFile {}
62
+ export class Video extends MaxFile {}
63
+
64
+ const allowedPhotoExtensions = new Set([".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp"]);
65
+
66
+ export class Photo extends MaxFile {
67
+ constructor(source: FileSource) {
68
+ super(source);
69
+ const extension = extname(
70
+ this.url ? new URL(this.url).pathname : (this.path ?? this.name),
71
+ ).toLowerCase();
72
+ if (!allowedPhotoExtensions.has(extension)) {
73
+ throw new TypeError(`Invalid photo extension: ${extension}`);
74
+ }
75
+ }
76
+
77
+ get format(): { extension: string; mimeType: string } {
78
+ const extension = extname(this.url ? new URL(this.url).pathname : (this.path ?? this.name))
79
+ .toLowerCase()
80
+ .slice(1);
81
+ const normalized = extension === "jpg" ? "jpeg" : extension;
82
+ return { extension, mimeType: `image/${normalized}` };
83
+ }
84
+ }
85
+
86
+ export type SendAttachment = File | Video | Photo;
87
+
88
+ function inferName(path?: string, url?: string): string {
89
+ if (path) return basename(path);
90
+ if (url) return basename(new URL(url).pathname);
91
+ return "";
92
+ }
@@ -0,0 +1 @@
1
+ export * from "./file.ts";
@@ -0,0 +1 @@
1
+ export * from "./markdown.ts";