@thingd/sdk 0.70.0 → 0.72.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/package.json +2 -3
- package/dist/client/http-thing-store.d.ts +0 -55
- package/dist/client/http-thing-store.d.ts.map +0 -1
- package/dist/client/http-thing-store.js +0 -233
- package/dist/client/in-memory-thing-store.d.ts +0 -41
- package/dist/client/in-memory-thing-store.d.ts.map +0 -1
- package/dist/client/in-memory-thing-store.js +0 -292
- package/dist/client/index.d.ts +0 -5
- package/dist/client/index.d.ts.map +0 -1
- package/dist/client/index.js +0 -3
- package/dist/client/thingd.d.ts +0 -24
- package/dist/client/thingd.d.ts.map +0 -1
- package/dist/client/thingd.js +0 -27
- package/dist/constants.d.ts +0 -2
- package/dist/constants.d.ts.map +0 -1
- package/dist/constants.js +0 -1
- package/dist/index.d.ts +0 -13
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -11
- package/dist/mcp/audit.d.ts +0 -27
- package/dist/mcp/audit.d.ts.map +0 -1
- package/dist/mcp/audit.js +0 -36
- package/dist/mcp/config.d.ts +0 -22
- package/dist/mcp/config.d.ts.map +0 -1
- package/dist/mcp/config.js +0 -52
- package/dist/mcp/index.d.ts +0 -6
- package/dist/mcp/index.d.ts.map +0 -1
- package/dist/mcp/index.js +0 -5
- package/dist/mcp/result.d.ts +0 -3
- package/dist/mcp/result.d.ts.map +0 -1
- package/dist/mcp/result.js +0 -10
- package/dist/mcp/server.d.ts +0 -19
- package/dist/mcp/server.d.ts.map +0 -1
- package/dist/mcp/server.js +0 -51
- package/dist/mcp/tools.d.ts +0 -10
- package/dist/mcp/tools.d.ts.map +0 -1
- package/dist/mcp/tools.js +0 -1033
- package/dist/memory/index.d.ts +0 -7
- package/dist/memory/index.d.ts.map +0 -1
- package/dist/memory/index.js +0 -7
- package/dist/rest/helpers.d.ts +0 -17
- package/dist/rest/helpers.d.ts.map +0 -1
- package/dist/rest/helpers.js +0 -60
- package/dist/rest/index.d.ts +0 -3
- package/dist/rest/index.d.ts.map +0 -1
- package/dist/rest/index.js +0 -2
- package/dist/rest/server.d.ts +0 -4
- package/dist/rest/server.d.ts.map +0 -1
- package/dist/rest/server.js +0 -472
- package/dist/scheduler.d.ts +0 -28
- package/dist/scheduler.d.ts.map +0 -1
- package/dist/scheduler.js +0 -451
- package/dist/stores/cloud-thing-store.d.ts +0 -63
- package/dist/stores/cloud-thing-store.d.ts.map +0 -1
- package/dist/stores/cloud-thing-store.js +0 -396
- package/dist/stores/in-memory-thing-store.d.ts +0 -62
- package/dist/stores/in-memory-thing-store.d.ts.map +0 -1
- package/dist/stores/in-memory-thing-store.js +0 -709
- package/dist/stores/native-thing-store.d.ts +0 -62
- package/dist/stores/native-thing-store.d.ts.map +0 -1
- package/dist/stores/native-thing-store.js +0 -374
- package/dist/thingd.d.ts +0 -87
- package/dist/thingd.d.ts.map +0 -1
- package/dist/thingd.js +0 -236
- package/dist/types/index.d.ts +0 -2
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types.d.ts +0 -435
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -1
- package/dist/version.d.ts +0 -6
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -5
|
@@ -1,709 +0,0 @@
|
|
|
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
|
-
}
|