@equationalapplications/prisma-outbox 4.9.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/LICENSE +21 -0
- package/README.md +93 -0
- package/dist/index.d.mts +46 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +86 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +84 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Equational Applications
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# @equationalapplications/prisma-outbox
|
|
2
|
+
|
|
3
|
+
Prisma adapter for the [expo-llm-wiki](https://github.com/equationalapplications/expo-llm-wiki) transactional outbox pattern.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@equationalapplications/prisma-outbox)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
Polls the SQLite outbox table written by `@equationalapplications/core-llm-wiki` and syncs events to your Prisma-backed system inside a Prisma transaction, with configurable batch size, poll interval, error handling, and a concurrency guard.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install @equationalapplications/prisma-outbox
|
|
15
|
+
# peer deps
|
|
16
|
+
npm install @equationalapplications/core-llm-wiki @prisma/client
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { PrismaOutboxWorker } from '@equationalapplications/prisma-outbox';
|
|
23
|
+
import { WikiMemory } from '@equationalapplications/core-llm-wiki';
|
|
24
|
+
import { PrismaClient } from '@prisma/client';
|
|
25
|
+
|
|
26
|
+
const wiki = new WikiMemory(db, {
|
|
27
|
+
llmProvider,
|
|
28
|
+
config: { enableOutbox: true },
|
|
29
|
+
});
|
|
30
|
+
await wiki.setup();
|
|
31
|
+
|
|
32
|
+
const prisma = new PrismaClient();
|
|
33
|
+
|
|
34
|
+
const worker = new PrismaOutboxWorker({
|
|
35
|
+
wikiMemory: wiki,
|
|
36
|
+
prisma,
|
|
37
|
+
mapEvent: async (event, tx) => {
|
|
38
|
+
// mapEvent must be idempotent: at-least-once delivery means the same event
|
|
39
|
+
// can be retried if acknowledgement fails after the Prisma transaction commits.
|
|
40
|
+
if (event.operation === 'INSERT' && event.table_name.includes('entries')) {
|
|
41
|
+
await tx.wikiEntry.upsert({
|
|
42
|
+
where: { id: event.record_id },
|
|
43
|
+
create: { id: event.record_id, ...(event.payload as Record<string, unknown>) },
|
|
44
|
+
update: {},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
pollIntervalMs: 5000,
|
|
49
|
+
batchSize: 100,
|
|
50
|
+
onError: (err, event) => {
|
|
51
|
+
console.error('Outbox event failed', event.id, err);
|
|
52
|
+
return false; // halt to preserve ordering; return true to skip poison-pill
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
worker.start();
|
|
57
|
+
|
|
58
|
+
// On shutdown:
|
|
59
|
+
worker.stop();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API
|
|
63
|
+
|
|
64
|
+
### `PrismaOutboxWorker`
|
|
65
|
+
|
|
66
|
+
| Method | Description |
|
|
67
|
+
|--------|-------------|
|
|
68
|
+
| `start()` | Begins polling on the configured interval. Idempotent. |
|
|
69
|
+
| `stop()` | Clears the poll interval and any pending backlog timeout. |
|
|
70
|
+
| `syncBatch()` | Manually trigger one poll cycle (useful for testing). |
|
|
71
|
+
|
|
72
|
+
### `PrismaOutboxConfig`
|
|
73
|
+
|
|
74
|
+
| Field | Type | Default | Description |
|
|
75
|
+
|-------|------|---------|-------------|
|
|
76
|
+
| `wikiMemory` | `WikiMemory` | required | The `WikiMemory` instance to poll. |
|
|
77
|
+
| `prisma` | `PrismaLike<TTx>` | required | Any Prisma client with a `$transaction` method (your generated `PrismaClient` satisfies this). |
|
|
78
|
+
| `mapEvent` | `(event, tx: TTx) => Promise<void>` | required | Maps one outbox event to Prisma operations inside a transaction. `tx` is inferred from your `PrismaClient`. |
|
|
79
|
+
| `batchSize` | `number` | `100` | Max events fetched per cycle. |
|
|
80
|
+
| `pollIntervalMs` | `number` | `5000` | Milliseconds between poll cycles. |
|
|
81
|
+
| `onError` | `(err, event) => boolean \| undefined` | — | Return `true` to skip a failing event; `false`/`undefined` to halt. |
|
|
82
|
+
| `onWorkerError` | `(err: Error) => void` | — | Called for worker-level errors (SQLite read/ack failures) not delivered to `onError`. |
|
|
83
|
+
|
|
84
|
+
## How it works
|
|
85
|
+
|
|
86
|
+
1. Every `WikiMemory` mutation (when `enableOutbox: true`) atomically writes an event to the SQLite `outbox` table in the same transaction as the domain write.
|
|
87
|
+
2. `PrismaOutboxWorker` polls `getUnprocessedOutboxEvents()` and calls `mapEvent` inside a Prisma transaction for each event.
|
|
88
|
+
3. Successfully processed event IDs are passed to `markOutboxEventsProcessed()`, which deletes them from SQLite.
|
|
89
|
+
4. If a full batch is consumed without error, an immediate follow-up cycle runs (backlog optimization) to drain queues faster than the poll interval.
|
|
90
|
+
|
|
91
|
+
## Limitations
|
|
92
|
+
|
|
93
|
+
- **Single-instance only.** The worker does not use row-level locking or leases. Running two `PrismaOutboxWorker` instances against the same SQLite file will cause duplicate Prisma writes. Run exactly one worker per SQLite database. `mapEvent` must still be idempotent to tolerate at-least-once delivery (acknowledgement can fail after a successful Prisma commit).
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { WikiMemory, WikiOutboxEvent } from '@equationalapplications/core-llm-wiki';
|
|
2
|
+
|
|
3
|
+
/** Minimal Prisma client shape required by the worker — avoids depending on generated types. */
|
|
4
|
+
interface PrismaLike<TTx> {
|
|
5
|
+
$transaction: (fn: (tx: TTx) => Promise<void>) => Promise<unknown>;
|
|
6
|
+
}
|
|
7
|
+
interface PrismaOutboxConfig<TTx = unknown> {
|
|
8
|
+
/**
|
|
9
|
+
* The WikiMemory instance to poll for outbox events.
|
|
10
|
+
*
|
|
11
|
+
* **Singleton requirement:** run at most one `PrismaOutboxWorker` per SQLite database
|
|
12
|
+
* at a time. The in-process `running` guard prevents overlapping calls within a single
|
|
13
|
+
* worker, but two workers (or two processes) sharing the same database can fetch and
|
|
14
|
+
* process the same unclaimed rows concurrently, producing duplicate Prisma writes.
|
|
15
|
+
* If you need multi-process fan-out, add a claim/lease column to the outbox table.
|
|
16
|
+
*/
|
|
17
|
+
wikiMemory: WikiMemory;
|
|
18
|
+
prisma: PrismaLike<TTx>;
|
|
19
|
+
/**
|
|
20
|
+
* Maps one outbox event to Prisma operations executed inside a Prisma transaction.
|
|
21
|
+
* `tx` is the Prisma transaction client passed by `prisma.$transaction`.
|
|
22
|
+
*/
|
|
23
|
+
mapEvent: (event: WikiOutboxEvent, tx: TTx) => Promise<void>;
|
|
24
|
+
/** Max events fetched per poll cycle. Default: 100 */
|
|
25
|
+
batchSize?: number;
|
|
26
|
+
/** Milliseconds between poll cycles. Default: 5000 */
|
|
27
|
+
pollIntervalMs?: number;
|
|
28
|
+
/** Called when an event fails; return true to skip and continue, false/undefined to halt. */
|
|
29
|
+
onError?: (error: Error, event: WikiOutboxEvent) => boolean | undefined;
|
|
30
|
+
/** Called when a worker-level error occurs (e.g. SQLite read/ack failure). Not called for per-event errors handled by onError. */
|
|
31
|
+
onWorkerError?: (error: Error) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare class PrismaOutboxWorker<TTx = unknown> {
|
|
35
|
+
#private;
|
|
36
|
+
private config;
|
|
37
|
+
private timer?;
|
|
38
|
+
private backlogTimer?;
|
|
39
|
+
private running;
|
|
40
|
+
constructor(config: PrismaOutboxConfig<TTx>);
|
|
41
|
+
start(): void;
|
|
42
|
+
stop(): void;
|
|
43
|
+
syncBatch(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { type PrismaLike, type PrismaOutboxConfig, PrismaOutboxWorker };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { WikiMemory, WikiOutboxEvent } from '@equationalapplications/core-llm-wiki';
|
|
2
|
+
|
|
3
|
+
/** Minimal Prisma client shape required by the worker — avoids depending on generated types. */
|
|
4
|
+
interface PrismaLike<TTx> {
|
|
5
|
+
$transaction: (fn: (tx: TTx) => Promise<void>) => Promise<unknown>;
|
|
6
|
+
}
|
|
7
|
+
interface PrismaOutboxConfig<TTx = unknown> {
|
|
8
|
+
/**
|
|
9
|
+
* The WikiMemory instance to poll for outbox events.
|
|
10
|
+
*
|
|
11
|
+
* **Singleton requirement:** run at most one `PrismaOutboxWorker` per SQLite database
|
|
12
|
+
* at a time. The in-process `running` guard prevents overlapping calls within a single
|
|
13
|
+
* worker, but two workers (or two processes) sharing the same database can fetch and
|
|
14
|
+
* process the same unclaimed rows concurrently, producing duplicate Prisma writes.
|
|
15
|
+
* If you need multi-process fan-out, add a claim/lease column to the outbox table.
|
|
16
|
+
*/
|
|
17
|
+
wikiMemory: WikiMemory;
|
|
18
|
+
prisma: PrismaLike<TTx>;
|
|
19
|
+
/**
|
|
20
|
+
* Maps one outbox event to Prisma operations executed inside a Prisma transaction.
|
|
21
|
+
* `tx` is the Prisma transaction client passed by `prisma.$transaction`.
|
|
22
|
+
*/
|
|
23
|
+
mapEvent: (event: WikiOutboxEvent, tx: TTx) => Promise<void>;
|
|
24
|
+
/** Max events fetched per poll cycle. Default: 100 */
|
|
25
|
+
batchSize?: number;
|
|
26
|
+
/** Milliseconds between poll cycles. Default: 5000 */
|
|
27
|
+
pollIntervalMs?: number;
|
|
28
|
+
/** Called when an event fails; return true to skip and continue, false/undefined to halt. */
|
|
29
|
+
onError?: (error: Error, event: WikiOutboxEvent) => boolean | undefined;
|
|
30
|
+
/** Called when a worker-level error occurs (e.g. SQLite read/ack failure). Not called for per-event errors handled by onError. */
|
|
31
|
+
onWorkerError?: (error: Error) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
declare class PrismaOutboxWorker<TTx = unknown> {
|
|
35
|
+
#private;
|
|
36
|
+
private config;
|
|
37
|
+
private timer?;
|
|
38
|
+
private backlogTimer?;
|
|
39
|
+
private running;
|
|
40
|
+
constructor(config: PrismaOutboxConfig<TTx>);
|
|
41
|
+
start(): void;
|
|
42
|
+
stop(): void;
|
|
43
|
+
syncBatch(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { type PrismaLike, type PrismaOutboxConfig, PrismaOutboxWorker };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var __typeError = (msg) => {
|
|
4
|
+
throw TypeError(msg);
|
|
5
|
+
};
|
|
6
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
7
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
8
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
9
|
+
|
|
10
|
+
// src/PrismaOutboxWorker.ts
|
|
11
|
+
var _PrismaOutboxWorker_instances, workerError_fn;
|
|
12
|
+
var PrismaOutboxWorker = class {
|
|
13
|
+
constructor(config) {
|
|
14
|
+
this.config = config;
|
|
15
|
+
__privateAdd(this, _PrismaOutboxWorker_instances);
|
|
16
|
+
this.running = false;
|
|
17
|
+
}
|
|
18
|
+
start() {
|
|
19
|
+
if (this.timer) return;
|
|
20
|
+
const rawInterval = this.config.pollIntervalMs ?? 5e3;
|
|
21
|
+
const pollIntervalMs = Number.isFinite(rawInterval) && rawInterval >= 1 ? Math.trunc(rawInterval) : 5e3;
|
|
22
|
+
this.timer = setInterval(
|
|
23
|
+
() => {
|
|
24
|
+
this.syncBatch().catch((err) => __privateMethod(this, _PrismaOutboxWorker_instances, workerError_fn).call(this, err));
|
|
25
|
+
},
|
|
26
|
+
pollIntervalMs
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
stop() {
|
|
30
|
+
clearInterval(this.timer);
|
|
31
|
+
this.timer = void 0;
|
|
32
|
+
clearTimeout(this.backlogTimer);
|
|
33
|
+
this.backlogTimer = void 0;
|
|
34
|
+
}
|
|
35
|
+
async syncBatch() {
|
|
36
|
+
if (this.running) return;
|
|
37
|
+
this.running = true;
|
|
38
|
+
try {
|
|
39
|
+
const rawSize = this.config.batchSize ?? 100;
|
|
40
|
+
const batchSize = Number.isFinite(rawSize) && rawSize >= 1 ? Math.trunc(rawSize) : 100;
|
|
41
|
+
const events = await this.config.wikiMemory.getUnprocessedOutboxEvents(batchSize);
|
|
42
|
+
if (events.length === 0) return;
|
|
43
|
+
const processedIds = [];
|
|
44
|
+
let halted = false;
|
|
45
|
+
for (const event of events) {
|
|
46
|
+
try {
|
|
47
|
+
await this.config.prisma.$transaction((tx) => this.config.mapEvent(event, tx));
|
|
48
|
+
processedIds.push(event.id);
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
51
|
+
let skip = false;
|
|
52
|
+
try {
|
|
53
|
+
skip = this.config.onError?.(error, event) ?? false;
|
|
54
|
+
} catch {
|
|
55
|
+
halted = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
if (skip) {
|
|
59
|
+
processedIds.push(event.id);
|
|
60
|
+
} else {
|
|
61
|
+
halted = true;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await this.config.wikiMemory.markOutboxEventsProcessed(processedIds);
|
|
67
|
+
if (!halted && events.length === batchSize && this.timer !== void 0) {
|
|
68
|
+
clearTimeout(this.backlogTimer);
|
|
69
|
+
this.backlogTimer = setTimeout(() => {
|
|
70
|
+
this.syncBatch().catch((err) => __privateMethod(this, _PrismaOutboxWorker_instances, workerError_fn).call(this, err));
|
|
71
|
+
}, 0);
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
this.running = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
_PrismaOutboxWorker_instances = new WeakSet();
|
|
79
|
+
workerError_fn = function(err) {
|
|
80
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
81
|
+
this.config.onWorkerError?.(error);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
exports.PrismaOutboxWorker = PrismaOutboxWorker;
|
|
85
|
+
//# sourceMappingURL=index.js.map
|
|
86
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/PrismaOutboxWorker.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["import type { PrismaOutboxConfig } from './types';\n\nexport class PrismaOutboxWorker<TTx = unknown> {\n private timer?: ReturnType<typeof setInterval>;\n private backlogTimer?: ReturnType<typeof setTimeout>;\n private running = false;\n\n constructor(private config: PrismaOutboxConfig<TTx>) {}\n\n start(): void {\n if (this.timer) return;\n const rawInterval = this.config.pollIntervalMs ?? 5000;\n const pollIntervalMs = Number.isFinite(rawInterval) && rawInterval >= 1 ? Math.trunc(rawInterval) : 5000;\n this.timer = setInterval(\n () => { this.syncBatch().catch(err => this.#workerError(err)); },\n pollIntervalMs\n );\n }\n\n #workerError(err: unknown): void {\n const error = err instanceof Error ? err : new Error(String(err));\n this.config.onWorkerError?.(error);\n }\n\n stop(): void {\n clearInterval(this.timer);\n this.timer = undefined;\n clearTimeout(this.backlogTimer);\n this.backlogTimer = undefined;\n }\n\n async syncBatch(): Promise<void> {\n if (this.running) return;\n this.running = true;\n try {\n const rawSize = this.config.batchSize ?? 100;\n const batchSize = Number.isFinite(rawSize) && rawSize >= 1 ? Math.trunc(rawSize) : 100;\n const events = await this.config.wikiMemory.getUnprocessedOutboxEvents(batchSize);\n if (events.length === 0) return;\n\n const processedIds: string[] = [];\n let halted = false;\n\n for (const event of events) {\n try {\n await this.config.prisma.$transaction(tx => this.config.mapEvent(event, tx));\n processedIds.push(event.id);\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n let skip = false;\n try {\n skip = this.config.onError?.(error, event) ?? false;\n } catch {\n // thrown handler treated as halt; processedIds acknowledged below\n halted = true;\n break;\n }\n if (skip) {\n processedIds.push(event.id); // acknowledge so the event isn't re-fetched\n } else {\n halted = true;\n break; // halt to preserve ordering\n }\n }\n }\n\n await this.config.wikiMemory.markOutboxEventsProcessed(processedIds);\n\n // Backlog optimization: full batch without halt means more events likely waiting.\n // Only schedule when worker is still running (stop() not called) to avoid post-stop leaks.\n // Use setTimeout(0) instead of setImmediate for React Native / Hermes compatibility.\n if (!halted && events.length === batchSize && this.timer !== undefined) {\n clearTimeout(this.backlogTimer);\n this.backlogTimer = setTimeout(() => { this.syncBatch().catch(err => this.#workerError(err)); }, 0);\n }\n } finally {\n this.running = false;\n }\n }\n}\n"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
var __typeError = (msg) => {
|
|
2
|
+
throw TypeError(msg);
|
|
3
|
+
};
|
|
4
|
+
var __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg);
|
|
5
|
+
var __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value);
|
|
6
|
+
var __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method);
|
|
7
|
+
|
|
8
|
+
// src/PrismaOutboxWorker.ts
|
|
9
|
+
var _PrismaOutboxWorker_instances, workerError_fn;
|
|
10
|
+
var PrismaOutboxWorker = class {
|
|
11
|
+
constructor(config) {
|
|
12
|
+
this.config = config;
|
|
13
|
+
__privateAdd(this, _PrismaOutboxWorker_instances);
|
|
14
|
+
this.running = false;
|
|
15
|
+
}
|
|
16
|
+
start() {
|
|
17
|
+
if (this.timer) return;
|
|
18
|
+
const rawInterval = this.config.pollIntervalMs ?? 5e3;
|
|
19
|
+
const pollIntervalMs = Number.isFinite(rawInterval) && rawInterval >= 1 ? Math.trunc(rawInterval) : 5e3;
|
|
20
|
+
this.timer = setInterval(
|
|
21
|
+
() => {
|
|
22
|
+
this.syncBatch().catch((err) => __privateMethod(this, _PrismaOutboxWorker_instances, workerError_fn).call(this, err));
|
|
23
|
+
},
|
|
24
|
+
pollIntervalMs
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
stop() {
|
|
28
|
+
clearInterval(this.timer);
|
|
29
|
+
this.timer = void 0;
|
|
30
|
+
clearTimeout(this.backlogTimer);
|
|
31
|
+
this.backlogTimer = void 0;
|
|
32
|
+
}
|
|
33
|
+
async syncBatch() {
|
|
34
|
+
if (this.running) return;
|
|
35
|
+
this.running = true;
|
|
36
|
+
try {
|
|
37
|
+
const rawSize = this.config.batchSize ?? 100;
|
|
38
|
+
const batchSize = Number.isFinite(rawSize) && rawSize >= 1 ? Math.trunc(rawSize) : 100;
|
|
39
|
+
const events = await this.config.wikiMemory.getUnprocessedOutboxEvents(batchSize);
|
|
40
|
+
if (events.length === 0) return;
|
|
41
|
+
const processedIds = [];
|
|
42
|
+
let halted = false;
|
|
43
|
+
for (const event of events) {
|
|
44
|
+
try {
|
|
45
|
+
await this.config.prisma.$transaction((tx) => this.config.mapEvent(event, tx));
|
|
46
|
+
processedIds.push(event.id);
|
|
47
|
+
} catch (err) {
|
|
48
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
49
|
+
let skip = false;
|
|
50
|
+
try {
|
|
51
|
+
skip = this.config.onError?.(error, event) ?? false;
|
|
52
|
+
} catch {
|
|
53
|
+
halted = true;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
if (skip) {
|
|
57
|
+
processedIds.push(event.id);
|
|
58
|
+
} else {
|
|
59
|
+
halted = true;
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
await this.config.wikiMemory.markOutboxEventsProcessed(processedIds);
|
|
65
|
+
if (!halted && events.length === batchSize && this.timer !== void 0) {
|
|
66
|
+
clearTimeout(this.backlogTimer);
|
|
67
|
+
this.backlogTimer = setTimeout(() => {
|
|
68
|
+
this.syncBatch().catch((err) => __privateMethod(this, _PrismaOutboxWorker_instances, workerError_fn).call(this, err));
|
|
69
|
+
}, 0);
|
|
70
|
+
}
|
|
71
|
+
} finally {
|
|
72
|
+
this.running = false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
_PrismaOutboxWorker_instances = new WeakSet();
|
|
77
|
+
workerError_fn = function(err) {
|
|
78
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
79
|
+
this.config.onWorkerError?.(error);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export { PrismaOutboxWorker };
|
|
83
|
+
//# sourceMappingURL=index.mjs.map
|
|
84
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/PrismaOutboxWorker.ts"],"names":[],"mappings":"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","file":"index.mjs","sourcesContent":["import type { PrismaOutboxConfig } from './types';\n\nexport class PrismaOutboxWorker<TTx = unknown> {\n private timer?: ReturnType<typeof setInterval>;\n private backlogTimer?: ReturnType<typeof setTimeout>;\n private running = false;\n\n constructor(private config: PrismaOutboxConfig<TTx>) {}\n\n start(): void {\n if (this.timer) return;\n const rawInterval = this.config.pollIntervalMs ?? 5000;\n const pollIntervalMs = Number.isFinite(rawInterval) && rawInterval >= 1 ? Math.trunc(rawInterval) : 5000;\n this.timer = setInterval(\n () => { this.syncBatch().catch(err => this.#workerError(err)); },\n pollIntervalMs\n );\n }\n\n #workerError(err: unknown): void {\n const error = err instanceof Error ? err : new Error(String(err));\n this.config.onWorkerError?.(error);\n }\n\n stop(): void {\n clearInterval(this.timer);\n this.timer = undefined;\n clearTimeout(this.backlogTimer);\n this.backlogTimer = undefined;\n }\n\n async syncBatch(): Promise<void> {\n if (this.running) return;\n this.running = true;\n try {\n const rawSize = this.config.batchSize ?? 100;\n const batchSize = Number.isFinite(rawSize) && rawSize >= 1 ? Math.trunc(rawSize) : 100;\n const events = await this.config.wikiMemory.getUnprocessedOutboxEvents(batchSize);\n if (events.length === 0) return;\n\n const processedIds: string[] = [];\n let halted = false;\n\n for (const event of events) {\n try {\n await this.config.prisma.$transaction(tx => this.config.mapEvent(event, tx));\n processedIds.push(event.id);\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n let skip = false;\n try {\n skip = this.config.onError?.(error, event) ?? false;\n } catch {\n // thrown handler treated as halt; processedIds acknowledged below\n halted = true;\n break;\n }\n if (skip) {\n processedIds.push(event.id); // acknowledge so the event isn't re-fetched\n } else {\n halted = true;\n break; // halt to preserve ordering\n }\n }\n }\n\n await this.config.wikiMemory.markOutboxEventsProcessed(processedIds);\n\n // Backlog optimization: full batch without halt means more events likely waiting.\n // Only schedule when worker is still running (stop() not called) to avoid post-stop leaks.\n // Use setTimeout(0) instead of setImmediate for React Native / Hermes compatibility.\n if (!halted && events.length === batchSize && this.timer !== undefined) {\n clearTimeout(this.backlogTimer);\n this.backlogTimer = setTimeout(() => { this.syncBatch().catch(err => this.#workerError(err)); }, 0);\n }\n } finally {\n this.running = false;\n }\n }\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@equationalapplications/prisma-outbox",
|
|
3
|
+
"version": "4.9.0",
|
|
4
|
+
"description": "Prisma adapter for the expo-llm-wiki transactional outbox pattern.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"require": "./dist/index.js",
|
|
12
|
+
"import": "./dist/index.mjs"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
"README.md"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"registry": "https://registry.npmjs.org"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@prisma/client": "^5.0.0 || ^6.0.0",
|
|
30
|
+
"@equationalapplications/core-llm-wiki": "4.9.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@prisma/client": "^5.0.0",
|
|
34
|
+
"prisma": "^5.0.0",
|
|
35
|
+
"tsup": "^8.0.0",
|
|
36
|
+
"typescript": "^5.4.0",
|
|
37
|
+
"vitest": "4.1.5",
|
|
38
|
+
"@equationalapplications/core-llm-wiki": "4.9.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"dev": "tsup --watch",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"typecheck": "tsc --noEmit"
|
|
46
|
+
}
|
|
47
|
+
}
|