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.
Files changed (36) hide show
  1. package/README.md +201 -0
  2. package/package.json +28 -0
  3. package/src/adapter.ts +306 -0
  4. package/src/index.ts +57 -0
  5. package/src/pglite.ts +189 -0
  6. package/src/streams/agent-mail.test.ts +777 -0
  7. package/src/streams/agent-mail.ts +535 -0
  8. package/src/streams/debug.test.ts +500 -0
  9. package/src/streams/debug.ts +727 -0
  10. package/src/streams/effect/ask.integration.test.ts +314 -0
  11. package/src/streams/effect/ask.ts +202 -0
  12. package/src/streams/effect/cursor.integration.test.ts +418 -0
  13. package/src/streams/effect/cursor.ts +288 -0
  14. package/src/streams/effect/deferred.test.ts +357 -0
  15. package/src/streams/effect/deferred.ts +445 -0
  16. package/src/streams/effect/index.ts +17 -0
  17. package/src/streams/effect/layers.ts +73 -0
  18. package/src/streams/effect/lock.test.ts +385 -0
  19. package/src/streams/effect/lock.ts +399 -0
  20. package/src/streams/effect/mailbox.test.ts +260 -0
  21. package/src/streams/effect/mailbox.ts +318 -0
  22. package/src/streams/events.test.ts +924 -0
  23. package/src/streams/events.ts +329 -0
  24. package/src/streams/index.test.ts +229 -0
  25. package/src/streams/index.ts +578 -0
  26. package/src/streams/migrations.test.ts +359 -0
  27. package/src/streams/migrations.ts +362 -0
  28. package/src/streams/projections.test.ts +611 -0
  29. package/src/streams/projections.ts +564 -0
  30. package/src/streams/store.integration.test.ts +658 -0
  31. package/src/streams/store.ts +1129 -0
  32. package/src/streams/swarm-mail.ts +552 -0
  33. package/src/types/adapter.ts +392 -0
  34. package/src/types/database.ts +127 -0
  35. package/src/types/index.ts +26 -0
  36. package/tsconfig.json +22 -0
package/src/pglite.ts ADDED
@@ -0,0 +1,189 @@
1
+ /**
2
+ * PGLite Convenience Layer - Simple API for PGLite users
3
+ *
4
+ * This file provides a simplified interface for users who just want to use
5
+ * PGLite without manually setting up adapters. For advanced use cases (custom
6
+ * database, connection pooling, etc.), use createSwarmMailAdapter directly.
7
+ *
8
+ * ## Simple API (this file)
9
+ * ```typescript
10
+ * import { getSwarmMail } from '@opencode/swarm-mail';
11
+ *
12
+ * const swarmMail = await getSwarmMail('/path/to/project');
13
+ * await swarmMail.registerAgent(projectKey, 'agent-name');
14
+ * ```
15
+ *
16
+ * ## Advanced API (adapter pattern)
17
+ * ```typescript
18
+ * import { createSwarmMailAdapter } from '@opencode/swarm-mail';
19
+ * import { createCustomDbAdapter } from './my-adapter';
20
+ *
21
+ * const db = createCustomDbAdapter({ path: './custom.db' });
22
+ * const swarmMail = createSwarmMailAdapter(db, '/path/to/project');
23
+ * ```
24
+ */
25
+
26
+ import { PGlite } from "@electric-sql/pglite";
27
+ import { createSwarmMailAdapter } from "./adapter";
28
+ import type { DatabaseAdapter, SwarmMailAdapter } from "./types";
29
+ import { existsSync, mkdirSync } from "node:fs";
30
+ import { join } from "node:path";
31
+ import { homedir } from "node:os";
32
+
33
+ /**
34
+ * Wrap PGLite to match DatabaseAdapter interface
35
+ *
36
+ * PGLite has query() and exec() methods that match DatabaseAdapter,
37
+ * but TypeScript needs the explicit wrapper for type safety.
38
+ * PGLite's exec() returns Results[] but DatabaseAdapter expects void.
39
+ */
40
+ function wrapPGlite(pglite: PGlite): DatabaseAdapter {
41
+ return {
42
+ query: <T>(sql: string, params?: unknown[]) => pglite.query<T>(sql, params),
43
+ exec: async (sql: string) => {
44
+ await pglite.exec(sql);
45
+ },
46
+ close: () => pglite.close(),
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Get database path (project-local or global fallback)
52
+ *
53
+ * Prefers project-local .opencode/streams
54
+ * Falls back to global ~/.opencode/streams
55
+ *
56
+ * @param projectPath - Optional project root path
57
+ * @returns Absolute path to database directory
58
+ */
59
+ export function getDatabasePath(projectPath?: string): string {
60
+ if (projectPath) {
61
+ const localDir = join(projectPath, ".opencode");
62
+ if (!existsSync(localDir)) {
63
+ mkdirSync(localDir, { recursive: true });
64
+ }
65
+ return join(localDir, "streams");
66
+ }
67
+ const globalDir = join(homedir(), ".opencode");
68
+ if (!existsSync(globalDir)) {
69
+ mkdirSync(globalDir, { recursive: true });
70
+ }
71
+ return join(globalDir, "streams");
72
+ }
73
+
74
+ /**
75
+ * Singleton cache for SwarmMail instances
76
+ *
77
+ * Key is database path, value is the adapter + PGLite instance
78
+ */
79
+ const instances = new Map<
80
+ string,
81
+ { adapter: SwarmMailAdapter; pglite: PGlite }
82
+ >();
83
+
84
+ /**
85
+ * Get or create SwarmMail instance for a project
86
+ *
87
+ * Uses singleton pattern - one instance per database path.
88
+ * Safe to call multiple times for the same project.
89
+ *
90
+ * @param projectPath - Optional project root path (defaults to global)
91
+ * @returns SwarmMailAdapter instance
92
+ *
93
+ * @example
94
+ * ```typescript
95
+ * // Project-local database
96
+ * const swarmMail = await getSwarmMail('/path/to/project');
97
+ *
98
+ * // Global database (shared across all projects)
99
+ * const swarmMail = await getSwarmMail();
100
+ * ```
101
+ */
102
+ export async function getSwarmMail(
103
+ projectPath?: string,
104
+ ): Promise<SwarmMailAdapter> {
105
+ const dbPath = getDatabasePath(projectPath);
106
+ const projectKey = projectPath || dbPath;
107
+
108
+ if (!instances.has(dbPath)) {
109
+ const pglite = new PGlite(dbPath);
110
+ const db = wrapPGlite(pglite);
111
+ const adapter = createSwarmMailAdapter(db, projectKey);
112
+ await adapter.runMigrations();
113
+ instances.set(dbPath, { adapter, pglite });
114
+ }
115
+
116
+ return instances.get(dbPath)!.adapter;
117
+ }
118
+
119
+ /**
120
+ * Create in-memory SwarmMail instance (for testing)
121
+ *
122
+ * Not cached - each call creates a new instance.
123
+ * Data is lost when instance is closed or process exits.
124
+ *
125
+ * @param projectKey - Project identifier (defaults to 'test')
126
+ * @returns SwarmMailAdapter instance
127
+ *
128
+ * @example
129
+ * ```typescript
130
+ * const swarmMail = await createInMemorySwarmMail('test-project');
131
+ * await swarmMail.registerAgent('test-project', 'test-agent');
132
+ * // ... test code ...
133
+ * await swarmMail.close();
134
+ * ```
135
+ */
136
+ export async function createInMemorySwarmMail(
137
+ projectKey = "test",
138
+ ): Promise<SwarmMailAdapter> {
139
+ const pglite = new PGlite(); // in-memory
140
+ const db = wrapPGlite(pglite);
141
+ const adapter = createSwarmMailAdapter(db, projectKey);
142
+ await adapter.runMigrations();
143
+ return adapter;
144
+ }
145
+
146
+ /**
147
+ * Close specific SwarmMail instance
148
+ *
149
+ * Closes the database connection and removes from cache.
150
+ *
151
+ * @param projectPath - Optional project root path (defaults to global)
152
+ *
153
+ * @example
154
+ * ```typescript
155
+ * await closeSwarmMail('/path/to/project');
156
+ * ```
157
+ */
158
+ export async function closeSwarmMail(projectPath?: string): Promise<void> {
159
+ const dbPath = getDatabasePath(projectPath);
160
+ const instance = instances.get(dbPath);
161
+ if (instance) {
162
+ await instance.pglite.close();
163
+ instances.delete(dbPath);
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Close all SwarmMail instances
169
+ *
170
+ * Closes all cached database connections.
171
+ * Useful for cleanup in test teardown or process shutdown.
172
+ *
173
+ * @example
174
+ * ```typescript
175
+ * // Test teardown
176
+ * afterAll(async () => {
177
+ * await closeAllSwarmMail();
178
+ * });
179
+ * ```
180
+ */
181
+ export async function closeAllSwarmMail(): Promise<void> {
182
+ for (const [path, instance] of instances) {
183
+ await instance.pglite.close();
184
+ instances.delete(path);
185
+ }
186
+ }
187
+
188
+ // Re-export PGlite for consumers who need it
189
+ export { PGlite };