@agent-glue/glue 0.1.8 → 0.2.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.
package/src/effects.ts CHANGED
@@ -5,55 +5,178 @@ function defaultDiscriminator<T extends Message>(m: Message): m is T {
5
5
  return m === m;
6
6
  }
7
7
 
8
+ export interface EffectOptions {
9
+ /** Timeout in ms. Rejects with TimeoutError (take) or idle timeout (generators). */
10
+ timeout?: number;
11
+ /** AbortSignal. Rejects with AbortError when aborted. */
12
+ signal?: AbortSignal;
13
+ }
14
+
15
+ export class TimeoutError extends Error {
16
+ constructor(message = "Operation timed out") {
17
+ super(message);
18
+ this.name = "TimeoutError";
19
+ }
20
+ }
21
+
22
+ export class AbortError extends Error {
23
+ constructor(message = "Operation aborted") {
24
+ super(message);
25
+ this.name = "AbortError";
26
+ }
27
+ }
28
+
29
+ function wrapAbortReason(reason: unknown): string {
30
+ return typeof reason === "string" ? reason : "Operation aborted";
31
+ }
32
+
8
33
  export function take<T extends Events, Events extends Message>(
9
34
  actor: ActorInterface<Message, Events>,
10
- discriminator: MessageDiscriminator<T> = defaultDiscriminator<T>
35
+ discriminator: MessageDiscriminator<T> = defaultDiscriminator<T>,
36
+ options?: EffectOptions,
11
37
  ): Promise<T> {
12
- return new Promise((resolve) => {
38
+ return new Promise((resolve, reject) => {
39
+ let settled = false;
40
+ let timer: ReturnType<typeof setTimeout> | undefined;
41
+
42
+ const cleanup = () => {
43
+ settled = true;
44
+ actor.disconnect(handler);
45
+ if (timer) clearTimeout(timer);
46
+ };
47
+
13
48
  const handler = (message: Message) => {
49
+ if (settled) return;
14
50
  if (discriminator(message)) {
15
- actor.disconnect(handler);
51
+ cleanup();
16
52
  resolve(message);
17
53
  }
18
54
  };
55
+
56
+ // AbortSignal - check before connecting
57
+ if (options?.signal) {
58
+ if (options.signal.aborted) {
59
+ reject(new AbortError(wrapAbortReason(options.signal.reason)));
60
+ return;
61
+ }
62
+ options.signal.addEventListener(
63
+ "abort",
64
+ () => {
65
+ if (!settled) {
66
+ cleanup();
67
+ reject(new AbortError(wrapAbortReason(options.signal!.reason)));
68
+ }
69
+ },
70
+ { once: true },
71
+ );
72
+ }
73
+
74
+ // Timeout
75
+ if (options?.timeout != null) {
76
+ timer = setTimeout(() => {
77
+ if (!settled) {
78
+ cleanup();
79
+ reject(new TimeoutError());
80
+ }
81
+ }, options.timeout);
82
+ timer.unref();
83
+ }
84
+
19
85
  actor.connect(handler);
20
86
  });
21
87
  }
22
88
 
23
89
  export async function* takeEvery<Events extends Message>(
24
- actor: ActorInterface<Message, Events>
90
+ actor: ActorInterface<Message, Events>,
91
+ options?: EffectOptions,
25
92
  ): AsyncGenerator<Events> {
26
93
  const eventQueue: Events[] = [];
27
94
  let resolve: ((value: Events) => void) | null = null;
95
+ let reject: ((reason: Error) => void) | null = null;
96
+ let timer: ReturnType<typeof setTimeout> | undefined;
97
+
98
+ const clearTimer = () => {
99
+ if (timer) {
100
+ clearTimeout(timer);
101
+ timer = undefined;
102
+ }
103
+ };
104
+
105
+ const resetTimer = () => {
106
+ clearTimer();
107
+ if (options?.timeout != null) {
108
+ timer = setTimeout(() => {
109
+ if (reject) {
110
+ const rej = reject;
111
+ reject = null;
112
+ resolve = null;
113
+ rej(new TimeoutError());
114
+ }
115
+ }, options.timeout);
116
+ timer.unref();
117
+ }
118
+ };
28
119
 
29
120
  const disconnect = actor.connect((message: Events) => {
30
121
  if (resolve) {
31
- resolve(message);
122
+ const res = resolve;
32
123
  resolve = null;
124
+ reject = null;
125
+ res(message);
33
126
  } else {
34
127
  eventQueue.push(message);
35
128
  }
36
129
  });
130
+
131
+ // AbortSignal
132
+ const onAbort = () => {
133
+ if (reject) {
134
+ const rej = reject;
135
+ reject = null;
136
+ resolve = null;
137
+ rej(new AbortError(wrapAbortReason(options?.signal?.reason)));
138
+ }
139
+ };
140
+
141
+ if (options?.signal) {
142
+ if (options.signal.aborted) {
143
+ disconnect();
144
+ return;
145
+ }
146
+ options.signal.addEventListener("abort", onAbort, { once: true });
147
+ }
148
+
37
149
  try {
38
150
  while (true) {
151
+ if (options?.signal?.aborted) break;
152
+
39
153
  if (eventQueue.length > 0) {
40
- yield eventQueue.shift()!;
154
+ const event = eventQueue.shift()!;
155
+ resetTimer(); // reset idle timer on event
156
+ yield event;
41
157
  } else {
42
- yield new Promise<Events>((res) => {
158
+ resetTimer(); // start idle timer while waiting
159
+ const event = await new Promise<Events>((res, rej) => {
43
160
  resolve = res;
161
+ reject = rej;
44
162
  });
163
+ clearTimer(); // event arrived, clear timer
164
+ yield event;
45
165
  }
46
166
  }
47
167
  } finally {
168
+ clearTimer();
169
+ options?.signal?.removeEventListener("abort", onAbort);
48
170
  disconnect();
49
171
  }
50
172
  }
51
173
 
52
174
  export async function* takeIf<T extends Events, Events extends Message>(
53
175
  actor: ActorInterface<Message, Events>,
54
- discriminator: MessageDiscriminator<T>
176
+ discriminator: MessageDiscriminator<T>,
177
+ options?: EffectOptions,
55
178
  ): AsyncGenerator<T> {
56
- for await (const event of takeEvery(actor)) {
179
+ for await (const event of takeEvery(actor, options)) {
57
180
  if (discriminator(event)) {
58
181
  yield event;
59
182
  }
@@ -63,9 +186,10 @@ export async function* takeIf<T extends Events, Events extends Message>(
63
186
  export async function* takeUntil<T extends Events, Events extends Message>(
64
187
  actor: ActorInterface<Message, Events>,
65
188
  discriminator: MessageDiscriminator<T>,
66
- endCondition: (message: Events) => boolean = () => true
189
+ endCondition: (message: Events) => boolean = () => true,
190
+ options?: EffectOptions,
67
191
  ): AsyncGenerator<T> {
68
- for await (const event of takeEvery(actor)) {
192
+ for await (const event of takeEvery(actor, options)) {
69
193
  if (endCondition(event)) {
70
194
  break;
71
195
  }
@@ -78,11 +202,28 @@ export async function* takeUntil<T extends Events, Events extends Message>(
78
202
  export function on<T extends Events, Events extends Message>(
79
203
  actor: ActorInterface<Message, Events>,
80
204
  discriminator: MessageDiscriminator<T>,
81
- handler: (message: T) => void
205
+ handler: (message: T) => void,
206
+ options?: { signal?: AbortSignal },
82
207
  ): () => void {
83
- return actor.connect((message) => {
208
+ if (options?.signal?.aborted) {
209
+ return () => {};
210
+ }
211
+
212
+ const disconnect = actor.connect((message) => {
84
213
  if (discriminator(message)) {
85
214
  handler(message);
86
215
  }
87
216
  });
217
+
218
+ if (options?.signal) {
219
+ options.signal.addEventListener(
220
+ "abort",
221
+ () => {
222
+ disconnect();
223
+ },
224
+ { once: true },
225
+ );
226
+ }
227
+
228
+ return disconnect;
88
229
  }