attio 0.0.1-experimental.20250815 → 0.0.1-experimental.20250815.2
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.
|
@@ -1,17 +1,37 @@
|
|
|
1
|
-
import { complete, isErrored } from "@attio/fetchable-npm";
|
|
2
|
-
import {
|
|
1
|
+
import { complete, errored, isErrored } from "@attio/fetchable-npm";
|
|
2
|
+
import { Realtime } from "../../util/realtime.js";
|
|
3
3
|
import { logEventSchema } from "./log-event.js";
|
|
4
4
|
import { LogsBuffer } from "./logs-buffer.js";
|
|
5
|
-
export async function subscribeToLogs({ workspaceId, appId, }, listener) {
|
|
5
|
+
export async function subscribeToLogs({ workspaceId, appId, }, listener, onConnectionSuspended) {
|
|
6
6
|
const logsBuffer = new LogsBuffer(200);
|
|
7
7
|
logsBuffer.listen((event) => event.forEach(listener));
|
|
8
|
-
const realtime =
|
|
8
|
+
const realtime = new Realtime();
|
|
9
|
+
realtime.onSuspended(() => {
|
|
10
|
+
onConnectionSuspended({
|
|
11
|
+
close: async () => {
|
|
12
|
+
await realtime.close();
|
|
13
|
+
},
|
|
14
|
+
reconnect: async () => {
|
|
15
|
+
const reconnectResult = await realtime.reconnect();
|
|
16
|
+
if (isErrored(reconnectResult)) {
|
|
17
|
+
return errored({
|
|
18
|
+
code: "FAILED_TO_CONNECT_TO_LOGS_SERVER",
|
|
19
|
+
error: reconnectResult.error.error,
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return complete(true);
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
});
|
|
9
26
|
const channelName = `ecosystem-app-logs:${workspaceId}:${appId}`;
|
|
10
27
|
const realtimeSubscriptionResult = await realtime.subscribe(channelName, "app-dev-log-emitted", logEventSchema, (event) => {
|
|
11
28
|
logsBuffer.add(event);
|
|
12
29
|
});
|
|
13
30
|
if (isErrored(realtimeSubscriptionResult)) {
|
|
14
|
-
return
|
|
31
|
+
return errored({
|
|
32
|
+
code: "FAILED_TO_CONNECT_TO_LOGS_SERVER",
|
|
33
|
+
error: realtimeSubscriptionResult.error.error,
|
|
34
|
+
});
|
|
15
35
|
}
|
|
16
36
|
const realtimeSubscription = realtimeSubscriptionResult.value;
|
|
17
37
|
return complete({
|
package/lib/commands/logs.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import readline from "node:readline/promises";
|
|
1
2
|
import chalk from "chalk";
|
|
2
3
|
import { Command, Option } from "commander";
|
|
3
4
|
import { z } from "zod";
|
|
@@ -12,6 +13,13 @@ import { subscribeToLogs } from "./log/subscribe-to-logs.js";
|
|
|
12
13
|
export const optionsSchema = z.object({
|
|
13
14
|
workspace: z.string().optional(),
|
|
14
15
|
});
|
|
16
|
+
const rl = readline.createInterface({
|
|
17
|
+
input: process.stdin,
|
|
18
|
+
output: process.stdout,
|
|
19
|
+
});
|
|
20
|
+
async function waitForEnter(prompt = "Press Enter to resume...") {
|
|
21
|
+
await rl.question(prompt);
|
|
22
|
+
}
|
|
15
23
|
export const logs = new Command("logs")
|
|
16
24
|
.description("Stream development server logs")
|
|
17
25
|
.addOption(new Option("-w, --workspace <slug>", "The slug of the workspace to get the logs from"))
|
|
@@ -54,6 +62,14 @@ export const logs = new Command("logs")
|
|
|
54
62
|
}
|
|
55
63
|
const logLine = `${timestamp} ${coloredSeverity}: ${message.trim()}`;
|
|
56
64
|
process.stdout.write(`\n${logLine}`);
|
|
65
|
+
}, async ({ close, reconnect }) => {
|
|
66
|
+
await close();
|
|
67
|
+
await waitForEnter("The connection to the logs server was closed. Press Enter to reconnect...");
|
|
68
|
+
const reconnectResult = await spinnerify("Connecting to log server...", "Connected to log server", () => reconnect());
|
|
69
|
+
if (isErrored(reconnectResult)) {
|
|
70
|
+
printLogSubscriptionError(reconnectResult.error);
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
57
73
|
});
|
|
58
74
|
});
|
|
59
75
|
if (isErrored(subscriptionResult)) {
|
package/lib/print-errors.js
CHANGED
|
@@ -165,13 +165,13 @@ export function printAuthenticationError(error) {
|
|
|
165
165
|
}
|
|
166
166
|
export function printLogSubscriptionError(error) {
|
|
167
167
|
switch (error.code) {
|
|
168
|
-
case "
|
|
169
|
-
process.stderr.write(chalk.red("Failed to
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
168
|
+
case "FAILED_TO_CONNECT_TO_LOGS_SERVER":
|
|
169
|
+
process.stderr.write(chalk.red("Failed to connect to logs server\n"));
|
|
170
|
+
if (error.error instanceof Error) {
|
|
171
|
+
process.stderr.write(chalk.red(error.error.message));
|
|
172
|
+
}
|
|
173
173
|
break;
|
|
174
174
|
default:
|
|
175
|
-
return error;
|
|
175
|
+
return error.code;
|
|
176
176
|
}
|
|
177
177
|
}
|
package/lib/util/realtime.js
CHANGED
|
@@ -1,11 +1,82 @@
|
|
|
1
1
|
import Ably from "ably";
|
|
2
|
-
import { complete, errored, isErrored } from "@attio/fetchable-npm";
|
|
2
|
+
import { complete, errored, isComplete, isErrored } from "@attio/fetchable-npm";
|
|
3
3
|
import { api } from "../api/api.js";
|
|
4
|
+
function makeConnectionError(error) {
|
|
5
|
+
return errored({
|
|
6
|
+
code: "FAILED_TO_CONNECT_TO_REALTIME_SERVER",
|
|
7
|
+
error,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
4
10
|
export class Realtime {
|
|
5
|
-
|
|
11
|
+
_ablyRealtime;
|
|
6
12
|
_channels = new Map();
|
|
13
|
+
_suspendListeners = new Set();
|
|
7
14
|
constructor() {
|
|
8
|
-
this.
|
|
15
|
+
this._ablyRealtime = this._createAblyRealtime();
|
|
16
|
+
}
|
|
17
|
+
async subscribe(channelName, eventName, eventDataSchema, listener) {
|
|
18
|
+
let channel = this._channels.get(channelName);
|
|
19
|
+
if (!channel) {
|
|
20
|
+
channel = {
|
|
21
|
+
listeners: new Map(),
|
|
22
|
+
};
|
|
23
|
+
const authorizeResult = await this._authorizeChannel(channelName);
|
|
24
|
+
if (isErrored(authorizeResult)) {
|
|
25
|
+
return makeConnectionError(authorizeResult.error.error);
|
|
26
|
+
}
|
|
27
|
+
this._channels.set(channelName, channel);
|
|
28
|
+
}
|
|
29
|
+
let eventListeners = channel.listeners.get(eventName);
|
|
30
|
+
if (!eventListeners) {
|
|
31
|
+
eventListeners = new Set();
|
|
32
|
+
channel.listeners.set(eventName, eventListeners);
|
|
33
|
+
}
|
|
34
|
+
const subscribeResult = await this._subscribe(channelName, eventName, (data) => {
|
|
35
|
+
const parsedEventData = eventDataSchema.safeParse(data);
|
|
36
|
+
if (parsedEventData.success) {
|
|
37
|
+
listener(parsedEventData.data);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
if (isErrored(subscribeResult)) {
|
|
41
|
+
return makeConnectionError(subscribeResult.error.error);
|
|
42
|
+
}
|
|
43
|
+
eventListeners.add(listener);
|
|
44
|
+
return complete({
|
|
45
|
+
unsubscribe: async () => {
|
|
46
|
+
await this._unsubscribe(channelName, eventName, listener);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
async reconnect() {
|
|
51
|
+
await this._teardown();
|
|
52
|
+
this._ablyRealtime = this._createAblyRealtime();
|
|
53
|
+
for (const [channelName, { listeners }] of this._channels) {
|
|
54
|
+
const authorizeResult = await this._authorizeChannel(channelName);
|
|
55
|
+
if (isErrored(authorizeResult)) {
|
|
56
|
+
return makeConnectionError(authorizeResult.error.error);
|
|
57
|
+
}
|
|
58
|
+
for (const [eventName, eventListeners] of listeners) {
|
|
59
|
+
for (const eventListener of eventListeners) {
|
|
60
|
+
const subscribeResult = await this._subscribe(channelName, eventName, eventListener);
|
|
61
|
+
if (isErrored(subscribeResult)) {
|
|
62
|
+
return makeConnectionError(subscribeResult.error.error);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return complete(true);
|
|
68
|
+
}
|
|
69
|
+
onSuspended(cb) {
|
|
70
|
+
this._suspendListeners.add(cb);
|
|
71
|
+
return () => {
|
|
72
|
+
this._suspendListeners.delete(cb);
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async close() {
|
|
76
|
+
await this._teardown();
|
|
77
|
+
}
|
|
78
|
+
_createAblyRealtime() {
|
|
79
|
+
const realtime = new Ably.Realtime({
|
|
9
80
|
useTokenAuth: true,
|
|
10
81
|
autoConnect: false,
|
|
11
82
|
tls: true,
|
|
@@ -18,45 +89,75 @@ export class Realtime {
|
|
|
18
89
|
return callback(null, tokenResult.value);
|
|
19
90
|
},
|
|
20
91
|
});
|
|
92
|
+
realtime.connection.on("suspended", () => {
|
|
93
|
+
this._suspendListeners.forEach((listener) => listener());
|
|
94
|
+
});
|
|
95
|
+
return realtime;
|
|
21
96
|
}
|
|
22
|
-
async
|
|
23
|
-
|
|
97
|
+
async _authorizeChannel(channelName) {
|
|
98
|
+
try {
|
|
99
|
+
await this._ablyRealtime.auth.authorize({
|
|
100
|
+
capability: {
|
|
101
|
+
[`${channelName}`]: ["subscribe"],
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
return complete(true);
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return errored({ code: "FAILED_TO_AUTHORIZE_CHANNEL", error });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async _subscribe(channelName, eventName, listener) {
|
|
111
|
+
try {
|
|
112
|
+
const ablyChannel = this._ablyRealtime.channels.get(channelName);
|
|
113
|
+
await ablyChannel.presence.enter();
|
|
114
|
+
await ablyChannel.subscribe(eventName, (event) => {
|
|
115
|
+
listener(event.data.data);
|
|
116
|
+
});
|
|
117
|
+
return complete(true);
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
return errored({ code: "FAILED_TO_SUBSCRIBE_TO_EVENTS", error });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async _unsubscribe(channelName, eventName, listener) {
|
|
124
|
+
const ablyChannel = this._ablyRealtime.channels.get(channelName);
|
|
125
|
+
ablyChannel.unsubscribe(eventName, listener);
|
|
126
|
+
const channel = this._channels.get(channelName);
|
|
24
127
|
if (!channel) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
128
|
+
return complete(true);
|
|
129
|
+
}
|
|
130
|
+
const eventListeners = channel.listeners.get(eventName);
|
|
131
|
+
if (eventListeners) {
|
|
132
|
+
eventListeners.delete(listener);
|
|
133
|
+
if (eventListeners?.size === 0) {
|
|
134
|
+
channel.listeners.delete(eventName);
|
|
33
135
|
}
|
|
34
|
-
|
|
35
|
-
|
|
136
|
+
}
|
|
137
|
+
if (channel.listeners.size === 0) {
|
|
138
|
+
const channelTeardownResult = await this._teardownChannel(channelName);
|
|
139
|
+
if (isComplete(channelTeardownResult)) {
|
|
140
|
+
this._channels.delete(channelName);
|
|
36
141
|
}
|
|
37
142
|
}
|
|
143
|
+
return complete(true);
|
|
144
|
+
}
|
|
145
|
+
async _teardown() {
|
|
146
|
+
for (const [channelName] of this._channels) {
|
|
147
|
+
await this._teardownChannel(channelName);
|
|
148
|
+
}
|
|
149
|
+
this._ablyRealtime.close();
|
|
150
|
+
return complete(true);
|
|
151
|
+
}
|
|
152
|
+
async _teardownChannel(channelName) {
|
|
153
|
+
const ablyChannel = this._ablyRealtime.channels.get(channelName);
|
|
38
154
|
try {
|
|
39
|
-
await
|
|
40
|
-
await
|
|
41
|
-
|
|
42
|
-
if (parsedEventData.success) {
|
|
43
|
-
listener(parsedEventData.data);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
return complete({
|
|
47
|
-
unsubscribe: async () => {
|
|
48
|
-
channel.unsubscribe(listener);
|
|
49
|
-
await channel.presence.leave();
|
|
50
|
-
},
|
|
51
|
-
});
|
|
155
|
+
await ablyChannel.presence.leave();
|
|
156
|
+
await ablyChannel.detach();
|
|
157
|
+
return complete(true);
|
|
52
158
|
}
|
|
53
159
|
catch (error) {
|
|
54
|
-
return errored({ code: "
|
|
160
|
+
return errored({ code: "FAILED_TO_TEARDOWN_CHANNEL", error });
|
|
55
161
|
}
|
|
56
162
|
}
|
|
57
163
|
}
|
|
58
|
-
let realtime;
|
|
59
|
-
export function getRealtime() {
|
|
60
|
-
realtime ??= new Realtime();
|
|
61
|
-
return realtime;
|
|
62
|
-
}
|