Skip to content

Commit

Permalink
chore: transaction guards
Browse files Browse the repository at this point in the history
* we don't allow cells, used in transaction to be mutated outside transaction once it's alive
* we don't allow cells, mutated during transaction (outside transaction) to be used in transaction
  • Loading branch information
lifeart committed Jul 6, 2024
1 parent 8091778 commit 6e9be59
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 1 deletion.
23 changes: 22 additions & 1 deletion src/transaction.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import type { Signal } from "signal-polyfill";

type Mutation = () => void;

const activeTransactions: Set<Transaction> = new Set();

let activeTransaction: null | Transaction = null;

export function setActiveTransaction(transaction: Transaction | null): void {
Expand All @@ -9,23 +12,37 @@ export function setActiveTransaction(transaction: Transaction | null): void {

export function signalTransactionSetter(signal: Signal.State<any>): void {
if (activeTransaction) {
const { cellState, usedCells } = activeTransaction;
const { cellState, usedCells, seenCells } = activeTransaction;
if (seenCells.has(signal)) {
throw new Error('Unable to consume signal because its mutated after transaction creation');
}
if (!cellState.has(signal)) {
cellState.set(signal, signal.get());
usedCells.add(signal);
}
} else {
for (const t of activeTransactions) {
if (t.usedCells.has(signal)) {
throw new Error('Unable to mutate signal used in ongoing transaction');
} else {
t.seenCells.add(signal);
}
}
}
}

export class Transaction {
constructor(fn?: Mutation) {
activeTransactions.add(this);
if (fn) {
this.execute(fn);
}
}
cellState: WeakMap<Signal.State<any>, unknown> = new WeakMap();
usedCells: Set<Signal.State<any>> = new Set();
seenCells: WeakSet<Signal.State<any>> = new WeakSet();
execute(fn: Mutation): void {
activeTransactions.add(this);
try {
setActiveTransaction(this);
fn();
Expand All @@ -34,6 +51,7 @@ export class Transaction {
}
}
commit(fn?: Mutation): void {
activeTransactions.add(this);
if (fn) {
this.execute(fn);
}
Expand All @@ -48,8 +66,11 @@ export class Transaction {
cleanup(): void {
this.cellState = new WeakMap();
this.usedCells = new Set();
this.seenCells = new WeakSet();
activeTransactions.delete(this);
}
follow(promise: Promise<any>): Promise<any> {
activeTransactions.add(this);
return promise
.then((result) => {
this.commit();
Expand Down
32 changes: 32 additions & 0 deletions tests/transaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,36 @@ describe("transaction", () => {
assert.equal(app.value, 10);
}
});
test('unable to mutate cell outside of transaction if its already used inside transaction', async () => {
const app = getApp();
app.value = 10;
const transaction = new Transaction(() => {
app.value = 20;
});
try {
app.value = 12;
} catch (error: any) {
assert.equal(error.message, "Unable to mutate signal used in ongoing transaction");
assert.equal(app.value, 20);
}
assert.ok(transaction);
});
test('unable to consume mutated cell in transaction if its mutated after transaction creation', async () => {
const v1 = getApp();
const v2 = getApp();
v1.value = 10;
v2.value = 10;
const transaction = new Transaction(() => {
v1.value = 20;
});
v2.value = 3
try {
transaction.execute(() => {
v2.value = 4
});
} catch(error: any) {
assert.equal(error.message, "Unable to consume signal because its mutated after transaction creation");
assert.equal(v2.value, 3);
}
});
});

0 comments on commit 6e9be59

Please sign in to comment.