swarm-mail 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.
- package/README.md +201 -0
- package/package.json +28 -0
- package/src/adapter.ts +306 -0
- package/src/index.ts +57 -0
- package/src/pglite.ts +189 -0
- package/src/streams/agent-mail.test.ts +777 -0
- package/src/streams/agent-mail.ts +535 -0
- package/src/streams/debug.test.ts +500 -0
- package/src/streams/debug.ts +727 -0
- package/src/streams/effect/ask.integration.test.ts +314 -0
- package/src/streams/effect/ask.ts +202 -0
- package/src/streams/effect/cursor.integration.test.ts +418 -0
- package/src/streams/effect/cursor.ts +288 -0
- package/src/streams/effect/deferred.test.ts +357 -0
- package/src/streams/effect/deferred.ts +445 -0
- package/src/streams/effect/index.ts +17 -0
- package/src/streams/effect/layers.ts +73 -0
- package/src/streams/effect/lock.test.ts +385 -0
- package/src/streams/effect/lock.ts +399 -0
- package/src/streams/effect/mailbox.test.ts +260 -0
- package/src/streams/effect/mailbox.ts +318 -0
- package/src/streams/events.test.ts +924 -0
- package/src/streams/events.ts +329 -0
- package/src/streams/index.test.ts +229 -0
- package/src/streams/index.ts +578 -0
- package/src/streams/migrations.test.ts +359 -0
- package/src/streams/migrations.ts +362 -0
- package/src/streams/projections.test.ts +611 -0
- package/src/streams/projections.ts +564 -0
- package/src/streams/store.integration.test.ts +658 -0
- package/src/streams/store.ts +1129 -0
- package/src/streams/swarm-mail.ts +552 -0
- package/src/types/adapter.ts +392 -0
- package/src/types/database.ts +127 -0
- package/src/types/index.ts +26 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,445 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Effect-TS services for durable event stream processing
|
|
3
|
+
*
|
|
4
|
+
* Exports:
|
|
5
|
+
* - DurableCursor: Positioned event consumption with checkpointing
|
|
6
|
+
* - DurableLock: Distributed mutual exclusion via CAS
|
|
7
|
+
* - DurableDeferred: Distributed promises
|
|
8
|
+
* - DurableMailbox: Actor message passing
|
|
9
|
+
* - ask: Request/response pattern (mailbox + deferred)
|
|
10
|
+
* - layers: Composed service layers for common use cases
|
|
11
|
+
*/
|
|
12
|
+
export * from "./cursor";
|
|
13
|
+
export * from "./deferred";
|
|
14
|
+
export * from "./lock";
|
|
15
|
+
export * from "./mailbox";
|
|
16
|
+
export * from "./ask";
|
|
17
|
+
export * from "./layers";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composed Layers for Durable Streams Services
|
|
3
|
+
*
|
|
4
|
+
* Provides pre-configured Layer compositions for common use cases.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // For ask pattern (request/response):
|
|
9
|
+
* const program = Effect.gen(function* () {
|
|
10
|
+
* const mailbox = yield* DurableMailbox;
|
|
11
|
+
* const deferred = yield* DurableDeferred;
|
|
12
|
+
* // Use services...
|
|
13
|
+
* }).pipe(Effect.provide(DurableAskLive));
|
|
14
|
+
*
|
|
15
|
+
* // For cursor + deferred only:
|
|
16
|
+
* const program = Effect.gen(function* () {
|
|
17
|
+
* const cursor = yield* DurableCursor;
|
|
18
|
+
* const deferred = yield* DurableDeferred;
|
|
19
|
+
* }).pipe(Effect.provide(DurableCursorDeferredLive));
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { Layer } from "effect";
|
|
24
|
+
import { DurableCursor, DurableCursorLive } from "./cursor";
|
|
25
|
+
import { DurableDeferred, DurableDeferredLive } from "./deferred";
|
|
26
|
+
import { DurableLock, DurableLockLive } from "./lock";
|
|
27
|
+
import { DurableMailbox, DurableMailboxLive } from "./mailbox";
|
|
28
|
+
|
|
29
|
+
// ============================================================================
|
|
30
|
+
// Layer Wrappers (convert Context.make to Layer.succeed)
|
|
31
|
+
// ============================================================================
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Cursor service as Layer
|
|
35
|
+
*/
|
|
36
|
+
const CursorLayer = Layer.succeed(DurableCursor, DurableCursorLive);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Mailbox service as Layer (with cursor dependency)
|
|
40
|
+
*/
|
|
41
|
+
const MailboxLayer = Layer.mergeAll(CursorLayer, DurableMailboxLive);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Minimal layer with just Cursor and Deferred
|
|
45
|
+
*
|
|
46
|
+
* Use when you only need event consumption and distributed promises.
|
|
47
|
+
*/
|
|
48
|
+
export const DurableCursorDeferredLive = Layer.mergeAll(
|
|
49
|
+
CursorLayer,
|
|
50
|
+
DurableDeferredLive,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Mailbox layer with dependencies
|
|
55
|
+
*
|
|
56
|
+
* Provides DurableMailbox + DurableCursor (required dependency).
|
|
57
|
+
*/
|
|
58
|
+
export const DurableMailboxWithDepsLive = MailboxLayer;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Ask pattern layer (Mailbox + Deferred)
|
|
62
|
+
*
|
|
63
|
+
* Provides all services needed for ask<Req, Res>() pattern:
|
|
64
|
+
* - DurableMailbox (with DurableCursor dependency)
|
|
65
|
+
* - DurableDeferred
|
|
66
|
+
*/
|
|
67
|
+
export const DurableAskLive = Layer.mergeAll(DurableDeferredLive, MailboxLayer);
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// Re-exports for convenience
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
export { DurableCursor, DurableDeferred, DurableLock, DurableMailbox };
|