@thingd/sdk 0.72.0 → 0.74.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/__type-tests__/exports.d.ts +178 -0
  2. package/dist/__type-tests__/exports.d.ts.map +1 -0
  3. package/dist/__type-tests__/exports.js +87 -0
  4. package/dist/client/http-thing-store.d.ts +55 -0
  5. package/dist/client/http-thing-store.d.ts.map +1 -0
  6. package/dist/client/http-thing-store.js +233 -0
  7. package/dist/client/in-memory-thing-store.d.ts +41 -0
  8. package/dist/client/in-memory-thing-store.d.ts.map +1 -0
  9. package/dist/client/in-memory-thing-store.js +292 -0
  10. package/dist/client/index.d.ts +5 -0
  11. package/dist/client/index.d.ts.map +1 -0
  12. package/dist/client/index.js +3 -0
  13. package/dist/client/thingd.d.ts +24 -0
  14. package/dist/client/thingd.d.ts.map +1 -0
  15. package/dist/client/thingd.js +27 -0
  16. package/dist/constants.d.ts +4 -0
  17. package/dist/constants.d.ts.map +1 -0
  18. package/dist/constants.js +3 -0
  19. package/dist/index.d.ts +13 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +11 -0
  22. package/dist/mcp/audit.d.ts +27 -0
  23. package/dist/mcp/audit.d.ts.map +1 -0
  24. package/dist/mcp/audit.js +36 -0
  25. package/dist/mcp/config.d.ts +22 -0
  26. package/dist/mcp/config.d.ts.map +1 -0
  27. package/dist/mcp/config.js +52 -0
  28. package/dist/mcp/index.d.ts +6 -0
  29. package/dist/mcp/index.d.ts.map +1 -0
  30. package/dist/mcp/index.js +5 -0
  31. package/dist/mcp/result.d.ts +3 -0
  32. package/dist/mcp/result.d.ts.map +1 -0
  33. package/dist/mcp/result.js +10 -0
  34. package/dist/mcp/server.d.ts +19 -0
  35. package/dist/mcp/server.d.ts.map +1 -0
  36. package/dist/mcp/server.js +51 -0
  37. package/dist/mcp/tools.d.ts +10 -0
  38. package/dist/mcp/tools.d.ts.map +1 -0
  39. package/dist/mcp/tools.js +1033 -0
  40. package/dist/memory/index.d.ts +7 -0
  41. package/dist/memory/index.d.ts.map +1 -0
  42. package/dist/memory/index.js +7 -0
  43. package/dist/rest/helpers.d.ts +17 -0
  44. package/dist/rest/helpers.d.ts.map +1 -0
  45. package/dist/rest/helpers.js +60 -0
  46. package/dist/rest/index.d.ts +3 -0
  47. package/dist/rest/index.d.ts.map +1 -0
  48. package/dist/rest/index.js +2 -0
  49. package/dist/rest/server.d.ts +4 -0
  50. package/dist/rest/server.d.ts.map +1 -0
  51. package/dist/rest/server.js +472 -0
  52. package/dist/scheduler.d.ts +28 -0
  53. package/dist/scheduler.d.ts.map +1 -0
  54. package/dist/scheduler.js +451 -0
  55. package/dist/stores/cloud-thing-store.d.ts +60 -0
  56. package/dist/stores/cloud-thing-store.d.ts.map +1 -0
  57. package/dist/stores/cloud-thing-store.js +396 -0
  58. package/dist/stores/in-memory-thing-store.d.ts +59 -0
  59. package/dist/stores/in-memory-thing-store.d.ts.map +1 -0
  60. package/dist/stores/in-memory-thing-store.js +709 -0
  61. package/dist/stores/native-thing-store.d.ts +62 -0
  62. package/dist/stores/native-thing-store.d.ts.map +1 -0
  63. package/dist/stores/native-thing-store.js +374 -0
  64. package/dist/thingd.d.ts +84 -0
  65. package/dist/thingd.d.ts.map +1 -0
  66. package/dist/thingd.js +236 -0
  67. package/dist/types/index.d.ts +2 -0
  68. package/dist/types/index.d.ts.map +1 -0
  69. package/dist/types/index.js +1 -0
  70. package/dist/types.d.ts +448 -0
  71. package/dist/types.d.ts.map +1 -0
  72. package/dist/types.js +1 -0
  73. package/dist/version.d.ts +6 -0
  74. package/dist/version.d.ts.map +1 -0
  75. package/dist/version.js +5 -0
  76. package/package.json +8 -7
  77. package/LICENSE +0 -201
@@ -0,0 +1,709 @@
1
+ import { randomUUID } from "node:crypto";
2
+ const DEFAULT_LEASE_MS = 30_000;
3
+ class Mutex {
4
+ queue = [];
5
+ locked = false;
6
+ async acquire() {
7
+ if (!this.locked) {
8
+ this.locked = true;
9
+ return () => this.release();
10
+ }
11
+ return new Promise((resolve) => {
12
+ this.queue.push(() => {
13
+ this.locked = true;
14
+ resolve(() => this.release());
15
+ });
16
+ });
17
+ }
18
+ release() {
19
+ const next = this.queue.shift();
20
+ if (next) {
21
+ next();
22
+ }
23
+ else {
24
+ this.locked = false;
25
+ }
26
+ }
27
+ }
28
+ export class InMemoryThingStore {
29
+ collections = new Map();
30
+ events = [];
31
+ nextEventSequence = 0;
32
+ queues = new Map();
33
+ links = new Map();
34
+ mutex = new Mutex();
35
+ eventIdempotencyKeys = new Map();
36
+ async withLock(fn) {
37
+ const release = await this.mutex.acquire();
38
+ try {
39
+ return fn();
40
+ }
41
+ finally {
42
+ release();
43
+ }
44
+ }
45
+ async put(collection, object, options) {
46
+ return this.withLock(() => {
47
+ const records = this.getCollection(collection);
48
+ const now = new Date().toISOString();
49
+ const existing = records.get(object.id);
50
+ // CAS check: if expectedVersion is set, verify it matches
51
+ if (options?.expectedVersion !== undefined) {
52
+ const currentVersion = existing?.version ?? 0;
53
+ if (currentVersion !== options.expectedVersion) {
54
+ throw new Error(`Conflict: version mismatch for ${collection}/${object.id}: expected ${options.expectedVersion}, got ${currentVersion}`);
55
+ }
56
+ }
57
+ const record = {
58
+ ...object,
59
+ id: object.id,
60
+ collection,
61
+ createdAt: existing?.createdAt ?? now,
62
+ updatedAt: now,
63
+ version: (existing?.version ?? 0) + 1,
64
+ };
65
+ records.set(object.id, record);
66
+ return record;
67
+ });
68
+ }
69
+ async get(collection, id) {
70
+ return this.collections.get(collection)?.get(id) ?? null;
71
+ }
72
+ async getBatch(collection, ids) {
73
+ const records = this.collections.get(collection);
74
+ return ids.map((id) => records?.get(id) ?? null);
75
+ }
76
+ async delete(collection, id) {
77
+ return this.withLock(() => ({
78
+ deleted: this.collections.get(collection)?.delete(id) ?? false,
79
+ }));
80
+ }
81
+ async listObjects(collection, options) {
82
+ const records = this.collections.get(collection);
83
+ if (!records) {
84
+ return [];
85
+ }
86
+ let results = Array.from(records.values());
87
+ const filter = options?.filter;
88
+ if (filter) {
89
+ results = results.filter((obj) => Object.entries(filter).every(([key, value]) => obj[key] === value));
90
+ }
91
+ if (options?.sortBy) {
92
+ const { field, direction } = options.sortBy;
93
+ const asc = direction !== "desc";
94
+ results.sort((a, b) => {
95
+ const va = a[field];
96
+ const vb = b[field];
97
+ if (va === vb) {
98
+ return 0;
99
+ }
100
+ if (va === undefined) {
101
+ return 1;
102
+ }
103
+ if (vb === undefined) {
104
+ return -1;
105
+ }
106
+ const cmp = va < vb ? -1 : 1;
107
+ return asc ? cmp : -cmp;
108
+ });
109
+ }
110
+ if (options?.offset) {
111
+ results = results.slice(options.offset);
112
+ }
113
+ if (options?.limit) {
114
+ results = results.slice(0, options.limit);
115
+ }
116
+ return results;
117
+ }
118
+ async appendEvent(stream, event) {
119
+ return this.withLock(() => {
120
+ // Idempotency check
121
+ const idempotencyKey = event.idempotencyKey;
122
+ if (idempotencyKey) {
123
+ const existing = this.eventIdempotencyKeys.get(`${stream}:${idempotencyKey}`);
124
+ if (existing !== undefined) {
125
+ const found = this.events.find((e) => e.sequence === existing);
126
+ if (found) {
127
+ return found;
128
+ }
129
+ }
130
+ }
131
+ this.nextEventSequence += 1;
132
+ const record = {
133
+ ...event,
134
+ id: randomUUID(),
135
+ stream,
136
+ sequence: this.nextEventSequence,
137
+ createdAt: new Date().toISOString(),
138
+ };
139
+ // Track idempotency key
140
+ if (idempotencyKey) {
141
+ this.eventIdempotencyKeys.set(`${stream}:${idempotencyKey}`, record.sequence);
142
+ }
143
+ this.events.push(record);
144
+ return record;
145
+ });
146
+ }
147
+ async listEvents(stream, options) {
148
+ let events = this.events;
149
+ if (stream) {
150
+ events = events.filter((event) => event.stream === stream);
151
+ }
152
+ const fromSeq = options?.fromSequence;
153
+ if (fromSeq) {
154
+ events = events.filter((event) => event.sequence > fromSeq);
155
+ }
156
+ if (options?.since) {
157
+ const since = options.since;
158
+ events = events.filter((event) => event.createdAt >= since);
159
+ }
160
+ if (options?.limit) {
161
+ events = events.slice(0, options.limit);
162
+ }
163
+ return [...events];
164
+ }
165
+ async pushJob(queue, payload, options = {}) {
166
+ return this.withLock(() => {
167
+ const jobs = this.getQueue(queue);
168
+ const now = new Date().toISOString();
169
+ const job = {
170
+ id: options.idempotencyKey ?? randomUUID(),
171
+ queue,
172
+ payload,
173
+ status: "ready",
174
+ attempts: 0,
175
+ maxAttempts: options.maxAttempts ?? 3,
176
+ createdAt: now,
177
+ availableAt: new Date(Date.now() + (options.delayMs ?? 0)).toISOString(),
178
+ priority: options.priority ?? 0,
179
+ };
180
+ const existing = jobs.find((candidate) => candidate.id === job.id);
181
+ if (existing) {
182
+ return this.cloneJob(existing);
183
+ }
184
+ jobs.push(job);
185
+ return this.cloneJob(job);
186
+ });
187
+ }
188
+ async claimJob(queue, options = {}) {
189
+ return this.withLock(() => {
190
+ this.releaseExpiredLeases(queue);
191
+ const now = new Date();
192
+ const candidates = this.queues
193
+ .get(queue)
194
+ ?.filter((candidate) => candidate.status === "ready" && candidate.availableAt <= now.toISOString())
195
+ ?.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
196
+ const job = candidates?.[0] ?? null;
197
+ if (!job) {
198
+ return null;
199
+ }
200
+ job.status = "leased";
201
+ job.attempts += 1;
202
+ job.leasedAt = now.toISOString();
203
+ job.leaseExpiresAt = new Date(now.getTime() + (options.leaseMs ?? DEFAULT_LEASE_MS)).toISOString();
204
+ return this.cloneJob(job);
205
+ });
206
+ }
207
+ async ackJob(queue, jobId) {
208
+ return this.withLock(() => {
209
+ const job = this.findJob(queue, jobId);
210
+ if (!job) {
211
+ return { ok: false, reason: "not_found" };
212
+ }
213
+ if (job.status === "completed" || job.status === "dead") {
214
+ return { ok: false, reason: "terminal" };
215
+ }
216
+ if (job.status !== "leased") {
217
+ return { ok: false, reason: "not_leased" };
218
+ }
219
+ job.status = "completed";
220
+ job.completedAt = new Date().toISOString();
221
+ return { ok: true, job: this.cloneJob(job) };
222
+ });
223
+ }
224
+ async nackJob(queue, jobId, options = {}) {
225
+ return this.withLock(() => {
226
+ const job = this.findJob(queue, jobId);
227
+ if (!job) {
228
+ return { ok: false, reason: "not_found" };
229
+ }
230
+ if (job.status === "completed" || job.status === "dead") {
231
+ return { ok: false, reason: "terminal" };
232
+ }
233
+ if (job.status !== "leased") {
234
+ return { ok: false, reason: "not_leased" };
235
+ }
236
+ job.lastError = options.error;
237
+ job.leasedAt = undefined;
238
+ job.leaseExpiresAt = undefined;
239
+ if (job.attempts >= job.maxAttempts) {
240
+ job.status = "dead";
241
+ job.deadAt = new Date().toISOString();
242
+ }
243
+ else {
244
+ job.status = "ready";
245
+ job.availableAt = new Date(Date.now() + (options.delayMs ?? 0)).toISOString();
246
+ }
247
+ return { ok: true, job: this.cloneJob(job) };
248
+ });
249
+ }
250
+ async listJobs(queue) {
251
+ return (this.queues.get(queue) ?? []).map((job) => this.cloneJob(job));
252
+ }
253
+ async listDeadJobs(queue) {
254
+ return (this.queues.get(queue) ?? [])
255
+ .filter((job) => job.status === "dead")
256
+ .map((job) => this.cloneJob(job));
257
+ }
258
+ async search(query, options = {}) {
259
+ const normalizedQuery = query.toLowerCase();
260
+ const collections = options.collections ? new Set(options.collections) : null;
261
+ const filter = options.filter;
262
+ const results = [];
263
+ for (const [collection, records] of this.collections) {
264
+ if (collections && !collections.has(collection)) {
265
+ continue;
266
+ }
267
+ for (const record of records.values()) {
268
+ if (filter && !this.matchesFilter(filter, record)) {
269
+ continue;
270
+ }
271
+ const haystack = JSON.stringify(record).toLowerCase();
272
+ if (haystack.includes(normalizedQuery)) {
273
+ results.push({
274
+ kind: "object",
275
+ id: record.id,
276
+ collection,
277
+ score: 1,
278
+ value: record,
279
+ });
280
+ }
281
+ }
282
+ }
283
+ for (const event of this.events) {
284
+ if (filter && !this.matchesFilter(filter, event)) {
285
+ continue;
286
+ }
287
+ const haystack = JSON.stringify(event).toLowerCase();
288
+ if (haystack.includes(normalizedQuery)) {
289
+ results.push({
290
+ kind: "event",
291
+ id: event.id,
292
+ stream: event.stream,
293
+ score: 1,
294
+ value: event,
295
+ });
296
+ }
297
+ }
298
+ return options.limit !== undefined ? results.slice(0, options.limit) : results;
299
+ }
300
+ async vectorSearch(collection, queryVector, options = {}) {
301
+ const records = this.collections.get(collection);
302
+ if (!records) {
303
+ return [];
304
+ }
305
+ const filter = options.filter;
306
+ const hits = [];
307
+ for (const record of records.values()) {
308
+ if (filter && !this.matchesFilter(filter, record)) {
309
+ continue;
310
+ }
311
+ const recordVector = record.vector;
312
+ if (!Array.isArray(recordVector)) {
313
+ continue;
314
+ }
315
+ const vec = recordVector;
316
+ if (vec.length !== queryVector.length) {
317
+ continue;
318
+ }
319
+ const score = cosineSimilarity(queryVector, vec);
320
+ hits.push({ id: record.id, score, value: record });
321
+ }
322
+ hits.sort((a, b) => b.score - a.score);
323
+ if (options.topK !== undefined) {
324
+ return hits.slice(0, options.topK);
325
+ }
326
+ return hits;
327
+ }
328
+ matchesFilter(filter, obj) {
329
+ return Object.entries(filter).every(([key, expected]) => {
330
+ return key in obj && obj[key] === expected;
331
+ });
332
+ }
333
+ async countObjects() {
334
+ let total = 0;
335
+ for (const records of this.collections.values()) {
336
+ total += records.size;
337
+ }
338
+ return total;
339
+ }
340
+ async countObjectsInCollection(collection) {
341
+ return this.collections.get(collection)?.size ?? 0;
342
+ }
343
+ async countEvents() {
344
+ return this.events.length;
345
+ }
346
+ async countActiveJobs() {
347
+ let total = 0;
348
+ for (const jobs of this.queues.values()) {
349
+ total += jobs.filter((job) => job.status !== "dead").length;
350
+ }
351
+ return total;
352
+ }
353
+ async countDeadJobs() {
354
+ let total = 0;
355
+ for (const jobs of this.queues.values()) {
356
+ total += jobs.filter((job) => job.status === "dead").length;
357
+ }
358
+ return total;
359
+ }
360
+ async countLinks() {
361
+ return this.links.size;
362
+ }
363
+ async putBatch(collection, objects) {
364
+ return this.withLock(() => {
365
+ const records = this.getCollection(collection);
366
+ const now = new Date().toISOString();
367
+ const results = [];
368
+ for (const object of objects) {
369
+ const existing = records.get(object.id);
370
+ const record = {
371
+ ...object,
372
+ id: object.id,
373
+ collection,
374
+ createdAt: existing?.createdAt ?? now,
375
+ updatedAt: now,
376
+ version: (existing?.version ?? 0) + 1,
377
+ };
378
+ records.set(object.id, record);
379
+ results.push(record);
380
+ }
381
+ return results;
382
+ });
383
+ }
384
+ async deleteBatch(collection, ids) {
385
+ return this.withLock(() => {
386
+ const records = this.collections.get(collection);
387
+ if (!records) {
388
+ return 0;
389
+ }
390
+ let count = 0;
391
+ for (const id of ids) {
392
+ if (records.delete(id)) {
393
+ count++;
394
+ }
395
+ }
396
+ return count;
397
+ });
398
+ }
399
+ async createLink(fromRef, linkType, toRef, weight, metadataJson) {
400
+ return this.withLock(() => {
401
+ const link = {
402
+ id: randomUUID(),
403
+ fromRef,
404
+ linkType,
405
+ toRef,
406
+ weight,
407
+ metadataJson: metadataJson ?? "{}",
408
+ createdAt: new Date().toISOString(),
409
+ };
410
+ this.links.set(link.id, link);
411
+ return link;
412
+ });
413
+ }
414
+ async deleteLink(id) {
415
+ return this.withLock(() => {
416
+ return this.links.delete(id);
417
+ });
418
+ }
419
+ async getLink(id) {
420
+ return this.links.get(id) ?? null;
421
+ }
422
+ async getNeighbors(reference, direction, options) {
423
+ let results = Array.from(this.links.values());
424
+ results = results.filter((link) => {
425
+ if (direction === "Outgoing") {
426
+ return link.fromRef === reference;
427
+ }
428
+ if (direction === "Incoming") {
429
+ return link.toRef === reference;
430
+ }
431
+ return link.fromRef === reference || link.toRef === reference;
432
+ });
433
+ if (options.linkType) {
434
+ results = results.filter((link) => link.linkType === options.linkType);
435
+ }
436
+ if (options.limit !== undefined) {
437
+ results = results.slice(0, options.limit);
438
+ }
439
+ return results;
440
+ }
441
+ async listCollections() {
442
+ return Array.from(this.collections.keys()).sort();
443
+ }
444
+ async listQueues() {
445
+ return Array.from(this.queues.keys()).sort();
446
+ }
447
+ async createIndex(_collection, _field) {
448
+ // No-op for in-memory store
449
+ }
450
+ async listIndexes() {
451
+ return [];
452
+ }
453
+ async listStreams() {
454
+ const streams = new Set();
455
+ for (const event of this.events) {
456
+ streams.add(event.stream);
457
+ }
458
+ return Array.from(streams).sort();
459
+ }
460
+ async aggregate(collection, options) {
461
+ const records = Array.from(this.collections.get(collection)?.values() ?? []);
462
+ // Apply filter
463
+ const filtered = options.filter
464
+ ? records.filter((obj) => Object.entries(options.filter).every(([key, value]) => obj[key] === value))
465
+ : records;
466
+ if (options.groupBy) {
467
+ const groups = new Map();
468
+ for (const obj of filtered) {
469
+ const key = String(obj[options.groupBy] ?? "");
470
+ const group = groups.get(key) ?? [];
471
+ group.push(obj);
472
+ groups.set(key, group);
473
+ }
474
+ const groupResults = Array.from(groups.entries())
475
+ .map(([key, objs]) => ({
476
+ key,
477
+ value: this.computeAggregate(objs, options.function, options.field),
478
+ }))
479
+ .sort((a, b) => a.key.localeCompare(b.key));
480
+ const total = groupResults.reduce((sum, g) => sum + g.value, 0);
481
+ return { total, groups: groupResults };
482
+ }
483
+ const total = this.computeAggregate(filtered, options.function, options.field);
484
+ return { total, groups: [] };
485
+ }
486
+ async timeseries(collection, options) {
487
+ const records = Array.from(this.collections.get(collection)?.values() ?? []);
488
+ // Apply filter
489
+ let filtered = options.filter
490
+ ? records.filter((obj) => Object.entries(options.filter).every(([key, value]) => obj[key] === value))
491
+ : records;
492
+ // Apply time range
493
+ if (options.from) {
494
+ filtered = filtered.filter((obj) => obj.createdAt >= options.from);
495
+ }
496
+ if (options.to) {
497
+ filtered = filtered.filter((obj) => obj.createdAt < options.to);
498
+ }
499
+ // Bucket by createdAt
500
+ const format = this.getTimeBucketFormat(options.bucket);
501
+ const buckets = new Map();
502
+ for (const obj of filtered) {
503
+ const label = this.formatTimestamp(obj.createdAt, format);
504
+ const group = buckets.get(label) ?? [];
505
+ group.push(obj);
506
+ buckets.set(label, group);
507
+ }
508
+ const resultBuckets = Array.from(buckets.entries())
509
+ .map(([label, objs]) => ({
510
+ label,
511
+ value: this.computeAggregate(objs, options.function, options.field),
512
+ }))
513
+ .sort((a, b) => a.label.localeCompare(b.label));
514
+ return { buckets: resultBuckets };
515
+ }
516
+ async schema(collection, options) {
517
+ const sampleSize = options?.sampleSize ?? 50;
518
+ const collections = collection ? [collection] : Array.from(this.collections.keys()).sort();
519
+ const result = [];
520
+ for (const col of collections) {
521
+ const objects = Array.from(this.collections.get(col)?.values() ?? []);
522
+ if (objects.length === 0) {
523
+ continue;
524
+ }
525
+ const sampled = objects.slice(0, sampleSize);
526
+ const fieldMap = new Map();
527
+ for (const obj of sampled) {
528
+ const body = typeof obj.body === "string" ? JSON.parse(obj.body) : obj.body;
529
+ if (!body || typeof body !== "object") {
530
+ continue;
531
+ }
532
+ for (const [key, value] of Object.entries(body)) {
533
+ let entry = fieldMap.get(key);
534
+ if (!entry) {
535
+ entry = { type: this.inferType(value), nullable: false, samples: [] };
536
+ fieldMap.set(key, entry);
537
+ }
538
+ if (value === null || value === undefined) {
539
+ entry.nullable = true;
540
+ }
541
+ else {
542
+ const inferred = this.inferType(value);
543
+ if (entry.type !== inferred) {
544
+ entry.type = "unknown";
545
+ }
546
+ if (entry.samples.length < 3) {
547
+ entry.samples.push(value);
548
+ }
549
+ }
550
+ }
551
+ }
552
+ result.push({
553
+ name: col,
554
+ objectCount: objects.length,
555
+ fields: Array.from(fieldMap.entries()).map(([name, { type, nullable, samples }]) => ({
556
+ name,
557
+ type,
558
+ nullable,
559
+ sampleValues: samples,
560
+ })),
561
+ });
562
+ }
563
+ return result;
564
+ }
565
+ inferType(value) {
566
+ if (value === null || value === undefined) {
567
+ return "null";
568
+ }
569
+ if (typeof value === "boolean") {
570
+ return "boolean";
571
+ }
572
+ if (typeof value === "number") {
573
+ return "number";
574
+ }
575
+ if (typeof value === "string") {
576
+ if (value.length > 10 &&
577
+ (value.includes("T") || value.includes("-")) &&
578
+ !Number.isNaN(Date.parse(value))) {
579
+ return "date";
580
+ }
581
+ return "string";
582
+ }
583
+ if (Array.isArray(value)) {
584
+ return "array";
585
+ }
586
+ if (typeof value === "object") {
587
+ return "object";
588
+ }
589
+ return "unknown";
590
+ }
591
+ computeAggregate(objects, function_, field) {
592
+ switch (function_) {
593
+ case "count":
594
+ return objects.length;
595
+ case "sum":
596
+ return objects.reduce((sum, obj) => sum + (Number(obj[field ?? ""]) || 0), 0);
597
+ case "avg": {
598
+ const values = objects
599
+ .map((obj) => Number(obj[field ?? ""]) || 0)
600
+ .filter((v) => !Number.isNaN(v));
601
+ return values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
602
+ }
603
+ case "min": {
604
+ const values = objects
605
+ .map((obj) => Number(obj[field ?? ""]) || 0)
606
+ .filter((v) => !Number.isNaN(v));
607
+ return values.length > 0 ? Math.min(...values) : 0;
608
+ }
609
+ case "max": {
610
+ const values = objects
611
+ .map((obj) => Number(obj[field ?? ""]) || 0)
612
+ .filter((v) => !Number.isNaN(v));
613
+ return values.length > 0 ? Math.max(...values) : 0;
614
+ }
615
+ default:
616
+ return 0;
617
+ }
618
+ }
619
+ getTimeBucketFormat(bucket) {
620
+ switch (bucket) {
621
+ case "hour":
622
+ return "YYYY-MM-DDTHH:00:00Z";
623
+ case "day":
624
+ return "YYYY-MM-DD";
625
+ case "week":
626
+ return "YYYY-[W]WW";
627
+ case "month":
628
+ return "YYYY-MM";
629
+ default:
630
+ return "YYYY-MM-DD";
631
+ }
632
+ }
633
+ formatTimestamp(ts, format) {
634
+ const date = new Date(ts);
635
+ if (Number.isNaN(date.getTime())) {
636
+ return ts;
637
+ }
638
+ const pad = (n) => String(n).padStart(2, "0");
639
+ const year = date.getUTCFullYear();
640
+ const month = pad(date.getUTCMonth() + 1);
641
+ const day = pad(date.getUTCDate());
642
+ const hours = pad(date.getUTCHours());
643
+ const weekNum = Math.ceil((date.getUTCDate() - date.getUTCDay() + 1) / 7);
644
+ return format
645
+ .replace("YYYY", String(year))
646
+ .replace("MM", month)
647
+ .replace("DD", day)
648
+ .replace("HH", hours)
649
+ .replace("[W]WW", `W${String(weekNum).padStart(2, "0")}`);
650
+ }
651
+ async close() {
652
+ // no-op for in-memory
653
+ }
654
+ walCheckpoint() {
655
+ throw new Error("WAL checkpoint is not supported for in-memory storage");
656
+ }
657
+ getCollection(collection) {
658
+ const records = this.collections.get(collection) ?? new Map();
659
+ this.collections.set(collection, records);
660
+ return records;
661
+ }
662
+ getQueue(queue) {
663
+ const jobs = this.queues.get(queue) ?? [];
664
+ this.queues.set(queue, jobs);
665
+ return jobs;
666
+ }
667
+ findJob(queue, jobId) {
668
+ return this.queues.get(queue)?.find((job) => job.id === jobId) ?? null;
669
+ }
670
+ releaseExpiredLeases(queue) {
671
+ const now = new Date().toISOString();
672
+ for (const job of this.queues.get(queue) ?? []) {
673
+ if (job.status === "leased" && job.leaseExpiresAt && job.leaseExpiresAt <= now) {
674
+ job.status = "ready";
675
+ job.leasedAt = undefined;
676
+ job.leaseExpiresAt = undefined;
677
+ }
678
+ }
679
+ }
680
+ cloneJob(job) {
681
+ return {
682
+ ...job,
683
+ payload: {
684
+ ...job.payload,
685
+ },
686
+ };
687
+ }
688
+ }
689
+ function cosineSimilarity(a, b) {
690
+ if (a.length !== b.length || a.length === 0) {
691
+ return 0;
692
+ }
693
+ let dot = 0;
694
+ let normA = 0;
695
+ let normB = 0;
696
+ for (const [i, ai] of a.entries()) {
697
+ const bi = b[i];
698
+ if (bi === undefined) {
699
+ break;
700
+ }
701
+ dot += ai * bi;
702
+ normA += ai * ai;
703
+ normB += bi * bi;
704
+ }
705
+ if (normA === 0 || normB === 0) {
706
+ return 0;
707
+ }
708
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
709
+ }