-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
241 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
import type { Signal } from "signal-polyfill"; | ||
|
||
type Mutation = () => void; | ||
let activeTransaction: null | Transaction = null; | ||
|
||
export function setActiveTransaction(transaction: Transaction | null): void { | ||
activeTransaction = transaction; | ||
} | ||
|
||
export function signalTransactionSetter(signal: Signal.State<any>): void { | ||
if (activeTransaction) { | ||
const { cellState, usedCells } = activeTransaction; | ||
if (!cellState.has(signal)) { | ||
cellState.set(signal, signal.get()); | ||
usedCells.add(signal); | ||
} | ||
} | ||
} | ||
|
||
export class Transaction { | ||
constructor(fn?: Mutation) { | ||
if (fn) { | ||
this.execute(fn); | ||
} | ||
} | ||
cellState: WeakMap<Signal.State<any>, unknown> = new WeakMap(); | ||
usedCells: Set<Signal.State<any>> = new Set(); | ||
execute(fn: Mutation): void { | ||
try { | ||
setActiveTransaction(this); | ||
fn(); | ||
} finally { | ||
setActiveTransaction(null); | ||
} | ||
} | ||
commit(fn?: Mutation): void { | ||
if (fn) { | ||
this.execute(fn); | ||
} | ||
this.cleanup(); | ||
} | ||
rollback(): void { | ||
this.usedCells.forEach((signal) => { | ||
signal.set(this.cellState.get(signal)); | ||
}); | ||
this.cleanup(); | ||
} | ||
cleanup(): void { | ||
this.cellState = new WeakMap(); | ||
this.usedCells = new Set(); | ||
} | ||
follow(promise: Promise<any>): Promise<any> { | ||
return promise | ||
.then((result) => { | ||
this.commit(); | ||
return result; | ||
}) | ||
.catch((error) => { | ||
this.rollback(); | ||
return Promise.reject(error); | ||
}); | ||
} | ||
} | ||
|
||
/* Usage sample: | ||
Let's say we managing add user form, we have input with user name and list of users. | ||
We have a state object that holds the user name and list of users. | ||
class App { | ||
@signal userName = ''; | ||
@signal users = []; | ||
async addUser() { | ||
const addUserTransaction = new Transaction(); | ||
// optimistic update | ||
addUserTransaction.execute(() => { | ||
this.users = [...this.users, this.userName]; | ||
this.userName = ''; | ||
}); | ||
fetch('/api/add-user', { | ||
method: 'POST', | ||
body: JSON.stringify({ userName: this.userName }), | ||
)).then(async (req) => { | ||
const serverUsers = await req.json(); | ||
// commit the transaction | ||
addUserTransaction.commit(() => { | ||
this.users = serverUsers; | ||
}); | ||
}).catch(() => { | ||
// rollback the transaction | ||
addUserTransaction.rollback(); | ||
}); | ||
} | ||
} | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
import { describe, test, assert } from "vitest"; | ||
import { signal } from "../src/index"; | ||
import { Transaction } from "../src/transaction"; | ||
|
||
function getApp() { | ||
class Obj { | ||
@signal accessor value = 0; | ||
} | ||
const app = new Obj(); | ||
return app; | ||
} | ||
|
||
describe("transaction", () => { | ||
test("rollback should work", () => { | ||
const app = getApp(); | ||
app.value = 10; | ||
const transaction = new Transaction(); | ||
transaction.execute(() => { | ||
app.value = 20; | ||
}); | ||
assert.equal(app.value, 20); | ||
transaction.rollback(); | ||
assert.equal(app.value, 10); | ||
}); | ||
test("commit should work", () => { | ||
const app = getApp(); | ||
app.value = 10; | ||
const transaction = new Transaction(); | ||
transaction.execute(() => { | ||
app.value = 20; | ||
}); | ||
assert.equal(app.value, 20); | ||
transaction.commit(); | ||
assert.equal(app.value, 20); | ||
}); | ||
test("should work with nested transactions", () => { | ||
const app = getApp(); | ||
app.value = 10; | ||
const transaction = new Transaction(); | ||
transaction.execute(() => { | ||
app.value = 20; | ||
const nestedTransaction = new Transaction(); | ||
nestedTransaction.execute(() => { | ||
app.value = 30; | ||
}); | ||
assert.equal(app.value, 30); | ||
nestedTransaction.rollback(); | ||
assert.equal(app.value, 20); | ||
}); | ||
assert.equal(app.value, 20); | ||
transaction.rollback(); | ||
assert.equal(app.value, 10); | ||
}); | ||
test("should execute mutation in constructor", () => { | ||
const app = getApp(); | ||
const transaction = new Transaction(() => { | ||
app.value = 20; | ||
}); | ||
assert.equal(app.value, 20); | ||
transaction.rollback(); | ||
assert.equal(app.value, 0); | ||
}); | ||
test("should work with promises", async () => { | ||
const app = getApp(); | ||
app.value = 10; | ||
const transaction = new Transaction(() => { | ||
app.value = 20; | ||
}); | ||
const asyncRequest = new Promise((resolve) => { | ||
setTimeout(() => { | ||
resolve(true); | ||
}, 100); | ||
}); | ||
await transaction.follow(asyncRequest); | ||
assert.equal(app.value, 20); | ||
}); | ||
test("should work with promises and rollback", async () => { | ||
const app = getApp(); | ||
app.value = 10; | ||
const transaction = new Transaction(() => { | ||
app.value = 20; | ||
}); | ||
const asyncRequest = new Promise((_, reject) => { | ||
setTimeout(() => { | ||
reject(new Error("Failed")); | ||
}, 100); | ||
}); | ||
try { | ||
await transaction.follow(asyncRequest); | ||
} catch (error: any) { | ||
assert.equal(error.message, "Failed"); | ||
assert.equal(app.value, 10); | ||
} | ||
}); | ||
}); |