snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,255 @@
import { promiseWithResolve, sleep } from '@tldraw/utils'
import { vi } from 'vitest'
import { atom } from '../Atom'
import { computed } from '../Computed'
import { react } from '../EffectScheduler'
import { deferAsyncEffects, transact, transaction } from '../transactions'
// Tests for SPEC.md §12 (async transactions: deferAsyncEffects).
// Rule IDs like [AT3] in test names refer to that document.
describe('deferAsyncEffects (AT)', () => {
it('[AT1] defers effects until the async transaction commits', async () => {
const a = atom('', 0)
const b = atom('', 0)
const effectCalls = vi.fn()
react('', () => {
a.get()
b.get()
effectCalls()
})
expect(effectCalls).toHaveBeenCalledTimes(1)
const txPromise = deferAsyncEffects(async () => {
a.set(1)
expect(effectCalls).toHaveBeenCalledTimes(1) // no effect yet
await sleep(1)
b.set(2)
expect(effectCalls).toHaveBeenCalledTimes(1) // still no effect
})
await txPromise
expect(effectCalls).toHaveBeenCalledTimes(2) // effect runs after commit
})
it('[AT1] keeps computed signals up to date during the async transaction', async () => {
const a = atom('', 1)
const doubled = computed('', () => a.get() * 2)
expect(doubled.get()).toBe(2)
await deferAsyncEffects(async () => {
a.set(5)
// computed should update during transaction
expect(doubled.get()).toBe(10)
await sleep(1)
a.set(10)
expect(doubled.get()).toBe(20)
})
// computed should update after commit
expect(doubled.get()).toBe(20)
expect(a.get()).toBe(10)
})
it('[AT2] throws if kicked off during a sync transaction', async () => {
const a = atom('', 0)
let txp: any = null
transact(() => {
txp = deferAsyncEffects(async () => {
expect(a.get()).toBe(1)
a.set(2)
})
a.set(1)
})
await expect(txp).rejects.toMatchInlineSnapshot(
`[Error: deferAsyncEffects cannot be called during a sync transaction]`
)
})
it('[AT2] allows sync transactions inside the async body', async () => {
const a = atom('', 0)
await deferAsyncEffects(async () => {
a.set(1)
transaction(() => {
a.set(2)
})
expect(a.get()).toBe(2)
})
expect(a.get()).toBe(2)
})
it('[AT2] allows transact inside the async body', async () => {
const a = atom('', 0)
await deferAsyncEffects(async () => {
a.set(1)
transact(() => {
a.set(2)
})
expect(a.get()).toBe(2)
})
expect(a.get()).toBe(2)
})
it('[AT3] rolls back all changes on exception', async () => {
const a = atom('', 0)
const b = atom('', 0)
await expect(
deferAsyncEffects(async () => {
a.set(1)
b.set(2)
throw new Error('test error')
})
).rejects.toThrow('test error')
expect(a.get()).toBe(0)
expect(b.get()).toBe(0)
})
it('[AT3][T6] effects never observe partially restored values when an async transaction aborts', async () => {
// Regression: the rollback used to run after the async transaction had been cleared, so each
// restoring set flushed effects on its own and they saw one atom restored but not the other.
const a = atom('', 1)
const b = atom('', 1)
const seen: string[] = []
react('', () => {
seen.push(`${a.get()},${b.get()}`)
})
seen.length = 0
await expect(
deferAsyncEffects(async () => {
a.set(2)
b.set(2)
await sleep(1)
throw new Error('boom')
})
).rejects.toThrow('boom')
expect(seen).toEqual(['1,1'])
})
it('[AT3][AT4] rolls back everything when a nested async transaction throws', async () => {
const a = atom('', 0)
const b = atom('', 0)
await expect(
deferAsyncEffects(async () => {
a.set(1)
await deferAsyncEffects(async () => {
b.set(2)
throw new Error('inner error')
})
})
).rejects.toThrow('inner error')
expect(a.get()).toBe(0) // all changes should be rolled back
expect(b.get()).toBe(0)
})
it('[AT4] nested async transactions join the outer one', async () => {
const a = atom('', 0)
await deferAsyncEffects(async () => {
a.set(1)
await deferAsyncEffects(async () => {
a.set(2)
})
expect(a.get()).toBe(2)
})
expect(a.get()).toBe(2)
})
it('[AT4] concurrent async transactions are merged', async () => {
const a = atom('', 0)
const b = atom('', 0)
const results: number[] = []
const tx1 = deferAsyncEffects(async () => {
a.set(1)
await sleep(10)
results.push(a.get())
return 'tx1'
})
const tx2 = deferAsyncEffects(async () => {
b.set(2)
await sleep(5)
results.push(b.get())
return 'tx2'
})
const [result1, result2] = await Promise.all([tx1, tx2])
expect(result1).toBe('tx1')
expect(result2).toBe('tx2')
expect(a.get()).toBe(1)
expect(b.get()).toBe(2)
expect(results).toEqual([2, 1])
})
it('[AT4] overlapping transactions leak state to each other but group their effects', async () => {
const a = atom('', 0)
let txp = null
const p = deferAsyncEffects(async () => {
a.set(1)
const x = promiseWithResolve()
txp = deferAsyncEffects(async () => {
a.set(2)
x.resolve(null)
await sleep(10)
a.set(3)
return 'inner'
})
await x
// inner transactions leak, this can't be avoided without AsyncContext
// but at least we can group effects.
expect(a.get()).toBe(2)
return 'outer'
})
await expect(p).resolves.toBe('outer')
await expect(txp).resolves.toBe('inner')
expect(a.get()).toBe(3)
})
it('[AT5] waits for the reaction phase to finish if kicked off during a reaction', async () => {
const a = atom('', 0)
const b = atom('', 0)
let txp: any = null
react('', () => {
a.get()
txp = deferAsyncEffects(async () => {
await sleep(1)
b.set(a.get() + 1)
})
})
await txp
expect(a.get()).toBe(0)
expect(b.get()).toBe(1)
a.set(1)
await txp
expect(a.get()).toBe(1)
expect(b.get()).toBe(2)
})
it('[AT6] resolves to the return value of the function', async () => {
await expect(deferAsyncEffects(async () => 'value')).resolves.toBe('value')
})
})