@ventually/memory 0.0.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/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # `@ventually/memory`
2
+
3
+ In-memory adapter for Eventually queues.
4
+
5
+ ## Usage
6
+
7
+ ```ts
8
+ import { MemoryAdapter } from "@ventually/memory";
9
+ import { Queue, Worker } from "@ventually/core";
10
+
11
+ const adapter = new MemoryAdapter();
12
+ ```
13
+
14
+ Works in both browser and server runtimes. State is instance-local and not persistent.
15
+
16
+ ## Dev
17
+
18
+ Typecheck:
19
+
20
+ ```bash
21
+ bun --filter @ventually/memory check-types
22
+ ```
23
+
24
+ Build:
25
+
26
+ ```bash
27
+ bun --filter @ventually/memory build
28
+ ```
29
+
30
+ Unit tests:
31
+
32
+ ```bash
33
+ bun --filter @ventually/memory test
34
+ ```
35
+
36
+ Stress test:
37
+
38
+ ```bash
39
+ bun --filter @ventually/memory test:stress
40
+ ```
41
+
42
+ Useful stress env vars:
43
+
44
+ - `EVENTUALLY_MEMORY_STRESS_JOBS`
45
+ - `EVENTUALLY_MEMORY_STRESS_WORKERS`
46
+ - `EVENTUALLY_MEMORY_STRESS_CONCURRENCY`
47
+ - `EVENTUALLY_MEMORY_STRESS_TIMEOUT_MS`
48
+ - `EVENTUALLY_MEMORY_STRESS_POLL_INTERVAL_MS`
49
+ - `EVENTUALLY_MEMORY_STRESS_STAGGER_MS`
50
+
51
+ Example:
52
+
53
+ ```bash
54
+ EVENTUALLY_MEMORY_STRESS_JOBS=5000 \
55
+ EVENTUALLY_MEMORY_STRESS_WORKERS=8 \
56
+ EVENTUALLY_MEMORY_STRESS_CONCURRENCY=16 \
57
+ bun --filter @ventually/memory test:stress
58
+ ```
59
+
60
+ ## Notes
61
+
62
+ - `close()` is non-destructive and does not clear queue state.
63
+ - Use `reset()` when you want to explicitly clear all in-memory state.
@@ -0,0 +1,3 @@
1
+ export { MemoryAdapter } from "./memory-adapter.js";
2
+ export type { MemoryAdapterOptions, MemoryJobSnapshot, MemoryQueueSnapshot, } from "./memory-adapter.js";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,YAAY,EACV,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,qBAAqB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { MemoryAdapter } from "./memory-adapter.js";
@@ -0,0 +1,53 @@
1
+ import type { AdapterClaimOptions, AdapterEnqueueRequest, AdapterFailureRecord, AdapterJobRecord, IQueueAdapter } from "@ventually/core";
2
+ export interface MemoryAdapterOptions {
3
+ now?: () => number;
4
+ }
5
+ export interface MemoryJobSnapshot extends AdapterJobRecord {
6
+ availableAt: number;
7
+ }
8
+ export interface MemoryQueueSnapshot {
9
+ name: string;
10
+ counts: Record<AdapterJobRecord["state"], number>;
11
+ jobs: MemoryJobSnapshot[];
12
+ }
13
+ export declare class MemoryAdapter implements IQueueAdapter {
14
+ private readonly queues;
15
+ private readonly baseNow;
16
+ private offsetMs;
17
+ private sequence;
18
+ constructor(options?: MemoryAdapterOptions);
19
+ now(): number;
20
+ advanceTime(ms: number): void;
21
+ reset(): void;
22
+ getQueueNames(): string[];
23
+ getQueueSnapshot(queueName: string): MemoryQueueSnapshot;
24
+ getAllQueuesSnapshot(): MemoryQueueSnapshot[];
25
+ enqueue(request: AdapterEnqueueRequest): Promise<AdapterJobRecord>;
26
+ enqueueMany(requests: AdapterEnqueueRequest[]): Promise<AdapterJobRecord[]>;
27
+ getJob(queueName: string, jobId: string): Promise<AdapterJobRecord | null>;
28
+ claimNext(queueName: string, options: AdapterClaimOptions): Promise<AdapterJobRecord[]>;
29
+ complete(queueName: string, jobId: string, lockToken: string, result: unknown, finishedAt: number, keep: {
30
+ mode: "all" | "none" | "count";
31
+ count?: number;
32
+ }): Promise<void>;
33
+ fail(queueName: string, jobId: string, lockToken: string, failure: AdapterFailureRecord, finishedAt: number, retryAt: number | null, keep: {
34
+ mode: "all" | "none" | "count";
35
+ count?: number;
36
+ }): Promise<void>;
37
+ updateProgress(queueName: string, jobId: string, lockToken: string, progress: unknown, updatedAt: number): Promise<void>;
38
+ recoverStalled(queueName: string, now: number, lockTtlMs: number, limit: number): Promise<AdapterJobRecord[]>;
39
+ close(): Promise<void>;
40
+ private enqueueInternal;
41
+ private getStore;
42
+ private pushWaiting;
43
+ private pushDelayed;
44
+ private promoteDueJobs;
45
+ private requireJob;
46
+ private assertLock;
47
+ private pushTerminal;
48
+ private applyKeepPolicy;
49
+ private resolveParent;
50
+ private failParent;
51
+ private finalizeParentFailure;
52
+ }
53
+ //# sourceMappingURL=memory-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-adapter.d.ts","sourceRoot":"","sources":["../src/memory-adapter.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,519 @@
1
+ class BinaryHeap {
2
+ compare;
3
+ values = [];
4
+ constructor(compare) {
5
+ this.compare = compare;
6
+ }
7
+ push(value) {
8
+ this.values.push(value);
9
+ this.bubbleUp(this.values.length - 1);
10
+ }
11
+ peek() {
12
+ return this.values[0];
13
+ }
14
+ pop() {
15
+ const first = this.values[0];
16
+ const last = this.values.pop();
17
+ if (last === undefined) {
18
+ return undefined;
19
+ }
20
+ if (this.values.length === 0) {
21
+ return last;
22
+ }
23
+ this.values[0] = last;
24
+ this.bubbleDown(0);
25
+ return first;
26
+ }
27
+ get size() {
28
+ return this.values.length;
29
+ }
30
+ clear() {
31
+ this.values.length = 0;
32
+ }
33
+ bubbleUp(index) {
34
+ let current = index;
35
+ while (current > 0) {
36
+ const parent = Math.floor((current - 1) / 2);
37
+ if (this.compare(this.values[current], this.values[parent]) >= 0) {
38
+ break;
39
+ }
40
+ this.swap(current, parent);
41
+ current = parent;
42
+ }
43
+ }
44
+ bubbleDown(index) {
45
+ let current = index;
46
+ while (true) {
47
+ const left = current * 2 + 1;
48
+ const right = left + 1;
49
+ let next = current;
50
+ if (left < this.values.length &&
51
+ this.compare(this.values[left], this.values[next]) < 0) {
52
+ next = left;
53
+ }
54
+ if (right < this.values.length &&
55
+ this.compare(this.values[right], this.values[next]) < 0) {
56
+ next = right;
57
+ }
58
+ if (next === current) {
59
+ return;
60
+ }
61
+ this.swap(current, next);
62
+ current = next;
63
+ }
64
+ }
65
+ swap(left, right) {
66
+ const current = this.values[left];
67
+ this.values[left] = this.values[right];
68
+ this.values[right] = current;
69
+ }
70
+ }
71
+ function cloneRecord(record) {
72
+ return {
73
+ ...record,
74
+ backoff: record.backoff ? { ...record.backoff } : null,
75
+ removeOnComplete: { ...record.removeOnComplete },
76
+ removeOnFail: { ...record.removeOnFail },
77
+ };
78
+ }
79
+ function computeRetryAt(record, now) {
80
+ if (!record.backoff) {
81
+ return now;
82
+ }
83
+ if (record.backoff.type === "fixed") {
84
+ return now + record.backoff.delayMs;
85
+ }
86
+ const delay = record.backoff.delayMs * 2 ** Math.max(0, record.attempt - 1);
87
+ return now + Math.min(delay, record.backoff.maxDelayMs ?? delay);
88
+ }
89
+ function createQueueStore() {
90
+ return {
91
+ jobs: new Map(),
92
+ waiting: new BinaryHeap((left, right) => {
93
+ if (left.priority !== right.priority) {
94
+ return right.priority - left.priority;
95
+ }
96
+ return left.sequence - right.sequence;
97
+ }),
98
+ delayed: new BinaryHeap((left, right) => {
99
+ if (left.availableAt !== right.availableAt) {
100
+ return left.availableAt - right.availableAt;
101
+ }
102
+ return left.sequence - right.sequence;
103
+ }),
104
+ active: new BinaryHeap((left, right) => {
105
+ if (left.availableAt !== right.availableAt) {
106
+ return left.availableAt - right.availableAt;
107
+ }
108
+ return left.sequence - right.sequence;
109
+ }),
110
+ activeExpiries: new Map(),
111
+ completed: [],
112
+ completedHead: 0,
113
+ failed: [],
114
+ failedHead: 0,
115
+ };
116
+ }
117
+ export class MemoryAdapter {
118
+ queues = new Map();
119
+ baseNow;
120
+ offsetMs = 0;
121
+ sequence = 0;
122
+ constructor(options = {}) {
123
+ this.baseNow = options.now ?? Date.now;
124
+ }
125
+ now() {
126
+ return this.baseNow() + this.offsetMs;
127
+ }
128
+ advanceTime(ms) {
129
+ this.offsetMs += ms;
130
+ }
131
+ reset() {
132
+ this.queues.clear();
133
+ this.offsetMs = 0;
134
+ this.sequence = 0;
135
+ }
136
+ getQueueNames() {
137
+ return [...this.queues.keys()].sort();
138
+ }
139
+ getQueueSnapshot(queueName) {
140
+ const store = this.getStore(queueName);
141
+ const jobs = [...store.jobs.values()]
142
+ .map((record) => ({
143
+ ...cloneRecord(record),
144
+ availableAt: record.availableAt,
145
+ }))
146
+ .sort((left, right) => left.createdAt - right.createdAt);
147
+ return {
148
+ name: queueName,
149
+ counts: {
150
+ waiting: jobs.filter((job) => job.state === "waiting").length,
151
+ blocked: jobs.filter((job) => job.state === "blocked").length,
152
+ active: jobs.filter((job) => job.state === "active").length,
153
+ delayed: jobs.filter((job) => job.state === "delayed").length,
154
+ completed: jobs.filter((job) => job.state === "completed").length,
155
+ failed: jobs.filter((job) => job.state === "failed").length,
156
+ },
157
+ jobs,
158
+ };
159
+ }
160
+ getAllQueuesSnapshot() {
161
+ return this.getQueueNames().map((queueName) => this.getQueueSnapshot(queueName));
162
+ }
163
+ async enqueue(request) {
164
+ return this.enqueueInternal(request);
165
+ }
166
+ async enqueueMany(requests) {
167
+ return requests.map((request) => this.enqueueInternal(request));
168
+ }
169
+ async getJob(queueName, jobId) {
170
+ const store = this.queues.get(queueName);
171
+ const record = store?.jobs.get(jobId);
172
+ return record ? cloneRecord(record) : null;
173
+ }
174
+ async claimNext(queueName, options) {
175
+ const store = this.getStore(queueName);
176
+ this.promoteDueJobs(store, options.now);
177
+ const claimed = [];
178
+ while (claimed.length < options.limit) {
179
+ const entry = store.waiting.pop();
180
+ if (!entry) {
181
+ break;
182
+ }
183
+ const record = store.jobs.get(entry.id);
184
+ if (!record ||
185
+ record.state !== "waiting" ||
186
+ record.sequence !== entry.sequence ||
187
+ record.priority !== entry.priority) {
188
+ continue;
189
+ }
190
+ record.state = "active";
191
+ record.attempt += 1;
192
+ record.processedAt ??= options.now;
193
+ record.lockToken = `${record.id}:${record.attempt}:${options.now}`;
194
+ record.availableAt = options.now + options.lockTtlMs;
195
+ store.activeExpiries.set(record.id, record.availableAt);
196
+ store.active.push({
197
+ id: record.id,
198
+ availableAt: record.availableAt,
199
+ sequence: record.sequence,
200
+ });
201
+ claimed.push(cloneRecord(record));
202
+ }
203
+ return claimed;
204
+ }
205
+ async complete(queueName, jobId, lockToken, result, finishedAt, keep) {
206
+ const record = this.requireJob(queueName, jobId);
207
+ this.assertLock(record, lockToken);
208
+ const store = this.getStore(queueName);
209
+ store.activeExpiries.delete(jobId);
210
+ record.state = "completed";
211
+ record.finishedAt = finishedAt;
212
+ record.result = result;
213
+ record.lockToken = null;
214
+ this.pushTerminal(store, "completed", record.id, finishedAt);
215
+ this.applyKeepPolicy(store, "completed", keep, record.id);
216
+ this.resolveParent(record, finishedAt);
217
+ }
218
+ async fail(queueName, jobId, lockToken, failure, finishedAt, retryAt, keep) {
219
+ const record = this.requireJob(queueName, jobId);
220
+ this.assertLock(record, lockToken);
221
+ const store = this.getStore(queueName);
222
+ store.activeExpiries.delete(jobId);
223
+ record.failedReason = failure.message;
224
+ record.stack = failure.stack;
225
+ record.result = null;
226
+ record.finishedAt = retryAt === null ? finishedAt : null;
227
+ record.lockToken = null;
228
+ if (retryAt !== null) {
229
+ record.availableAt = retryAt;
230
+ if (retryAt <= finishedAt) {
231
+ record.state = "waiting";
232
+ this.pushWaiting(store, record);
233
+ }
234
+ else {
235
+ record.state = "delayed";
236
+ this.pushDelayed(store, record);
237
+ }
238
+ return;
239
+ }
240
+ record.state = "failed";
241
+ this.pushTerminal(store, "failed", record.id, finishedAt);
242
+ this.applyKeepPolicy(store, "failed", keep, record.id);
243
+ this.failParent(record, failure.message, finishedAt);
244
+ }
245
+ async updateProgress(queueName, jobId, lockToken, progress, updatedAt) {
246
+ const record = this.requireJob(queueName, jobId);
247
+ this.assertLock(record, lockToken);
248
+ record.progress = progress;
249
+ record.availableAt = Math.max(record.availableAt, updatedAt);
250
+ const store = this.getStore(queueName);
251
+ store.activeExpiries.set(record.id, record.availableAt);
252
+ store.active.push({
253
+ id: record.id,
254
+ availableAt: record.availableAt,
255
+ sequence: record.sequence,
256
+ });
257
+ }
258
+ async recoverStalled(queueName, now, lockTtlMs, limit) {
259
+ void lockTtlMs;
260
+ const store = this.getStore(queueName);
261
+ const recovered = [];
262
+ while (recovered.length < limit) {
263
+ const entry = store.active.peek();
264
+ if (!entry || entry.availableAt > now) {
265
+ break;
266
+ }
267
+ store.active.pop();
268
+ const record = store.jobs.get(entry.id);
269
+ if (!record ||
270
+ record.state !== "active" ||
271
+ record.sequence !== entry.sequence ||
272
+ store.activeExpiries.get(record.id) !== entry.availableAt) {
273
+ continue;
274
+ }
275
+ store.activeExpiries.delete(record.id);
276
+ record.lockToken = null;
277
+ if (record.attempt < record.attempts) {
278
+ const retryAt = computeRetryAt(record, now);
279
+ record.availableAt = retryAt;
280
+ if (retryAt <= now) {
281
+ record.state = "waiting";
282
+ this.pushWaiting(store, record);
283
+ }
284
+ else {
285
+ record.state = "delayed";
286
+ this.pushDelayed(store, record);
287
+ }
288
+ }
289
+ else {
290
+ record.state = "failed";
291
+ record.finishedAt = now;
292
+ this.pushTerminal(store, "failed", record.id, now);
293
+ this.applyKeepPolicy(store, "failed", record.removeOnFail, record.id);
294
+ this.failParent(record, record.failedReason ?? "stalled job failed", now);
295
+ }
296
+ recovered.push(cloneRecord(record));
297
+ }
298
+ return recovered;
299
+ }
300
+ async close() {
301
+ return;
302
+ }
303
+ enqueueInternal(request) {
304
+ const store = this.getStore(request.queueName);
305
+ const existing = store.jobs.get(request.id);
306
+ if (existing) {
307
+ return cloneRecord(existing);
308
+ }
309
+ const record = {
310
+ id: request.id,
311
+ queueName: request.queueName,
312
+ name: request.name,
313
+ data: request.data,
314
+ state: (request.pendingChildren ?? 0) > 0
315
+ ? "blocked"
316
+ : request.delay > 0
317
+ ? "delayed"
318
+ : "waiting",
319
+ attempt: 0,
320
+ attempts: request.attempts,
321
+ priority: request.priority,
322
+ delay: request.delay,
323
+ createdAt: request.createdAt,
324
+ processedAt: null,
325
+ finishedAt: null,
326
+ result: null,
327
+ failedReason: null,
328
+ stack: null,
329
+ progress: null,
330
+ lockToken: null,
331
+ backoff: request.backoff ?? null,
332
+ removeOnComplete: request.removeOnComplete,
333
+ removeOnFail: request.removeOnFail,
334
+ parentKey: request.parentKey ?? null,
335
+ pendingChildren: request.pendingChildren ?? 0,
336
+ childFailure: null,
337
+ sequence: this.sequence++,
338
+ availableAt: request.createdAt + request.delay,
339
+ };
340
+ store.jobs.set(record.id, record);
341
+ if (record.state === "waiting") {
342
+ this.pushWaiting(store, record);
343
+ }
344
+ else if (record.state === "delayed") {
345
+ this.pushDelayed(store, record);
346
+ }
347
+ return cloneRecord(record);
348
+ }
349
+ getStore(queueName) {
350
+ let store = this.queues.get(queueName);
351
+ if (!store) {
352
+ store = createQueueStore();
353
+ this.queues.set(queueName, store);
354
+ }
355
+ return store;
356
+ }
357
+ pushWaiting(store, record) {
358
+ store.waiting.push({
359
+ id: record.id,
360
+ priority: record.priority,
361
+ sequence: record.sequence,
362
+ });
363
+ }
364
+ pushDelayed(store, record) {
365
+ store.delayed.push({
366
+ id: record.id,
367
+ availableAt: record.availableAt,
368
+ sequence: record.sequence,
369
+ });
370
+ }
371
+ promoteDueJobs(store, now) {
372
+ while (true) {
373
+ const entry = store.delayed.peek();
374
+ if (!entry || entry.availableAt > now) {
375
+ return;
376
+ }
377
+ store.delayed.pop();
378
+ const record = store.jobs.get(entry.id);
379
+ if (!record ||
380
+ record.state !== "delayed" ||
381
+ record.sequence !== entry.sequence ||
382
+ record.availableAt !== entry.availableAt) {
383
+ continue;
384
+ }
385
+ record.state = "waiting";
386
+ this.pushWaiting(store, record);
387
+ }
388
+ }
389
+ requireJob(queueName, jobId) {
390
+ const store = this.queues.get(queueName);
391
+ const record = store?.jobs.get(jobId);
392
+ if (!record) {
393
+ throw new Error(`Job ${jobId} was not found in queue ${queueName}.`);
394
+ }
395
+ return record;
396
+ }
397
+ assertLock(record, lockToken) {
398
+ if (record.lockToken !== lockToken) {
399
+ throw new Error(`Job ${record.id} lock token mismatch.`);
400
+ }
401
+ }
402
+ pushTerminal(store, kind, jobId, finishedAt) {
403
+ const record = store.jobs.get(jobId);
404
+ if (!record) {
405
+ return;
406
+ }
407
+ record.finishedAt = finishedAt;
408
+ if (kind === "completed") {
409
+ store.completed.push(jobId);
410
+ }
411
+ else {
412
+ store.failed.push(jobId);
413
+ }
414
+ }
415
+ applyKeepPolicy(store, kind, keep, currentJobId) {
416
+ if (keep.mode === "all") {
417
+ return;
418
+ }
419
+ const entries = kind === "completed" ? store.completed : store.failed;
420
+ const headKey = kind === "completed" ? "completedHead" : "failedHead";
421
+ if (keep.mode === "none") {
422
+ store.jobs.delete(currentJobId);
423
+ if (entries.length > 0) {
424
+ entries[entries.length - 1] = "";
425
+ }
426
+ while (entries[store[headKey]] === "") {
427
+ if (headKey === "completedHead") {
428
+ store.completedHead += 1;
429
+ }
430
+ else {
431
+ store.failedHead += 1;
432
+ }
433
+ }
434
+ return;
435
+ }
436
+ const count = keep.count ?? 0;
437
+ while (entries.length - store[headKey] > count) {
438
+ const removed = entries[store[headKey]];
439
+ if (headKey === "completedHead") {
440
+ store.completedHead += 1;
441
+ }
442
+ else {
443
+ store.failedHead += 1;
444
+ }
445
+ if (removed) {
446
+ store.jobs.delete(removed);
447
+ }
448
+ }
449
+ if (store[headKey] > 256 && store[headKey] > entries.length / 2) {
450
+ if (kind === "completed") {
451
+ store.completed = entries.slice(store.completedHead);
452
+ store.completedHead = 0;
453
+ }
454
+ else {
455
+ store.failed = entries.slice(store.failedHead);
456
+ store.failedHead = 0;
457
+ }
458
+ }
459
+ }
460
+ resolveParent(record, finishedAt) {
461
+ if (!record.parentKey) {
462
+ return;
463
+ }
464
+ const [parentQueueName, parentJobId] = record.parentKey.split(":");
465
+ if (!parentQueueName || !parentJobId) {
466
+ return;
467
+ }
468
+ const parent = this.queues.get(parentQueueName)?.jobs.get(parentJobId);
469
+ if (!parent) {
470
+ return;
471
+ }
472
+ parent.pendingChildren = Math.max(0, parent.pendingChildren - 1);
473
+ if (parent.pendingChildren > 0) {
474
+ return;
475
+ }
476
+ if (parent.childFailure) {
477
+ this.finalizeParentFailure(parent, parent.childFailure, finishedAt);
478
+ return;
479
+ }
480
+ const store = this.getStore(parent.queueName);
481
+ if (parent.delay > 0 && parent.createdAt + parent.delay > finishedAt) {
482
+ parent.state = "delayed";
483
+ parent.availableAt = parent.createdAt + parent.delay;
484
+ this.pushDelayed(store, parent);
485
+ return;
486
+ }
487
+ parent.state = "waiting";
488
+ this.pushWaiting(store, parent);
489
+ }
490
+ failParent(record, reason, finishedAt) {
491
+ if (!record.parentKey) {
492
+ return;
493
+ }
494
+ const [parentQueueName, parentJobId] = record.parentKey.split(":");
495
+ if (!parentQueueName || !parentJobId) {
496
+ return;
497
+ }
498
+ const parent = this.queues.get(parentQueueName)?.jobs.get(parentJobId);
499
+ if (!parent) {
500
+ return;
501
+ }
502
+ parent.pendingChildren = Math.max(0, parent.pendingChildren - 1);
503
+ parent.childFailure = reason;
504
+ if (parent.pendingChildren === 0) {
505
+ this.finalizeParentFailure(parent, reason, finishedAt);
506
+ }
507
+ }
508
+ finalizeParentFailure(parent, reason, finishedAt) {
509
+ const store = this.getStore(parent.queueName);
510
+ store.activeExpiries.delete(parent.id);
511
+ parent.state = "failed";
512
+ parent.finishedAt = finishedAt;
513
+ parent.failedReason = reason;
514
+ parent.lockToken = null;
515
+ this.pushTerminal(store, "failed", parent.id, finishedAt);
516
+ this.applyKeepPolicy(store, "failed", parent.removeOnFail, parent.id);
517
+ this.failParent(parent, reason, finishedAt);
518
+ }
519
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@ventually/memory",
3
+ "version": "0.0.0",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/index.d.ts",
8
+ "default": "./dist/index.js"
9
+ }
10
+ },
11
+ "main": "./dist/index.js",
12
+ "types": "./dist/index.d.ts",
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "scripts": {
20
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
21
+ "check-types": "tsc --noEmit -p tsconfig.json",
22
+ "test": "vitest run --project unit",
23
+ "test:stress": "bun run ./test/memory-adapter.stress.ts"
24
+ },
25
+ "dependencies": {
26
+ "@ventually/core": "*"
27
+ },
28
+ "devDependencies": {
29
+ "@repo/typescript-config": "*",
30
+ "@types/node": "^22.15.3",
31
+ "typescript": "5.9.2",
32
+ "vitest": "^3.2.4"
33
+ }
34
+ }