@skein-js/storage-memory 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -1
- package/dist/index.js +91 -6
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { SkeinStore, AssistantRepo, ThreadRepo, RunRepo, StoreRepo, SkeinStoreSnapshot, RunEventBus, RunFrame, RunQueue, QueuedRun, RunProcessor, RunConsumerOptions, RunConsumer } from '@skein-js/core';
|
|
1
|
+
import { SkeinStore, StoreTtlConfig, AssistantRepo, ThreadRepo, RunRepo, StoreRepo, SkeinStoreSnapshot, RunEventBus, RunFrame, RunQueue, QueuedRun, RunProcessor, RunConsumerOptions, RunConsumer } from '@skein-js/core';
|
|
2
2
|
|
|
3
3
|
/** In-process SkeinStore for development and tests. */
|
|
4
4
|
declare class MemorySkeinStore implements SkeinStore {
|
|
5
5
|
#private;
|
|
6
|
+
constructor(options?: {
|
|
7
|
+
ttl?: StoreTtlConfig;
|
|
8
|
+
});
|
|
6
9
|
readonly assistants: AssistantRepo;
|
|
7
10
|
readonly threads: ThreadRepo;
|
|
8
11
|
readonly runs: RunRepo;
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/memory-skein-store.ts
|
|
2
2
|
import { randomUUID } from "crypto";
|
|
3
3
|
import {
|
|
4
|
+
isMetadataSubset,
|
|
4
5
|
isTerminalRunStatus,
|
|
5
6
|
SkeinHttpError
|
|
6
7
|
} from "@skein-js/core";
|
|
@@ -33,6 +34,37 @@ var MemorySkeinStore = class {
|
|
|
33
34
|
// The opaque execution payload lives beside the run row (it is not part of the wire `Run`).
|
|
34
35
|
#runKwargs = /* @__PURE__ */ new Map();
|
|
35
36
|
#items = /* @__PURE__ */ new Map();
|
|
37
|
+
// Item expiry lives beside the item (never on the wire `Item`), keyed the same way as #items:
|
|
38
|
+
// `expiresAt` is epoch-ms (null = never expires), `ttlMinutes` is what a refresh-on-read extends by.
|
|
39
|
+
#itemExpiry = /* @__PURE__ */ new Map();
|
|
40
|
+
#ttl;
|
|
41
|
+
constructor(options) {
|
|
42
|
+
this.#ttl = options?.ttl;
|
|
43
|
+
}
|
|
44
|
+
/** Record (or clear) an item's expiry from a resolved per-item TTL in minutes. */
|
|
45
|
+
#setExpiry(id, ttlMinutes) {
|
|
46
|
+
if (ttlMinutes === null || ttlMinutes === void 0) {
|
|
47
|
+
this.#itemExpiry.delete(id);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
this.#itemExpiry.set(id, { expiresAt: Date.now() + ttlMinutes * 6e4, ttlMinutes });
|
|
51
|
+
}
|
|
52
|
+
/** True if the item has expired and should read as absent. */
|
|
53
|
+
#isExpired(id) {
|
|
54
|
+
const entry = this.#itemExpiry.get(id);
|
|
55
|
+
return entry?.expiresAt != null && entry.expiresAt <= Date.now();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Extend a live item's expiry on read when TTL is configured and `refresh_on_read` isn't disabled
|
|
59
|
+
* (it defaults on). With no configured TTL we never refresh, matching the Postgres driver.
|
|
60
|
+
*/
|
|
61
|
+
#maybeRefresh(id) {
|
|
62
|
+
if (this.#ttl === void 0 || this.#ttl.refreshOnRead === false) return;
|
|
63
|
+
const entry = this.#itemExpiry.get(id);
|
|
64
|
+
if (entry?.ttlMinutes != null) {
|
|
65
|
+
this.#itemExpiry.set(id, { ...entry, expiresAt: Date.now() + entry.ttlMinutes * 6e4 });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
36
68
|
assistants = {
|
|
37
69
|
list: async () => readAll(this.#assistants),
|
|
38
70
|
get: async (assistantId) => readOne(this.#assistants, assistantId),
|
|
@@ -58,6 +90,22 @@ var MemorySkeinStore = class {
|
|
|
58
90
|
};
|
|
59
91
|
threads = {
|
|
60
92
|
list: async () => readAll(this.#threads),
|
|
93
|
+
search: async (query) => {
|
|
94
|
+
const matched = readAll(this.#threads).filter(
|
|
95
|
+
(thread) => (!query.ids || query.ids.includes(thread.thread_id)) && (!query.status || thread.status === query.status) && isMetadataSubset(thread.metadata, query.metadata) && isMetadataSubset(thread.values, query.values)
|
|
96
|
+
);
|
|
97
|
+
const sortBy = query.sortBy ?? "created_at";
|
|
98
|
+
const direction = query.sortOrder === "asc" ? 1 : -1;
|
|
99
|
+
const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
100
|
+
matched.sort((a, b) => {
|
|
101
|
+
const primary = compare(String(a[sortBy] ?? ""), String(b[sortBy] ?? ""));
|
|
102
|
+
const ordered = primary !== 0 ? primary : compare(a.thread_id, b.thread_id);
|
|
103
|
+
return direction * ordered;
|
|
104
|
+
});
|
|
105
|
+
const offset = query.offset ?? 0;
|
|
106
|
+
const limit = query.limit ?? matched.length;
|
|
107
|
+
return matched.slice(offset, offset + limit);
|
|
108
|
+
},
|
|
61
109
|
get: async (threadId) => readOne(this.#threads, threadId),
|
|
62
110
|
create: async (input) => {
|
|
63
111
|
const at = nowIso();
|
|
@@ -88,6 +136,19 @@ var MemorySkeinStore = class {
|
|
|
88
136
|
};
|
|
89
137
|
return write(this.#threads, threadId, updated);
|
|
90
138
|
},
|
|
139
|
+
copy: async (threadId) => {
|
|
140
|
+
const existing = this.#threads.get(threadId);
|
|
141
|
+
if (!existing) throw SkeinHttpError.notFound(`Thread "${threadId}" not found.`);
|
|
142
|
+
const at = nowIso();
|
|
143
|
+
const copy = {
|
|
144
|
+
...clone(existing),
|
|
145
|
+
thread_id: randomUUID(),
|
|
146
|
+
created_at: at,
|
|
147
|
+
updated_at: at,
|
|
148
|
+
state_updated_at: at
|
|
149
|
+
};
|
|
150
|
+
return write(this.#threads, copy.thread_id, copy);
|
|
151
|
+
},
|
|
91
152
|
delete: async (threadId) => {
|
|
92
153
|
this.#threads.delete(threadId);
|
|
93
154
|
for (const [runId, run] of this.#runs) {
|
|
@@ -139,10 +200,18 @@ var MemorySkeinStore = class {
|
|
|
139
200
|
};
|
|
140
201
|
store = {
|
|
141
202
|
get: async (namespace, key) => {
|
|
142
|
-
const
|
|
143
|
-
|
|
203
|
+
const id = itemKey(namespace, key);
|
|
204
|
+
const found = this.#items.get(id);
|
|
205
|
+
if (!found) return null;
|
|
206
|
+
if (this.#isExpired(id)) {
|
|
207
|
+
this.#items.delete(id);
|
|
208
|
+
this.#itemExpiry.delete(id);
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
this.#maybeRefresh(id);
|
|
212
|
+
return clone(found);
|
|
144
213
|
},
|
|
145
|
-
put: async (namespace, key, value) => {
|
|
214
|
+
put: async (namespace, key, value, options) => {
|
|
146
215
|
const id = itemKey(namespace, key);
|
|
147
216
|
const at = nowIso();
|
|
148
217
|
const existing = this.#items.get(id);
|
|
@@ -155,14 +224,17 @@ var MemorySkeinStore = class {
|
|
|
155
224
|
};
|
|
156
225
|
const stored = clone(item);
|
|
157
226
|
this.#items.set(id, stored);
|
|
227
|
+
this.#setExpiry(id, options?.ttl ?? this.#ttl?.defaultTtl ?? null);
|
|
158
228
|
return clone(stored);
|
|
159
229
|
},
|
|
160
230
|
delete: async (namespace, key) => {
|
|
161
|
-
|
|
231
|
+
const id = itemKey(namespace, key);
|
|
232
|
+
this.#items.delete(id);
|
|
233
|
+
this.#itemExpiry.delete(id);
|
|
162
234
|
},
|
|
163
235
|
search: async (query) => {
|
|
164
236
|
const needle = query.query?.toLowerCase();
|
|
165
|
-
const matches = [...this.#items.
|
|
237
|
+
const matches = [...this.#items.entries()].filter(([id]) => !this.#isExpired(id)).map(([, item]) => item).filter((item) => hasPrefix(item.namespace, query.prefix)).filter(
|
|
166
238
|
(item) => needle ? JSON.stringify(item.value).toLowerCase().includes(needle) : true
|
|
167
239
|
).map((item) => {
|
|
168
240
|
const result = clone(item);
|
|
@@ -174,12 +246,24 @@ var MemorySkeinStore = class {
|
|
|
174
246
|
},
|
|
175
247
|
listNamespaces: async (prefix) => {
|
|
176
248
|
const seen = /* @__PURE__ */ new Map();
|
|
177
|
-
for (const item of this.#items.
|
|
249
|
+
for (const [id, item] of this.#items.entries()) {
|
|
250
|
+
if (this.#isExpired(id)) continue;
|
|
178
251
|
if (hasPrefix(item.namespace, prefix)) {
|
|
179
252
|
seen.set(JSON.stringify(item.namespace), item.namespace);
|
|
180
253
|
}
|
|
181
254
|
}
|
|
182
255
|
return [...seen.values()].map((namespace) => [...namespace]);
|
|
256
|
+
},
|
|
257
|
+
sweepExpired: async () => {
|
|
258
|
+
let removed = 0;
|
|
259
|
+
for (const id of [...this.#itemExpiry.keys()]) {
|
|
260
|
+
if (this.#isExpired(id)) {
|
|
261
|
+
this.#items.delete(id);
|
|
262
|
+
this.#itemExpiry.delete(id);
|
|
263
|
+
removed += 1;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return removed;
|
|
183
267
|
}
|
|
184
268
|
};
|
|
185
269
|
/**
|
|
@@ -207,6 +291,7 @@ var MemorySkeinStore = class {
|
|
|
207
291
|
fill(this.#runs, snapshot.runs);
|
|
208
292
|
fill(this.#runKwargs, snapshot.runKwargs);
|
|
209
293
|
fill(this.#items, snapshot.items);
|
|
294
|
+
this.#itemExpiry.clear();
|
|
210
295
|
}
|
|
211
296
|
/**
|
|
212
297
|
* Bulk-load rows from a snapshot, preserving ids + timestamps and *without* clearing what's
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skein-js/storage-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "In-memory SkeinStore + queue driver for development and tests.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Maina Wycliffe <wmmaina7@gmail.com>",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"node": ">=20"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@skein-js/core": "0.
|
|
43
|
+
"@skein-js/core": "0.5.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@skein-js/test-support": "0.
|
|
46
|
+
"@skein-js/test-support": "0.5.0"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public"
|