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 { getRealtime } from "../../util/realtime.js";
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 = getRealtime();
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 realtimeSubscriptionResult;
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({
@@ -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)) {
@@ -165,13 +165,13 @@ export function printAuthenticationError(error) {
165
165
  }
166
166
  export function printLogSubscriptionError(error) {
167
167
  switch (error.code) {
168
- case "FAILED_TO_AUTHENTICATE_CHANNEL":
169
- process.stderr.write(chalk.red("Failed to authenticate to logs server\n"));
170
- break;
171
- case "FAILED_TO_SUBSCRIBE_TO_EVENTS":
172
- process.stderr.write(chalk.red("Failed to subscribe to logs\n"));
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
  }
@@ -0,0 +1,9 @@
1
+ {
2
+ "$schema": "https://unpkg.com/graphql-config@5.1.5/config-schema.json",
3
+ "projects": {
4
+ "attio": {
5
+ "schema": "./node_modules/attio/schema.graphql",
6
+ "documents": ["./src/**/*.{graphql,gql}"]
7
+ }
8
+ }
9
+ }
@@ -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
- _realtime;
11
+ _ablyRealtime;
6
12
  _channels = new Map();
13
+ _suspendListeners = new Set();
7
14
  constructor() {
8
- this._realtime = new Ably.Realtime({
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 subscribe(channelName, eventName, eventDataSchema, listener) {
23
- let channel = this._channels.get(channelName);
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
- channel = this._realtime.channels.get(channelName);
26
- this._channels.set(channelName, channel);
27
- try {
28
- await this._realtime.auth.authorize({
29
- capability: {
30
- [`${channelName}`]: ["subscribe"],
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
- catch (error) {
35
- return errored({ code: "FAILED_TO_AUTHENTICATE_CHANNEL", error });
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 channel.presence.enter();
40
- await channel.subscribe(eventName, (event) => {
41
- const parsedEventData = eventDataSchema.safeParse(event.data.data);
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: "FAILED_TO_SUBSCRIBE_TO_EVENTS", error });
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
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "attio",
3
- "version": "0.0.1-experimental.20250815",
3
+ "version": "0.0.1-experimental.20250815.2",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "lib",