@shivaedev/effect-changes-prisma 0.1.0 → 0.2.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/CHANGELOG.md +53 -0
- package/README.md +19 -7
- package/dist/changes.d.ts +2 -2
- package/dist/changes.d.ts.map +1 -1
- package/dist/changes.js +22 -9
- package/dist/changes.js.map +1 -1
- package/dist/error.d.ts +8 -0
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +2 -0
- package/dist/error.js.map +1 -1
- package/dist/expiry.d.ts +10 -0
- package/dist/expiry.d.ts.map +1 -0
- package/dist/expiry.js +23 -0
- package/dist/expiry.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/tables.d.ts +3 -1
- package/dist/tables.d.ts.map +1 -1
- package/dist/tables.js +14 -6
- package/dist/tables.js.map +1 -1
- package/dist/transaction.d.ts +7 -3
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +19 -6
- package/dist/transaction.js.map +1 -1
- package/package.json +2 -2
- package/src/changes.ts +25 -10
- package/src/error.ts +5 -0
- package/src/expiry.ts +49 -0
- package/src/index.ts +2 -2
- package/src/tables.ts +18 -6
- package/src/transaction.ts +27 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,58 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.0 - 2026-09-26
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `tableWrites(client)` reads the current transaction's per-table write counts
|
|
8
|
+
as a baseline for `writtenTables`.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- **Breaking:** `transaction` can fail with the new `TransactionExpired`, so its
|
|
13
|
+
error type is now `E | TransactionExpired | PrismaError`. A transaction that
|
|
14
|
+
outlives its timeout, and a savepoint start, `RELEASE` or `COMMIT` that
|
|
15
|
+
Prisma refuses with `P2028` because the transaction is closed, used to fail
|
|
16
|
+
with `PrismaError` and now fail with `TransactionExpired`. A transaction that
|
|
17
|
+
cannot start at all, such as one waiting past `maxWait` for a connection,
|
|
18
|
+
still fails with `PrismaError`, although Prisma also reports it as `P2028`.
|
|
19
|
+
Migrate by adding `TransactionExpired` wherever the error type is written
|
|
20
|
+
out: `Effect.Effect<A, PrismaError>` becomes
|
|
21
|
+
`Effect.Effect<A, TransactionExpired | PrismaError>`.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `writtenTables` reported tables that earlier transactions wrote on the same
|
|
26
|
+
pooled connection, because PostgreSQL keeps `pg_stat_xact_user_tables`
|
|
27
|
+
counters per connection until it flushes them. Take a baseline with
|
|
28
|
+
`tableWrites` when the test transaction starts and pass it as
|
|
29
|
+
`writtenTables(client, since)` to report only the tables written after it.
|
|
30
|
+
A `TRUNCATE` inside the test transaction resets its counts to the baseline,
|
|
31
|
+
so `writtenTables` does not see it.
|
|
32
|
+
- A `transaction` body kept running after Prisma expired the transaction,
|
|
33
|
+
keeping the caller waiting and free to make HTTP calls or publish while its
|
|
34
|
+
queries were refused. The body is now interrupted when the transaction's
|
|
35
|
+
timeout passes, or when one of its queries or nested `transaction`s fails
|
|
36
|
+
with `P2028` because the transaction is closed. The call then fails with
|
|
37
|
+
`TransactionExpired`, and nothing publishes. The timeout is the `timeout`
|
|
38
|
+
option, else the `transactionOptions.timeout` Prisma applies to the current
|
|
39
|
+
client, extended or not, 5 seconds unless the client sets another. It is
|
|
40
|
+
timed from the start of the transaction on the wall clock. When the client's
|
|
41
|
+
timeout cannot be read, `transaction` sets no deadline of its own, and the
|
|
42
|
+
body stops at its first query after Prisma closes the transaction. A
|
|
43
|
+
`transaction` on a transaction client is a savepoint and has no timeout of
|
|
44
|
+
its own; on any other client it is a separate transaction whose expiry does
|
|
45
|
+
not stop the body it runs in.
|
|
46
|
+
- Two copies of the package with bindings of the same name shared their
|
|
47
|
+
`Client` and expiry signal. Each binding is now keyed by a random id.
|
|
48
|
+
|
|
49
|
+
## 0.1.1 - 2026-09-26
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- Pin the `@prisma/client` peer dependency to exactly 7.9.1, the version the
|
|
54
|
+
package is tested against, instead of `^7.9.1`.
|
|
55
|
+
|
|
3
56
|
## 0.1.0 - 2026-09-26
|
|
4
57
|
|
|
5
58
|
### Added
|
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ interface PrismaChanges<Tx, A, R> {
|
|
|
52
52
|
readonly channel: Channel<A, R>;
|
|
53
53
|
readonly Client: Context.Reference<Tx>;
|
|
54
54
|
readonly use: <X>(query: (client: Tx) => PromiseLike<X>) => Effect<X, PrismaError, R>;
|
|
55
|
-
readonly transaction: <X, E, R2>(body: Effect<X, E, R2>, options?: TransactionOptions) => Effect<X, E | PrismaError, R | R2>;
|
|
55
|
+
readonly transaction: <X, E, R2>(body: Effect<X, E, R2>, options?: TransactionOptions) => Effect<X, E | TransactionExpired | PrismaError, R | R2>;
|
|
56
56
|
readonly recordWrite: (write: Write) => Effect<void, never, R>;
|
|
57
57
|
readonly Unnamed: Context.Reference<(write: UnnamedWrite) => Effect<void>>;
|
|
58
58
|
}
|
|
@@ -63,9 +63,11 @@ type UnnamedWrite =
|
|
|
63
63
|
| { readonly model: string; readonly operation: string; readonly reason: "countOnly" }
|
|
64
64
|
| { readonly model: string; readonly operation: string; readonly reason: "narrowed"; readonly field: string };
|
|
65
65
|
class PrismaError { readonly _tag: "PrismaError"; readonly cause: unknown }
|
|
66
|
+
class TransactionExpired { readonly _tag: "TransactionExpired"; readonly message: string; readonly cause?: unknown }
|
|
66
67
|
|
|
67
68
|
tablesOf(schema: string): ReadonlyMap<string, string>
|
|
68
|
-
|
|
69
|
+
tableWrites(client: RawQueryClient): Effect<TableWrites, PrismaError>
|
|
70
|
+
writtenTables(client: RawQueryClient, since?: TableWrites): Effect<ReadonlyArray<string>, PrismaError>
|
|
69
71
|
checkCoverage<A>(coverage: Coverage<A>): ReadonlyArray<CoverageViolation>
|
|
70
72
|
```
|
|
71
73
|
|
|
@@ -82,12 +84,18 @@ checkCoverage<A>(coverage: Coverage<A>): ReadonlyArray<CoverageViolation>
|
|
|
82
84
|
|
|
83
85
|
| Situation | Result |
|
|
84
86
|
| --- | --- |
|
|
87
|
+
| `$transaction` cannot start, for example because no connection frees up within `maxWait` | The body never runs; fail with `PrismaError`, even though Prisma reports it as `P2028` |
|
|
85
88
|
| The body succeeds and `$transaction` resolves | Publish once, before `transaction` returns; a savepoint merges into its parent |
|
|
86
89
|
| The body fails, dies or is interrupted | Prisma rolls back; discard; the body's failure is returned unchanged |
|
|
87
|
-
| The body
|
|
90
|
+
| The body is still running when the transaction's timeout passes | The body is interrupted at its next step; a query already in flight completes first. Prisma rolls back; discard; fail with `TransactionExpired` |
|
|
91
|
+
| A query or a nested `transaction` in the body fails because the transaction is already closed | The body is stopped, whatever it does with the error; discard; fail with `TransactionExpired` |
|
|
92
|
+
| The body succeeds but `COMMIT` finds the transaction closed | Discard; fail with `TransactionExpired` |
|
|
93
|
+
| The body succeeds but `COMMIT` fails for another reason | Discard; fail with `PrismaError` |
|
|
88
94
|
| The caller is interrupted while the body runs | The body is interrupted, Prisma rolls back, nothing publishes |
|
|
89
95
|
| The caller is interrupted while `COMMIT` is in flight | Keep waiting; publish if the database committed; the caller still sees the interruption |
|
|
90
96
|
|
|
97
|
+
The timeout is the `timeout` option, otherwise the one Prisma applies to the current client, extended or not: its `transactionOptions.timeout`, 5 seconds unless the client was constructed with another. Like Prisma's, the clock starts once the transaction has begun, and it reads wall-clock time, so a `TestClock` does not move it. A `transaction` running on a transaction client, which Prisma makes a savepoint, has no timeout of its own; the outer transaction's applies. On any other client, such as a per-request extended client or the base client provided inside a body, `transaction` starts a separate transaction with its own timeout, and that transaction closing does not stop the body it runs in. Prisma reports a closed transaction as `P2028`, for example when a test harness's own transaction, which `transaction` does not time, has expired.
|
|
98
|
+
|
|
91
99
|
A frame is a root when there is no frame for the client further out, not when the current client is the base client. A test harness can therefore run each test inside a Prisma transaction that always rolls back, provide its transaction client as `Client` without opening a frame, and the application's first `transaction` inside it publishes when its savepoint is released.
|
|
92
100
|
|
|
93
101
|
## Recording
|
|
@@ -99,15 +107,17 @@ A frame is a root when there is no frame for the client further out, not when th
|
|
|
99
107
|
|
|
100
108
|
## Coverage check
|
|
101
109
|
|
|
102
|
-
The check runs in tests.
|
|
110
|
+
The check runs in tests. Take a baseline from `pg_stat_xact_user_tables` when the test transaction starts, read the tables written since just before it rolls back, then compare them with the changes the channel observed:
|
|
103
111
|
|
|
104
112
|
```ts
|
|
105
113
|
const tables = tablesOf(await readFile("prisma/schema.prisma", "utf8"));
|
|
106
114
|
const observations: Array<Observation<Change>> = [];
|
|
107
115
|
const unnamed: Array<UnnamedWrite> = [];
|
|
116
|
+
// first, as the test transaction starts:
|
|
117
|
+
const since = yield* tableWrites(testTransaction);
|
|
108
118
|
// run the test with changes.channel.Observer and changes.Unnamed pushing into those arrays,
|
|
109
119
|
// and changes.Client set to the test's transaction client; then, before rolling back:
|
|
110
|
-
const written = yield* writtenTables(testTransaction);
|
|
120
|
+
const written = yield* writtenTables(testTransaction, since);
|
|
111
121
|
const violations = checkCoverage({
|
|
112
122
|
written,
|
|
113
123
|
tables,
|
|
@@ -119,7 +129,7 @@ const violations = checkCoverage({
|
|
|
119
129
|
```
|
|
120
130
|
|
|
121
131
|
- `tablesOf` reads the table of every model from Prisma schema text, honouring `@@map`; a model without it uses its name. Pass an explicit `Map` instead if your tables come from elsewhere.
|
|
122
|
-
- `writtenTables(client)` must run on the test's own transaction
|
|
132
|
+
- `tableWrites(client)` and `writtenTables(client, since)` must run on the test's own transaction. PostgreSQL counts inserted, updated and deleted rows, including rows written in savepoints that rolled back, but it keeps those counters per connection until it flushes them, at most once a second. On a pooled connection they still hold the writes of earlier transactions, so `writtenTables` reports only the tables whose counters grew past the `since` baseline. Without `since` it reports every table with an unflushed count. The binding cannot take the baseline for you: the test transaction is the harness's own `$transaction`, which it never sees begin.
|
|
123
133
|
- `checkCoverage` is a pure function and throws only if `covers` throws. It reports each written table whose model is not mapped to `null` and for which no `Recorded` observation satisfies `covers`, as `Unrecorded` (with `model: undefined` when no model owns the table), and each distinct unnamed write as `Unnamed`. `Recorded` includes changes whose frame was later discarded, matching the statistics.
|
|
124
134
|
- Raw SQL, relation writes nested in `data`, cascades and triggers write tables that nothing records; the check is how they surface. Filter the result if a table is written legitimately without a change.
|
|
125
135
|
|
|
@@ -127,8 +137,10 @@ const violations = checkCoverage({
|
|
|
127
137
|
|
|
128
138
|
- The client handed to `use` wraps write methods of mapped models, so their results are plain promises: pass them to `transaction`, not to the array form of `$transaction`, and do not chain fluent relation calls on them. Calling `$transaction` on that client runs outside the binding; use `transaction`.
|
|
129
139
|
- A relation write nested in `data` records only the top-level model.
|
|
130
|
-
- Prisma's interactive transaction timeout (5 seconds by default) still applies; pass `{ timeout }` to `transaction
|
|
140
|
+
- Prisma's interactive transaction timeout (5 seconds by default) still applies; pass `{ timeout }` to `transaction` for a longer one.
|
|
141
|
+
- The client's timeout is read from Prisma's internal client configuration, which Prisma `7.9.1` keeps on the client. If a later Prisma moves it, `transaction` sets no deadline of its own, and a body that outlives Prisma's timeout stops at its first query after the transaction closes; pass `{ timeout }` to have it interrupted on time.
|
|
131
142
|
- `checkCoverage` matches tables by name, not by schema.
|
|
143
|
+
- `writtenTables` does not see a `TRUNCATE` inside the test transaction: `TRUNCATE` resets the transaction's insert, update and delete counts in `pg_stat_xact_user_tables`, so they fall back to the baseline, and a table that was written and then truncated counts as unwritten.
|
|
132
144
|
- Delivery is in-process, as for every effect-changes channel.
|
|
133
145
|
|
|
134
146
|
Tested with Prisma `7.9.1` and `@prisma/adapter-pg` on PostgreSQL 18, and Effect `4.0.0-rc.112`.
|
package/dist/changes.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Channel, type ChannelOptions } from "@shivaedev/effect-changes";
|
|
2
2
|
import { Context, Effect } from "effect";
|
|
3
|
-
import { PrismaError } from "./error.ts";
|
|
3
|
+
import { PrismaError, type TransactionExpired } from "./error.ts";
|
|
4
4
|
import type { ChangeMap, Transactional, TransactionOptions } from "./model.ts";
|
|
5
5
|
import { type UnnamedWrite, type Write } from "./write.ts";
|
|
6
6
|
export type UnnamedObserver = (write: UnnamedWrite) => Effect.Effect<void>;
|
|
@@ -12,7 +12,7 @@ export interface PrismaChanges<Tx, A, R> {
|
|
|
12
12
|
readonly channel: Channel<A, R>;
|
|
13
13
|
readonly Client: Context.Reference<Tx>;
|
|
14
14
|
readonly use: <X>(query: (client: Tx) => PromiseLike<X>) => Effect.Effect<X, PrismaError, R>;
|
|
15
|
-
readonly transaction: <X, E, R2>(body: Effect.Effect<X, E, R2>, options?: TransactionOptions) => Effect.Effect<X, E | PrismaError, R | R2>;
|
|
15
|
+
readonly transaction: <X, E, R2>(body: Effect.Effect<X, E, R2>, options?: TransactionOptions) => Effect.Effect<X, E | TransactionExpired | PrismaError, R | R2>;
|
|
16
16
|
readonly recordWrite: (write: Write) => Effect.Effect<void, never, R>;
|
|
17
17
|
readonly Unnamed: Context.Reference<UnnamedObserver>;
|
|
18
18
|
}
|
package/dist/changes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"changes.d.ts","sourceRoot":"","sources":["../src/changes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,cAAc,EAAe,MAAM,2BAA2B,CAAC;AAC3F,OAAO,
|
|
1
|
+
{"version":3,"file":"changes.d.ts","sourceRoot":"","sources":["../src/changes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,OAAO,EAAE,KAAK,cAAc,EAAe,MAAM,2BAA2B,CAAC;AAC3F,OAAO,EAAS,OAAO,EAAE,MAAM,EAAgB,MAAM,QAAQ,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,KAAK,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAElE,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAG/E,OAAO,EAA4B,KAAK,YAAY,EAAE,KAAK,KAAK,EAAE,MAAM,YAAY,CAAC;AAErF,MAAM,MAAM,eAAe,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAE3E,MAAM,WAAW,oBAAoB,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAE,SAAQ,IAAI,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACtG,QAAQ,CAAC,MAAM,EAAE,EAAE,GAAG,aAAa,CAAC,EAAE,CAAC,CAAC;IACxC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;IACtC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACvC,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,MAAM,EAAE,EAAE,KAAK,WAAW,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC;IAC7F,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAC9B,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAC7B,OAAO,CAAC,EAAE,kBAAkB,KACxB,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,kBAAkB,GAAG,WAAW,EAAE,CAAC,GAAG,EAAE,CAAC,CAAC;IACpE,QAAQ,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACtE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,SAAS,CAAC,eAAe,CAAC,CAAC;CACrD;AAUD,eAAO,MAAM,iBAAiB,GAAI,EAAE,SAAS,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,SAAS,oBAAoB,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,KAAG,aAAa,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CA0C7I,CAAC"}
|
package/dist/changes.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { makeChannel } from "@shivaedev/effect-changes";
|
|
2
|
-
import { Context, Effect } from "effect";
|
|
2
|
+
import { Cause, Context, Effect, Exit, Option } from "effect";
|
|
3
3
|
import { PrismaError } from "./error.js";
|
|
4
|
+
import { configuredTimeout, isTransactionClient, isTransactionClosed, makeExpiry, unlinked } from "./expiry.js";
|
|
4
5
|
import { delegatesOf, recordingClient } from "./recording.js";
|
|
5
6
|
import { runTransaction } from "./transaction.js";
|
|
6
7
|
import { interpret } from "./write.js";
|
|
7
|
-
let bindings = 0;
|
|
8
8
|
const ignoreUnnamed = () => Effect.void;
|
|
9
|
+
const expireOnClosed = (Expiry, exit) => Option.match(Exit.isFailure(exit) ? Cause.findErrorOption(exit.cause) : Option.none(), {
|
|
10
|
+
onNone: () => Effect.void,
|
|
11
|
+
onSome: ({ cause }) => (isTransactionClosed(cause) ? Effect.flatMap(Effect.service(Expiry), (expire) => expire(cause)) : Effect.void),
|
|
12
|
+
});
|
|
9
13
|
export const makePrismaChanges = (options) => {
|
|
10
14
|
const { client, models: typed, ...channelOptions } = options;
|
|
11
15
|
const models = typed;
|
|
12
|
-
const prefix = `@shivaedev/effect-changes-prisma/${options.name}/${
|
|
16
|
+
const prefix = `@shivaedev/effect-changes-prisma/${options.name}/${crypto.randomUUID()}`;
|
|
13
17
|
const channel = makeChannel({ ...channelOptions, owner: Effect.succeed(client) });
|
|
14
18
|
const Client = Context.Reference(`${prefix}/Client`, { defaultValue: () => client });
|
|
15
19
|
const Unnamed = Context.Reference(`${prefix}/Unnamed`, { defaultValue: () => ignoreUnnamed });
|
|
20
|
+
const Expiry = makeExpiry(`${prefix}/Expiry`);
|
|
16
21
|
const delegates = delegatesOf(models);
|
|
17
22
|
const recordWrite = Effect.fn("PrismaChanges.recordWrite")(function* (write) {
|
|
18
23
|
const { changes, unnamed } = interpret(models, write);
|
|
@@ -27,14 +32,22 @@ export const makePrismaChanges = (options) => {
|
|
|
27
32
|
const recording = recordingClient(current, delegates, (write) => writes.push(write));
|
|
28
33
|
const exit = yield* Effect.exit(Effect.tryPromise({ try: () => query(recording), catch: (cause) => new PrismaError({ cause }) }));
|
|
29
34
|
yield* Effect.forEach(writes, recordWrite, { discard: true });
|
|
35
|
+
yield* expireOnClosed(Expiry, exit);
|
|
30
36
|
return yield* exit;
|
|
31
37
|
}, Effect.uninterruptible);
|
|
32
|
-
const transaction = (body, transactionOptions) => Effect.flatMap(Effect.service(Client), (current) =>
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
const transaction = (body, transactionOptions) => Effect.flatMap(Effect.service(Client), (current) => {
|
|
39
|
+
const savepoint = isTransactionClient(current);
|
|
40
|
+
const run = runTransaction({
|
|
41
|
+
client: current,
|
|
42
|
+
open: channel.open,
|
|
43
|
+
body: (tx) => Effect.provideService(body, Client, tx),
|
|
44
|
+
transaction: transactionOptions,
|
|
45
|
+
savepoint,
|
|
46
|
+
deadline: savepoint ? undefined : (transactionOptions?.timeout ?? configuredTimeout(current)),
|
|
47
|
+
Expiry,
|
|
48
|
+
});
|
|
49
|
+
return savepoint ? run : Effect.provideService(run, Expiry, unlinked);
|
|
50
|
+
}).pipe(Effect.withSpan("PrismaChanges.transaction"));
|
|
38
51
|
return { channel, Client, use, transaction, recordWrite, Unnamed };
|
|
39
52
|
};
|
|
40
53
|
//# sourceMappingURL=changes.js.map
|
package/dist/changes.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"changes.js","sourceRoot":"","sources":["../src/changes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqC,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAC3F,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"changes.js","sourceRoot":"","sources":["../src/changes.ts"],"names":[],"mappings":"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"}
|
package/dist/error.d.ts
CHANGED
|
@@ -5,5 +5,13 @@ export declare class PrismaError extends PrismaError_base<{
|
|
|
5
5
|
readonly cause: unknown;
|
|
6
6
|
}> {
|
|
7
7
|
}
|
|
8
|
+
declare const TransactionExpired_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
9
|
+
readonly _tag: "TransactionExpired";
|
|
10
|
+
} & Readonly<A>;
|
|
11
|
+
export declare class TransactionExpired extends TransactionExpired_base<{
|
|
12
|
+
readonly message: string;
|
|
13
|
+
readonly cause?: unknown;
|
|
14
|
+
}> {
|
|
15
|
+
}
|
|
8
16
|
export {};
|
|
9
17
|
//# sourceMappingURL=error.d.ts.map
|
package/dist/error.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":";;;AAEA,qBAAa,WAAY,SAAQ,iBAAgC;IAChE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB,CAAC;CAAG"}
|
|
1
|
+
{"version":3,"file":"error.d.ts","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":";;;AAEA,qBAAa,WAAY,SAAQ,iBAAgC;IAChE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;CACxB,CAAC;CAAG;;;;AAEL,qBAAa,kBAAmB,SAAQ,wBAAuC;IAC9E,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;CAAG"}
|
package/dist/error.js
CHANGED
package/dist/error.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.js","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE9B,MAAM,OAAO,WAAY,SAAQ,IAAI,CAAC,WAAW,CAAC,aAAa,CAE7D;CAAG"}
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"","sources":["../src/error.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE9B,MAAM,OAAO,WAAY,SAAQ,IAAI,CAAC,WAAW,CAAC,aAAa,CAE7D;CAAG;AAEL,MAAM,OAAO,kBAAmB,SAAQ,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAG3E;CAAG"}
|
package/dist/expiry.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Context, Effect } from "effect";
|
|
2
|
+
import { TransactionExpired } from "./error.ts";
|
|
3
|
+
export type Expire = (cause: unknown) => Effect.Effect<void>;
|
|
4
|
+
export declare const unlinked: Expire;
|
|
5
|
+
export declare const makeExpiry: (key: string) => Context.Reference<Expire>;
|
|
6
|
+
export declare const isTransactionClient: (client: object) => boolean;
|
|
7
|
+
export declare const isTransactionClosed: (cause: unknown) => boolean;
|
|
8
|
+
export declare const configuredTimeout: (client: object) => number | undefined;
|
|
9
|
+
export declare const expiring: <X, E, R>(body: Effect.Effect<X, E, R>, timeout: number | undefined, Expiry: Context.Reference<Expire>) => Effect.Effect<X, E | TransactionExpired, R>;
|
|
10
|
+
//# sourceMappingURL=expiry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expiry.d.ts","sourceRoot":"","sources":["../src/expiry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,OAAO,EAAY,MAAM,EAA6B,MAAM,QAAQ,CAAC;AACrF,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,MAAM,MAAM,GAAG,CAAC,KAAK,EAAE,OAAO,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAE7D,eAAO,MAAM,QAAQ,EAAE,MAA0B,CAAC;AAElD,eAAO,MAAM,UAAU,GAAI,KAAK,MAAM,8BAAqE,CAAC;AAE5G,eAAO,MAAM,mBAAmB,GAAI,QAAQ,MAAM,KAAG,OAAqD,CAAC;AAE3G,eAAO,MAAM,mBAAmB,GAAI,OAAO,OAAO,KAAG,OAAyE,CAAC;AAM/H,eAAO,MAAM,iBAAiB,GAAI,QAAQ,MAAM,KAAG,MAAM,GAAG,SAG/C,CAAC;AAId,eAAO,MAAM,QAAQ,GAAI,CAAC,EAAE,CAAC,EAAE,CAAC,EAC/B,MAAM,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAC5B,SAAS,MAAM,GAAG,SAAS,EAC3B,QAAQ,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,KAC/B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,kBAAkB,EAAE,CAAC,CAoB1C,CAAC"}
|
package/dist/expiry.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Clock, Context, Deferred, Effect, Option, Predicate, Schema } from "effect";
|
|
2
|
+
import { TransactionExpired } from "./error.js";
|
|
3
|
+
export const unlinked = () => Effect.void;
|
|
4
|
+
export const makeExpiry = (key) => Context.Reference(key, { defaultValue: () => unlinked });
|
|
5
|
+
export const isTransactionClient = (client) => !Predicate.hasProperty(client, "$connect");
|
|
6
|
+
export const isTransactionClosed = (cause) => Predicate.hasProperty(cause, "code") && cause.code === "P2028";
|
|
7
|
+
const EngineConfig = Schema.Struct({ transactionOptions: Schema.Struct({ timeout: Schema.Number }) });
|
|
8
|
+
const decodeEngineConfig = Schema.decodeUnknownOption(EngineConfig);
|
|
9
|
+
export const configuredTimeout = (client) => Predicate.hasProperty(client, "_engineConfig")
|
|
10
|
+
? Option.getOrUndefined(Option.map(decodeEngineConfig(client._engineConfig), (config) => config.transactionOptions.timeout))
|
|
11
|
+
: undefined;
|
|
12
|
+
const wallClock = Clock.Clock.defaultValue();
|
|
13
|
+
export const expiring = (body, timeout, Expiry) => Effect.gen(function* () {
|
|
14
|
+
const outer = yield* Expiry;
|
|
15
|
+
const expired = yield* Deferred.make();
|
|
16
|
+
const closed = (cause) => Effect.andThen(Deferred.fail(expired, new TransactionExpired({ message: "The transaction was closed before its body finished", cause })), outer(cause));
|
|
17
|
+
const deadline = timeout === undefined
|
|
18
|
+
? Effect.never
|
|
19
|
+
: Effect.andThen(Effect.provideService(Effect.sleep(timeout), Clock.Clock, wallClock), Deferred.fail(expired, new TransactionExpired({ message: `The transaction expired after its ${timeout} ms timeout` })));
|
|
20
|
+
const guard = Effect.andThen(Effect.raceFirst(deadline, Deferred.await(expired)), Deferred.await(expired));
|
|
21
|
+
return yield* Effect.raceFirst(Effect.provideService(body, Expiry, closed), guard).pipe(Effect.catchCause((cause) => Effect.flatMap(Deferred.isDone(expired), (done) => (done ? Deferred.await(expired) : Effect.failCause(cause)))));
|
|
22
|
+
});
|
|
23
|
+
//# sourceMappingURL=expiry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expiry.js","sourceRoot":"","sources":["../src/expiry.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { makePrismaChanges, type PrismaChanges, type PrismaChangesOptions, type UnnamedObserver } from "./changes.ts";
|
|
2
2
|
export { type Coverage, type CoverageViolation, checkCoverage } from "./coverage.ts";
|
|
3
|
-
export { PrismaError } from "./error.ts";
|
|
3
|
+
export { PrismaError, TransactionExpired } from "./error.ts";
|
|
4
4
|
export type { ChangeMap, CountOperation, ModelChanges, ModelName, ModelRow, RowOperation, Transactional, TransactionOptions, } from "./model.ts";
|
|
5
|
-
export { type RawQueryClient, tablesOf, writtenTables } from "./tables.ts";
|
|
5
|
+
export { type RawQueryClient, type TableWrites, tablesOf, tableWrites, writtenTables } from "./tables.ts";
|
|
6
6
|
export type { UnnamedWrite, Write } from "./write.ts";
|
|
7
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACtH,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,iBAAiB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACrF,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,KAAK,aAAa,EAAE,KAAK,oBAAoB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AACtH,OAAO,EAAE,KAAK,QAAQ,EAAE,KAAK,iBAAiB,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACrF,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC7D,YAAY,EACX,SAAS,EACT,cAAc,EACd,YAAY,EACZ,SAAS,EACT,QAAQ,EACR,YAAY,EACZ,aAAa,EACb,kBAAkB,GAClB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,KAAK,cAAc,EAAE,KAAK,WAAW,EAAE,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC1G,YAAY,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { makePrismaChanges } from "./changes.js";
|
|
2
2
|
export { checkCoverage } from "./coverage.js";
|
|
3
|
-
export { PrismaError } from "./error.js";
|
|
4
|
-
export { tablesOf, writtenTables } from "./tables.js";
|
|
3
|
+
export { PrismaError, TransactionExpired } from "./error.js";
|
|
4
|
+
export { tablesOf, tableWrites, writtenTables } from "./tables.js";
|
|
5
5
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAuE,MAAM,cAAc,CAAC;AACtH,OAAO,EAAyC,aAAa,EAAE,MAAM,eAAe,CAAC;AACrF,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAuE,MAAM,cAAc,CAAC;AACtH,OAAO,EAAyC,aAAa,EAAE,MAAM,eAAe,CAAC;AACrF,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAW7D,OAAO,EAAyC,QAAQ,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/tables.d.ts
CHANGED
|
@@ -4,5 +4,7 @@ export declare const tablesOf: (schema: string) => ReadonlyMap<string, string>;
|
|
|
4
4
|
export interface RawQueryClient {
|
|
5
5
|
$queryRawUnsafe(query: string): PromiseLike<unknown>;
|
|
6
6
|
}
|
|
7
|
-
export
|
|
7
|
+
export type TableWrites = ReadonlyMap<string, bigint>;
|
|
8
|
+
export declare const tableWrites: (client: RawQueryClient) => Effect.Effect<Map<string, bigint>, PrismaError, never>;
|
|
9
|
+
export declare const writtenTables: (client: RawQueryClient, since?: TableWrites | undefined) => Effect.Effect<string[], PrismaError, never>;
|
|
8
10
|
//# sourceMappingURL=tables.d.ts.map
|
package/dist/tables.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAMzC,eAAO,MAAM,QAAQ,GAAI,QAAQ,MAAM,KAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAC4E,CAAC;AAEjJ,MAAM,WAAW,cAAc;IAC9B,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;CACrD;
|
|
1
|
+
{"version":3,"file":"tables.d.ts","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAMzC,eAAO,MAAM,QAAQ,GAAI,QAAQ,MAAM,KAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAC4E,CAAC;AAEjJ,MAAM,WAAW,cAAc;IAC9B,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;CACrD;AAED,MAAM,MAAM,WAAW,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAStD,eAAO,MAAM,WAAW,oFAMtB,CAAC;AAEH,eAAO,MAAM,aAAa,0GAGxB,CAAC"}
|
package/dist/tables.js
CHANGED
|
@@ -4,11 +4,19 @@ const MODEL = /^[ \t]*model[ \t]+(\w+)[ \t]*\{([\s\S]*?)^[ \t]*\}/gm;
|
|
|
4
4
|
const MAPPED = /@@map\(\s*(?:name\s*:\s*)?"([^"]+)"/;
|
|
5
5
|
const COMMENT = /\/\/.*$/gm;
|
|
6
6
|
export const tablesOf = (schema) => new Map([...schema.replace(COMMENT, "").matchAll(MODEL)].map(([, model = "", body = ""]) => [MAPPED.exec(body)?.[1] ?? model, model]));
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const
|
|
12
|
-
|
|
7
|
+
const Counted = Schema.Array(Schema.Struct({ relname: Schema.String, writes: Schema.BigInt }));
|
|
8
|
+
const COUNTED = "select relname, n_tup_ins + n_tup_upd + n_tup_del as writes from pg_stat_xact_user_tables where n_tup_ins + n_tup_upd + n_tup_del > 0 order by relname";
|
|
9
|
+
const none = new Map();
|
|
10
|
+
export const tableWrites = Effect.fn("PrismaChanges.tableWrites")(function* (client) {
|
|
11
|
+
const rows = yield* Effect.tryPromise({ try: () => client.$queryRawUnsafe(COUNTED), catch: (cause) => new PrismaError({ cause }) });
|
|
12
|
+
const decoded = yield* Effect.mapError(Schema.decodeUnknownEffect(Counted)(rows), (cause) => new PrismaError({ cause }));
|
|
13
|
+
const counts = new Map();
|
|
14
|
+
for (const { relname, writes } of decoded)
|
|
15
|
+
counts.set(relname, (counts.get(relname) ?? 0n) + writes);
|
|
16
|
+
return counts;
|
|
17
|
+
});
|
|
18
|
+
export const writtenTables = Effect.fn("PrismaChanges.writtenTables")(function* (client, since = none) {
|
|
19
|
+
const counts = yield* tableWrites(client);
|
|
20
|
+
return [...counts].flatMap(([table, writes]) => (writes > (since.get(table) ?? 0n) ? [table] : []));
|
|
13
21
|
});
|
|
14
22
|
//# sourceMappingURL=tables.js.map
|
package/dist/tables.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tables.js","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,KAAK,GAAG,sDAAsD,CAAC;AACrE,MAAM,MAAM,GAAG,qCAAqC,CAAC;AACrD,MAAM,OAAO,GAAG,WAAW,CAAC;AAE5B,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,MAAc,EAA+B,EAAE,CACvE,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,EAAE,EAAE,IAAI,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,KAAK,CAAU,CAAC,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"tables.js","sourceRoot":"","sources":["../src/tables.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzC,MAAM,KAAK,GAAG,sDAAsD,CAAC;AACrE,MAAM,MAAM,GAAG,qCAAqC,CAAC;AACrD,MAAM,OAAO,GAAG,WAAW,CAAC;AAE5B,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,MAAc,EAA+B,EAAE,CACvE,IAAI,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,EAAE,EAAE,IAAI,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,EAAE,KAAK,CAAU,CAAC,CAAC,CAAC;AAQjJ,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AAE/F,MAAM,OAAO,GACZ,wJAAwJ,CAAC;AAE1J,MAAM,IAAI,GAAgB,IAAI,GAAG,EAAE,CAAC;AAEpC,MAAM,CAAC,MAAM,WAAW,GAAG,MAAM,CAAC,EAAE,CAAC,2BAA2B,CAAC,CAAC,QAAQ,CAAC,EAAE,MAAsB;IAClG,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,WAAW,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IACpI,MAAM,OAAO,GAAG,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,IAAI,WAAW,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACzH,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAC;IACzC,KAAK,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,OAAO;QAAE,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,GAAG,MAAM,CAAC,CAAC;IACrG,OAAO,MAA4B,CAAC;AACrC,CAAC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,EAAE,CAAC,6BAA6B,CAAC,CAAC,QAAQ,CAAC,EAAE,MAAsB,EAAE,QAAqB,IAAI;IACjI,MAAM,MAAM,GAAG,KAAK,CAAC,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,OAAO,CAAC,GAAG,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACrG,CAAC,CAAC,CAAC"}
|
package/dist/transaction.d.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import type { Frame } from "@shivaedev/effect-changes";
|
|
2
|
-
import { Effect } from "effect";
|
|
3
|
-
import { PrismaError } from "./error.ts";
|
|
2
|
+
import { type Context, Effect } from "effect";
|
|
3
|
+
import { PrismaError, TransactionExpired } from "./error.ts";
|
|
4
|
+
import { type Expire } from "./expiry.ts";
|
|
4
5
|
import type { Transactional, TransactionOptions } from "./model.ts";
|
|
5
6
|
export declare const runTransaction: <Tx extends Transactional<Tx>, X, E, R, RO>(options: {
|
|
6
7
|
readonly client: Tx;
|
|
7
8
|
readonly open: Effect.Effect<Frame, never, RO>;
|
|
8
9
|
readonly body: (tx: Tx) => Effect.Effect<X, E, R>;
|
|
9
10
|
readonly transaction: TransactionOptions | undefined;
|
|
10
|
-
|
|
11
|
+
readonly savepoint: boolean;
|
|
12
|
+
readonly deadline: number | undefined;
|
|
13
|
+
readonly Expiry: Context.Reference<Expire>;
|
|
14
|
+
}) => Effect.Effect<X, E | TransactionExpired | PrismaError, R | RO>;
|
|
11
15
|
//# sourceMappingURL=transaction.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,
|
|
1
|
+
{"version":3,"file":"transaction.d.ts","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,EAAQ,MAAM,QAAQ,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAC7D,OAAO,EAAE,KAAK,MAAM,EAAiC,MAAM,aAAa,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAuCpE,eAAO,MAAM,cAAc,GAAI,EAAE,SAAS,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,SAAS;IAClF,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;IAC/C,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,WAAW,EAAE,kBAAkB,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAAC;IACtC,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;CAC3C,KAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,kBAAkB,GAAG,WAAW,EAAE,CAAC,GAAG,EAAE,CA4B/D,CAAC"}
|
package/dist/transaction.js
CHANGED
|
@@ -1,25 +1,38 @@
|
|
|
1
1
|
import { Effect, Exit } from "effect";
|
|
2
|
-
import { PrismaError } from "./error.js";
|
|
2
|
+
import { PrismaError, TransactionExpired } from "./error.js";
|
|
3
|
+
import { expiring, isTransactionClosed } from "./expiry.js";
|
|
3
4
|
class BodyFailed {
|
|
4
5
|
}
|
|
5
|
-
const commit = (client, options, body, context, signal) => {
|
|
6
|
+
const commit = (client, options, savepoint, body, context, signal) => {
|
|
7
|
+
let started = false;
|
|
6
8
|
let failed;
|
|
7
9
|
const run = async (tx) => {
|
|
10
|
+
started = true;
|
|
8
11
|
const exit = await Effect.runPromiseExitWith(context)(body(tx), { signal });
|
|
9
12
|
if (Exit.isSuccess(exit))
|
|
10
13
|
return exit.value;
|
|
11
14
|
failed = exit;
|
|
12
15
|
throw new BodyFailed();
|
|
13
16
|
};
|
|
14
|
-
|
|
17
|
+
const rejected = (cause) => {
|
|
18
|
+
if (failed !== undefined)
|
|
19
|
+
return { committed: false, exit: failed };
|
|
20
|
+
if (!isTransactionClosed(cause) || !(started || savepoint))
|
|
21
|
+
return { committed: false, exit: Exit.fail(new PrismaError({ cause })) };
|
|
22
|
+
const closed = new TransactionExpired({ message: "The transaction was closed before it could finish", cause });
|
|
23
|
+
return { committed: false, exit: Exit.fail(closed), closed };
|
|
24
|
+
};
|
|
25
|
+
return Promise.resolve(client.$transaction(run, options)).then((value) => ({ committed: true, exit: Exit.succeed(value) }), rejected);
|
|
15
26
|
};
|
|
16
27
|
export const runTransaction = (options) => Effect.uninterruptibleMask((restore) => Effect.gen(function* () {
|
|
17
|
-
const { client, body } = options;
|
|
28
|
+
const { client, savepoint, body, deadline, Expiry } = options;
|
|
18
29
|
const frame = yield* options.open;
|
|
19
30
|
const context = yield* Effect.context();
|
|
20
31
|
const abort = new AbortController();
|
|
21
|
-
const
|
|
22
|
-
const
|
|
32
|
+
const guarded = (tx) => expiring(frame.provide(body(tx)), deadline, Expiry);
|
|
33
|
+
const outcome = commit(client, options.transaction, savepoint, guarded, context, abort.signal);
|
|
34
|
+
const expire = yield* Expiry;
|
|
35
|
+
const settle = (settled) => Effect.andThen(settled.closed === undefined ? Effect.void : expire(settled.closed.cause), Effect.as(frame.settle(settled.committed ? "committed" : "rolledBack"), settled.exit));
|
|
23
36
|
const settled = yield* restore(Effect.promise(() => outcome)).pipe(Effect.onInterrupt(() => Effect.andThen(Effect.sync(() => abort.abort()), Effect.flatMap(Effect.promise(() => outcome), settle))));
|
|
24
37
|
return yield* Effect.flatten(settle(settled));
|
|
25
38
|
}));
|
package/dist/transaction.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transaction.js","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,MAAM,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AACpD,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"transaction.js","sourceRoot":"","sources":["../src/transaction.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shivaedev/effect-changes-prisma",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Commit-bound change channels for Prisma Classic: automatic change recording, publish after $transaction commits, and a test-time coverage check",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"@shivaedev/effect-changes": "0.1.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
|
-
"@prisma/client": "
|
|
43
|
+
"@prisma/client": "7.9.1",
|
|
44
44
|
"effect": "4.0.0-rc.112"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
package/src/changes.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Channel, type ChannelOptions, makeChannel } from "@shivaedev/effect-changes";
|
|
2
|
-
import { Context, Effect } from "effect";
|
|
3
|
-
import { PrismaError } from "./error.ts";
|
|
2
|
+
import { Cause, Context, Effect, Exit, Option } from "effect";
|
|
3
|
+
import { PrismaError, type TransactionExpired } from "./error.ts";
|
|
4
|
+
import { configuredTimeout, type Expire, isTransactionClient, isTransactionClosed, makeExpiry, unlinked } from "./expiry.ts";
|
|
4
5
|
import type { ChangeMap, Transactional, TransactionOptions } from "./model.ts";
|
|
5
6
|
import { delegatesOf, recordingClient } from "./recording.ts";
|
|
6
7
|
import { runTransaction } from "./transaction.ts";
|
|
@@ -17,22 +18,30 @@ export interface PrismaChanges<Tx, A, R> {
|
|
|
17
18
|
readonly channel: Channel<A, R>;
|
|
18
19
|
readonly Client: Context.Reference<Tx>;
|
|
19
20
|
readonly use: <X>(query: (client: Tx) => PromiseLike<X>) => Effect.Effect<X, PrismaError, R>;
|
|
20
|
-
readonly transaction: <X, E, R2>(
|
|
21
|
+
readonly transaction: <X, E, R2>(
|
|
22
|
+
body: Effect.Effect<X, E, R2>,
|
|
23
|
+
options?: TransactionOptions,
|
|
24
|
+
) => Effect.Effect<X, E | TransactionExpired | PrismaError, R | R2>;
|
|
21
25
|
readonly recordWrite: (write: Write) => Effect.Effect<void, never, R>;
|
|
22
26
|
readonly Unnamed: Context.Reference<UnnamedObserver>;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
let bindings = 0;
|
|
26
|
-
|
|
27
29
|
const ignoreUnnamed: UnnamedObserver = () => Effect.void;
|
|
28
30
|
|
|
31
|
+
const expireOnClosed = (Expiry: Context.Reference<Expire>, exit: Exit.Exit<unknown, PrismaError>): Effect.Effect<void> =>
|
|
32
|
+
Option.match(Exit.isFailure(exit) ? Cause.findErrorOption(exit.cause) : Option.none(), {
|
|
33
|
+
onNone: () => Effect.void,
|
|
34
|
+
onSome: ({ cause }) => (isTransactionClosed(cause) ? Effect.flatMap(Effect.service(Expiry), (expire) => expire(cause)) : Effect.void),
|
|
35
|
+
});
|
|
36
|
+
|
|
29
37
|
export const makePrismaChanges = <Tx extends Transactional<Tx>, A, R = never>(options: PrismaChangesOptions<Tx, A, R>): PrismaChanges<Tx, A, R> => {
|
|
30
38
|
const { client, models: typed, ...channelOptions } = options;
|
|
31
39
|
const models: LooseMap<A> = typed;
|
|
32
|
-
const prefix = `@shivaedev/effect-changes-prisma/${options.name}/${
|
|
40
|
+
const prefix = `@shivaedev/effect-changes-prisma/${options.name}/${crypto.randomUUID()}`;
|
|
33
41
|
const channel = makeChannel<A, R>({ ...channelOptions, owner: Effect.succeed(client) });
|
|
34
42
|
const Client = Context.Reference<Tx>(`${prefix}/Client`, { defaultValue: () => client });
|
|
35
43
|
const Unnamed = Context.Reference<UnnamedObserver>(`${prefix}/Unnamed`, { defaultValue: () => ignoreUnnamed });
|
|
44
|
+
const Expiry = makeExpiry(`${prefix}/Expiry`);
|
|
36
45
|
const delegates = delegatesOf(models);
|
|
37
46
|
|
|
38
47
|
const recordWrite = Effect.fn("PrismaChanges.recordWrite")(function* (write: Write) {
|
|
@@ -47,18 +56,24 @@ export const makePrismaChanges = <Tx extends Transactional<Tx>, A, R = never>(op
|
|
|
47
56
|
const recording = recordingClient(current, delegates, (write) => writes.push(write));
|
|
48
57
|
const exit = yield* Effect.exit(Effect.tryPromise({ try: () => query(recording), catch: (cause) => new PrismaError({ cause }) }));
|
|
49
58
|
yield* Effect.forEach(writes, recordWrite, { discard: true });
|
|
59
|
+
yield* expireOnClosed(Expiry, exit);
|
|
50
60
|
return yield* exit;
|
|
51
61
|
}, Effect.uninterruptible);
|
|
52
62
|
|
|
53
63
|
const transaction = <X, E, R2>(body: Effect.Effect<X, E, R2>, transactionOptions?: TransactionOptions) =>
|
|
54
|
-
Effect.flatMap(Effect.service(Client), (current) =>
|
|
55
|
-
|
|
64
|
+
Effect.flatMap(Effect.service(Client), (current) => {
|
|
65
|
+
const savepoint = isTransactionClient(current);
|
|
66
|
+
const run = runTransaction({
|
|
56
67
|
client: current,
|
|
57
68
|
open: channel.open,
|
|
58
69
|
body: (tx: Tx) => Effect.provideService(body, Client, tx),
|
|
59
70
|
transaction: transactionOptions,
|
|
60
|
-
|
|
61
|
-
|
|
71
|
+
savepoint,
|
|
72
|
+
deadline: savepoint ? undefined : (transactionOptions?.timeout ?? configuredTimeout(current)),
|
|
73
|
+
Expiry,
|
|
74
|
+
});
|
|
75
|
+
return savepoint ? run : Effect.provideService(run, Expiry, unlinked);
|
|
76
|
+
}).pipe(Effect.withSpan("PrismaChanges.transaction"));
|
|
62
77
|
|
|
63
78
|
return { channel, Client, use, transaction, recordWrite, Unnamed };
|
|
64
79
|
};
|
package/src/error.ts
CHANGED
|
@@ -3,3 +3,8 @@ import { Data } from "effect";
|
|
|
3
3
|
export class PrismaError extends Data.TaggedError("PrismaError")<{
|
|
4
4
|
readonly cause: unknown;
|
|
5
5
|
}> {}
|
|
6
|
+
|
|
7
|
+
export class TransactionExpired extends Data.TaggedError("TransactionExpired")<{
|
|
8
|
+
readonly message: string;
|
|
9
|
+
readonly cause?: unknown;
|
|
10
|
+
}> {}
|
package/src/expiry.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Clock, Context, Deferred, Effect, Option, Predicate, Schema } from "effect";
|
|
2
|
+
import { TransactionExpired } from "./error.ts";
|
|
3
|
+
|
|
4
|
+
export type Expire = (cause: unknown) => Effect.Effect<void>;
|
|
5
|
+
|
|
6
|
+
export const unlinked: Expire = () => Effect.void;
|
|
7
|
+
|
|
8
|
+
export const makeExpiry = (key: string) => Context.Reference<Expire>(key, { defaultValue: () => unlinked });
|
|
9
|
+
|
|
10
|
+
export const isTransactionClient = (client: object): boolean => !Predicate.hasProperty(client, "$connect");
|
|
11
|
+
|
|
12
|
+
export const isTransactionClosed = (cause: unknown): boolean => Predicate.hasProperty(cause, "code") && cause.code === "P2028";
|
|
13
|
+
|
|
14
|
+
const EngineConfig = Schema.Struct({ transactionOptions: Schema.Struct({ timeout: Schema.Number }) });
|
|
15
|
+
|
|
16
|
+
const decodeEngineConfig = Schema.decodeUnknownOption(EngineConfig);
|
|
17
|
+
|
|
18
|
+
export const configuredTimeout = (client: object): number | undefined =>
|
|
19
|
+
Predicate.hasProperty(client, "_engineConfig")
|
|
20
|
+
? Option.getOrUndefined(Option.map(decodeEngineConfig(client._engineConfig), (config) => config.transactionOptions.timeout))
|
|
21
|
+
: undefined;
|
|
22
|
+
|
|
23
|
+
const wallClock = Clock.Clock.defaultValue();
|
|
24
|
+
|
|
25
|
+
export const expiring = <X, E, R>(
|
|
26
|
+
body: Effect.Effect<X, E, R>,
|
|
27
|
+
timeout: number | undefined,
|
|
28
|
+
Expiry: Context.Reference<Expire>,
|
|
29
|
+
): Effect.Effect<X, E | TransactionExpired, R> =>
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
const outer = yield* Expiry;
|
|
32
|
+
const expired = yield* Deferred.make<never, TransactionExpired>();
|
|
33
|
+
const closed: Expire = (cause) =>
|
|
34
|
+
Effect.andThen(
|
|
35
|
+
Deferred.fail(expired, new TransactionExpired({ message: "The transaction was closed before its body finished", cause })),
|
|
36
|
+
outer(cause),
|
|
37
|
+
);
|
|
38
|
+
const deadline =
|
|
39
|
+
timeout === undefined
|
|
40
|
+
? Effect.never
|
|
41
|
+
: Effect.andThen(
|
|
42
|
+
Effect.provideService(Effect.sleep(timeout), Clock.Clock, wallClock),
|
|
43
|
+
Deferred.fail(expired, new TransactionExpired({ message: `The transaction expired after its ${timeout} ms timeout` })),
|
|
44
|
+
);
|
|
45
|
+
const guard = Effect.andThen(Effect.raceFirst(deadline, Deferred.await(expired)), Deferred.await(expired));
|
|
46
|
+
return yield* Effect.raceFirst(Effect.provideService(body, Expiry, closed), guard).pipe(
|
|
47
|
+
Effect.catchCause((cause) => Effect.flatMap(Deferred.isDone(expired), (done) => (done ? Deferred.await(expired) : Effect.failCause(cause)))),
|
|
48
|
+
);
|
|
49
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { makePrismaChanges, type PrismaChanges, type PrismaChangesOptions, type UnnamedObserver } from "./changes.ts";
|
|
2
2
|
export { type Coverage, type CoverageViolation, checkCoverage } from "./coverage.ts";
|
|
3
|
-
export { PrismaError } from "./error.ts";
|
|
3
|
+
export { PrismaError, TransactionExpired } from "./error.ts";
|
|
4
4
|
export type {
|
|
5
5
|
ChangeMap,
|
|
6
6
|
CountOperation,
|
|
@@ -11,5 +11,5 @@ export type {
|
|
|
11
11
|
Transactional,
|
|
12
12
|
TransactionOptions,
|
|
13
13
|
} from "./model.ts";
|
|
14
|
-
export { type RawQueryClient, tablesOf, writtenTables } from "./tables.ts";
|
|
14
|
+
export { type RawQueryClient, type TableWrites, tablesOf, tableWrites, writtenTables } from "./tables.ts";
|
|
15
15
|
export type { UnnamedWrite, Write } from "./write.ts";
|
package/src/tables.ts
CHANGED
|
@@ -12,12 +12,24 @@ export interface RawQueryClient {
|
|
|
12
12
|
$queryRawUnsafe(query: string): PromiseLike<unknown>;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
export type TableWrites = ReadonlyMap<string, bigint>;
|
|
16
16
|
|
|
17
|
-
const
|
|
17
|
+
const Counted = Schema.Array(Schema.Struct({ relname: Schema.String, writes: Schema.BigInt }));
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const COUNTED =
|
|
20
|
+
"select relname, n_tup_ins + n_tup_upd + n_tup_del as writes from pg_stat_xact_user_tables where n_tup_ins + n_tup_upd + n_tup_del > 0 order by relname";
|
|
21
|
+
|
|
22
|
+
const none: TableWrites = new Map();
|
|
23
|
+
|
|
24
|
+
export const tableWrites = Effect.fn("PrismaChanges.tableWrites")(function* (client: RawQueryClient) {
|
|
25
|
+
const rows = yield* Effect.tryPromise({ try: () => client.$queryRawUnsafe(COUNTED), catch: (cause) => new PrismaError({ cause }) });
|
|
26
|
+
const decoded = yield* Effect.mapError(Schema.decodeUnknownEffect(Counted)(rows), (cause) => new PrismaError({ cause }));
|
|
27
|
+
const counts = new Map<string, bigint>();
|
|
28
|
+
for (const { relname, writes } of decoded) counts.set(relname, (counts.get(relname) ?? 0n) + writes);
|
|
29
|
+
return counts satisfies TableWrites;
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const writtenTables = Effect.fn("PrismaChanges.writtenTables")(function* (client: RawQueryClient, since: TableWrites = none) {
|
|
33
|
+
const counts = yield* tableWrites(client);
|
|
34
|
+
return [...counts].flatMap(([table, writes]) => (writes > (since.get(table) ?? 0n) ? [table] : []));
|
|
23
35
|
});
|
package/src/transaction.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type { Frame } from "@shivaedev/effect-changes";
|
|
2
2
|
import { type Context, Effect, Exit } from "effect";
|
|
3
|
-
import { PrismaError } from "./error.ts";
|
|
3
|
+
import { PrismaError, TransactionExpired } from "./error.ts";
|
|
4
|
+
import { type Expire, expiring, isTransactionClosed } from "./expiry.ts";
|
|
4
5
|
import type { Transactional, TransactionOptions } from "./model.ts";
|
|
5
6
|
|
|
6
7
|
interface Settled<X, E> {
|
|
7
8
|
readonly committed: boolean;
|
|
8
|
-
readonly exit: Exit.Exit<X, E | PrismaError>;
|
|
9
|
+
readonly exit: Exit.Exit<X, E | TransactionExpired | PrismaError>;
|
|
10
|
+
readonly closed?: TransactionExpired;
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
class BodyFailed {}
|
|
@@ -13,20 +15,29 @@ class BodyFailed {}
|
|
|
13
15
|
const commit = <Tx extends Transactional<Tx>, X, E, R>(
|
|
14
16
|
client: Tx,
|
|
15
17
|
options: TransactionOptions | undefined,
|
|
18
|
+
savepoint: boolean,
|
|
16
19
|
body: (tx: Tx) => Effect.Effect<X, E, R>,
|
|
17
20
|
context: Context.Context<R>,
|
|
18
21
|
signal: AbortSignal,
|
|
19
22
|
): Promise<Settled<X, E>> => {
|
|
23
|
+
let started = false;
|
|
20
24
|
let failed: Exit.Exit<X, E> | undefined;
|
|
21
25
|
const run = async (tx: Tx) => {
|
|
26
|
+
started = true;
|
|
22
27
|
const exit = await Effect.runPromiseExitWith(context)(body(tx), { signal });
|
|
23
28
|
if (Exit.isSuccess(exit)) return exit.value;
|
|
24
29
|
failed = exit;
|
|
25
30
|
throw new BodyFailed();
|
|
26
31
|
};
|
|
32
|
+
const rejected = (cause: unknown): Settled<X, E> => {
|
|
33
|
+
if (failed !== undefined) return { committed: false, exit: failed };
|
|
34
|
+
if (!isTransactionClosed(cause) || !(started || savepoint)) return { committed: false, exit: Exit.fail(new PrismaError({ cause })) };
|
|
35
|
+
const closed = new TransactionExpired({ message: "The transaction was closed before it could finish", cause });
|
|
36
|
+
return { committed: false, exit: Exit.fail(closed), closed };
|
|
37
|
+
};
|
|
27
38
|
return Promise.resolve(client.$transaction(run, options)).then(
|
|
28
39
|
(value): Settled<X, E> => ({ committed: true, exit: Exit.succeed(value) }),
|
|
29
|
-
|
|
40
|
+
rejected,
|
|
30
41
|
);
|
|
31
42
|
};
|
|
32
43
|
|
|
@@ -35,15 +46,24 @@ export const runTransaction = <Tx extends Transactional<Tx>, X, E, R, RO>(option
|
|
|
35
46
|
readonly open: Effect.Effect<Frame, never, RO>;
|
|
36
47
|
readonly body: (tx: Tx) => Effect.Effect<X, E, R>;
|
|
37
48
|
readonly transaction: TransactionOptions | undefined;
|
|
38
|
-
|
|
49
|
+
readonly savepoint: boolean;
|
|
50
|
+
readonly deadline: number | undefined;
|
|
51
|
+
readonly Expiry: Context.Reference<Expire>;
|
|
52
|
+
}): Effect.Effect<X, E | TransactionExpired | PrismaError, R | RO> =>
|
|
39
53
|
Effect.uninterruptibleMask((restore) =>
|
|
40
54
|
Effect.gen(function* () {
|
|
41
|
-
const { client, body } = options;
|
|
55
|
+
const { client, savepoint, body, deadline, Expiry } = options;
|
|
42
56
|
const frame = yield* options.open;
|
|
43
57
|
const context = yield* Effect.context<R>();
|
|
44
58
|
const abort = new AbortController();
|
|
45
|
-
const
|
|
46
|
-
const
|
|
59
|
+
const guarded = (tx: Tx) => expiring(frame.provide(body(tx)), deadline, Expiry);
|
|
60
|
+
const outcome = commit(client, options.transaction, savepoint, guarded, context, abort.signal);
|
|
61
|
+
const expire = yield* Expiry;
|
|
62
|
+
const settle = (settled: Settled<X, E | TransactionExpired>) =>
|
|
63
|
+
Effect.andThen(
|
|
64
|
+
settled.closed === undefined ? Effect.void : expire(settled.closed.cause),
|
|
65
|
+
Effect.as(frame.settle(settled.committed ? "committed" : "rolledBack"), settled.exit),
|
|
66
|
+
);
|
|
47
67
|
const settled = yield* restore(Effect.promise(() => outcome)).pipe(
|
|
48
68
|
Effect.onInterrupt(() =>
|
|
49
69
|
Effect.andThen(
|