@skein-js/storage-memory 0.2.1 → 0.4.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 +18 -9
- package/dist/index.js +108 -6
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
import { SkeinStore, AssistantRepo, ThreadRepo, RunRepo, StoreRepo,
|
|
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;
|
|
@@ -14,15 +17,21 @@ declare class MemorySkeinStore implements SkeinStore {
|
|
|
14
17
|
snapshot(): MemoryStoreSnapshot;
|
|
15
18
|
/** Replace all rows with a {@link snapshot}. Used by `skein dev` to restore persisted dev state. */
|
|
16
19
|
hydrate(snapshot: MemoryStoreSnapshot): void;
|
|
20
|
+
/**
|
|
21
|
+
* Bulk-load rows from a snapshot, preserving ids + timestamps and *without* clearing what's
|
|
22
|
+
* already here: existing ids are left untouched (insert-if-absent). Unlike {@link hydrate} (a
|
|
23
|
+
* full replace for cross-restart persistence), this is the additive, lossless sink used by
|
|
24
|
+
* migration tooling — see `@skein-js/express`'s LangGraph importer. `loadSnapshotIntoStore`
|
|
25
|
+
* feature-detects this method, so the memory and Postgres drivers import identically.
|
|
26
|
+
*/
|
|
27
|
+
restore(snapshot: SkeinStoreSnapshot): Promise<void>;
|
|
17
28
|
}
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
items: [string, Item][];
|
|
25
|
-
}
|
|
29
|
+
/**
|
|
30
|
+
* A JSON-serializable copy of a {@link MemorySkeinStore}'s rows. An alias of the driver-agnostic
|
|
31
|
+
* {@link SkeinStoreSnapshot} (identical shape); the name predates the shared type and is kept so
|
|
32
|
+
* the dev-persistence snapshot format reads the same.
|
|
33
|
+
*/
|
|
34
|
+
type MemoryStoreSnapshot = SkeinStoreSnapshot;
|
|
26
35
|
|
|
27
36
|
/**
|
|
28
37
|
* In-memory FIFO of background runs, drained by a single-process consumer — all `skein dev` needs.
|
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,24 @@ 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();
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* Bulk-load rows from a snapshot, preserving ids + timestamps and *without* clearing what's
|
|
298
|
+
* already here: existing ids are left untouched (insert-if-absent). Unlike {@link hydrate} (a
|
|
299
|
+
* full replace for cross-restart persistence), this is the additive, lossless sink used by
|
|
300
|
+
* migration tooling — see `@skein-js/express`'s LangGraph importer. `loadSnapshotIntoStore`
|
|
301
|
+
* feature-detects this method, so the memory and Postgres drivers import identically.
|
|
302
|
+
*/
|
|
303
|
+
async restore(snapshot) {
|
|
304
|
+
const add = (map, rows) => {
|
|
305
|
+
for (const [id, row] of rows) if (!map.has(id)) map.set(id, clone(row));
|
|
306
|
+
};
|
|
307
|
+
add(this.#assistants, snapshot.assistants);
|
|
308
|
+
add(this.#threads, snapshot.threads);
|
|
309
|
+
add(this.#runs, snapshot.runs);
|
|
310
|
+
add(this.#runKwargs, snapshot.runKwargs);
|
|
311
|
+
add(this.#items, snapshot.items);
|
|
210
312
|
}
|
|
211
313
|
};
|
|
212
314
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skein-js/storage-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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.4.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@skein-js/test-support": "0.
|
|
46
|
+
"@skein-js/test-support": "0.4.0"
|
|
47
47
|
},
|
|
48
48
|
"publishConfig": {
|
|
49
49
|
"access": "public"
|