@thingd/sdk 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +39 -0
  3. package/dist/client/http-thing-store.d.ts +41 -0
  4. package/dist/client/http-thing-store.d.ts.map +1 -0
  5. package/dist/client/http-thing-store.js +178 -0
  6. package/dist/client/in-memory-thing-store.d.ts +38 -0
  7. package/dist/client/in-memory-thing-store.d.ts.map +1 -0
  8. package/dist/client/in-memory-thing-store.js +270 -0
  9. package/dist/client/index.d.ts +5 -0
  10. package/dist/client/index.d.ts.map +1 -0
  11. package/dist/client/index.js +3 -0
  12. package/dist/client/thingd.d.ts +46 -0
  13. package/dist/client/thingd.d.ts.map +1 -0
  14. package/dist/client/thingd.js +115 -0
  15. package/dist/index.d.ts +11 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +9 -0
  18. package/dist/mcp/audit.d.ts +27 -0
  19. package/dist/mcp/audit.d.ts.map +1 -0
  20. package/dist/mcp/audit.js +36 -0
  21. package/dist/mcp/config.d.ts +22 -0
  22. package/dist/mcp/config.d.ts.map +1 -0
  23. package/dist/mcp/config.js +52 -0
  24. package/dist/mcp/index.d.ts +6 -0
  25. package/dist/mcp/index.d.ts.map +1 -0
  26. package/dist/mcp/index.js +5 -0
  27. package/dist/mcp/result.d.ts +3 -0
  28. package/dist/mcp/result.d.ts.map +1 -0
  29. package/dist/mcp/result.js +10 -0
  30. package/dist/mcp/server.d.ts +19 -0
  31. package/dist/mcp/server.d.ts.map +1 -0
  32. package/dist/mcp/server.js +51 -0
  33. package/dist/mcp/tools.d.ts +10 -0
  34. package/dist/mcp/tools.d.ts.map +1 -0
  35. package/dist/mcp/tools.js +568 -0
  36. package/dist/memory/index.d.ts +36 -0
  37. package/dist/memory/index.d.ts.map +1 -0
  38. package/dist/memory/index.js +86 -0
  39. package/dist/rest/helpers.d.ts +17 -0
  40. package/dist/rest/helpers.d.ts.map +1 -0
  41. package/dist/rest/helpers.js +55 -0
  42. package/dist/rest/index.d.ts +3 -0
  43. package/dist/rest/index.d.ts.map +1 -0
  44. package/dist/rest/index.js +2 -0
  45. package/dist/rest/server.d.ts +4 -0
  46. package/dist/rest/server.d.ts.map +1 -0
  47. package/dist/rest/server.js +317 -0
  48. package/dist/stores/cloud-thing-store.d.ts +49 -0
  49. package/dist/stores/cloud-thing-store.d.ts.map +1 -0
  50. package/dist/stores/cloud-thing-store.js +243 -0
  51. package/dist/stores/in-memory-thing-store.d.ts +44 -0
  52. package/dist/stores/in-memory-thing-store.d.ts.map +1 -0
  53. package/dist/stores/in-memory-thing-store.js +411 -0
  54. package/dist/stores/native-thing-store.d.ts +53 -0
  55. package/dist/stores/native-thing-store.d.ts.map +1 -0
  56. package/dist/stores/native-thing-store.js +312 -0
  57. package/dist/stores/remote-thing-store.d.ts +27 -0
  58. package/dist/stores/remote-thing-store.d.ts.map +1 -0
  59. package/dist/stores/remote-thing-store.js +131 -0
  60. package/dist/thingd.d.ts +48 -0
  61. package/dist/thingd.d.ts.map +1 -0
  62. package/dist/thingd.js +147 -0
  63. package/dist/types/index.d.ts +2 -0
  64. package/dist/types/index.d.ts.map +1 -0
  65. package/dist/types/index.js +1 -0
  66. package/dist/types.d.ts +185 -0
  67. package/dist/types.d.ts.map +1 -0
  68. package/dist/types.js +1 -0
  69. package/dist/version.d.ts +6 -0
  70. package/dist/version.d.ts.map +1 -0
  71. package/dist/version.js +5 -0
  72. package/package.json +79 -0
@@ -0,0 +1,243 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
+ import { SDK_VERSION } from "../version.js";
4
+ export class CloudThingStore {
5
+ client;
6
+ connectOptions;
7
+ static async open(urlOrOptions) {
8
+ const options = typeof urlOrOptions === "string"
9
+ ? {
10
+ url: urlOrOptions,
11
+ }
12
+ : urlOrOptions;
13
+ const client = new Client({
14
+ name: options.clientName ?? "thingd-node-sdk",
15
+ version: options.clientVersion ?? SDK_VERSION,
16
+ });
17
+ const transport = new StreamableHTTPClientTransport(new URL(resolveMcpUrl(options.url)), {
18
+ requestInit: options.authToken
19
+ ? {
20
+ headers: {
21
+ Authorization: `Bearer ${options.authToken}`,
22
+ },
23
+ }
24
+ : undefined,
25
+ });
26
+ await client.connect(transport);
27
+ return new CloudThingStore(client, options);
28
+ }
29
+ constructor(client, connectOptions) {
30
+ this.client = client;
31
+ this.connectOptions = connectOptions;
32
+ }
33
+ /**
34
+ * Explicitly reconnect the transport — useful if you detect the connection
35
+ * has dropped and want to recover without recreating the store.
36
+ */
37
+ async reconnect() {
38
+ try {
39
+ await this.client.close();
40
+ }
41
+ catch {
42
+ // ignore close errors
43
+ }
44
+ const client = new Client({
45
+ name: this.connectOptions.clientName ?? "thingd-node-sdk",
46
+ version: this.connectOptions.clientVersion ?? SDK_VERSION,
47
+ });
48
+ const transport = new StreamableHTTPClientTransport(new URL(resolveMcpUrl(this.connectOptions.url)), {
49
+ requestInit: this.connectOptions.authToken
50
+ ? { headers: { Authorization: `Bearer ${this.connectOptions.authToken}` } }
51
+ : undefined,
52
+ });
53
+ await client.connect(transport);
54
+ this.client = client;
55
+ }
56
+ put(collection, object) {
57
+ return this.callTool("thing_put", {
58
+ collection,
59
+ object,
60
+ });
61
+ }
62
+ get(collection, id) {
63
+ return this.callTool("thing_get", {
64
+ collection,
65
+ id,
66
+ });
67
+ }
68
+ delete(collection, id) {
69
+ return this.callTool("thing_delete", {
70
+ collection,
71
+ id,
72
+ });
73
+ }
74
+ listObjects(collection, options) {
75
+ const params = { collection };
76
+ if (options?.filter) {
77
+ params.filter = options.filter;
78
+ }
79
+ if (options?.limit) {
80
+ params.limit = options.limit;
81
+ }
82
+ if (options?.offset) {
83
+ params.offset = options.offset;
84
+ }
85
+ return this.callTool("thing_objects_list", params);
86
+ }
87
+ appendEvent(stream, event) {
88
+ return this.callTool("thing_events_append", {
89
+ stream,
90
+ event,
91
+ });
92
+ }
93
+ listEvents(stream, options) {
94
+ const params = {};
95
+ if (stream) {
96
+ params.stream = stream;
97
+ }
98
+ if (options?.fromSequence) {
99
+ params.fromSequence = options.fromSequence;
100
+ }
101
+ if (options?.limit) {
102
+ params.limit = options.limit;
103
+ }
104
+ return this.callTool("thing_events_list", params);
105
+ }
106
+ pushJob(queue, payload, options = {}) {
107
+ return this.callTool("thing_queue_push", {
108
+ queue,
109
+ payload,
110
+ idempotencyKey: options.idempotencyKey,
111
+ maxAttempts: options.maxAttempts,
112
+ delayMs: options.delayMs,
113
+ });
114
+ }
115
+ claimJob(queue, options = {}) {
116
+ return this.callTool("thing_queue_claim", {
117
+ queue,
118
+ leaseMs: options.leaseMs,
119
+ });
120
+ }
121
+ ackJob(queue, jobId) {
122
+ return this.callTool("thing_queue_ack", {
123
+ queue,
124
+ id: jobId,
125
+ });
126
+ }
127
+ nackJob(queue, jobId, options = {}) {
128
+ return this.callTool("thing_queue_nack", {
129
+ queue,
130
+ id: jobId,
131
+ delayMs: options.delayMs,
132
+ error: options.error,
133
+ });
134
+ }
135
+ listJobs(queue) {
136
+ return this.callTool("thing_queue_list", {
137
+ queue,
138
+ });
139
+ }
140
+ listDeadJobs(queue) {
141
+ return this.callTool("thing_queue_dead", {
142
+ queue,
143
+ });
144
+ }
145
+ search(query, options = {}) {
146
+ return this.callTool("thing_search", {
147
+ query,
148
+ collections: options.collections,
149
+ limit: options.limit,
150
+ filter: options.filter,
151
+ });
152
+ }
153
+ async countObjects() {
154
+ return this.callTool("thing_count_objects", {});
155
+ }
156
+ async countEvents() {
157
+ return this.callTool("thing_count_events", {});
158
+ }
159
+ async countActiveJobs() {
160
+ return this.callTool("thing_count_active_jobs", {});
161
+ }
162
+ async countDeadJobs() {
163
+ return this.callTool("thing_count_dead_jobs", {});
164
+ }
165
+ async countLinks() {
166
+ return Promise.reject(new Error("Graph links not supported by cloud driver yet"));
167
+ }
168
+ async putBatch(_collection, _objects) {
169
+ return Promise.reject(new Error("Batch put not supported by cloud driver yet"));
170
+ }
171
+ async deleteBatch(_collection, _ids) {
172
+ return Promise.reject(new Error("Batch delete not supported by cloud driver yet"));
173
+ }
174
+ async createLink(_fromRef, _linkType, _toRef, _weight, _metadataJson) {
175
+ return Promise.reject(new Error("Graph links not supported by cloud driver yet"));
176
+ }
177
+ async deleteLink(_id) {
178
+ return Promise.reject(new Error("Graph links not supported by cloud driver yet"));
179
+ }
180
+ async getLink(_id) {
181
+ return Promise.reject(new Error("Graph links not supported by cloud driver yet"));
182
+ }
183
+ async getNeighbors(_reference, _direction, _options) {
184
+ return Promise.reject(new Error("Graph links not supported by cloud driver yet"));
185
+ }
186
+ async listCollections() {
187
+ return this.callTool("thing_list_collections", {});
188
+ }
189
+ async listStreams() {
190
+ return this.callTool("thing_list_streams", {});
191
+ }
192
+ async listQueues() {
193
+ return this.callTool("thing_list_queues", {});
194
+ }
195
+ async close() {
196
+ await this.client.close();
197
+ }
198
+ async callTool(name, args) {
199
+ return this.callToolOnce(name, args, true);
200
+ }
201
+ async callToolOnce(name, args, retryOnTransportError) {
202
+ let result;
203
+ try {
204
+ result = (await this.client.callTool({ name, arguments: args }));
205
+ }
206
+ catch (error) {
207
+ // Transport-level error (connection dropped, ECONNRESET, etc.).
208
+ // Attempt one reconnect and retry before propagating.
209
+ if (retryOnTransportError) {
210
+ try {
211
+ await this.reconnect();
212
+ }
213
+ catch (reconnectError) {
214
+ throw new Error(`thingd cloud: transport error calling "${name}" and reconnect failed: ${reconnectError instanceof Error ? reconnectError.message : String(reconnectError)}`);
215
+ }
216
+ return this.callToolOnce(name, args, false);
217
+ }
218
+ throw error;
219
+ }
220
+ if (result.isError) {
221
+ const text = result.content.find((part) => part.type === "text")?.text;
222
+ throw new Error(text ?? `thingd cloud tool "${name}" returned an error`);
223
+ }
224
+ return parseJsonToolResult(result);
225
+ }
226
+ }
227
+ function resolveMcpUrl(value) {
228
+ const normalized = value.startsWith("thingd://")
229
+ ? `http://${value.slice("thingd://".length)}`
230
+ : value;
231
+ const url = new URL(normalized);
232
+ if (url.pathname === "" || url.pathname === "/") {
233
+ url.pathname = "/mcp";
234
+ }
235
+ return url.toString();
236
+ }
237
+ function parseJsonToolResult(result) {
238
+ const text = result.content.find((part) => part.type === "text" && typeof part.text === "string")?.text;
239
+ if (!text) {
240
+ throw new Error("thingd cloud tool did not return JSON text content");
241
+ }
242
+ return JSON.parse(text);
243
+ }
@@ -0,0 +1,44 @@
1
+ import type { ListEventsOptions, ListObjectsOptions, MemoryEvent, MemoryObject, MemorySearchOptions, MemorySearchResult, QueueClaimOptions, QueueJob, QueueJobOptions, QueueJobPayload, QueueJobResult, QueueNackOptions, StoredMemoryEvent, StoredMemoryObject, ThingDeleteResult, ThingStore } from "../types.js";
2
+ export declare class InMemoryThingStore implements ThingStore {
3
+ private readonly collections;
4
+ private readonly events;
5
+ private nextEventSequence;
6
+ private readonly queues;
7
+ private readonly links;
8
+ private readonly mutex;
9
+ private withLock;
10
+ put(collection: string, object: MemoryObject): Promise<StoredMemoryObject>;
11
+ get<T = StoredMemoryObject>(collection: string, id: string): Promise<T | null>;
12
+ delete(collection: string, id: string): Promise<ThingDeleteResult>;
13
+ listObjects<T = StoredMemoryObject>(collection: string, options?: ListObjectsOptions): Promise<T[]>;
14
+ appendEvent(stream: string, event: MemoryEvent): Promise<StoredMemoryEvent>;
15
+ listEvents<T = StoredMemoryEvent>(stream?: string, options?: ListEventsOptions): Promise<T[]>;
16
+ pushJob(queue: string, payload: QueueJobPayload, options?: QueueJobOptions): Promise<QueueJob>;
17
+ claimJob(queue: string, options?: QueueClaimOptions): Promise<QueueJob | null>;
18
+ ackJob(queue: string, jobId: string): Promise<QueueJobResult>;
19
+ nackJob(queue: string, jobId: string, options?: QueueNackOptions): Promise<QueueJobResult>;
20
+ listJobs(queue: string): Promise<QueueJob[]>;
21
+ listDeadJobs(queue: string): Promise<QueueJob[]>;
22
+ search(query: string, options?: MemorySearchOptions): Promise<MemorySearchResult[]>;
23
+ countObjects(): Promise<number>;
24
+ countEvents(): Promise<number>;
25
+ countActiveJobs(): Promise<number>;
26
+ countDeadJobs(): Promise<number>;
27
+ countLinks(): Promise<number>;
28
+ putBatch(collection: string, objects: MemoryObject[]): Promise<StoredMemoryObject[]>;
29
+ deleteBatch(collection: string, ids: string[]): Promise<number>;
30
+ createLink(fromRef: string, linkType: string, toRef: string, weight?: number, metadataJson?: string): Promise<import("../types.js").Link>;
31
+ deleteLink(id: string): Promise<boolean>;
32
+ getLink(id: string): Promise<import("../types.js").Link | null>;
33
+ getNeighbors(reference: string, direction: import("../types.js").LinkDirection, options: import("../types.js").LinkQueryOptions): Promise<import("../types.js").Link[]>;
34
+ listCollections(): Promise<string[]>;
35
+ listQueues(): Promise<string[]>;
36
+ listStreams(): Promise<string[]>;
37
+ close(): Promise<void>;
38
+ private getCollection;
39
+ private getQueue;
40
+ private findJob;
41
+ private releaseExpiredLeases;
42
+ private cloneJob;
43
+ }
44
+ //# sourceMappingURL=in-memory-thing-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"in-memory-thing-store.d.ts","sourceRoot":"","sources":["../../src/stores/in-memory-thing-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,411 @@
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
+ async withLock(fn) {
36
+ const release = await this.mutex.acquire();
37
+ try {
38
+ return fn();
39
+ }
40
+ finally {
41
+ release();
42
+ }
43
+ }
44
+ async put(collection, object) {
45
+ return this.withLock(() => {
46
+ const records = this.getCollection(collection);
47
+ const now = new Date().toISOString();
48
+ const existing = records.get(object.id);
49
+ const record = {
50
+ ...object,
51
+ id: object.id,
52
+ collection,
53
+ createdAt: existing?.createdAt ?? now,
54
+ updatedAt: now,
55
+ version: (existing?.version ?? 0) + 1,
56
+ };
57
+ records.set(object.id, record);
58
+ return record;
59
+ });
60
+ }
61
+ async get(collection, id) {
62
+ return this.collections.get(collection)?.get(id) ?? null;
63
+ }
64
+ async delete(collection, id) {
65
+ return this.withLock(() => ({
66
+ deleted: this.collections.get(collection)?.delete(id) ?? false,
67
+ }));
68
+ }
69
+ async listObjects(collection, options) {
70
+ const records = this.collections.get(collection);
71
+ if (!records) {
72
+ return [];
73
+ }
74
+ let results = Array.from(records.values());
75
+ const filter = options?.filter;
76
+ if (filter) {
77
+ results = results.filter((obj) => Object.entries(filter).every(([key, value]) => obj[key] === value));
78
+ }
79
+ if (options?.sortBy) {
80
+ const { field, direction } = options.sortBy;
81
+ const asc = direction !== "desc";
82
+ results.sort((a, b) => {
83
+ const va = a[field];
84
+ const vb = b[field];
85
+ if (va === vb) {
86
+ return 0;
87
+ }
88
+ if (va === undefined) {
89
+ return 1;
90
+ }
91
+ if (vb === undefined) {
92
+ return -1;
93
+ }
94
+ const cmp = va < vb ? -1 : 1;
95
+ return asc ? cmp : -cmp;
96
+ });
97
+ }
98
+ if (options?.offset) {
99
+ results = results.slice(options.offset);
100
+ }
101
+ if (options?.limit) {
102
+ results = results.slice(0, options.limit);
103
+ }
104
+ return results;
105
+ }
106
+ async appendEvent(stream, event) {
107
+ return this.withLock(() => {
108
+ this.nextEventSequence += 1;
109
+ const record = {
110
+ ...event,
111
+ id: randomUUID(),
112
+ stream,
113
+ sequence: this.nextEventSequence,
114
+ createdAt: new Date().toISOString(),
115
+ };
116
+ this.events.push(record);
117
+ return record;
118
+ });
119
+ }
120
+ async listEvents(stream, options) {
121
+ let events = this.events;
122
+ if (stream) {
123
+ events = events.filter((event) => event.stream === stream);
124
+ }
125
+ const fromSeq = options?.fromSequence;
126
+ if (fromSeq) {
127
+ events = events.filter((event) => event.sequence > fromSeq);
128
+ }
129
+ if (options?.limit) {
130
+ events = events.slice(0, options.limit);
131
+ }
132
+ return [...events];
133
+ }
134
+ async pushJob(queue, payload, options = {}) {
135
+ return this.withLock(() => {
136
+ const jobs = this.getQueue(queue);
137
+ const now = new Date().toISOString();
138
+ const job = {
139
+ id: options.idempotencyKey ?? randomUUID(),
140
+ queue,
141
+ payload,
142
+ status: "ready",
143
+ attempts: 0,
144
+ maxAttempts: options.maxAttempts ?? 3,
145
+ createdAt: now,
146
+ availableAt: new Date(Date.now() + (options.delayMs ?? 0)).toISOString(),
147
+ };
148
+ const existing = jobs.find((candidate) => candidate.id === job.id);
149
+ if (existing) {
150
+ return this.cloneJob(existing);
151
+ }
152
+ jobs.push(job);
153
+ return this.cloneJob(job);
154
+ });
155
+ }
156
+ async claimJob(queue, options = {}) {
157
+ return this.withLock(() => {
158
+ this.releaseExpiredLeases(queue);
159
+ const now = new Date();
160
+ const job = this.queues
161
+ .get(queue)
162
+ ?.find((candidate) => candidate.status === "ready" && candidate.availableAt <= now.toISOString());
163
+ if (!job) {
164
+ return null;
165
+ }
166
+ job.status = "leased";
167
+ job.attempts += 1;
168
+ job.leasedAt = now.toISOString();
169
+ job.leaseExpiresAt = new Date(now.getTime() + (options.leaseMs ?? DEFAULT_LEASE_MS)).toISOString();
170
+ return this.cloneJob(job);
171
+ });
172
+ }
173
+ async ackJob(queue, jobId) {
174
+ return this.withLock(() => {
175
+ const job = this.findJob(queue, jobId);
176
+ if (!job) {
177
+ return { ok: false, reason: "not_found" };
178
+ }
179
+ if (job.status === "completed" || job.status === "dead") {
180
+ return { ok: false, reason: "terminal" };
181
+ }
182
+ if (job.status !== "leased") {
183
+ return { ok: false, reason: "not_leased" };
184
+ }
185
+ job.status = "completed";
186
+ job.completedAt = new Date().toISOString();
187
+ return { ok: true, job: this.cloneJob(job) };
188
+ });
189
+ }
190
+ async nackJob(queue, jobId, options = {}) {
191
+ return this.withLock(() => {
192
+ const job = this.findJob(queue, jobId);
193
+ if (!job) {
194
+ return { ok: false, reason: "not_found" };
195
+ }
196
+ if (job.status === "completed" || job.status === "dead") {
197
+ return { ok: false, reason: "terminal" };
198
+ }
199
+ if (job.status !== "leased") {
200
+ return { ok: false, reason: "not_leased" };
201
+ }
202
+ job.lastError = options.error;
203
+ job.leasedAt = undefined;
204
+ job.leaseExpiresAt = undefined;
205
+ if (job.attempts >= job.maxAttempts) {
206
+ job.status = "dead";
207
+ job.deadAt = new Date().toISOString();
208
+ }
209
+ else {
210
+ job.status = "ready";
211
+ job.availableAt = new Date(Date.now() + (options.delayMs ?? 0)).toISOString();
212
+ }
213
+ return { ok: true, job: this.cloneJob(job) };
214
+ });
215
+ }
216
+ async listJobs(queue) {
217
+ return (this.queues.get(queue) ?? []).map((job) => this.cloneJob(job));
218
+ }
219
+ async listDeadJobs(queue) {
220
+ return (this.queues.get(queue) ?? [])
221
+ .filter((job) => job.status === "dead")
222
+ .map((job) => this.cloneJob(job));
223
+ }
224
+ async search(query, options = {}) {
225
+ const normalizedQuery = query.toLowerCase();
226
+ const collections = options.collections ? new Set(options.collections) : null;
227
+ const results = [];
228
+ for (const [collection, records] of this.collections) {
229
+ if (collections && !collections.has(collection)) {
230
+ continue;
231
+ }
232
+ for (const record of records.values()) {
233
+ const haystack = JSON.stringify(record).toLowerCase();
234
+ if (haystack.includes(normalizedQuery)) {
235
+ results.push({
236
+ kind: "object",
237
+ id: record.id,
238
+ collection,
239
+ score: 1,
240
+ value: record,
241
+ });
242
+ }
243
+ }
244
+ }
245
+ for (const event of this.events) {
246
+ const haystack = JSON.stringify(event).toLowerCase();
247
+ if (haystack.includes(normalizedQuery)) {
248
+ results.push({
249
+ kind: "event",
250
+ id: event.id,
251
+ stream: event.stream,
252
+ score: 1,
253
+ value: event,
254
+ });
255
+ }
256
+ }
257
+ return options.limit !== undefined ? results.slice(0, options.limit) : results;
258
+ }
259
+ async countObjects() {
260
+ let total = 0;
261
+ for (const records of this.collections.values()) {
262
+ total += records.size;
263
+ }
264
+ return total;
265
+ }
266
+ async countEvents() {
267
+ return this.events.length;
268
+ }
269
+ async countActiveJobs() {
270
+ let total = 0;
271
+ for (const jobs of this.queues.values()) {
272
+ total += jobs.filter((job) => job.status !== "dead").length;
273
+ }
274
+ return total;
275
+ }
276
+ async countDeadJobs() {
277
+ let total = 0;
278
+ for (const jobs of this.queues.values()) {
279
+ total += jobs.filter((job) => job.status === "dead").length;
280
+ }
281
+ return total;
282
+ }
283
+ async countLinks() {
284
+ return this.links.size;
285
+ }
286
+ async putBatch(collection, objects) {
287
+ return this.withLock(() => {
288
+ const records = this.getCollection(collection);
289
+ const now = new Date().toISOString();
290
+ const results = [];
291
+ for (const object of objects) {
292
+ const existing = records.get(object.id);
293
+ const record = {
294
+ ...object,
295
+ id: object.id,
296
+ collection,
297
+ createdAt: existing?.createdAt ?? now,
298
+ updatedAt: now,
299
+ version: (existing?.version ?? 0) + 1,
300
+ };
301
+ records.set(object.id, record);
302
+ results.push(record);
303
+ }
304
+ return results;
305
+ });
306
+ }
307
+ async deleteBatch(collection, ids) {
308
+ return this.withLock(() => {
309
+ const records = this.collections.get(collection);
310
+ if (!records) {
311
+ return 0;
312
+ }
313
+ let count = 0;
314
+ for (const id of ids) {
315
+ if (records.delete(id)) {
316
+ count++;
317
+ }
318
+ }
319
+ return count;
320
+ });
321
+ }
322
+ async createLink(fromRef, linkType, toRef, weight, metadataJson) {
323
+ return this.withLock(() => {
324
+ const link = {
325
+ id: randomUUID(),
326
+ fromRef,
327
+ linkType,
328
+ toRef,
329
+ weight,
330
+ metadataJson: metadataJson ?? "{}",
331
+ createdAt: new Date().toISOString(),
332
+ };
333
+ this.links.set(link.id, link);
334
+ return link;
335
+ });
336
+ }
337
+ async deleteLink(id) {
338
+ return this.withLock(() => {
339
+ return this.links.delete(id);
340
+ });
341
+ }
342
+ async getLink(id) {
343
+ return this.links.get(id) ?? null;
344
+ }
345
+ async getNeighbors(reference, direction, options) {
346
+ let results = Array.from(this.links.values());
347
+ results = results.filter((link) => {
348
+ if (direction === "Outgoing") {
349
+ return link.fromRef === reference;
350
+ }
351
+ if (direction === "Incoming") {
352
+ return link.toRef === reference;
353
+ }
354
+ return link.fromRef === reference || link.toRef === reference;
355
+ });
356
+ if (options.linkType) {
357
+ results = results.filter((link) => link.linkType === options.linkType);
358
+ }
359
+ if (options.limit !== undefined) {
360
+ results = results.slice(0, options.limit);
361
+ }
362
+ return results;
363
+ }
364
+ async listCollections() {
365
+ return Array.from(this.collections.keys()).sort();
366
+ }
367
+ async listQueues() {
368
+ return Array.from(this.queues.keys()).sort();
369
+ }
370
+ async listStreams() {
371
+ const streams = new Set();
372
+ for (const event of this.events) {
373
+ streams.add(event.stream);
374
+ }
375
+ return Array.from(streams).sort();
376
+ }
377
+ async close() {
378
+ // no-op for in-memory store
379
+ }
380
+ getCollection(collection) {
381
+ const records = this.collections.get(collection) ?? new Map();
382
+ this.collections.set(collection, records);
383
+ return records;
384
+ }
385
+ getQueue(queue) {
386
+ const jobs = this.queues.get(queue) ?? [];
387
+ this.queues.set(queue, jobs);
388
+ return jobs;
389
+ }
390
+ findJob(queue, jobId) {
391
+ return this.queues.get(queue)?.find((job) => job.id === jobId) ?? null;
392
+ }
393
+ releaseExpiredLeases(queue) {
394
+ const now = new Date().toISOString();
395
+ for (const job of this.queues.get(queue) ?? []) {
396
+ if (job.status === "leased" && job.leaseExpiresAt && job.leaseExpiresAt <= now) {
397
+ job.status = "ready";
398
+ job.leasedAt = undefined;
399
+ job.leaseExpiresAt = undefined;
400
+ }
401
+ }
402
+ }
403
+ cloneJob(job) {
404
+ return {
405
+ ...job,
406
+ payload: {
407
+ ...job.payload,
408
+ },
409
+ };
410
+ }
411
+ }