ai-database 2.1.3 → 2.3.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/CHANGELOG.md +35 -1
- package/README.md +880 -669
- package/dist/actions.d.ts +2 -2
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +1 -1
- package/dist/actions.js.map +1 -1
- package/dist/ai-promise-db.d.ts +49 -23
- package/dist/ai-promise-db.d.ts.map +1 -1
- package/dist/ai-promise-db.js +91 -63
- package/dist/ai-promise-db.js.map +1 -1
- package/dist/authorization.d.ts.map +1 -1
- package/dist/authorization.js +38 -30
- package/dist/authorization.js.map +1 -1
- package/dist/cascade-orchestrator.d.ts +404 -0
- package/dist/cascade-orchestrator.d.ts.map +1 -0
- package/dist/cascade-orchestrator.js +828 -0
- package/dist/cascade-orchestrator.js.map +1 -0
- package/dist/cascade-write-strategy.d.ts +584 -0
- package/dist/cascade-write-strategy.d.ts.map +1 -0
- package/dist/cascade-write-strategy.js +590 -0
- package/dist/cascade-write-strategy.js.map +1 -0
- package/dist/ch-adapter.d.ts +358 -0
- package/dist/ch-adapter.d.ts.map +1 -0
- package/dist/ch-adapter.js +929 -0
- package/dist/ch-adapter.js.map +1 -0
- package/dist/client/index.d.ts +42 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +43 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client.d.ts +266 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +81 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +64 -1
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +52 -2
- package/dist/constants.js.map +1 -1
- package/dist/dataloader.d.ts +99 -0
- package/dist/dataloader.d.ts.map +1 -0
- package/dist/dataloader.js +225 -0
- package/dist/dataloader.js.map +1 -0
- package/dist/db-provider-port.d.ts +501 -0
- package/dist/db-provider-port.d.ts.map +1 -0
- package/dist/db-provider-port.js +113 -0
- package/dist/db-provider-port.js.map +1 -0
- package/dist/digital-objects-provider.d.ts +49 -0
- package/dist/digital-objects-provider.d.ts.map +1 -0
- package/dist/digital-objects-provider.js +55 -0
- package/dist/digital-objects-provider.js.map +1 -0
- package/dist/do-sqlite-adapter.d.ts +402 -0
- package/dist/do-sqlite-adapter.d.ts.map +1 -0
- package/dist/do-sqlite-adapter.js +745 -0
- package/dist/do-sqlite-adapter.js.map +1 -0
- package/dist/docs-rels/custom-types.d.ts +134 -0
- package/dist/docs-rels/custom-types.d.ts.map +1 -0
- package/dist/docs-rels/custom-types.js +70 -0
- package/dist/docs-rels/custom-types.js.map +1 -0
- package/dist/docs-rels/index.d.ts +16 -0
- package/dist/docs-rels/index.d.ts.map +1 -0
- package/dist/docs-rels/index.js +16 -0
- package/dist/docs-rels/index.js.map +1 -0
- package/dist/docs-rels/migrations/index.d.ts +30 -0
- package/dist/docs-rels/migrations/index.d.ts.map +1 -0
- package/dist/docs-rels/migrations/index.js +128 -0
- package/dist/docs-rels/migrations/index.js.map +1 -0
- package/dist/docs-rels/schema.d.ts +2961 -0
- package/dist/docs-rels/schema.d.ts.map +1 -0
- package/dist/docs-rels/schema.js +244 -0
- package/dist/docs-rels/schema.js.map +1 -0
- package/dist/durable-clickhouse.d.ts.map +1 -1
- package/dist/durable-clickhouse.js +16 -13
- package/dist/durable-clickhouse.js.map +1 -1
- package/dist/durable-promise.d.ts.map +1 -1
- package/dist/durable-promise.js +34 -15
- package/dist/durable-promise.js.map +1 -1
- package/dist/errors.d.ts +127 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +210 -0
- package/dist/errors.js.map +1 -0
- package/dist/eventbridge.d.ts +117 -0
- package/dist/eventbridge.d.ts.map +1 -0
- package/dist/eventbridge.js +238 -0
- package/dist/eventbridge.js.map +1 -0
- package/dist/events.d.ts +2 -2
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +1 -1
- package/dist/events.js.map +1 -1
- package/dist/execution-queue.d.ts.map +1 -1
- package/dist/execution-queue.js +4 -5
- package/dist/execution-queue.js.map +1 -1
- package/dist/index.d.ts +35 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -6
- package/dist/index.js.map +1 -1
- package/dist/linguistic.d.ts +3 -108
- package/dist/linguistic.d.ts.map +1 -1
- package/dist/linguistic.js +3 -372
- package/dist/linguistic.js.map +1 -1
- package/dist/logger.d.ts +132 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +137 -0
- package/dist/logger.js.map +1 -0
- package/dist/memory-provider.d.ts +128 -0
- package/dist/memory-provider.d.ts.map +1 -1
- package/dist/memory-provider.js +592 -257
- package/dist/memory-provider.js.map +1 -1
- package/dist/pg-adapter.d.ts +424 -0
- package/dist/pg-adapter.d.ts.map +1 -0
- package/dist/pg-adapter.js +921 -0
- package/dist/pg-adapter.js.map +1 -0
- package/dist/pipelines-iceberg-emitter.d.ts +327 -0
- package/dist/pipelines-iceberg-emitter.d.ts.map +1 -0
- package/dist/pipelines-iceberg-emitter.js +351 -0
- package/dist/pipelines-iceberg-emitter.js.map +1 -0
- package/dist/provider-capabilities.d.ts +146 -0
- package/dist/provider-capabilities.d.ts.map +1 -0
- package/dist/provider-capabilities.js +214 -0
- package/dist/provider-capabilities.js.map +1 -0
- package/dist/rdb-provider-adapter.d.ts +195 -0
- package/dist/rdb-provider-adapter.d.ts.map +1 -0
- package/dist/rdb-provider-adapter.js +291 -0
- package/dist/rdb-provider-adapter.js.map +1 -0
- package/dist/schema/cascade.d.ts +48 -17
- package/dist/schema/cascade.d.ts.map +1 -1
- package/dist/schema/cascade.js +477 -278
- package/dist/schema/cascade.js.map +1 -1
- package/dist/schema/definition-caches.d.ts +24 -0
- package/dist/schema/definition-caches.d.ts.map +1 -0
- package/dist/schema/definition-caches.js +26 -0
- package/dist/schema/definition-caches.js.map +1 -0
- package/dist/schema/dependency-graph.d.ts +21 -109
- package/dist/schema/dependency-graph.d.ts.map +1 -1
- package/dist/schema/dependency-graph.js +25 -333
- package/dist/schema/dependency-graph.js.map +1 -1
- package/dist/schema/diff.d.ts +103 -0
- package/dist/schema/diff.d.ts.map +1 -0
- package/dist/schema/diff.js +329 -0
- package/dist/schema/diff.js.map +1 -0
- package/dist/schema/entity-operations.d.ts +99 -0
- package/dist/schema/entity-operations.d.ts.map +1 -0
- package/dist/schema/entity-operations.js +818 -0
- package/dist/schema/entity-operations.js.map +1 -0
- package/dist/schema/index.d.ts +28 -34
- package/dist/schema/index.d.ts.map +1 -1
- package/dist/schema/index.js +454 -521
- package/dist/schema/index.js.map +1 -1
- package/dist/schema/migration.d.ts +205 -0
- package/dist/schema/migration.d.ts.map +1 -0
- package/dist/schema/migration.js +327 -0
- package/dist/schema/migration.js.map +1 -0
- package/dist/schema/nl-query-generator.d.ts +68 -0
- package/dist/schema/nl-query-generator.d.ts.map +1 -0
- package/dist/schema/nl-query-generator.js +362 -0
- package/dist/schema/nl-query-generator.js.map +1 -0
- package/dist/schema/nl-query.d.ts +65 -0
- package/dist/schema/nl-query.d.ts.map +1 -0
- package/dist/schema/nl-query.js +178 -0
- package/dist/schema/nl-query.js.map +1 -0
- package/dist/schema/parse.d.ts.map +1 -1
- package/dist/schema/parse.js +144 -89
- package/dist/schema/parse.js.map +1 -1
- package/dist/schema/provider.d.ts +37 -0
- package/dist/schema/provider.d.ts.map +1 -1
- package/dist/schema/provider.js +15 -7
- package/dist/schema/provider.js.map +1 -1
- package/dist/schema/resolve.d.ts +46 -5
- package/dist/schema/resolve.d.ts.map +1 -1
- package/dist/schema/resolve.js +237 -95
- package/dist/schema/resolve.js.map +1 -1
- package/dist/schema/search-utils.d.ts +76 -0
- package/dist/schema/search-utils.d.ts.map +1 -0
- package/dist/schema/search-utils.js +86 -0
- package/dist/schema/search-utils.js.map +1 -0
- package/dist/schema/seed.d.ts +53 -0
- package/dist/schema/seed.d.ts.map +1 -0
- package/dist/schema/seed.js +94 -0
- package/dist/schema/seed.js.map +1 -0
- package/dist/schema/semantic.d.ts +10 -0
- package/dist/schema/semantic.d.ts.map +1 -1
- package/dist/schema/semantic.js +192 -86
- package/dist/schema/semantic.js.map +1 -1
- package/dist/schema/sub-apis.d.ts +52 -0
- package/dist/schema/sub-apis.d.ts.map +1 -0
- package/dist/schema/sub-apis.js +216 -0
- package/dist/schema/sub-apis.js.map +1 -0
- package/dist/schema/system-entities.d.ts +42 -0
- package/dist/schema/system-entities.d.ts.map +1 -0
- package/dist/schema/system-entities.js +101 -0
- package/dist/schema/system-entities.js.map +1 -0
- package/dist/schema/types.d.ts +91 -9
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/schema/union-fallback.d.ts.map +1 -1
- package/dist/schema/union-fallback.js +21 -15
- package/dist/schema/union-fallback.js.map +1 -1
- package/dist/schema/value-generators/ai.d.ts +54 -0
- package/dist/schema/value-generators/ai.d.ts.map +1 -0
- package/dist/schema/value-generators/ai.js +136 -0
- package/dist/schema/value-generators/ai.js.map +1 -0
- package/dist/schema/value-generators/index.d.ts +126 -0
- package/dist/schema/value-generators/index.d.ts.map +1 -0
- package/dist/schema/value-generators/index.js +219 -0
- package/dist/schema/value-generators/index.js.map +1 -0
- package/dist/schema/value-generators/placeholder.d.ts +52 -0
- package/dist/schema/value-generators/placeholder.d.ts.map +1 -0
- package/dist/schema/value-generators/placeholder.js +328 -0
- package/dist/schema/value-generators/placeholder.js.map +1 -0
- package/dist/schema/value-generators/types.d.ts +116 -0
- package/dist/schema/value-generators/types.d.ts.map +1 -0
- package/dist/schema/value-generators/types.js +11 -0
- package/dist/schema/value-generators/types.js.map +1 -0
- package/dist/schema/version.d.ts +111 -0
- package/dist/schema/version.d.ts.map +1 -0
- package/dist/schema/version.js +190 -0
- package/dist/schema/version.js.map +1 -0
- package/dist/schema.d.ts +1095 -24
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2852 -40
- package/dist/schema.js.map +1 -1
- package/dist/semantic-vectors.d.ts +39 -0
- package/dist/semantic-vectors.d.ts.map +1 -0
- package/dist/semantic-vectors.js +334 -0
- package/dist/semantic-vectors.js.map +1 -0
- package/dist/semantic.d.ts +29 -1
- package/dist/semantic.d.ts.map +1 -1
- package/dist/semantic.js +26 -16
- package/dist/semantic.js.map +1 -1
- package/dist/telemetry.d.ts +128 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +305 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/tests.d.ts.map +1 -1
- package/dist/tests.js +30 -22
- package/dist/tests.js.map +1 -1
- package/dist/type-guards.d.ts +50 -5
- package/dist/type-guards.d.ts.map +1 -1
- package/dist/type-guards.js +87 -16
- package/dist/type-guards.js.map +1 -1
- package/dist/types.d.ts +33 -245
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +62 -72
- package/dist/types.js.map +1 -1
- package/dist/validation.d.ts +2 -5
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +65 -93
- package/dist/validation.js.map +1 -1
- package/dist/worker/db-provider.d.ts +168 -0
- package/dist/worker/db-provider.d.ts.map +1 -0
- package/dist/worker/db-provider.js +277 -0
- package/dist/worker/db-provider.js.map +1 -0
- package/dist/worker/index.d.ts +35 -0
- package/dist/worker/index.d.ts.map +1 -0
- package/dist/worker/index.js +37 -0
- package/dist/worker/index.js.map +1 -0
- package/dist/worker.d.ts +779 -0
- package/dist/worker.d.ts.map +1 -0
- package/dist/worker.js +2786 -0
- package/dist/worker.js.map +1 -0
- package/package.json +46 -16
- package/src/docs-rels/migrations/0001-init.sql +125 -0
- package/LICENSE +0 -21
package/dist/memory-provider.js
CHANGED
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
* Supports automatic embedding generation on create/update.
|
|
7
7
|
*/
|
|
8
8
|
import { cosineSimilarity, computeRRF, extractEmbeddableText, generateContentHash, } from './semantic.js';
|
|
9
|
-
import { EMBEDDING_DIMENSIONS } from './constants.js';
|
|
9
|
+
import { DEFAULT_EMBEDDING_DIMENSIONS, EMBEDDING_DIMENSIONS } from './constants.js';
|
|
10
|
+
import { SEMANTIC_VECTORS, DEFAULT_VECTOR, BASE_VECTOR_DIMENSIONS } from './semantic-vectors.js';
|
|
11
|
+
import { validateTypeName, validateEntityId, validateSearchQuery, validateEntityData, validateRelationName, validateEventPattern, validateActionType, validateArtifactUrl, validateListOptions, validateSearchOptions, validateFieldName, isDangerousField, } from './validation.js';
|
|
12
|
+
import { EntityNotFoundError, EntityAlreadyExistsError } from './errors.js';
|
|
13
|
+
import { logWarn } from './logger.js';
|
|
10
14
|
// =============================================================================
|
|
11
15
|
// Semaphore for Concurrency Control
|
|
12
16
|
// =============================================================================
|
|
@@ -83,6 +87,157 @@ function generateId() {
|
|
|
83
87
|
return crypto.randomUUID();
|
|
84
88
|
}
|
|
85
89
|
// =============================================================================
|
|
90
|
+
// Embedding Helper Functions
|
|
91
|
+
// =============================================================================
|
|
92
|
+
/**
|
|
93
|
+
* Simple hash function for deterministic randomness
|
|
94
|
+
*
|
|
95
|
+
* Generates a consistent hash value for any input string, used for
|
|
96
|
+
* creating deterministic "random" variations in embedding generation.
|
|
97
|
+
*
|
|
98
|
+
* @param str - The string to hash
|
|
99
|
+
* @returns A positive integer hash value
|
|
100
|
+
*
|
|
101
|
+
* @internal
|
|
102
|
+
*/
|
|
103
|
+
function simpleHash(str) {
|
|
104
|
+
let hash = 0;
|
|
105
|
+
for (let i = 0; i < str.length; i++) {
|
|
106
|
+
const char = str.charCodeAt(i);
|
|
107
|
+
hash = (hash << 5) - hash + char;
|
|
108
|
+
hash = hash & hash; // Convert to 32-bit integer
|
|
109
|
+
}
|
|
110
|
+
return Math.abs(hash);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Generate a deterministic pseudo-random number from seed
|
|
114
|
+
*
|
|
115
|
+
* Uses sin function to generate predictable values that appear random
|
|
116
|
+
* but are reproducible given the same seed and index.
|
|
117
|
+
*
|
|
118
|
+
* @param seed - The seed value (typically from a hash)
|
|
119
|
+
* @param index - The position in the sequence
|
|
120
|
+
* @returns A number between 0 and 1
|
|
121
|
+
*
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
124
|
+
function seededRandom(seed, index) {
|
|
125
|
+
const x = Math.sin(seed + index) * 10000;
|
|
126
|
+
return x - Math.floor(x);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Tokenize text into lowercase words
|
|
130
|
+
*
|
|
131
|
+
* Splits text on whitespace and punctuation, filters empty strings,
|
|
132
|
+
* and converts all words to lowercase for consistent matching.
|
|
133
|
+
*
|
|
134
|
+
* @param text - The text to tokenize
|
|
135
|
+
* @returns Array of lowercase word tokens
|
|
136
|
+
*
|
|
137
|
+
* @internal
|
|
138
|
+
*/
|
|
139
|
+
function tokenizeText(text) {
|
|
140
|
+
return text
|
|
141
|
+
.toLowerCase()
|
|
142
|
+
.replace(/[^\w\s]/g, ' ')
|
|
143
|
+
.split(/\s+/)
|
|
144
|
+
.filter((w) => w.length > 0);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Get semantic vector for a word
|
|
148
|
+
*
|
|
149
|
+
* Looks up the word in SEMANTIC_VECTORS, or generates a deterministic
|
|
150
|
+
* fallback vector based on the word's hash if not found.
|
|
151
|
+
*
|
|
152
|
+
* @param word - The word to get a vector for
|
|
153
|
+
* @returns A 4-dimensional semantic vector
|
|
154
|
+
*
|
|
155
|
+
* @internal
|
|
156
|
+
*/
|
|
157
|
+
function getWordVector(word) {
|
|
158
|
+
const lower = word.toLowerCase();
|
|
159
|
+
const known = SEMANTIC_VECTORS[lower];
|
|
160
|
+
if (known) {
|
|
161
|
+
return known;
|
|
162
|
+
}
|
|
163
|
+
// Generate deterministic vector based on word hash
|
|
164
|
+
const hash = simpleHash(lower);
|
|
165
|
+
return DEFAULT_VECTOR.map((v, i) => v + seededRandom(hash, i) * 0.1);
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Aggregate word vectors into a single base vector
|
|
169
|
+
*
|
|
170
|
+
* Sums up the semantic vectors for all words in the input,
|
|
171
|
+
* creating a combined representation of the text's meaning.
|
|
172
|
+
*
|
|
173
|
+
* @param words - Array of word tokens
|
|
174
|
+
* @returns Aggregated 4-dimensional vector (not normalized)
|
|
175
|
+
*
|
|
176
|
+
* @internal
|
|
177
|
+
*/
|
|
178
|
+
function aggregateWordVectors(words) {
|
|
179
|
+
const aggregated = new Array(BASE_VECTOR_DIMENSIONS).fill(0);
|
|
180
|
+
for (const word of words) {
|
|
181
|
+
const vec = getWordVector(word);
|
|
182
|
+
for (let i = 0; i < BASE_VECTOR_DIMENSIONS; i++) {
|
|
183
|
+
aggregated[i] += vec[i];
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return aggregated;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Normalize a vector to unit length
|
|
190
|
+
*
|
|
191
|
+
* Divides each component by the vector's magnitude to create
|
|
192
|
+
* a unit vector (length = 1), enabling cosine similarity comparison.
|
|
193
|
+
*
|
|
194
|
+
* @param vector - The vector to normalize
|
|
195
|
+
* @returns A new unit-length vector
|
|
196
|
+
*
|
|
197
|
+
* @internal
|
|
198
|
+
*/
|
|
199
|
+
function normalizeVector(vector) {
|
|
200
|
+
const norm = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
|
|
201
|
+
return vector.map((v) => v / (norm || 1));
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Expand base vector to full embedding dimensions
|
|
205
|
+
*
|
|
206
|
+
* Takes a 4-dimensional base vector and expands it to the target
|
|
207
|
+
* dimensions by cycling through base values and adding deterministic noise.
|
|
208
|
+
*
|
|
209
|
+
* @param normalized - Normalized base vector (4 dimensions)
|
|
210
|
+
* @param dimensions - Target number of dimensions
|
|
211
|
+
* @param textHash - Hash of original text for deterministic noise
|
|
212
|
+
* @returns Expanded vector (not normalized)
|
|
213
|
+
*
|
|
214
|
+
* @internal
|
|
215
|
+
*/
|
|
216
|
+
function expandToFullDimensions(normalized, dimensions, textHash) {
|
|
217
|
+
const embedding = new Array(dimensions);
|
|
218
|
+
for (let i = 0; i < dimensions; i++) {
|
|
219
|
+
const baseIndex = i % BASE_VECTOR_DIMENSIONS;
|
|
220
|
+
const base = normalized[baseIndex];
|
|
221
|
+
const noise = seededRandom(textHash, i) * 0.1 - 0.05;
|
|
222
|
+
embedding[i] = base + noise;
|
|
223
|
+
}
|
|
224
|
+
return embedding;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Generate embedding for empty text
|
|
228
|
+
*
|
|
229
|
+
* Creates a low-magnitude embedding for empty input,
|
|
230
|
+
* using deterministic small values based on position.
|
|
231
|
+
*
|
|
232
|
+
* @param dimensions - Number of embedding dimensions
|
|
233
|
+
* @returns A low-magnitude embedding vector
|
|
234
|
+
*
|
|
235
|
+
* @internal
|
|
236
|
+
*/
|
|
237
|
+
function generateEmptyEmbedding(dimensions) {
|
|
238
|
+
return Array.from({ length: dimensions }, (_, i) => seededRandom(0, i) * 0.01);
|
|
239
|
+
}
|
|
240
|
+
// =============================================================================
|
|
86
241
|
// Verb Conjugation (Linguistic Helpers)
|
|
87
242
|
// =============================================================================
|
|
88
243
|
/**
|
|
@@ -196,8 +351,11 @@ function toPresent(verb) {
|
|
|
196
351
|
if (verb.endsWith('y') && !isVowel(verb[verb.length - 2])) {
|
|
197
352
|
return verb.slice(0, -1) + 'ies';
|
|
198
353
|
}
|
|
199
|
-
if (verb.endsWith('s') ||
|
|
200
|
-
verb.endsWith('
|
|
354
|
+
if (verb.endsWith('s') ||
|
|
355
|
+
verb.endsWith('x') ||
|
|
356
|
+
verb.endsWith('z') ||
|
|
357
|
+
verb.endsWith('ch') ||
|
|
358
|
+
verb.endsWith('sh')) {
|
|
201
359
|
return verb + 'es';
|
|
202
360
|
}
|
|
203
361
|
return verb + 's';
|
|
@@ -255,9 +413,37 @@ export class MemoryProvider {
|
|
|
255
413
|
semaphore;
|
|
256
414
|
// Embedding configuration
|
|
257
415
|
embeddingsConfig;
|
|
416
|
+
// Flag to use ai-functions for embeddings
|
|
417
|
+
useAiFunctions;
|
|
418
|
+
// Custom embedding provider (for testing or alternative services)
|
|
419
|
+
embeddingProvider;
|
|
420
|
+
// Embedding dimensions for mock provider
|
|
421
|
+
embeddingDimensions;
|
|
258
422
|
constructor(options = {}) {
|
|
259
423
|
this.semaphore = new Semaphore(options.concurrency ?? 10);
|
|
260
424
|
this.embeddingsConfig = options.embeddings ?? {};
|
|
425
|
+
this.useAiFunctions = options.useAiFunctions ?? false;
|
|
426
|
+
if (options.embeddingProvider !== undefined) {
|
|
427
|
+
this.embeddingProvider = options.embeddingProvider;
|
|
428
|
+
}
|
|
429
|
+
this.embeddingDimensions = options.embeddingDimensions ?? DEFAULT_EMBEDDING_DIMENSIONS;
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* Enable or disable ai-functions for embeddings
|
|
433
|
+
*/
|
|
434
|
+
setUseAiFunctions(enabled) {
|
|
435
|
+
this.useAiFunctions = enabled;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Set a custom embedding provider
|
|
439
|
+
*/
|
|
440
|
+
setEmbeddingProvider(provider) {
|
|
441
|
+
if (provider !== undefined) {
|
|
442
|
+
this.embeddingProvider = provider;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
this.embeddingProvider = undefined;
|
|
446
|
+
}
|
|
261
447
|
}
|
|
262
448
|
/**
|
|
263
449
|
* Set embeddings configuration
|
|
@@ -273,210 +459,28 @@ export class MemoryProvider {
|
|
|
273
459
|
*
|
|
274
460
|
* Uses semantic word vectors to create meaningful embeddings
|
|
275
461
|
* where similar concepts have higher cosine similarity.
|
|
462
|
+
*
|
|
463
|
+
* The embedding process:
|
|
464
|
+
* 1. Tokenize text into words
|
|
465
|
+
* 2. Look up semantic vectors for each word
|
|
466
|
+
* 3. Aggregate word vectors into a base vector
|
|
467
|
+
* 4. Normalize the aggregated vector
|
|
468
|
+
* 5. Expand to full embedding dimensions with deterministic noise
|
|
469
|
+
* 6. Final normalization to unit vector
|
|
470
|
+
*
|
|
471
|
+
* @param text - The text to generate an embedding for
|
|
472
|
+
* @returns A normalized embedding vector
|
|
276
473
|
*/
|
|
277
474
|
generateEmbedding(text) {
|
|
278
|
-
|
|
279
|
-
const SEMANTIC_VECTORS = {
|
|
280
|
-
// AI/ML domain
|
|
281
|
-
machine: [0.9, 0.1, 0.05, 0.02],
|
|
282
|
-
learning: [0.85, 0.15, 0.08, 0.03],
|
|
283
|
-
artificial: [0.88, 0.12, 0.06, 0.04],
|
|
284
|
-
intelligence: [0.87, 0.13, 0.07, 0.05],
|
|
285
|
-
neural: [0.82, 0.18, 0.09, 0.06],
|
|
286
|
-
network: [0.75, 0.2, 0.15, 0.1],
|
|
287
|
-
deep: [0.8, 0.17, 0.1, 0.08],
|
|
288
|
-
ai: [0.92, 0.08, 0.04, 0.02],
|
|
289
|
-
ml: [0.88, 0.12, 0.06, 0.03],
|
|
290
|
-
// Programming domain
|
|
291
|
-
programming: [0.15, 0.85, 0.1, 0.05],
|
|
292
|
-
code: [0.12, 0.88, 0.12, 0.06],
|
|
293
|
-
software: [0.18, 0.82, 0.15, 0.08],
|
|
294
|
-
development: [0.2, 0.8, 0.18, 0.1],
|
|
295
|
-
typescript: [0.1, 0.9, 0.08, 0.04],
|
|
296
|
-
javascript: [0.12, 0.88, 0.1, 0.05],
|
|
297
|
-
python: [0.25, 0.75, 0.12, 0.06],
|
|
298
|
-
react: [0.08, 0.85, 0.2, 0.1],
|
|
299
|
-
vue: [0.06, 0.84, 0.18, 0.08],
|
|
300
|
-
frontend: [0.05, 0.8, 0.25, 0.12],
|
|
301
|
-
// Database domain
|
|
302
|
-
database: [0.1, 0.7, 0.08, 0.6],
|
|
303
|
-
query: [0.12, 0.65, 0.1, 0.7],
|
|
304
|
-
sql: [0.08, 0.6, 0.05, 0.75],
|
|
305
|
-
index: [0.1, 0.58, 0.08, 0.72],
|
|
306
|
-
optimization: [0.15, 0.55, 0.12, 0.68],
|
|
307
|
-
performance: [0.18, 0.5, 0.15, 0.65],
|
|
308
|
-
// DevOps domain
|
|
309
|
-
kubernetes: [0.05, 0.6, 0.8, 0.15],
|
|
310
|
-
docker: [0.08, 0.55, 0.82, 0.12],
|
|
311
|
-
container: [0.06, 0.5, 0.85, 0.1],
|
|
312
|
-
deployment: [0.1, 0.45, 0.78, 0.18],
|
|
313
|
-
devops: [0.12, 0.48, 0.75, 0.2],
|
|
314
|
-
// Food domain (distinctly different direction - high in dim 3, low elsewhere)
|
|
315
|
-
cooking: [0.05, 0.08, 0.05, 0.95],
|
|
316
|
-
recipe: [0.06, 0.07, 0.04, 0.93],
|
|
317
|
-
food: [0.04, 0.06, 0.04, 0.96],
|
|
318
|
-
pasta: [0.03, 0.05, 0.03, 0.97],
|
|
319
|
-
pizza: [0.03, 0.06, 0.04, 0.96],
|
|
320
|
-
italian: [0.04, 0.07, 0.04, 0.94],
|
|
321
|
-
garden: [0.05, 0.04, 0.03, 0.92],
|
|
322
|
-
flowers: [0.04, 0.03, 0.03, 0.91],
|
|
323
|
-
chef: [0.05, 0.1, 0.05, 0.95],
|
|
324
|
-
restaurant: [0.06, 0.08, 0.04, 0.93],
|
|
325
|
-
kitchen: [0.05, 0.09, 0.05, 0.94],
|
|
326
|
-
antonio: [0.05, 0.08, 0.04, 0.92],
|
|
327
|
-
// Research/Academic domain (similar to AI/ML)
|
|
328
|
-
researcher: [0.82, 0.2, 0.1, 0.08],
|
|
329
|
-
phd: [0.8, 0.18, 0.12, 0.1],
|
|
330
|
-
research: [0.85, 0.15, 0.1, 0.07],
|
|
331
|
-
professor: [0.78, 0.22, 0.12, 0.1],
|
|
332
|
-
academic: [0.75, 0.2, 0.15, 0.12],
|
|
333
|
-
// Location/Venue domain (for fuzzy threshold tests - need distinct clusters)
|
|
334
|
-
// "conference center downtown" cluster - high values in different dimensions
|
|
335
|
-
conference: [0.2, 0.25, 0.85, 0.2],
|
|
336
|
-
center: [0.18, 0.22, 0.88, 0.18],
|
|
337
|
-
downtown: [0.15, 0.2, 0.9, 0.15],
|
|
338
|
-
// "tech hub 123 main st" cluster - completely different direction
|
|
339
|
-
hub: [0.85, 0.15, 0.2, 0.15],
|
|
340
|
-
main: [0.12, 0.12, 0.15, 0.1],
|
|
341
|
-
st: [0.1, 0.1, 0.12, 0.08],
|
|
342
|
-
'123': [0.08, 0.08, 0.1, 0.05],
|
|
343
|
-
// GraphQL/API
|
|
344
|
-
graphql: [0.1, 0.75, 0.15, 0.55],
|
|
345
|
-
api: [0.15, 0.7, 0.2, 0.5],
|
|
346
|
-
rest: [0.12, 0.68, 0.18, 0.48],
|
|
347
|
-
queries: [0.14, 0.65, 0.12, 0.6],
|
|
348
|
-
// Testing
|
|
349
|
-
testing: [0.1, 0.78, 0.08, 0.15],
|
|
350
|
-
test: [0.08, 0.8, 0.06, 0.12],
|
|
351
|
-
unit: [0.06, 0.82, 0.05, 0.1],
|
|
352
|
-
integration: [0.12, 0.75, 0.1, 0.18],
|
|
353
|
-
// State management
|
|
354
|
-
state: [0.08, 0.82, 0.2, 0.08],
|
|
355
|
-
management: [0.15, 0.75, 0.25, 0.12],
|
|
356
|
-
hooks: [0.06, 0.88, 0.15, 0.05],
|
|
357
|
-
usestate: [0.05, 0.9, 0.12, 0.04],
|
|
358
|
-
useeffect: [0.04, 0.88, 0.1, 0.03],
|
|
359
|
-
// Related/Concept domain (for semantic similarity tests)
|
|
360
|
-
related: [0.5, 0.5, 0.5, 0.5],
|
|
361
|
-
concept: [0.55, 0.45, 0.55, 0.45],
|
|
362
|
-
similar: [0.52, 0.48, 0.52, 0.48],
|
|
363
|
-
different: [0.48, 0.52, 0.48, 0.52],
|
|
364
|
-
words: [0.45, 0.55, 0.45, 0.55],
|
|
365
|
-
semantically: [0.6, 0.4, 0.6, 0.4],
|
|
366
|
-
// Exact match domain (distinctly different vectors)
|
|
367
|
-
exact: [0.1, 0.1, 0.1, 0.9],
|
|
368
|
-
match: [0.15, 0.15, 0.1, 0.85],
|
|
369
|
-
title: [0.1, 0.2, 0.1, 0.8],
|
|
370
|
-
contains: [0.12, 0.18, 0.12, 0.78],
|
|
371
|
-
search: [0.08, 0.22, 0.08, 0.82],
|
|
372
|
-
terms: [0.05, 0.25, 0.05, 0.85],
|
|
373
|
-
// Business domain (for fuzzy forward resolution tests)
|
|
374
|
-
enterprise: [0.7, 0.3, 0.8, 0.6],
|
|
375
|
-
large: [0.65, 0.25, 0.75, 0.55],
|
|
376
|
-
corporations: [0.68, 0.28, 0.78, 0.58],
|
|
377
|
-
companies: [0.6, 0.4, 0.7, 0.5],
|
|
378
|
-
company: [0.62, 0.38, 0.72, 0.52],
|
|
379
|
-
thousands: [0.7, 0.2, 0.7, 0.5],
|
|
380
|
-
employees: [0.55, 0.35, 0.65, 0.45],
|
|
381
|
-
big: [0.68, 0.3, 0.75, 0.58],
|
|
382
|
-
small: [0.3, 0.6, 0.3, 0.4],
|
|
383
|
-
business: [0.5, 0.5, 0.6, 0.5],
|
|
384
|
-
owners: [0.4, 0.5, 0.5, 0.45],
|
|
385
|
-
consumer: [0.35, 0.55, 0.35, 0.35],
|
|
386
|
-
individual: [0.32, 0.58, 0.32, 0.32],
|
|
387
|
-
b2c: [0.3, 0.6, 0.3, 0.35],
|
|
388
|
-
// Tech professional domain
|
|
389
|
-
developer: [0.2, 0.85, 0.15, 0.1],
|
|
390
|
-
engineer: [0.25, 0.82, 0.18, 0.12],
|
|
391
|
-
engineers: [0.27, 0.8, 0.2, 0.14],
|
|
392
|
-
builds: [0.18, 0.78, 0.16, 0.08],
|
|
393
|
-
writes: [0.15, 0.75, 0.12, 0.06],
|
|
394
|
-
professional: [0.22, 0.72, 0.2, 0.15],
|
|
395
|
-
applications: [0.2, 0.78, 0.18, 0.1],
|
|
396
|
-
tech: [0.25, 0.8, 0.2, 0.12],
|
|
397
|
-
technology: [0.28, 0.78, 0.22, 0.14],
|
|
398
|
-
electronics: [0.3, 0.75, 0.25, 0.15],
|
|
399
|
-
device: [0.25, 0.82, 0.2, 0.1],
|
|
400
|
-
furniture: [0.1, 0.15, 0.2, 0.85],
|
|
401
|
-
home: [0.12, 0.18, 0.22, 0.8],
|
|
402
|
-
living: [0.1, 0.15, 0.2, 0.82],
|
|
403
|
-
goods: [0.3, 0.5, 0.35, 0.4],
|
|
404
|
-
leaders: [0.4, 0.5, 0.6, 0.4],
|
|
405
|
-
senior: [0.35, 0.55, 0.55, 0.35],
|
|
406
|
-
// Data science domain
|
|
407
|
-
data: [0.75, 0.3, 0.15, 0.55],
|
|
408
|
-
science: [0.78, 0.25, 0.12, 0.5],
|
|
409
|
-
scientist: [0.8, 0.28, 0.1, 0.52],
|
|
410
|
-
background: [0.72, 0.32, 0.14, 0.48],
|
|
411
|
-
// DevOps/cloud domain
|
|
412
|
-
cloud: [0.1, 0.55, 0.85, 0.15],
|
|
413
|
-
expertise: [0.15, 0.5, 0.8, 0.18],
|
|
414
|
-
// Support domain
|
|
415
|
-
support: [0.2, 0.45, 0.3, 0.55],
|
|
416
|
-
specialist: [0.22, 0.48, 0.32, 0.52],
|
|
417
|
-
technical: [0.25, 0.65, 0.35, 0.4],
|
|
418
|
-
issues: [0.18, 0.42, 0.28, 0.48],
|
|
419
|
-
// Security domain
|
|
420
|
-
security: [0.3, 0.6, 0.4, 0.7],
|
|
421
|
-
auth: [0.28, 0.58, 0.38, 0.72],
|
|
422
|
-
authentication: [0.32, 0.55, 0.42, 0.75],
|
|
423
|
-
identity: [0.35, 0.52, 0.45, 0.68],
|
|
424
|
-
oauth: [0.3, 0.62, 0.4, 0.7],
|
|
425
|
-
// CRM domain
|
|
426
|
-
crm: [0.45, 0.4, 0.7, 0.55],
|
|
427
|
-
sales: [0.42, 0.38, 0.68, 0.52],
|
|
428
|
-
salesforce: [0.48, 0.42, 0.72, 0.58],
|
|
429
|
-
provider: [0.5, 0.45, 0.65, 0.5],
|
|
430
|
-
};
|
|
431
|
-
const DEFAULT_VECTOR = [0.1, 0.1, 0.1, 0.1];
|
|
432
|
-
// Simple hash function
|
|
433
|
-
const simpleHash = (str) => {
|
|
434
|
-
let hash = 0;
|
|
435
|
-
for (let i = 0; i < str.length; i++) {
|
|
436
|
-
const char = str.charCodeAt(i);
|
|
437
|
-
hash = ((hash << 5) - hash) + char;
|
|
438
|
-
hash = hash & hash;
|
|
439
|
-
}
|
|
440
|
-
return Math.abs(hash);
|
|
441
|
-
};
|
|
442
|
-
// Seeded random
|
|
443
|
-
const seededRandom = (seed, index) => {
|
|
444
|
-
const x = Math.sin(seed + index) * 10000;
|
|
445
|
-
return x - Math.floor(x);
|
|
446
|
-
};
|
|
447
|
-
// Tokenize
|
|
448
|
-
const words = text
|
|
449
|
-
.toLowerCase()
|
|
450
|
-
.replace(/[^\w\s]/g, ' ')
|
|
451
|
-
.split(/\s+/)
|
|
452
|
-
.filter(w => w.length > 0);
|
|
475
|
+
const words = tokenizeText(text);
|
|
453
476
|
if (words.length === 0) {
|
|
454
|
-
return
|
|
455
|
-
}
|
|
456
|
-
// Aggregate word vectors
|
|
457
|
-
const aggregated = [0, 0, 0, 0];
|
|
458
|
-
for (const word of words) {
|
|
459
|
-
const lower = word.toLowerCase();
|
|
460
|
-
const vec = SEMANTIC_VECTORS[lower] ?? DEFAULT_VECTOR.map((v, i) => v + seededRandom(simpleHash(lower), i) * 0.1);
|
|
461
|
-
for (let i = 0; i < 4; i++) {
|
|
462
|
-
aggregated[i] += vec[i];
|
|
463
|
-
}
|
|
477
|
+
return generateEmptyEmbedding(this.embeddingDimensions);
|
|
464
478
|
}
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
const normalized = aggregated.map(v => v / (norm || 1));
|
|
468
|
-
// Expand to full dimensions
|
|
479
|
+
const aggregated = aggregateWordVectors(words);
|
|
480
|
+
const normalized = normalizeVector(aggregated);
|
|
469
481
|
const textHash = simpleHash(text);
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
const baseIndex = i % 4;
|
|
473
|
-
const base = normalized[baseIndex];
|
|
474
|
-
const noise = seededRandom(textHash, i) * 0.1 - 0.05;
|
|
475
|
-
embedding[i] = base + noise;
|
|
476
|
-
}
|
|
477
|
-
// Final normalization
|
|
478
|
-
const finalNorm = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0));
|
|
479
|
-
return embedding.map((v) => v / (finalNorm || 1));
|
|
482
|
+
const expanded = expandToFullDimensions(normalized, this.embeddingDimensions, textHash);
|
|
483
|
+
return normalizeVector(expanded);
|
|
480
484
|
}
|
|
481
485
|
/**
|
|
482
486
|
* Check if embeddings should be generated for a given entity type
|
|
@@ -509,6 +513,11 @@ export class MemoryProvider {
|
|
|
509
513
|
* embeddings for entities based on their text content. The embedding
|
|
510
514
|
* is stored as an artifact associated with the entity.
|
|
511
515
|
*
|
|
516
|
+
* Priority for embedding generation:
|
|
517
|
+
* 1. Custom embeddingProvider if set (for testing or alternative services)
|
|
518
|
+
* 2. ai-functions if useAiFunctions is enabled
|
|
519
|
+
* 3. Deterministic mock embedding (default for testing)
|
|
520
|
+
*
|
|
512
521
|
* @param type - The entity type name
|
|
513
522
|
* @param id - The entity ID
|
|
514
523
|
* @param data - The entity data to extract text from
|
|
@@ -523,8 +532,39 @@ export class MemoryProvider {
|
|
|
523
532
|
const { text, fields: embeddedFields } = extractEmbeddableText(data, fields);
|
|
524
533
|
if (!text.trim())
|
|
525
534
|
return;
|
|
526
|
-
|
|
527
|
-
|
|
535
|
+
let embedding;
|
|
536
|
+
let dimensions = this.embeddingDimensions;
|
|
537
|
+
let source = 'mock';
|
|
538
|
+
// Priority: embeddingProvider > useAiFunctions > mock
|
|
539
|
+
if (this.embeddingProvider) {
|
|
540
|
+
try {
|
|
541
|
+
const result = await this.embeddingProvider.embedTexts([text]);
|
|
542
|
+
embedding = result.embeddings[0] ?? this.generateEmbedding(text);
|
|
543
|
+
dimensions = embedding.length;
|
|
544
|
+
source = 'custom-provider';
|
|
545
|
+
}
|
|
546
|
+
catch (err) {
|
|
547
|
+
logWarn('Custom embedding provider failed, falling back to mock:', err);
|
|
548
|
+
embedding = this.generateEmbedding(text);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
else if (this.useAiFunctions) {
|
|
552
|
+
try {
|
|
553
|
+
const { embedTexts } = await import('ai-functions');
|
|
554
|
+
const result = await embedTexts([text]);
|
|
555
|
+
embedding = result.embeddings[0] ?? this.generateEmbedding(text);
|
|
556
|
+
dimensions = embedding.length;
|
|
557
|
+
source = 'ai-functions';
|
|
558
|
+
}
|
|
559
|
+
catch (err) {
|
|
560
|
+
// Fallback to mock embedding if ai-functions fails
|
|
561
|
+
logWarn('ai-functions embedTexts failed, falling back to mock:', err);
|
|
562
|
+
embedding = this.generateEmbedding(text);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
else {
|
|
566
|
+
embedding = this.generateEmbedding(text);
|
|
567
|
+
}
|
|
528
568
|
const contentHash = generateContentHash(text);
|
|
529
569
|
// Store as artifact with complete metadata
|
|
530
570
|
const url = `${type}/${id}`;
|
|
@@ -533,8 +573,9 @@ export class MemoryProvider {
|
|
|
533
573
|
sourceHash: contentHash,
|
|
534
574
|
metadata: {
|
|
535
575
|
fields: embeddedFields,
|
|
536
|
-
dimensions
|
|
576
|
+
dimensions,
|
|
537
577
|
text: text.slice(0, 200),
|
|
578
|
+
source,
|
|
538
579
|
},
|
|
539
580
|
});
|
|
540
581
|
}
|
|
@@ -559,11 +600,15 @@ export class MemoryProvider {
|
|
|
559
600
|
return this.entities.get(type);
|
|
560
601
|
}
|
|
561
602
|
async get(type, id) {
|
|
603
|
+
validateTypeName(type);
|
|
604
|
+
validateEntityId(id);
|
|
562
605
|
const store = this.getTypeStore(type);
|
|
563
606
|
const entity = store.get(id);
|
|
564
607
|
return entity ? { ...entity, $id: id, $type: type } : null;
|
|
565
608
|
}
|
|
566
609
|
async list(type, options) {
|
|
610
|
+
validateTypeName(type);
|
|
611
|
+
validateListOptions(options);
|
|
567
612
|
const store = this.getTypeStore(type);
|
|
568
613
|
let results = [];
|
|
569
614
|
for (const [id, entity] of store) {
|
|
@@ -612,9 +657,18 @@ export class MemoryProvider {
|
|
|
612
657
|
return results;
|
|
613
658
|
}
|
|
614
659
|
async search(type, query, options) {
|
|
660
|
+
validateTypeName(type);
|
|
661
|
+
validateSearchQuery(query);
|
|
662
|
+
validateSearchOptions(options);
|
|
615
663
|
const all = await this.list(type, options);
|
|
616
664
|
const queryLower = query.toLowerCase();
|
|
617
|
-
|
|
665
|
+
let fields = options?.fields || ['$all'];
|
|
666
|
+
// Filter out dangerous field names
|
|
667
|
+
fields = fields.filter((f) => !isDangerousField(f));
|
|
668
|
+
// If all fields were dangerous, return empty results
|
|
669
|
+
if (fields.length === 0) {
|
|
670
|
+
return [];
|
|
671
|
+
}
|
|
618
672
|
const scored = [];
|
|
619
673
|
for (const entity of all) {
|
|
620
674
|
let searchText;
|
|
@@ -640,28 +694,111 @@ export class MemoryProvider {
|
|
|
640
694
|
}
|
|
641
695
|
/**
|
|
642
696
|
* Semantic search using embedding similarity
|
|
697
|
+
*
|
|
698
|
+
* Priority for embedding and similarity operations:
|
|
699
|
+
* 1. Custom embeddingProvider if set
|
|
700
|
+
* 2. ai-functions if useAiFunctions is enabled
|
|
701
|
+
* 3. Local mock implementations (default)
|
|
643
702
|
*/
|
|
644
703
|
async semanticSearch(type, query, options) {
|
|
645
704
|
const store = this.getTypeStore(type);
|
|
646
705
|
const limit = options?.limit ?? 10;
|
|
647
706
|
const minScore = options?.minScore ?? 0;
|
|
648
707
|
// Generate query embedding
|
|
649
|
-
|
|
650
|
-
|
|
708
|
+
let queryEmbedding;
|
|
709
|
+
if (this.embeddingProvider) {
|
|
710
|
+
try {
|
|
711
|
+
const result = await this.embeddingProvider.embedTexts([query]);
|
|
712
|
+
queryEmbedding = result.embeddings[0] ?? this.generateEmbedding(query);
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
logWarn('Custom embedding provider failed for query, falling back to mock:', err);
|
|
716
|
+
queryEmbedding = this.generateEmbedding(query);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
else if (this.useAiFunctions) {
|
|
720
|
+
try {
|
|
721
|
+
const { embedTexts } = await import('ai-functions');
|
|
722
|
+
const result = await embedTexts([query]);
|
|
723
|
+
queryEmbedding = result.embeddings[0] ?? this.generateEmbedding(query);
|
|
724
|
+
}
|
|
725
|
+
catch (err) {
|
|
726
|
+
logWarn('ai-functions embedTexts failed for query, falling back to mock:', err);
|
|
727
|
+
queryEmbedding = this.generateEmbedding(query);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
queryEmbedding = this.generateEmbedding(query);
|
|
732
|
+
}
|
|
733
|
+
// Get similarity function
|
|
734
|
+
let similarityFn;
|
|
735
|
+
if (this.embeddingProvider?.cosineSimilarity) {
|
|
736
|
+
similarityFn = this.embeddingProvider.cosineSimilarity;
|
|
737
|
+
}
|
|
738
|
+
else if (this.useAiFunctions) {
|
|
739
|
+
try {
|
|
740
|
+
const { cosineSimilarity: aiCosineSimilarity } = await import('ai-functions');
|
|
741
|
+
similarityFn = aiCosineSimilarity;
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
logWarn('ai-functions cosineSimilarity not available, using local:', err);
|
|
745
|
+
similarityFn = cosineSimilarity;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
similarityFn = cosineSimilarity;
|
|
750
|
+
}
|
|
751
|
+
// Collect embeddings and entities for potential findSimilar usage
|
|
752
|
+
const embeddings = [];
|
|
753
|
+
const entities = [];
|
|
651
754
|
for (const [id, entity] of store) {
|
|
652
|
-
// Get stored embedding from artifacts
|
|
653
755
|
const url = `${type}/${id}`;
|
|
654
756
|
const artifact = await this.getArtifact(url, 'embedding');
|
|
655
757
|
if (!artifact || !Array.isArray(artifact.content)) {
|
|
656
758
|
continue;
|
|
657
759
|
}
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
760
|
+
embeddings.push(artifact.content);
|
|
761
|
+
entities.push({ entity: { ...entity, $id: id, $type: type }, id });
|
|
762
|
+
}
|
|
763
|
+
// If using embeddingProvider with findSimilar, use it
|
|
764
|
+
if (this.embeddingProvider?.findSimilar && entities.length > 0) {
|
|
765
|
+
try {
|
|
766
|
+
const results = this.embeddingProvider.findSimilar(queryEmbedding, embeddings, entities, {
|
|
767
|
+
topK: limit,
|
|
768
|
+
minScore,
|
|
664
769
|
});
|
|
770
|
+
return results.map(({ item, score }) => ({
|
|
771
|
+
...item.entity,
|
|
772
|
+
$score: score,
|
|
773
|
+
}));
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
logWarn('Custom embedding provider findSimilar failed, falling back to manual scoring:', err);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
// If using ai-functions and we have entities, try to use findSimilar
|
|
780
|
+
if (this.useAiFunctions && entities.length > 0) {
|
|
781
|
+
try {
|
|
782
|
+
const { findSimilar } = await import('ai-functions');
|
|
783
|
+
const results = findSimilar(queryEmbedding, embeddings, entities, { topK: limit, minScore });
|
|
784
|
+
return results.map(({ item, score }) => ({
|
|
785
|
+
...item.entity,
|
|
786
|
+
$score: score,
|
|
787
|
+
}));
|
|
788
|
+
}
|
|
789
|
+
catch (err) {
|
|
790
|
+
// Fall through to manual scoring if findSimilar fails
|
|
791
|
+
logWarn('ai-functions findSimilar failed, falling back to manual scoring:', err);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
// Manual scoring fallback
|
|
795
|
+
const scored = [];
|
|
796
|
+
for (let i = 0; i < entities.length; i++) {
|
|
797
|
+
const embedding = embeddings[i];
|
|
798
|
+
const { entity } = entities[i];
|
|
799
|
+
const score = similarityFn(queryEmbedding, embedding);
|
|
800
|
+
if (score >= minScore) {
|
|
801
|
+
scored.push({ entity, score });
|
|
665
802
|
}
|
|
666
803
|
}
|
|
667
804
|
// Sort by score descending
|
|
@@ -686,15 +823,18 @@ export class MemoryProvider {
|
|
|
686
823
|
const ftsResults = await this.search(type, query);
|
|
687
824
|
const ftsRanks = new Map();
|
|
688
825
|
ftsResults.forEach((entity, index) => {
|
|
689
|
-
const id = entity
|
|
826
|
+
const id = entity['$id'] || entity['id'];
|
|
690
827
|
ftsRanks.set(id, index + 1); // 1-indexed rank
|
|
691
828
|
});
|
|
692
829
|
// Get semantic results with their ranks and scores
|
|
693
830
|
// Get more results to ensure we have enough after offset
|
|
694
|
-
const semanticResults = await this.semanticSearch(type, query, {
|
|
831
|
+
const semanticResults = await this.semanticSearch(type, query, {
|
|
832
|
+
limit: (limit + offset) * 2,
|
|
833
|
+
minScore,
|
|
834
|
+
});
|
|
695
835
|
const semanticRanks = new Map();
|
|
696
836
|
semanticResults.forEach((entity, index) => {
|
|
697
|
-
const id = entity
|
|
837
|
+
const id = entity['$id'] || entity['id'];
|
|
698
838
|
semanticRanks.set(id, { rank: index + 1, score: entity.$score });
|
|
699
839
|
});
|
|
700
840
|
// Combine results with RRF
|
|
@@ -724,7 +864,9 @@ export class MemoryProvider {
|
|
|
724
864
|
// Sort by RRF score descending
|
|
725
865
|
combined.sort((a, b) => b.rrfScore - a.rrfScore);
|
|
726
866
|
// Apply offset and limit, then return with scoring fields
|
|
727
|
-
return combined
|
|
867
|
+
return combined
|
|
868
|
+
.slice(offset, offset + limit)
|
|
869
|
+
.map(({ entity, rrfScore, ftsRank, semanticRank, semanticScore }) => ({
|
|
728
870
|
...entity,
|
|
729
871
|
$rrfScore: rrfScore,
|
|
730
872
|
$ftsRank: ftsRank,
|
|
@@ -751,10 +893,15 @@ export class MemoryProvider {
|
|
|
751
893
|
return results;
|
|
752
894
|
}
|
|
753
895
|
async create(type, id, data) {
|
|
896
|
+
validateTypeName(type);
|
|
897
|
+
if (id !== undefined) {
|
|
898
|
+
validateEntityId(id);
|
|
899
|
+
}
|
|
900
|
+
validateEntityData(data);
|
|
754
901
|
const store = this.getTypeStore(type);
|
|
755
902
|
const entityId = id || generateId();
|
|
756
903
|
if (store.has(entityId)) {
|
|
757
|
-
throw new
|
|
904
|
+
throw new EntityAlreadyExistsError(type, entityId, 'create');
|
|
758
905
|
}
|
|
759
906
|
const entity = {
|
|
760
907
|
...data,
|
|
@@ -766,15 +913,26 @@ export class MemoryProvider {
|
|
|
766
913
|
await this.autoEmbed(type, entityId, entity);
|
|
767
914
|
// Emit type-specific and global events
|
|
768
915
|
const eventData = { $id: entityId, $type: type, ...entity };
|
|
769
|
-
await this.emit(
|
|
770
|
-
|
|
916
|
+
await this.emit({
|
|
917
|
+
event: `${type}.created`,
|
|
918
|
+
object: `${type}/${entityId}`,
|
|
919
|
+
objectData: eventData,
|
|
920
|
+
});
|
|
921
|
+
await this.emit({
|
|
922
|
+
event: 'entity:created',
|
|
923
|
+
object: `${type}/${entityId}`,
|
|
924
|
+
objectData: eventData,
|
|
925
|
+
});
|
|
771
926
|
return { ...entity, $id: entityId, $type: type };
|
|
772
927
|
}
|
|
773
928
|
async update(type, id, data) {
|
|
929
|
+
validateTypeName(type);
|
|
930
|
+
validateEntityId(id);
|
|
931
|
+
validateEntityData(data);
|
|
774
932
|
const store = this.getTypeStore(type);
|
|
775
933
|
const existing = store.get(id);
|
|
776
934
|
if (!existing) {
|
|
777
|
-
throw new
|
|
935
|
+
throw new EntityNotFoundError(type, id, 'update');
|
|
778
936
|
}
|
|
779
937
|
const updated = {
|
|
780
938
|
...existing,
|
|
@@ -788,11 +946,21 @@ export class MemoryProvider {
|
|
|
788
946
|
await this.invalidateArtifacts(`${type}/${id}`);
|
|
789
947
|
// Emit type-specific and global events
|
|
790
948
|
const eventData = { $id: id, $type: type, ...updated };
|
|
791
|
-
await this.emit(
|
|
792
|
-
|
|
949
|
+
await this.emit({
|
|
950
|
+
event: `${type}.updated`,
|
|
951
|
+
object: `${type}/${id}`,
|
|
952
|
+
objectData: eventData,
|
|
953
|
+
});
|
|
954
|
+
await this.emit({
|
|
955
|
+
event: 'entity:updated',
|
|
956
|
+
object: `${type}/${id}`,
|
|
957
|
+
objectData: eventData,
|
|
958
|
+
});
|
|
793
959
|
return { ...updated, $id: id, $type: type };
|
|
794
960
|
}
|
|
795
961
|
async delete(type, id) {
|
|
962
|
+
validateTypeName(type);
|
|
963
|
+
validateEntityId(id);
|
|
796
964
|
const store = this.getTypeStore(type);
|
|
797
965
|
if (!store.has(id)) {
|
|
798
966
|
return false;
|
|
@@ -800,8 +968,16 @@ export class MemoryProvider {
|
|
|
800
968
|
store.delete(id);
|
|
801
969
|
// Emit type-specific and global events
|
|
802
970
|
const eventData = { $id: id, $type: type };
|
|
803
|
-
await this.emit(
|
|
804
|
-
|
|
971
|
+
await this.emit({
|
|
972
|
+
event: `${type}.deleted`,
|
|
973
|
+
object: `${type}/${id}`,
|
|
974
|
+
objectData: eventData,
|
|
975
|
+
});
|
|
976
|
+
await this.emit({
|
|
977
|
+
event: 'entity:deleted',
|
|
978
|
+
object: `${type}/${id}`,
|
|
979
|
+
objectData: eventData,
|
|
980
|
+
});
|
|
805
981
|
// Clean up relations
|
|
806
982
|
for (const [key, targets] of this.relations) {
|
|
807
983
|
if (key.startsWith(`${type}:${id}:`)) {
|
|
@@ -848,6 +1024,11 @@ export class MemoryProvider {
|
|
|
848
1024
|
return results;
|
|
849
1025
|
}
|
|
850
1026
|
async relate(fromType, fromId, relation, toType, toId, metadata) {
|
|
1027
|
+
validateTypeName(fromType);
|
|
1028
|
+
validateEntityId(fromId);
|
|
1029
|
+
validateRelationName(relation);
|
|
1030
|
+
validateTypeName(toType);
|
|
1031
|
+
validateEntityId(toId);
|
|
851
1032
|
const key = this.relationKey(fromType, fromId, relation);
|
|
852
1033
|
if (!this.relations.has(key)) {
|
|
853
1034
|
this.relations.set(key, new Set());
|
|
@@ -903,11 +1084,11 @@ export class MemoryProvider {
|
|
|
903
1084
|
id: generateId(),
|
|
904
1085
|
actor: 'system',
|
|
905
1086
|
event: eventOrType,
|
|
906
|
-
objectData: data,
|
|
907
1087
|
timestamp: new Date(),
|
|
908
1088
|
// Legacy fields
|
|
909
1089
|
type: eventOrType,
|
|
910
1090
|
data,
|
|
1091
|
+
...(data !== undefined && { objectData: data }),
|
|
911
1092
|
};
|
|
912
1093
|
}
|
|
913
1094
|
else {
|
|
@@ -915,16 +1096,17 @@ export class MemoryProvider {
|
|
|
915
1096
|
event = {
|
|
916
1097
|
id: generateId(),
|
|
917
1098
|
actor: eventOrType.actor ?? 'system',
|
|
918
|
-
actorData: eventOrType.actorData,
|
|
919
1099
|
event: eventOrType.event,
|
|
920
|
-
object: eventOrType.object,
|
|
921
|
-
objectData: eventOrType.objectData,
|
|
922
|
-
result: eventOrType.result,
|
|
923
|
-
resultData: eventOrType.resultData,
|
|
924
|
-
meta: eventOrType.meta,
|
|
925
1100
|
timestamp: new Date(),
|
|
926
|
-
// Legacy fields
|
|
1101
|
+
// Legacy fields for backward compatibility
|
|
927
1102
|
type: eventOrType.event,
|
|
1103
|
+
...(eventOrType.objectData !== undefined && { data: eventOrType.objectData }),
|
|
1104
|
+
...(eventOrType.actorData !== undefined && { actorData: eventOrType.actorData }),
|
|
1105
|
+
...(eventOrType.object !== undefined && { object: eventOrType.object }),
|
|
1106
|
+
...(eventOrType.objectData !== undefined && { objectData: eventOrType.objectData }),
|
|
1107
|
+
...(eventOrType.result !== undefined && { result: eventOrType.result }),
|
|
1108
|
+
...(eventOrType.resultData !== undefined && { resultData: eventOrType.resultData }),
|
|
1109
|
+
...(eventOrType.meta !== undefined && { meta: eventOrType.meta }),
|
|
928
1110
|
};
|
|
929
1111
|
}
|
|
930
1112
|
this.events.push(event);
|
|
@@ -947,9 +1129,9 @@ export class MemoryProvider {
|
|
|
947
1129
|
*/
|
|
948
1130
|
getEventHandlers(type) {
|
|
949
1131
|
const handlers = [];
|
|
950
|
-
for (const [pattern, patternHandlers] of this.eventHandlers) {
|
|
1132
|
+
for (const [pattern, patternHandlers] of [...this.eventHandlers]) {
|
|
951
1133
|
if (this.matchesPattern(type, pattern)) {
|
|
952
|
-
handlers.push(...patternHandlers);
|
|
1134
|
+
handlers.push(...[...patternHandlers]);
|
|
953
1135
|
}
|
|
954
1136
|
}
|
|
955
1137
|
return handlers;
|
|
@@ -985,6 +1167,7 @@ export class MemoryProvider {
|
|
|
985
1167
|
return false;
|
|
986
1168
|
}
|
|
987
1169
|
on(pattern, handler) {
|
|
1170
|
+
validateEventPattern(pattern);
|
|
988
1171
|
if (!this.eventHandlers.has(pattern)) {
|
|
989
1172
|
this.eventHandlers.set(pattern, []);
|
|
990
1173
|
}
|
|
@@ -1024,10 +1207,11 @@ export class MemoryProvider {
|
|
|
1024
1207
|
return results;
|
|
1025
1208
|
}
|
|
1026
1209
|
async replayEvents(options) {
|
|
1210
|
+
const eventPattern = options.event ?? options.type;
|
|
1027
1211
|
const events = await this.listEvents({
|
|
1028
|
-
event:
|
|
1029
|
-
actor: options.actor,
|
|
1030
|
-
since: options.since,
|
|
1212
|
+
...(eventPattern !== undefined && { event: eventPattern }),
|
|
1213
|
+
...(options.actor !== undefined && { actor: options.actor }),
|
|
1214
|
+
...(options.since !== undefined && { since: options.since }),
|
|
1031
1215
|
});
|
|
1032
1216
|
for (const event of events) {
|
|
1033
1217
|
await this.semaphore.run(() => Promise.resolve(options.handler(event)));
|
|
@@ -1061,33 +1245,39 @@ export class MemoryProvider {
|
|
|
1061
1245
|
async createAction(data) {
|
|
1062
1246
|
// Get base verb from action or legacy type
|
|
1063
1247
|
const baseVerb = data.action ?? data.type ?? 'process';
|
|
1248
|
+
// Validate action type
|
|
1249
|
+
validateActionType(baseVerb);
|
|
1064
1250
|
// Auto-conjugate verb forms
|
|
1065
1251
|
const conjugated = conjugateVerb(baseVerb);
|
|
1252
|
+
const objectData = data.objectData ?? data.data;
|
|
1066
1253
|
const action = {
|
|
1067
1254
|
id: generateId(),
|
|
1068
1255
|
actor: data.actor ?? 'system',
|
|
1069
|
-
actorData: data.actorData,
|
|
1070
1256
|
act: conjugated.act,
|
|
1071
1257
|
action: conjugated.action,
|
|
1072
1258
|
activity: conjugated.activity,
|
|
1073
|
-
object: data.object,
|
|
1074
|
-
objectData: data.objectData ?? data.data,
|
|
1075
1259
|
status: 'pending',
|
|
1076
1260
|
progress: 0,
|
|
1077
|
-
total: data.total,
|
|
1078
|
-
meta: data.meta,
|
|
1079
1261
|
createdAt: new Date(),
|
|
1080
1262
|
// Legacy fields
|
|
1081
1263
|
type: baseVerb,
|
|
1082
1264
|
data: data.data,
|
|
1265
|
+
...(data.actorData !== undefined && { actorData: data.actorData }),
|
|
1266
|
+
...(data.object !== undefined && { object: data.object }),
|
|
1267
|
+
...(objectData !== undefined && { objectData }),
|
|
1268
|
+
...(data.total !== undefined && { total: data.total }),
|
|
1269
|
+
...(data.meta !== undefined && { meta: data.meta }),
|
|
1083
1270
|
};
|
|
1084
1271
|
this.actions.set(action.id, action);
|
|
1085
1272
|
await this.emit({
|
|
1086
1273
|
actor: action.actor,
|
|
1087
|
-
actorData: action.actorData,
|
|
1088
1274
|
event: 'Action.created',
|
|
1089
1275
|
object: action.id,
|
|
1090
|
-
objectData: {
|
|
1276
|
+
objectData: {
|
|
1277
|
+
action: action.action,
|
|
1278
|
+
...(action.object !== undefined && { object: action.object }),
|
|
1279
|
+
},
|
|
1280
|
+
...(action.actorData !== undefined && { actorData: action.actorData }),
|
|
1091
1281
|
});
|
|
1092
1282
|
return action;
|
|
1093
1283
|
}
|
|
@@ -1097,7 +1287,7 @@ export class MemoryProvider {
|
|
|
1097
1287
|
async updateAction(id, updates) {
|
|
1098
1288
|
const action = this.actions.get(id);
|
|
1099
1289
|
if (!action) {
|
|
1100
|
-
throw new
|
|
1290
|
+
throw new EntityNotFoundError('Action', id, 'updateAction');
|
|
1101
1291
|
}
|
|
1102
1292
|
Object.assign(action, updates);
|
|
1103
1293
|
if (updates.status === 'active' && !action.startedAt) {
|
|
@@ -1116,8 +1306,8 @@ export class MemoryProvider {
|
|
|
1116
1306
|
event: 'Action.completed',
|
|
1117
1307
|
object: action.id,
|
|
1118
1308
|
objectData: { action: action.action },
|
|
1119
|
-
result: action.object,
|
|
1120
|
-
resultData: action.result,
|
|
1309
|
+
...(action.object !== undefined && { result: action.object }),
|
|
1310
|
+
...(action.result !== undefined && { resultData: action.result }),
|
|
1121
1311
|
});
|
|
1122
1312
|
}
|
|
1123
1313
|
if (updates.status === 'failed') {
|
|
@@ -1170,15 +1360,15 @@ export class MemoryProvider {
|
|
|
1170
1360
|
async retryAction(id) {
|
|
1171
1361
|
const action = this.actions.get(id);
|
|
1172
1362
|
if (!action) {
|
|
1173
|
-
throw new
|
|
1363
|
+
throw new EntityNotFoundError('Action', id, 'retryAction');
|
|
1174
1364
|
}
|
|
1175
1365
|
if (action.status !== 'failed') {
|
|
1176
1366
|
throw new Error(`Can only retry failed actions: ${id}`);
|
|
1177
1367
|
}
|
|
1178
1368
|
action.status = 'pending';
|
|
1179
|
-
action.error
|
|
1180
|
-
action.startedAt
|
|
1181
|
-
action.completedAt
|
|
1369
|
+
delete action.error;
|
|
1370
|
+
delete action.startedAt;
|
|
1371
|
+
delete action.completedAt;
|
|
1182
1372
|
await this.emit({
|
|
1183
1373
|
actor: action.actor,
|
|
1184
1374
|
event: 'Action.retried',
|
|
@@ -1190,9 +1380,11 @@ export class MemoryProvider {
|
|
|
1190
1380
|
async cancelAction(id) {
|
|
1191
1381
|
const action = this.actions.get(id);
|
|
1192
1382
|
if (!action) {
|
|
1193
|
-
throw new
|
|
1383
|
+
throw new EntityNotFoundError('Action', id, 'cancelAction');
|
|
1194
1384
|
}
|
|
1195
|
-
if (action.status === 'completed' ||
|
|
1385
|
+
if (action.status === 'completed' ||
|
|
1386
|
+
action.status === 'failed' ||
|
|
1387
|
+
action.status === 'cancelled') {
|
|
1196
1388
|
throw new Error(`Cannot cancel finished action: ${id}`);
|
|
1197
1389
|
}
|
|
1198
1390
|
action.status = 'cancelled';
|
|
@@ -1226,13 +1418,14 @@ export class MemoryProvider {
|
|
|
1226
1418
|
return this.artifacts.get(this.artifactKey(url, type)) ?? null;
|
|
1227
1419
|
}
|
|
1228
1420
|
async setArtifact(url, type, data) {
|
|
1421
|
+
validateArtifactUrl(url);
|
|
1229
1422
|
const artifact = {
|
|
1230
1423
|
url,
|
|
1231
1424
|
type,
|
|
1232
1425
|
sourceHash: data.sourceHash,
|
|
1233
1426
|
content: data.content,
|
|
1234
|
-
metadata: data.metadata,
|
|
1235
1427
|
createdAt: new Date(),
|
|
1428
|
+
...(data.metadata !== undefined && { metadata: data.metadata }),
|
|
1236
1429
|
};
|
|
1237
1430
|
this.artifacts.set(this.artifactKey(url, type), artifact);
|
|
1238
1431
|
}
|
|
@@ -1292,6 +1485,19 @@ export class MemoryProvider {
|
|
|
1292
1485
|
async mapWithConcurrency(items, fn) {
|
|
1293
1486
|
return this.semaphore.map(items, fn);
|
|
1294
1487
|
}
|
|
1488
|
+
// ===========================================================================
|
|
1489
|
+
// Transactions
|
|
1490
|
+
// ===========================================================================
|
|
1491
|
+
/**
|
|
1492
|
+
* Begin a new transaction.
|
|
1493
|
+
*
|
|
1494
|
+
* All writes (create, update, delete, relate) are buffered in memory.
|
|
1495
|
+
* On commit(), they are applied to the provider atomically.
|
|
1496
|
+
* On rollback(), all buffered writes are discarded.
|
|
1497
|
+
*/
|
|
1498
|
+
async beginTransaction() {
|
|
1499
|
+
return new MemoryTransaction(this);
|
|
1500
|
+
}
|
|
1295
1501
|
/**
|
|
1296
1502
|
* Clear all data (useful for testing)
|
|
1297
1503
|
*/
|
|
@@ -1332,6 +1538,135 @@ export class MemoryProvider {
|
|
|
1332
1538
|
};
|
|
1333
1539
|
}
|
|
1334
1540
|
}
|
|
1541
|
+
/**
|
|
1542
|
+
* In-memory transaction that buffers writes and applies them on commit.
|
|
1543
|
+
*
|
|
1544
|
+
* - get() checks the write buffer first, then falls through to the provider.
|
|
1545
|
+
* - create/update/delete/relate are buffered.
|
|
1546
|
+
* - commit() replays all buffered operations against the real provider.
|
|
1547
|
+
* - rollback() discards the buffer.
|
|
1548
|
+
*/
|
|
1549
|
+
export class MemoryTransaction {
|
|
1550
|
+
provider;
|
|
1551
|
+
ops = [];
|
|
1552
|
+
committed = false;
|
|
1553
|
+
rolledBack = false;
|
|
1554
|
+
/** Buffered creates/updates: type -> id -> data */
|
|
1555
|
+
buffer = new Map();
|
|
1556
|
+
/** Buffered deletes: type -> Set<id> */
|
|
1557
|
+
deletions = new Map();
|
|
1558
|
+
/** Counter for generating temporary IDs */
|
|
1559
|
+
tempIdCounter = 0;
|
|
1560
|
+
constructor(provider) {
|
|
1561
|
+
this.provider = provider;
|
|
1562
|
+
}
|
|
1563
|
+
assertActive() {
|
|
1564
|
+
if (this.committed)
|
|
1565
|
+
throw new Error('Transaction already committed');
|
|
1566
|
+
if (this.rolledBack)
|
|
1567
|
+
throw new Error('Transaction already rolled back');
|
|
1568
|
+
}
|
|
1569
|
+
getBuffer(type) {
|
|
1570
|
+
if (!this.buffer.has(type)) {
|
|
1571
|
+
this.buffer.set(type, new Map());
|
|
1572
|
+
}
|
|
1573
|
+
return this.buffer.get(type);
|
|
1574
|
+
}
|
|
1575
|
+
async get(type, id) {
|
|
1576
|
+
this.assertActive();
|
|
1577
|
+
// Check if deleted in this transaction
|
|
1578
|
+
if (this.deletions.get(type)?.has(id))
|
|
1579
|
+
return null;
|
|
1580
|
+
// Check buffer first
|
|
1581
|
+
const buf = this.buffer.get(type);
|
|
1582
|
+
if (buf?.has(id)) {
|
|
1583
|
+
return { ...buf.get(id), $id: id, $type: type };
|
|
1584
|
+
}
|
|
1585
|
+
// Fall through to provider
|
|
1586
|
+
return this.provider.get(type, id);
|
|
1587
|
+
}
|
|
1588
|
+
async create(type, id, data) {
|
|
1589
|
+
this.assertActive();
|
|
1590
|
+
const entityId = id || `txn-temp-${++this.tempIdCounter}`;
|
|
1591
|
+
const entity = {
|
|
1592
|
+
...data,
|
|
1593
|
+
createdAt: new Date().toISOString(),
|
|
1594
|
+
updatedAt: new Date().toISOString(),
|
|
1595
|
+
};
|
|
1596
|
+
this.getBuffer(type).set(entityId, entity);
|
|
1597
|
+
const result = { ...entity, $id: entityId, $type: type };
|
|
1598
|
+
this.ops.push({ kind: 'create', type, id: entityId, data, result });
|
|
1599
|
+
return result;
|
|
1600
|
+
}
|
|
1601
|
+
async update(type, id, data) {
|
|
1602
|
+
this.assertActive();
|
|
1603
|
+
// Get current state (from buffer or provider)
|
|
1604
|
+
const existing = await this.get(type, id);
|
|
1605
|
+
if (!existing)
|
|
1606
|
+
throw new Error(`update ${type}/${id}: Entity not found`);
|
|
1607
|
+
const { $id: _id, $type: _type, ...rest } = existing;
|
|
1608
|
+
const updated = { ...rest, ...data, updatedAt: new Date().toISOString() };
|
|
1609
|
+
this.getBuffer(type).set(id, updated);
|
|
1610
|
+
const result = { ...updated, $id: id, $type: type };
|
|
1611
|
+
this.ops.push({ kind: 'update', type, id, data, result });
|
|
1612
|
+
return result;
|
|
1613
|
+
}
|
|
1614
|
+
async delete(type, id) {
|
|
1615
|
+
this.assertActive();
|
|
1616
|
+
// Check existence
|
|
1617
|
+
const existing = await this.get(type, id);
|
|
1618
|
+
if (!existing)
|
|
1619
|
+
return false;
|
|
1620
|
+
// Remove from buffer if present
|
|
1621
|
+
this.buffer.get(type)?.delete(id);
|
|
1622
|
+
// Mark as deleted
|
|
1623
|
+
if (!this.deletions.has(type))
|
|
1624
|
+
this.deletions.set(type, new Set());
|
|
1625
|
+
this.deletions.get(type).add(id);
|
|
1626
|
+
this.ops.push({ kind: 'delete', type, id });
|
|
1627
|
+
return true;
|
|
1628
|
+
}
|
|
1629
|
+
async relate(fromType, fromId, relation, toType, toId, metadata) {
|
|
1630
|
+
this.assertActive();
|
|
1631
|
+
this.ops.push({
|
|
1632
|
+
kind: 'relate',
|
|
1633
|
+
fromType,
|
|
1634
|
+
fromId,
|
|
1635
|
+
relation,
|
|
1636
|
+
toType,
|
|
1637
|
+
toId,
|
|
1638
|
+
...(metadata != null ? { metadata } : {}),
|
|
1639
|
+
});
|
|
1640
|
+
}
|
|
1641
|
+
async commit() {
|
|
1642
|
+
this.assertActive();
|
|
1643
|
+
this.committed = true;
|
|
1644
|
+
// Replay all operations against the real provider
|
|
1645
|
+
for (const op of this.ops) {
|
|
1646
|
+
switch (op.kind) {
|
|
1647
|
+
case 'create':
|
|
1648
|
+
await this.provider.create(op.type, op.id, op.data);
|
|
1649
|
+
break;
|
|
1650
|
+
case 'update':
|
|
1651
|
+
await this.provider.update(op.type, op.id, op.data);
|
|
1652
|
+
break;
|
|
1653
|
+
case 'delete':
|
|
1654
|
+
await this.provider.delete(op.type, op.id);
|
|
1655
|
+
break;
|
|
1656
|
+
case 'relate':
|
|
1657
|
+
await this.provider.relate(op.fromType, op.fromId, op.relation, op.toType, op.toId, op.metadata);
|
|
1658
|
+
break;
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
async rollback() {
|
|
1663
|
+
this.assertActive();
|
|
1664
|
+
this.rolledBack = true;
|
|
1665
|
+
this.ops = [];
|
|
1666
|
+
this.buffer.clear();
|
|
1667
|
+
this.deletions.clear();
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1335
1670
|
/**
|
|
1336
1671
|
* Create an in-memory provider
|
|
1337
1672
|
*/
|