@soulcraft/sdk 1.0.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/dist/client/index.d.ts +62 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +60 -0
- package/dist/client/index.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/modules/ai/index.d.ts +55 -0
- package/dist/modules/ai/index.d.ts.map +1 -0
- package/dist/modules/ai/index.js +263 -0
- package/dist/modules/ai/index.js.map +1 -0
- package/dist/modules/ai/types.d.ts +216 -0
- package/dist/modules/ai/types.d.ts.map +1 -0
- package/dist/modules/ai/types.js +30 -0
- package/dist/modules/ai/types.js.map +1 -0
- package/dist/modules/auth/backchannel.d.ts +85 -0
- package/dist/modules/auth/backchannel.d.ts.map +1 -0
- package/dist/modules/auth/backchannel.js +168 -0
- package/dist/modules/auth/backchannel.js.map +1 -0
- package/dist/modules/auth/config.d.ts +122 -0
- package/dist/modules/auth/config.d.ts.map +1 -0
- package/dist/modules/auth/config.js +158 -0
- package/dist/modules/auth/config.js.map +1 -0
- package/dist/modules/auth/middleware.d.ts +146 -0
- package/dist/modules/auth/middleware.d.ts.map +1 -0
- package/dist/modules/auth/middleware.js +204 -0
- package/dist/modules/auth/middleware.js.map +1 -0
- package/dist/modules/auth/types.d.ts +162 -0
- package/dist/modules/auth/types.d.ts.map +1 -0
- package/dist/modules/auth/types.js +14 -0
- package/dist/modules/auth/types.js.map +1 -0
- package/dist/modules/billing/types.d.ts +7 -0
- package/dist/modules/billing/types.d.ts.map +1 -0
- package/dist/modules/billing/types.js +7 -0
- package/dist/modules/billing/types.js.map +1 -0
- package/dist/modules/brainy/auth.d.ts +104 -0
- package/dist/modules/brainy/auth.d.ts.map +1 -0
- package/dist/modules/brainy/auth.js +144 -0
- package/dist/modules/brainy/auth.js.map +1 -0
- package/dist/modules/brainy/errors.d.ts +118 -0
- package/dist/modules/brainy/errors.d.ts.map +1 -0
- package/dist/modules/brainy/errors.js +142 -0
- package/dist/modules/brainy/errors.js.map +1 -0
- package/dist/modules/brainy/events.d.ts +63 -0
- package/dist/modules/brainy/events.d.ts.map +1 -0
- package/dist/modules/brainy/events.js +14 -0
- package/dist/modules/brainy/events.js.map +1 -0
- package/dist/modules/brainy/proxy.d.ts +48 -0
- package/dist/modules/brainy/proxy.d.ts.map +1 -0
- package/dist/modules/brainy/proxy.js +95 -0
- package/dist/modules/brainy/proxy.js.map +1 -0
- package/dist/modules/brainy/types.d.ts +83 -0
- package/dist/modules/brainy/types.d.ts.map +1 -0
- package/dist/modules/brainy/types.js +21 -0
- package/dist/modules/brainy/types.js.map +1 -0
- package/dist/modules/events/index.d.ts +41 -0
- package/dist/modules/events/index.d.ts.map +1 -0
- package/dist/modules/events/index.js +53 -0
- package/dist/modules/events/index.js.map +1 -0
- package/dist/modules/events/types.d.ts +129 -0
- package/dist/modules/events/types.d.ts.map +1 -0
- package/dist/modules/events/types.js +32 -0
- package/dist/modules/events/types.js.map +1 -0
- package/dist/modules/formats/types.d.ts +7 -0
- package/dist/modules/formats/types.d.ts.map +1 -0
- package/dist/modules/formats/types.js +7 -0
- package/dist/modules/formats/types.js.map +1 -0
- package/dist/modules/hall/types.d.ts +56 -0
- package/dist/modules/hall/types.d.ts.map +1 -0
- package/dist/modules/hall/types.js +16 -0
- package/dist/modules/hall/types.js.map +1 -0
- package/dist/modules/kits/types.d.ts +7 -0
- package/dist/modules/kits/types.d.ts.map +1 -0
- package/dist/modules/kits/types.js +7 -0
- package/dist/modules/kits/types.js.map +1 -0
- package/dist/modules/license/types.d.ts +7 -0
- package/dist/modules/license/types.d.ts.map +1 -0
- package/dist/modules/license/types.js +7 -0
- package/dist/modules/license/types.js.map +1 -0
- package/dist/modules/notifications/types.d.ts +7 -0
- package/dist/modules/notifications/types.d.ts.map +1 -0
- package/dist/modules/notifications/types.js +7 -0
- package/dist/modules/notifications/types.js.map +1 -0
- package/dist/modules/skills/index.d.ts +60 -0
- package/dist/modules/skills/index.d.ts.map +1 -0
- package/dist/modules/skills/index.js +253 -0
- package/dist/modules/skills/index.js.map +1 -0
- package/dist/modules/skills/types.d.ts +127 -0
- package/dist/modules/skills/types.d.ts.map +1 -0
- package/dist/modules/skills/types.js +23 -0
- package/dist/modules/skills/types.js.map +1 -0
- package/dist/modules/versions/types.d.ts +31 -0
- package/dist/modules/versions/types.d.ts.map +1 -0
- package/dist/modules/versions/types.js +9 -0
- package/dist/modules/versions/types.js.map +1 -0
- package/dist/modules/vfs/types.d.ts +26 -0
- package/dist/modules/vfs/types.d.ts.map +1 -0
- package/dist/modules/vfs/types.js +11 -0
- package/dist/modules/vfs/types.js.map +1 -0
- package/dist/server/create-sdk.d.ts +70 -0
- package/dist/server/create-sdk.d.ts.map +1 -0
- package/dist/server/create-sdk.js +125 -0
- package/dist/server/create-sdk.js.map +1 -0
- package/dist/server/hall-handlers.d.ts +195 -0
- package/dist/server/hall-handlers.d.ts.map +1 -0
- package/dist/server/hall-handlers.js +239 -0
- package/dist/server/hall-handlers.js.map +1 -0
- package/dist/server/handlers.d.ts +216 -0
- package/dist/server/handlers.d.ts.map +1 -0
- package/dist/server/handlers.js +214 -0
- package/dist/server/handlers.js.map +1 -0
- package/dist/server/index.d.ts +52 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +50 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/instance-pool.d.ts +299 -0
- package/dist/server/instance-pool.d.ts.map +1 -0
- package/dist/server/instance-pool.js +359 -0
- package/dist/server/instance-pool.js.map +1 -0
- package/dist/transports/http.d.ts +86 -0
- package/dist/transports/http.d.ts.map +1 -0
- package/dist/transports/http.js +134 -0
- package/dist/transports/http.js.map +1 -0
- package/dist/transports/local.d.ts +76 -0
- package/dist/transports/local.d.ts.map +1 -0
- package/dist/transports/local.js +101 -0
- package/dist/transports/local.js.map +1 -0
- package/dist/transports/sse.d.ts +99 -0
- package/dist/transports/sse.d.ts.map +1 -0
- package/dist/transports/sse.js +192 -0
- package/dist/transports/sse.js.map +1 -0
- package/dist/transports/transport.d.ts +68 -0
- package/dist/transports/transport.d.ts.map +1 -0
- package/dist/transports/transport.js +14 -0
- package/dist/transports/transport.js.map +1 -0
- package/dist/transports/ws.d.ts +135 -0
- package/dist/transports/ws.d.ts.map +1 -0
- package/dist/transports/ws.js +331 -0
- package/dist/transports/ws.js.map +1 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/docs/ADR-001-sdk-design.md +282 -0
- package/docs/IMPLEMENTATION-PLAN.md +708 -0
- package/docs/USAGE.md +646 -0
- package/docs/kit-sdk-guide.md +474 -0
- package/package.json +61 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module server/instance-pool
|
|
3
|
+
* @description Brainy instance pool for server-mode SDK.
|
|
4
|
+
*
|
|
5
|
+
* Manages a pool of live Brainy instances using one of three strategies:
|
|
6
|
+
*
|
|
7
|
+
* - **`per-user`** (Workshop): one instance per `emailHash:workspaceId`. Storage path:
|
|
8
|
+
* `{dataPath}/{emailHash}/{workspaceId}/`. Suited to many users each with their own
|
|
9
|
+
* graph.
|
|
10
|
+
* - **`per-tenant`** (Venue): one instance per tenant slug. Storage path:
|
|
11
|
+
* `{dataPath}/{tenantSlug}/`. The LRU max prevents memory exhaustion on high-traffic
|
|
12
|
+
* installations.
|
|
13
|
+
* - **`per-scope`**: caller-supplied key function `scopeKey(userId, workspaceId)`. Covers
|
|
14
|
+
* Academy and any bespoke use case.
|
|
15
|
+
*
|
|
16
|
+
* ## LRU eviction
|
|
17
|
+
*
|
|
18
|
+
* The pool is backed by `lru-cache`. When an entry is evicted (because the cache is
|
|
19
|
+
* full), the `dispose` callback fires synchronously. If `flushOnEvict` is `true`, a
|
|
20
|
+
* non-blocking `brain.flush()` call is queued — the eviction itself is not delayed.
|
|
21
|
+
* Call `flushAll()` or `shutdown()` before process exit to guarantee all writes are
|
|
22
|
+
* persisted.
|
|
23
|
+
*
|
|
24
|
+
* ## Concurrency
|
|
25
|
+
*
|
|
26
|
+
* Concurrent requests for the same scope key are deduplicated via a pending-init map.
|
|
27
|
+
* Only one Brainy instance is ever created per key, regardless of how many requests
|
|
28
|
+
* arrive simultaneously during cold start.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import { BrainyInstancePool } from '@soulcraft/sdk/server'
|
|
33
|
+
*
|
|
34
|
+
* const pool = new BrainyInstancePool({
|
|
35
|
+
* storage: 'mmap-filesystem',
|
|
36
|
+
* dataPath: '/mnt/brainy-data',
|
|
37
|
+
* strategy: 'per-user',
|
|
38
|
+
* maxInstances: 200,
|
|
39
|
+
* flushOnEvict: true,
|
|
40
|
+
* })
|
|
41
|
+
*
|
|
42
|
+
* // In a request handler:
|
|
43
|
+
* const brain = await pool.forUser(session.user.emailHash, workspaceId)
|
|
44
|
+
* const results = await brain.find({ query: 'inventory items' })
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
import { Brainy } from '@soulcraft/brainy';
|
|
48
|
+
import type { InstanceStrategy } from '../types.js';
|
|
49
|
+
/**
|
|
50
|
+
* Configuration for {@link BrainyInstancePool}.
|
|
51
|
+
*/
|
|
52
|
+
export interface InstancePoolConfig {
|
|
53
|
+
/**
|
|
54
|
+
* Brainy storage backend.
|
|
55
|
+
*
|
|
56
|
+
* - `'filesystem'` — local dev, plain filesystem storage.
|
|
57
|
+
* - `'mmap-filesystem'` — production on GCE; Cortex upgrades filesystem to mmap
|
|
58
|
+
* automatically via the `@soulcraft/cortex` plugin.
|
|
59
|
+
*/
|
|
60
|
+
storage: 'filesystem' | 'mmap-filesystem';
|
|
61
|
+
/**
|
|
62
|
+
* Root directory for Brainy data. Instances are stored in sub-directories
|
|
63
|
+
* determined by the pooling strategy.
|
|
64
|
+
*/
|
|
65
|
+
dataPath: string;
|
|
66
|
+
/** Instance pooling strategy. */
|
|
67
|
+
strategy: InstanceStrategy;
|
|
68
|
+
/**
|
|
69
|
+
* Custom scope-key function for `'per-scope'` strategy.
|
|
70
|
+
*
|
|
71
|
+
* Called with `(userId, workspaceId)` and must return a unique, stable string
|
|
72
|
+
* key that identifies the Brainy instance for that request.
|
|
73
|
+
*
|
|
74
|
+
* @param userId - The authenticated user's identifier (usually email hash).
|
|
75
|
+
* @param workspaceId - The workspace or resource identifier.
|
|
76
|
+
* @returns A unique string key for this scope.
|
|
77
|
+
*/
|
|
78
|
+
scopeKey?: (userId: string, workspaceId: string) => string;
|
|
79
|
+
/**
|
|
80
|
+
* Maximum number of Brainy instances to keep in memory simultaneously.
|
|
81
|
+
*
|
|
82
|
+
* When the limit is reached the least-recently-used instance is evicted.
|
|
83
|
+
* Defaults to `200` for per-user, `50` for per-tenant strategies.
|
|
84
|
+
*/
|
|
85
|
+
maxInstances?: number;
|
|
86
|
+
/**
|
|
87
|
+
* Whether to call `brain.flush()` when an instance is evicted from the LRU.
|
|
88
|
+
*
|
|
89
|
+
* The flush is non-blocking from the pool's perspective — eviction completes
|
|
90
|
+
* immediately and the flush continues in the background.
|
|
91
|
+
*
|
|
92
|
+
* @default true
|
|
93
|
+
*/
|
|
94
|
+
flushOnEvict?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Optional async hook called after a new Brainy instance is initialized.
|
|
97
|
+
*
|
|
98
|
+
* Use this to run product-specific post-init logic: VFS integrity checks,
|
|
99
|
+
* data migrations, version tracking, plugin setup, etc. The brain is fully
|
|
100
|
+
* initialized before this callback fires and will be added to the pool after
|
|
101
|
+
* it resolves.
|
|
102
|
+
*
|
|
103
|
+
* If the callback throws, the init fails and the brain is not cached.
|
|
104
|
+
*
|
|
105
|
+
* @param brain - The newly initialized Brainy instance.
|
|
106
|
+
* @param storagePath - The filesystem path where this instance's data lives.
|
|
107
|
+
*
|
|
108
|
+
* @example Workshop VFS integrity check
|
|
109
|
+
* ```typescript
|
|
110
|
+
* onInit: async (brain, storagePath) => {
|
|
111
|
+
* await verifyVFSIntegrity(brain, storagePath)
|
|
112
|
+
* await migrateProjectMetadata(brain)
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
onInit?: (brain: Brainy, storagePath: string) => Promise<void>;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Runtime statistics about the instance pool.
|
|
120
|
+
*/
|
|
121
|
+
export interface InstancePoolStats {
|
|
122
|
+
/** Number of instances currently in the cache. */
|
|
123
|
+
size: number;
|
|
124
|
+
/** Maximum number of instances the cache will hold. */
|
|
125
|
+
maxSize: number;
|
|
126
|
+
/** Scope keys of all currently cached instances. */
|
|
127
|
+
keys: string[];
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* LRU pool of live Brainy instances for server-mode SDK.
|
|
131
|
+
*
|
|
132
|
+
* Manages creation, caching, eviction, flushing, and graceful shutdown of
|
|
133
|
+
* Brainy instances. Supports per-user (Workshop), per-tenant (Venue), and
|
|
134
|
+
* per-scope (Academy / custom) pooling strategies.
|
|
135
|
+
*
|
|
136
|
+
* @example Workshop (per-user)
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const pool = new BrainyInstancePool({
|
|
139
|
+
* storage: 'mmap-filesystem',
|
|
140
|
+
* dataPath: '/mnt/brainy-data',
|
|
141
|
+
* strategy: 'per-user',
|
|
142
|
+
* maxInstances: 200,
|
|
143
|
+
* flushOnEvict: true,
|
|
144
|
+
* })
|
|
145
|
+
*
|
|
146
|
+
* const brain = await pool.forUser(emailHash, workspaceId)
|
|
147
|
+
* ```
|
|
148
|
+
*
|
|
149
|
+
* @example Venue (per-tenant)
|
|
150
|
+
* ```typescript
|
|
151
|
+
* const pool = new BrainyInstancePool({
|
|
152
|
+
* storage: 'mmap-filesystem',
|
|
153
|
+
* dataPath: '/mnt/brainy-data',
|
|
154
|
+
* strategy: 'per-tenant',
|
|
155
|
+
* maxInstances: 50,
|
|
156
|
+
* flushOnEvict: true,
|
|
157
|
+
* })
|
|
158
|
+
*
|
|
159
|
+
* const brain = await pool.forTenant('wicks-and-whiskers')
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export declare class BrainyInstancePool {
|
|
163
|
+
private readonly config;
|
|
164
|
+
private readonly cache;
|
|
165
|
+
/** Deduplicates concurrent init requests for the same scope key. */
|
|
166
|
+
private readonly pending;
|
|
167
|
+
/**
|
|
168
|
+
* @param config - Pool configuration.
|
|
169
|
+
*/
|
|
170
|
+
constructor(config: InstancePoolConfig);
|
|
171
|
+
/**
|
|
172
|
+
* Returns the Brainy instance for the given user and workspace.
|
|
173
|
+
*
|
|
174
|
+
* Uses `per-user` strategy: storage path is `{dataPath}/{emailHash}/{workspaceId}/`.
|
|
175
|
+
* The `userId` should be the SHA-256 email hash (8-char prefix) for path safety.
|
|
176
|
+
*
|
|
177
|
+
* @param userId - The user's email hash (e.g. from `computeEmailHash()`).
|
|
178
|
+
* @param workspaceId - The workspace identifier.
|
|
179
|
+
* @returns The cached or newly initialized Brainy instance.
|
|
180
|
+
* @throws Error if `strategy` is not `'per-user'`.
|
|
181
|
+
*/
|
|
182
|
+
forUser(userId: string, workspaceId: string): Promise<Brainy>;
|
|
183
|
+
/**
|
|
184
|
+
* Returns the Brainy instance for the given tenant slug.
|
|
185
|
+
*
|
|
186
|
+
* Uses `per-tenant` strategy: storage path is `{dataPath}/{tenantSlug}/`.
|
|
187
|
+
*
|
|
188
|
+
* @param tenantSlug - The tenant identifier (URL-safe slug).
|
|
189
|
+
* @returns The cached or newly initialized Brainy instance.
|
|
190
|
+
* @throws Error if `strategy` is not `'per-tenant'`.
|
|
191
|
+
*/
|
|
192
|
+
forTenant(tenantSlug: string): Promise<Brainy>;
|
|
193
|
+
/**
|
|
194
|
+
* Returns the Brainy instance for an arbitrary scope key.
|
|
195
|
+
*
|
|
196
|
+
* Uses `per-scope` strategy. The `factory` is called to create a Brainy instance
|
|
197
|
+
* if one does not already exist for `key`. The factory is called at most once per
|
|
198
|
+
* unique key — concurrent calls with the same key wait for the first creation.
|
|
199
|
+
*
|
|
200
|
+
* @param key - Unique stable string identifying this scope.
|
|
201
|
+
* @param factory - Async factory that creates a new Brainy instance for this scope.
|
|
202
|
+
* @returns The cached or newly created Brainy instance.
|
|
203
|
+
* @throws Error if `strategy` is not `'per-scope'`.
|
|
204
|
+
*/
|
|
205
|
+
forScope(key: string, factory: () => Promise<Brainy>): Promise<Brainy>;
|
|
206
|
+
/**
|
|
207
|
+
* Flushes and removes a specific instance from the pool.
|
|
208
|
+
*
|
|
209
|
+
* Useful when a workspace is explicitly cleared or deleted.
|
|
210
|
+
*
|
|
211
|
+
* @param key - The scope key (as stored in the pool).
|
|
212
|
+
*/
|
|
213
|
+
flush(key: string): Promise<void>;
|
|
214
|
+
/**
|
|
215
|
+
* Flushes all cached instances.
|
|
216
|
+
*
|
|
217
|
+
* Calls `brain.flush()` on every live instance. Does not remove them from the
|
|
218
|
+
* cache — they remain available for subsequent requests.
|
|
219
|
+
*/
|
|
220
|
+
flushAll(): Promise<void>;
|
|
221
|
+
/**
|
|
222
|
+
* Gracefully shuts down the pool.
|
|
223
|
+
*
|
|
224
|
+
* Flushes all instances, then clears the cache. After `shutdown()`, the pool
|
|
225
|
+
* is empty and cannot be used without re-creating instances.
|
|
226
|
+
*/
|
|
227
|
+
shutdown(): Promise<void>;
|
|
228
|
+
/**
|
|
229
|
+
* Returns current runtime statistics about the pool.
|
|
230
|
+
*/
|
|
231
|
+
getStats(): InstancePoolStats;
|
|
232
|
+
/**
|
|
233
|
+
* Gets a cached Brainy instance by key, or creates and caches a new one.
|
|
234
|
+
*
|
|
235
|
+
* Concurrent calls with the same key wait for the first initialization
|
|
236
|
+
* rather than spawning duplicate Brainy instances.
|
|
237
|
+
*
|
|
238
|
+
* @param key - LRU cache key.
|
|
239
|
+
* @param storagePath - Filesystem path for this instance's Brainy data directory.
|
|
240
|
+
* @returns The cached or newly initialized Brainy instance.
|
|
241
|
+
*/
|
|
242
|
+
private _getOrCreate;
|
|
243
|
+
/**
|
|
244
|
+
* Creates and initializes a Brainy instance at the given storage path.
|
|
245
|
+
*
|
|
246
|
+
* For `'mmap-filesystem'` storage the `@soulcraft/cortex` plugin is loaded
|
|
247
|
+
* automatically — Cortex intercepts Brainy's storage layer and upgrades it to
|
|
248
|
+
* native Rust mmap SSTables at runtime.
|
|
249
|
+
*
|
|
250
|
+
* @param storagePath - Absolute or relative path to the Brainy data directory.
|
|
251
|
+
* @returns A fully initialized Brainy instance.
|
|
252
|
+
*/
|
|
253
|
+
private _initBrainy;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Thrown when a Brainy instance is still initializing and `waitForInit` is false.
|
|
257
|
+
*
|
|
258
|
+
* Products that want non-blocking behaviour (e.g. return HTTP 503 during cold
|
|
259
|
+
* start rather than waiting) can catch this error and respond with a
|
|
260
|
+
* `Retry-After` header using the suggested `retryAfter` value.
|
|
261
|
+
*
|
|
262
|
+
* @example
|
|
263
|
+
* ```typescript
|
|
264
|
+
* app.onError((err, c) => {
|
|
265
|
+
* if (err instanceof BrainyInitializingError) {
|
|
266
|
+
* return c.json({ error: 'Service starting', retryAfter: err.retryAfter }, 503, {
|
|
267
|
+
* 'Retry-After': String(err.retryAfter),
|
|
268
|
+
* })
|
|
269
|
+
* }
|
|
270
|
+
* })
|
|
271
|
+
* ```
|
|
272
|
+
*/
|
|
273
|
+
export declare class BrainyInitializingError extends Error {
|
|
274
|
+
/** Suggested number of seconds the client should wait before retrying. */
|
|
275
|
+
readonly retryAfter: number;
|
|
276
|
+
/**
|
|
277
|
+
* @param message - Error message.
|
|
278
|
+
* @param retryAfter - Suggested retry delay in seconds. Defaults to 5.
|
|
279
|
+
*/
|
|
280
|
+
constructor(message: string, retryAfter?: number);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Computes an 8-character SHA-256 email hash used as the user-level directory
|
|
284
|
+
* component in `per-user` storage paths.
|
|
285
|
+
*
|
|
286
|
+
* Normalizes the email to lowercase + trimmed before hashing.
|
|
287
|
+
*
|
|
288
|
+
* @param email - The authenticated user's email address.
|
|
289
|
+
* @returns An 8-character lowercase hex string.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* ```typescript
|
|
293
|
+
* const hash = computeEmailHash('Alice@Workshop.soulcraft.com')
|
|
294
|
+
* // → 'a3f8c2b1' (deterministic)
|
|
295
|
+
* const brain = await pool.forUser(hash, workspaceId)
|
|
296
|
+
* ```
|
|
297
|
+
*/
|
|
298
|
+
export declare function computeEmailHash(email: string): string;
|
|
299
|
+
//# sourceMappingURL=instance-pool.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance-pool.d.ts","sourceRoot":"","sources":["../../src/server/instance-pool.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AAMH,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAC1C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAMnD;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;OAMG;IACH,OAAO,EAAE,YAAY,GAAG,iBAAiB,CAAA;IAEzC;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAA;IAEhB,iCAAiC;IACjC,QAAQ,EAAE,gBAAgB,CAAA;IAE1B;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,MAAM,CAAA;IAE1D;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB;;;;;;;OAOG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IAEtB;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/D;AAMD;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,kDAAkD;IAClD,IAAI,EAAE,MAAM,CAAA;IACZ,uDAAuD;IACvD,OAAO,EAAE,MAAM,CAAA;IACf,oDAAoD;IACpD,IAAI,EAAE,MAAM,EAAE,CAAA;CACf;AAMD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA8B;IACrD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0B;IAChD,oEAAoE;IACpE,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqC;IAE7D;;OAEG;gBACS,MAAM,EAAE,kBAAkB;IA4BtC;;;;;;;;;;OAUG;IACG,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASnE;;;;;;;;OAQG;IACG,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IASpD;;;;;;;;;;;OAWG;IACG,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC;IAyB5E;;;;;;OAMG;IACG,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUvC;;;;;OAKG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAY/B;;;;;OAKG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAM/B;;OAEG;IACH,QAAQ,IAAI,iBAAiB;IAU7B;;;;;;;;;OASG;IACH,OAAO,CAAC,YAAY;IAqBpB;;;;;;;;;OASG;YACW,WAAW;CA0B1B;AAMD;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,uBAAwB,SAAQ,KAAK;IAChD,0EAA0E;IAC1E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;IAE3B;;;OAGG;gBACS,OAAO,EAAE,MAAM,EAAE,UAAU,SAAI;CAK5C;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEtD"}
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module server/instance-pool
|
|
3
|
+
* @description Brainy instance pool for server-mode SDK.
|
|
4
|
+
*
|
|
5
|
+
* Manages a pool of live Brainy instances using one of three strategies:
|
|
6
|
+
*
|
|
7
|
+
* - **`per-user`** (Workshop): one instance per `emailHash:workspaceId`. Storage path:
|
|
8
|
+
* `{dataPath}/{emailHash}/{workspaceId}/`. Suited to many users each with their own
|
|
9
|
+
* graph.
|
|
10
|
+
* - **`per-tenant`** (Venue): one instance per tenant slug. Storage path:
|
|
11
|
+
* `{dataPath}/{tenantSlug}/`. The LRU max prevents memory exhaustion on high-traffic
|
|
12
|
+
* installations.
|
|
13
|
+
* - **`per-scope`**: caller-supplied key function `scopeKey(userId, workspaceId)`. Covers
|
|
14
|
+
* Academy and any bespoke use case.
|
|
15
|
+
*
|
|
16
|
+
* ## LRU eviction
|
|
17
|
+
*
|
|
18
|
+
* The pool is backed by `lru-cache`. When an entry is evicted (because the cache is
|
|
19
|
+
* full), the `dispose` callback fires synchronously. If `flushOnEvict` is `true`, a
|
|
20
|
+
* non-blocking `brain.flush()` call is queued — the eviction itself is not delayed.
|
|
21
|
+
* Call `flushAll()` or `shutdown()` before process exit to guarantee all writes are
|
|
22
|
+
* persisted.
|
|
23
|
+
*
|
|
24
|
+
* ## Concurrency
|
|
25
|
+
*
|
|
26
|
+
* Concurrent requests for the same scope key are deduplicated via a pending-init map.
|
|
27
|
+
* Only one Brainy instance is ever created per key, regardless of how many requests
|
|
28
|
+
* arrive simultaneously during cold start.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import { BrainyInstancePool } from '@soulcraft/sdk/server'
|
|
33
|
+
*
|
|
34
|
+
* const pool = new BrainyInstancePool({
|
|
35
|
+
* storage: 'mmap-filesystem',
|
|
36
|
+
* dataPath: '/mnt/brainy-data',
|
|
37
|
+
* strategy: 'per-user',
|
|
38
|
+
* maxInstances: 200,
|
|
39
|
+
* flushOnEvict: true,
|
|
40
|
+
* })
|
|
41
|
+
*
|
|
42
|
+
* // In a request handler:
|
|
43
|
+
* const brain = await pool.forUser(session.user.emailHash, workspaceId)
|
|
44
|
+
* const results = await brain.find({ query: 'inventory items' })
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
import { createHash } from 'crypto';
|
|
48
|
+
import fs from 'fs/promises';
|
|
49
|
+
import path from 'path';
|
|
50
|
+
import { LRUCache } from 'lru-cache';
|
|
51
|
+
import { Brainy } from '@soulcraft/brainy';
|
|
52
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
53
|
+
// BrainyInstancePool
|
|
54
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
55
|
+
/**
|
|
56
|
+
* LRU pool of live Brainy instances for server-mode SDK.
|
|
57
|
+
*
|
|
58
|
+
* Manages creation, caching, eviction, flushing, and graceful shutdown of
|
|
59
|
+
* Brainy instances. Supports per-user (Workshop), per-tenant (Venue), and
|
|
60
|
+
* per-scope (Academy / custom) pooling strategies.
|
|
61
|
+
*
|
|
62
|
+
* @example Workshop (per-user)
|
|
63
|
+
* ```typescript
|
|
64
|
+
* const pool = new BrainyInstancePool({
|
|
65
|
+
* storage: 'mmap-filesystem',
|
|
66
|
+
* dataPath: '/mnt/brainy-data',
|
|
67
|
+
* strategy: 'per-user',
|
|
68
|
+
* maxInstances: 200,
|
|
69
|
+
* flushOnEvict: true,
|
|
70
|
+
* })
|
|
71
|
+
*
|
|
72
|
+
* const brain = await pool.forUser(emailHash, workspaceId)
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @example Venue (per-tenant)
|
|
76
|
+
* ```typescript
|
|
77
|
+
* const pool = new BrainyInstancePool({
|
|
78
|
+
* storage: 'mmap-filesystem',
|
|
79
|
+
* dataPath: '/mnt/brainy-data',
|
|
80
|
+
* strategy: 'per-tenant',
|
|
81
|
+
* maxInstances: 50,
|
|
82
|
+
* flushOnEvict: true,
|
|
83
|
+
* })
|
|
84
|
+
*
|
|
85
|
+
* const brain = await pool.forTenant('wicks-and-whiskers')
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export class BrainyInstancePool {
|
|
89
|
+
config;
|
|
90
|
+
cache;
|
|
91
|
+
/** Deduplicates concurrent init requests for the same scope key. */
|
|
92
|
+
pending = new Map();
|
|
93
|
+
/**
|
|
94
|
+
* @param config - Pool configuration.
|
|
95
|
+
*/
|
|
96
|
+
constructor(config) {
|
|
97
|
+
const defaultMax = config.strategy === 'per-tenant' ? 50 : 200;
|
|
98
|
+
this.config = {
|
|
99
|
+
storage: config.storage,
|
|
100
|
+
dataPath: config.dataPath,
|
|
101
|
+
strategy: config.strategy,
|
|
102
|
+
scopeKey: config.scopeKey ?? ((userId, workspaceId) => `${userId}:${workspaceId}`),
|
|
103
|
+
maxInstances: config.maxInstances ?? defaultMax,
|
|
104
|
+
flushOnEvict: config.flushOnEvict ?? true,
|
|
105
|
+
onInit: config.onInit ?? (async () => { }),
|
|
106
|
+
};
|
|
107
|
+
this.cache = new LRUCache({
|
|
108
|
+
max: this.config.maxInstances,
|
|
109
|
+
dispose: (brain, _key, _reason) => {
|
|
110
|
+
if (this.config.flushOnEvict) {
|
|
111
|
+
// Non-blocking flush — eviction is synchronous, flush runs in background.
|
|
112
|
+
brain.flush().catch((err) => {
|
|
113
|
+
console.error('[SDK/pool] flush-on-evict failed:', err);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// ── Public API ─────────────────────────────────────────────────────────────
|
|
120
|
+
/**
|
|
121
|
+
* Returns the Brainy instance for the given user and workspace.
|
|
122
|
+
*
|
|
123
|
+
* Uses `per-user` strategy: storage path is `{dataPath}/{emailHash}/{workspaceId}/`.
|
|
124
|
+
* The `userId` should be the SHA-256 email hash (8-char prefix) for path safety.
|
|
125
|
+
*
|
|
126
|
+
* @param userId - The user's email hash (e.g. from `computeEmailHash()`).
|
|
127
|
+
* @param workspaceId - The workspace identifier.
|
|
128
|
+
* @returns The cached or newly initialized Brainy instance.
|
|
129
|
+
* @throws Error if `strategy` is not `'per-user'`.
|
|
130
|
+
*/
|
|
131
|
+
async forUser(userId, workspaceId) {
|
|
132
|
+
if (this.config.strategy !== 'per-user') {
|
|
133
|
+
throw new Error(`BrainyInstancePool.forUser() called but strategy is '${this.config.strategy}'`);
|
|
134
|
+
}
|
|
135
|
+
const key = `${userId}:${workspaceId}`;
|
|
136
|
+
const storagePath = path.join(this.config.dataPath, userId, workspaceId);
|
|
137
|
+
return this._getOrCreate(key, storagePath);
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Returns the Brainy instance for the given tenant slug.
|
|
141
|
+
*
|
|
142
|
+
* Uses `per-tenant` strategy: storage path is `{dataPath}/{tenantSlug}/`.
|
|
143
|
+
*
|
|
144
|
+
* @param tenantSlug - The tenant identifier (URL-safe slug).
|
|
145
|
+
* @returns The cached or newly initialized Brainy instance.
|
|
146
|
+
* @throws Error if `strategy` is not `'per-tenant'`.
|
|
147
|
+
*/
|
|
148
|
+
async forTenant(tenantSlug) {
|
|
149
|
+
if (this.config.strategy !== 'per-tenant') {
|
|
150
|
+
throw new Error(`BrainyInstancePool.forTenant() called but strategy is '${this.config.strategy}'`);
|
|
151
|
+
}
|
|
152
|
+
const key = tenantSlug;
|
|
153
|
+
const storagePath = path.join(this.config.dataPath, tenantSlug);
|
|
154
|
+
return this._getOrCreate(key, storagePath);
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Returns the Brainy instance for an arbitrary scope key.
|
|
158
|
+
*
|
|
159
|
+
* Uses `per-scope` strategy. The `factory` is called to create a Brainy instance
|
|
160
|
+
* if one does not already exist for `key`. The factory is called at most once per
|
|
161
|
+
* unique key — concurrent calls with the same key wait for the first creation.
|
|
162
|
+
*
|
|
163
|
+
* @param key - Unique stable string identifying this scope.
|
|
164
|
+
* @param factory - Async factory that creates a new Brainy instance for this scope.
|
|
165
|
+
* @returns The cached or newly created Brainy instance.
|
|
166
|
+
* @throws Error if `strategy` is not `'per-scope'`.
|
|
167
|
+
*/
|
|
168
|
+
async forScope(key, factory) {
|
|
169
|
+
if (this.config.strategy !== 'per-scope') {
|
|
170
|
+
throw new Error(`BrainyInstancePool.forScope() called but strategy is '${this.config.strategy}'`);
|
|
171
|
+
}
|
|
172
|
+
const cached = this.cache.get(key);
|
|
173
|
+
if (cached)
|
|
174
|
+
return cached;
|
|
175
|
+
if (this.pending.has(key)) {
|
|
176
|
+
return this.pending.get(key);
|
|
177
|
+
}
|
|
178
|
+
const initPromise = factory().then((brain) => {
|
|
179
|
+
this.cache.set(key, brain);
|
|
180
|
+
this.pending.delete(key);
|
|
181
|
+
return brain;
|
|
182
|
+
}).catch((err) => {
|
|
183
|
+
this.pending.delete(key);
|
|
184
|
+
throw err;
|
|
185
|
+
});
|
|
186
|
+
this.pending.set(key, initPromise);
|
|
187
|
+
return initPromise;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Flushes and removes a specific instance from the pool.
|
|
191
|
+
*
|
|
192
|
+
* Useful when a workspace is explicitly cleared or deleted.
|
|
193
|
+
*
|
|
194
|
+
* @param key - The scope key (as stored in the pool).
|
|
195
|
+
*/
|
|
196
|
+
async flush(key) {
|
|
197
|
+
const brain = this.cache.get(key);
|
|
198
|
+
if (!brain)
|
|
199
|
+
return;
|
|
200
|
+
try {
|
|
201
|
+
await brain.flush();
|
|
202
|
+
}
|
|
203
|
+
finally {
|
|
204
|
+
this.cache.delete(key);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Flushes all cached instances.
|
|
209
|
+
*
|
|
210
|
+
* Calls `brain.flush()` on every live instance. Does not remove them from the
|
|
211
|
+
* cache — they remain available for subsequent requests.
|
|
212
|
+
*/
|
|
213
|
+
async flushAll() {
|
|
214
|
+
const flushes = [];
|
|
215
|
+
for (const brain of this.cache.values()) {
|
|
216
|
+
flushes.push(brain.flush().catch((err) => {
|
|
217
|
+
console.error('[SDK/pool] flushAll error:', err);
|
|
218
|
+
}));
|
|
219
|
+
}
|
|
220
|
+
await Promise.all(flushes);
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Gracefully shuts down the pool.
|
|
224
|
+
*
|
|
225
|
+
* Flushes all instances, then clears the cache. After `shutdown()`, the pool
|
|
226
|
+
* is empty and cannot be used without re-creating instances.
|
|
227
|
+
*/
|
|
228
|
+
async shutdown() {
|
|
229
|
+
await this.flushAll();
|
|
230
|
+
this.cache.clear();
|
|
231
|
+
this.pending.clear();
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Returns current runtime statistics about the pool.
|
|
235
|
+
*/
|
|
236
|
+
getStats() {
|
|
237
|
+
return {
|
|
238
|
+
size: this.cache.size,
|
|
239
|
+
maxSize: this.config.maxInstances,
|
|
240
|
+
keys: [...this.cache.keys()],
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// ── Internal ───────────────────────────────────────────────────────────────
|
|
244
|
+
/**
|
|
245
|
+
* Gets a cached Brainy instance by key, or creates and caches a new one.
|
|
246
|
+
*
|
|
247
|
+
* Concurrent calls with the same key wait for the first initialization
|
|
248
|
+
* rather than spawning duplicate Brainy instances.
|
|
249
|
+
*
|
|
250
|
+
* @param key - LRU cache key.
|
|
251
|
+
* @param storagePath - Filesystem path for this instance's Brainy data directory.
|
|
252
|
+
* @returns The cached or newly initialized Brainy instance.
|
|
253
|
+
*/
|
|
254
|
+
_getOrCreate(key, storagePath) {
|
|
255
|
+
const cached = this.cache.get(key);
|
|
256
|
+
if (cached)
|
|
257
|
+
return Promise.resolve(cached);
|
|
258
|
+
if (this.pending.has(key)) {
|
|
259
|
+
return this.pending.get(key);
|
|
260
|
+
}
|
|
261
|
+
const initPromise = this._initBrainy(storagePath).then((brain) => {
|
|
262
|
+
this.cache.set(key, brain);
|
|
263
|
+
this.pending.delete(key);
|
|
264
|
+
return brain;
|
|
265
|
+
}).catch((err) => {
|
|
266
|
+
this.pending.delete(key);
|
|
267
|
+
throw err;
|
|
268
|
+
});
|
|
269
|
+
this.pending.set(key, initPromise);
|
|
270
|
+
return initPromise;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Creates and initializes a Brainy instance at the given storage path.
|
|
274
|
+
*
|
|
275
|
+
* For `'mmap-filesystem'` storage the `@soulcraft/cortex` plugin is loaded
|
|
276
|
+
* automatically — Cortex intercepts Brainy's storage layer and upgrades it to
|
|
277
|
+
* native Rust mmap SSTables at runtime.
|
|
278
|
+
*
|
|
279
|
+
* @param storagePath - Absolute or relative path to the Brainy data directory.
|
|
280
|
+
* @returns A fully initialized Brainy instance.
|
|
281
|
+
*/
|
|
282
|
+
async _initBrainy(storagePath) {
|
|
283
|
+
await fs.mkdir(storagePath, { recursive: true });
|
|
284
|
+
const initStart = Date.now();
|
|
285
|
+
const brain = new Brainy({
|
|
286
|
+
storage: {
|
|
287
|
+
type: 'filesystem',
|
|
288
|
+
options: { rootDirectory: storagePath },
|
|
289
|
+
},
|
|
290
|
+
plugins: this.config.storage === 'mmap-filesystem'
|
|
291
|
+
? ['@soulcraft/cortex']
|
|
292
|
+
: [],
|
|
293
|
+
});
|
|
294
|
+
await brain.init();
|
|
295
|
+
if (this.config.onInit) {
|
|
296
|
+
await this.config.onInit(brain, storagePath);
|
|
297
|
+
}
|
|
298
|
+
const elapsed = Date.now() - initStart;
|
|
299
|
+
console.log(`[SDK/pool] Brainy ready: ${path.basename(storagePath)} (${elapsed}ms) [${this.config.storage}]`);
|
|
300
|
+
return brain;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
304
|
+
// BrainyInitializingError
|
|
305
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
306
|
+
/**
|
|
307
|
+
* Thrown when a Brainy instance is still initializing and `waitForInit` is false.
|
|
308
|
+
*
|
|
309
|
+
* Products that want non-blocking behaviour (e.g. return HTTP 503 during cold
|
|
310
|
+
* start rather than waiting) can catch this error and respond with a
|
|
311
|
+
* `Retry-After` header using the suggested `retryAfter` value.
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```typescript
|
|
315
|
+
* app.onError((err, c) => {
|
|
316
|
+
* if (err instanceof BrainyInitializingError) {
|
|
317
|
+
* return c.json({ error: 'Service starting', retryAfter: err.retryAfter }, 503, {
|
|
318
|
+
* 'Retry-After': String(err.retryAfter),
|
|
319
|
+
* })
|
|
320
|
+
* }
|
|
321
|
+
* })
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
export class BrainyInitializingError extends Error {
|
|
325
|
+
/** Suggested number of seconds the client should wait before retrying. */
|
|
326
|
+
retryAfter;
|
|
327
|
+
/**
|
|
328
|
+
* @param message - Error message.
|
|
329
|
+
* @param retryAfter - Suggested retry delay in seconds. Defaults to 5.
|
|
330
|
+
*/
|
|
331
|
+
constructor(message, retryAfter = 5) {
|
|
332
|
+
super(message);
|
|
333
|
+
this.name = 'BrainyInitializingError';
|
|
334
|
+
this.retryAfter = retryAfter;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
338
|
+
// Helpers
|
|
339
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
340
|
+
/**
|
|
341
|
+
* Computes an 8-character SHA-256 email hash used as the user-level directory
|
|
342
|
+
* component in `per-user` storage paths.
|
|
343
|
+
*
|
|
344
|
+
* Normalizes the email to lowercase + trimmed before hashing.
|
|
345
|
+
*
|
|
346
|
+
* @param email - The authenticated user's email address.
|
|
347
|
+
* @returns An 8-character lowercase hex string.
|
|
348
|
+
*
|
|
349
|
+
* @example
|
|
350
|
+
* ```typescript
|
|
351
|
+
* const hash = computeEmailHash('Alice@Workshop.soulcraft.com')
|
|
352
|
+
* // → 'a3f8c2b1' (deterministic)
|
|
353
|
+
* const brain = await pool.forUser(hash, workspaceId)
|
|
354
|
+
* ```
|
|
355
|
+
*/
|
|
356
|
+
export function computeEmailHash(email) {
|
|
357
|
+
return createHash('sha256').update(email.toLowerCase().trim()).digest('hex').substring(0, 8);
|
|
358
|
+
}
|
|
359
|
+
//# sourceMappingURL=instance-pool.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance-pool.js","sourceRoot":"","sources":["../../src/server/instance-pool.ts"],"names":[],"mappings":"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"}
|