swarm-mail 0.1.0 → 0.1.3

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 (75) hide show
  1. package/README.md +28 -0
  2. package/dist/adapter.d.ts +36 -0
  3. package/dist/adapter.d.ts.map +1 -0
  4. package/dist/index.d.ts +22 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +16710 -0
  7. package/{src/pglite.ts → dist/pglite.d.ts} +7 -93
  8. package/dist/pglite.d.ts.map +1 -0
  9. package/dist/streams/agent-mail.d.ts +139 -0
  10. package/dist/streams/agent-mail.d.ts.map +1 -0
  11. package/dist/streams/debug.d.ts +173 -0
  12. package/dist/streams/debug.d.ts.map +1 -0
  13. package/dist/streams/effect/ask.d.ts +124 -0
  14. package/dist/streams/effect/ask.d.ts.map +1 -0
  15. package/dist/streams/effect/cursor.d.ts +87 -0
  16. package/dist/streams/effect/cursor.d.ts.map +1 -0
  17. package/dist/streams/effect/deferred.d.ts +108 -0
  18. package/dist/streams/effect/deferred.d.ts.map +1 -0
  19. package/{src/streams/effect/index.ts → dist/streams/effect/index.d.ts} +1 -0
  20. package/dist/streams/effect/index.d.ts.map +1 -0
  21. package/{src/streams/effect/layers.ts → dist/streams/effect/layers.d.ts} +8 -33
  22. package/dist/streams/effect/layers.d.ts.map +1 -0
  23. package/dist/streams/effect/lock.d.ts +137 -0
  24. package/dist/streams/effect/lock.d.ts.map +1 -0
  25. package/dist/streams/effect/mailbox.d.ts +98 -0
  26. package/dist/streams/effect/mailbox.d.ts.map +1 -0
  27. package/dist/streams/events.d.ts +487 -0
  28. package/dist/streams/events.d.ts.map +1 -0
  29. package/dist/streams/index.d.ts +106 -0
  30. package/dist/streams/index.d.ts.map +1 -0
  31. package/dist/streams/migrations.d.ts +102 -0
  32. package/dist/streams/migrations.d.ts.map +1 -0
  33. package/dist/streams/projections.d.ts +173 -0
  34. package/dist/streams/projections.d.ts.map +1 -0
  35. package/dist/streams/store.d.ts +171 -0
  36. package/dist/streams/store.d.ts.map +1 -0
  37. package/dist/streams/swarm-mail.d.ts +153 -0
  38. package/dist/streams/swarm-mail.d.ts.map +1 -0
  39. package/dist/types/adapter.d.ts +267 -0
  40. package/dist/types/adapter.d.ts.map +1 -0
  41. package/dist/types/database.d.ts +117 -0
  42. package/dist/types/database.d.ts.map +1 -0
  43. package/{src/types/index.ts → dist/types/index.d.ts} +2 -15
  44. package/dist/types/index.d.ts.map +1 -0
  45. package/package.json +21 -5
  46. package/src/adapter.ts +0 -306
  47. package/src/index.ts +0 -57
  48. package/src/streams/agent-mail.test.ts +0 -777
  49. package/src/streams/agent-mail.ts +0 -535
  50. package/src/streams/debug.test.ts +0 -500
  51. package/src/streams/debug.ts +0 -727
  52. package/src/streams/effect/ask.integration.test.ts +0 -314
  53. package/src/streams/effect/ask.ts +0 -202
  54. package/src/streams/effect/cursor.integration.test.ts +0 -418
  55. package/src/streams/effect/cursor.ts +0 -288
  56. package/src/streams/effect/deferred.test.ts +0 -357
  57. package/src/streams/effect/deferred.ts +0 -445
  58. package/src/streams/effect/lock.test.ts +0 -385
  59. package/src/streams/effect/lock.ts +0 -399
  60. package/src/streams/effect/mailbox.test.ts +0 -260
  61. package/src/streams/effect/mailbox.ts +0 -318
  62. package/src/streams/events.test.ts +0 -924
  63. package/src/streams/events.ts +0 -329
  64. package/src/streams/index.test.ts +0 -229
  65. package/src/streams/index.ts +0 -578
  66. package/src/streams/migrations.test.ts +0 -359
  67. package/src/streams/migrations.ts +0 -362
  68. package/src/streams/projections.test.ts +0 -611
  69. package/src/streams/projections.ts +0 -564
  70. package/src/streams/store.integration.test.ts +0 -658
  71. package/src/streams/store.ts +0 -1129
  72. package/src/streams/swarm-mail.ts +0 -552
  73. package/src/types/adapter.ts +0 -392
  74. package/src/types/database.ts +0 -127
  75. package/tsconfig.json +0 -22
@@ -1,445 +0,0 @@
1
- /**
2
- * DurableDeferred Service - Distributed Promises
3
- *
4
- * Creates a "distributed promise" that can be resolved from anywhere.
5
- * Useful for request/response patterns over streams.
6
- *
7
- * @example
8
- * ```typescript
9
- * const response = await DurableDeferred.create<Response>({ ttlSeconds: 60 })
10
- * await actor.append({ payload: message, replyTo: response.url })
11
- * return response.value // blocks until resolved or timeout
12
- * ```
13
- *
14
- * Implementation:
15
- * - Uses Effect.Deferred internally for blocking await
16
- * - Stores pending promises in 'deferred' table with TTL
17
- * - Polls database for resolution (could be upgraded to NOTIFY/LISTEN)
18
- * - Cleans up expired entries automatically
19
- */
20
-
21
- import { Context, Deferred, Duration, Effect, Layer } from "effect";
22
- import { nanoid } from "nanoid";
23
- import { getDatabase } from "../index";
24
-
25
- // ============================================================================
26
- // Errors
27
- // ============================================================================
28
-
29
- /**
30
- * Timeout error when deferred expires before resolution
31
- */
32
- export class TimeoutError extends Error {
33
- readonly _tag = "TimeoutError";
34
- constructor(
35
- public readonly url: string,
36
- public readonly ttlSeconds: number,
37
- ) {
38
- super(`Deferred ${url} timed out after ${ttlSeconds}s`);
39
- }
40
- }
41
-
42
- /**
43
- * Not found error when deferred URL doesn't exist
44
- */
45
- export class NotFoundError extends Error {
46
- readonly _tag = "NotFoundError";
47
- constructor(public readonly url: string) {
48
- super(`Deferred ${url} not found`);
49
- }
50
- }
51
-
52
- // ============================================================================
53
- // Types
54
- // ============================================================================
55
-
56
- /**
57
- * Handle for a pending deferred promise
58
- */
59
- export interface DeferredHandle<T> {
60
- /** Unique URL/identifier for this deferred */
61
- readonly url: string;
62
- /** Blocks until resolved/rejected or timeout */
63
- readonly value: Effect.Effect<T, TimeoutError | NotFoundError>;
64
- }
65
-
66
- /**
67
- * Configuration for creating a deferred
68
- */
69
- export interface DeferredConfig {
70
- /** Time-to-live in seconds before timeout */
71
- readonly ttlSeconds: number;
72
- /** Optional project path for database isolation */
73
- readonly projectPath?: string;
74
- }
75
-
76
- // ============================================================================
77
- // Service Interface
78
- // ============================================================================
79
-
80
- /**
81
- * DurableDeferred service for distributed promises
82
- */
83
- export class DurableDeferred extends Context.Tag("DurableDeferred")<
84
- DurableDeferred,
85
- {
86
- /**
87
- * Create a new deferred promise
88
- *
89
- * @returns Handle with URL and value getter
90
- */
91
- readonly create: <T>(
92
- config: DeferredConfig,
93
- ) => Effect.Effect<DeferredHandle<T>>;
94
-
95
- /**
96
- * Resolve a deferred with a value
97
- *
98
- * @param url - Deferred identifier
99
- * @param value - Resolution value
100
- */
101
- readonly resolve: <T>(
102
- url: string,
103
- value: T,
104
- projectPath?: string,
105
- ) => Effect.Effect<void, NotFoundError>;
106
-
107
- /**
108
- * Reject a deferred with an error
109
- *
110
- * @param url - Deferred identifier
111
- * @param error - Error to reject with
112
- */
113
- readonly reject: (
114
- url: string,
115
- error: Error,
116
- projectPath?: string,
117
- ) => Effect.Effect<void, NotFoundError>;
118
-
119
- /**
120
- * Await a deferred's resolution (internal - use handle.value instead)
121
- */
122
- readonly await: <T>(
123
- url: string,
124
- ttlSeconds: number,
125
- projectPath?: string,
126
- ) => Effect.Effect<T, TimeoutError | NotFoundError>;
127
- }
128
- >() {}
129
-
130
- // ============================================================================
131
- // Implementation
132
- // ============================================================================
133
-
134
- /**
135
- * In-memory registry of active deferreds
136
- * Maps URL -> Effect.Deferred for instant resolution without polling
137
- */
138
- const activeDefersMap = new Map<string, Deferred.Deferred<unknown, Error>>();
139
-
140
- /**
141
- * Ensure deferred table exists in database
142
- */
143
- async function ensureDeferredTable(projectPath?: string): Promise<void> {
144
- const db = await getDatabase(projectPath);
145
- await db.exec(`
146
- CREATE TABLE IF NOT EXISTS deferred (
147
- id SERIAL PRIMARY KEY,
148
- url TEXT NOT NULL UNIQUE,
149
- resolved BOOLEAN NOT NULL DEFAULT FALSE,
150
- value JSONB,
151
- error TEXT,
152
- expires_at BIGINT NOT NULL,
153
- created_at BIGINT NOT NULL
154
- );
155
-
156
- CREATE INDEX IF NOT EXISTS idx_deferred_url ON deferred(url);
157
- CREATE INDEX IF NOT EXISTS idx_deferred_expires ON deferred(expires_at);
158
- `);
159
- }
160
-
161
- /**
162
- * Clean up expired deferred entries
163
- */
164
- async function cleanupExpired(projectPath?: string): Promise<number> {
165
- const db = await getDatabase(projectPath);
166
- const now = Date.now();
167
- // DELETE...RETURNING returns the deleted rows, so count them directly
168
- const result = await db.query<{ url: string }>(
169
- `DELETE FROM deferred WHERE expires_at < $1 RETURNING url`,
170
- [now],
171
- );
172
- return result.rows.length;
173
- }
174
-
175
- /**
176
- * Create implementation
177
- */
178
- function createImpl<T>(
179
- config: DeferredConfig,
180
- ): Effect.Effect<DeferredHandle<T>> {
181
- return Effect.gen(function* (_) {
182
- // Ensure table exists
183
- yield* _(Effect.promise(() => ensureDeferredTable(config.projectPath)));
184
-
185
- // Generate unique URL
186
- const url = `deferred:${nanoid()}`;
187
- const expiresAt = Date.now() + config.ttlSeconds * 1000;
188
-
189
- // Create Effect.Deferred for instant resolution
190
- const deferred = yield* _(Deferred.make<T, Error>());
191
- activeDefersMap.set(url, deferred as Deferred.Deferred<unknown, Error>);
192
-
193
- // Insert into database
194
- const db = yield* _(Effect.promise(() => getDatabase(config.projectPath)));
195
- yield* _(
196
- Effect.promise(() =>
197
- db.query(
198
- `INSERT INTO deferred (url, resolved, expires_at, created_at)
199
- VALUES ($1, $2, $3, $4)`,
200
- [url, false, expiresAt, Date.now()],
201
- ),
202
- ),
203
- );
204
-
205
- // Create value getter that directly calls awaitImpl (doesn't need service context)
206
- const value: Effect.Effect<T, TimeoutError | NotFoundError> = awaitImpl<T>(
207
- url,
208
- config.ttlSeconds,
209
- config.projectPath,
210
- );
211
-
212
- return { url, value };
213
- });
214
- }
215
-
216
- /**
217
- * Resolve implementation
218
- */
219
- function resolveImpl<T>(
220
- url: string,
221
- value: T,
222
- projectPath?: string,
223
- ): Effect.Effect<void, NotFoundError> {
224
- return Effect.gen(function* (_) {
225
- yield* _(Effect.promise(() => ensureDeferredTable(projectPath)));
226
-
227
- const db = yield* _(Effect.promise(() => getDatabase(projectPath)));
228
-
229
- // Update database
230
- const result = yield* _(
231
- Effect.promise(() =>
232
- db.query<{ url: string }>(
233
- `UPDATE deferred
234
- SET resolved = TRUE, value = $1::jsonb
235
- WHERE url = $2 AND resolved = FALSE
236
- RETURNING url`,
237
- [JSON.stringify(value), url],
238
- ),
239
- ),
240
- );
241
-
242
- if (result.rows.length === 0) {
243
- yield* _(Effect.fail(new NotFoundError(url)));
244
- }
245
-
246
- // Resolve in-memory deferred if it exists
247
- const deferred = activeDefersMap.get(url);
248
- if (deferred) {
249
- yield* _(
250
- Deferred.succeed(deferred, value as unknown) as Effect.Effect<
251
- boolean,
252
- never
253
- >,
254
- );
255
- }
256
- });
257
- }
258
-
259
- /**
260
- * Reject implementation
261
- */
262
- function rejectImpl(
263
- url: string,
264
- error: Error,
265
- projectPath?: string,
266
- ): Effect.Effect<void, NotFoundError> {
267
- return Effect.gen(function* (_) {
268
- yield* _(Effect.promise(() => ensureDeferredTable(projectPath)));
269
-
270
- const db = yield* _(Effect.promise(() => getDatabase(projectPath)));
271
-
272
- // Update database
273
- const result = yield* _(
274
- Effect.promise(() =>
275
- db.query<{ url: string }>(
276
- `UPDATE deferred
277
- SET resolved = TRUE, error = $1
278
- WHERE url = $2 AND resolved = FALSE
279
- RETURNING url`,
280
- [error.message, url],
281
- ),
282
- ),
283
- );
284
-
285
- if (result.rows.length === 0) {
286
- yield* _(Effect.fail(new NotFoundError(url)));
287
- }
288
-
289
- // Reject in-memory deferred if it exists
290
- const deferred = activeDefersMap.get(url);
291
- if (deferred) {
292
- yield* _(Deferred.fail(deferred, error) as Effect.Effect<boolean, never>);
293
- }
294
- });
295
- }
296
-
297
- /**
298
- * Await implementation (uses in-memory deferred if available, otherwise polls)
299
- */
300
- function awaitImpl<T>(
301
- url: string,
302
- ttlSeconds: number,
303
- projectPath?: string,
304
- ): Effect.Effect<T, TimeoutError | NotFoundError> {
305
- return Effect.gen(function* (_) {
306
- yield* _(Effect.promise(() => ensureDeferredTable(projectPath)));
307
-
308
- // Check if we have an in-memory deferred
309
- const deferred = activeDefersMap.get(url);
310
- if (deferred) {
311
- // Use in-memory deferred with timeout
312
- const result = yield* _(
313
- Deferred.await(deferred as Deferred.Deferred<T, Error>).pipe(
314
- Effect.timeoutFail({
315
- duration: Duration.seconds(ttlSeconds),
316
- onTimeout: () => new TimeoutError(url, ttlSeconds),
317
- }),
318
- Effect.catchAll((error) =>
319
- Effect.fail(
320
- error instanceof NotFoundError || error instanceof TimeoutError
321
- ? error
322
- : new NotFoundError(url),
323
- ),
324
- ),
325
- ),
326
- );
327
-
328
- // Cleanup
329
- activeDefersMap.delete(url);
330
- return result as T;
331
- }
332
-
333
- // Fall back to polling database
334
- const db = yield* _(Effect.promise(() => getDatabase(projectPath)));
335
- const startTime = Date.now();
336
- const timeoutMs = ttlSeconds * 1000;
337
-
338
- // Poll loop
339
- while (true) {
340
- // Check timeout
341
- if (Date.now() - startTime > timeoutMs) {
342
- return yield* _(Effect.fail(new TimeoutError(url, ttlSeconds)));
343
- }
344
-
345
- // Query database
346
- const result = yield* _(
347
- Effect.promise(() =>
348
- db.query<{ resolved: boolean; value: unknown; error: string | null }>(
349
- `SELECT resolved, value, error FROM deferred WHERE url = $1`,
350
- [url],
351
- ),
352
- ),
353
- );
354
-
355
- const row = result.rows[0];
356
- if (!row) {
357
- return yield* _(Effect.fail(new NotFoundError(url)));
358
- }
359
-
360
- // Check if resolved
361
- if (row.resolved) {
362
- if (row.error) {
363
- // Convert stored error message to NotFoundError
364
- return yield* _(Effect.fail(new NotFoundError(url)));
365
- }
366
- // Value should exist if resolved=true and error=null
367
- if (!row.value) {
368
- return yield* _(Effect.fail(new NotFoundError(url)));
369
- }
370
- // PGLite returns JSONB as parsed object already
371
- return (
372
- typeof row.value === "string" ? JSON.parse(row.value) : row.value
373
- ) as T;
374
- }
375
-
376
- // Sleep before next poll (100ms)
377
- yield* _(Effect.sleep(Duration.millis(100)));
378
- }
379
- });
380
- }
381
-
382
- // ============================================================================
383
- // Layer
384
- // ============================================================================
385
-
386
- /**
387
- * Live implementation of DurableDeferred service
388
- */
389
- export const DurableDeferredLive = Layer.succeed(DurableDeferred, {
390
- create: createImpl,
391
- resolve: resolveImpl,
392
- reject: rejectImpl,
393
- await: awaitImpl,
394
- });
395
-
396
- // ============================================================================
397
- // Convenience Functions
398
- // ============================================================================
399
-
400
- /**
401
- * Create a deferred promise
402
- */
403
- export function createDeferred<T>(
404
- config: DeferredConfig,
405
- ): Effect.Effect<DeferredHandle<T>, never, DurableDeferred> {
406
- return Effect.gen(function* (_) {
407
- const service = yield* _(DurableDeferred);
408
- return yield* _(service.create<T>(config));
409
- });
410
- }
411
-
412
- /**
413
- * Resolve a deferred
414
- */
415
- export function resolveDeferred<T>(
416
- url: string,
417
- value: T,
418
- projectPath?: string,
419
- ): Effect.Effect<void, NotFoundError, DurableDeferred> {
420
- return Effect.gen(function* (_) {
421
- const service = yield* _(DurableDeferred);
422
- return yield* _(service.resolve(url, value, projectPath));
423
- });
424
- }
425
-
426
- /**
427
- * Reject a deferred
428
- */
429
- export function rejectDeferred(
430
- url: string,
431
- error: Error,
432
- projectPath?: string,
433
- ): Effect.Effect<void, NotFoundError, DurableDeferred> {
434
- return Effect.gen(function* (_) {
435
- const service = yield* _(DurableDeferred);
436
- return yield* _(service.reject(url, error, projectPath));
437
- });
438
- }
439
-
440
- /**
441
- * Cleanup expired deferred entries (call periodically)
442
- */
443
- export function cleanupDeferreds(projectPath?: string): Effect.Effect<number> {
444
- return Effect.promise(() => cleanupExpired(projectPath));
445
- }