/** * @license * Copyright 2022-2026 Matter.js Authors * SPDX-License-Identifier: Apache-2.0 */ import { Lifetime } from "#util/Lifetime.js"; import { MaybePromise } from "#util/Promises.js"; import { Participant } from "./Participant.js"; import { Resource } from "./Resource.js"; import { ResourceSet } from "./ResourceSet.js"; import { Status } from "./Status.js"; import { open } from "./Tx.js"; /** * Two-phase commit implementation. * * Transactions are either shared (for reads) or exclusive (for writes). Exclusive transactions do not block shared * transactions but state updates will not be visible until the transaction completes. * * Writes do block other writes. Transactions start automatically when a write occurs. Since this usually happens * synchronously, the best Matter.js can do is throw an error if two write transactions would conflict. However, you * can avoid this by using {@link begin} which will wait for other transactions to complete before acquiring resource * locks. * * Persistence is implemented by a list of participants. Commits are two phase. If an error throws in phase one all * participants roll back. An error in phase 2 could result in data inconsistency. * * TODO - does prevent deadlock but we should probably add a timeout for resource locking */ export interface Transaction extends Lifetime.Owner { /** * Diagnostic description of the transaction's source. */ readonly via: string; /** * The {@link Transaction.IsolationLevel} of this transaction. */ readonly isolation: Transaction.IsolationLevel; /** * The status of the transaction. */ readonly status: Status; /** * Transaction participants. */ readonly participants: Set; /** * Resources addressed by the participants. */ readonly resources: Set; /** * The transactions currently blocking this transaction, if any. */ readonly waitingOn: Iterable | undefined; /** * Listen for transaction commit or roll back. This may occur more than once for a given transaction. */ onShared(actor: () => void, once?: boolean): void; /** * Listen for {@link Transaction.status} close. */ onClose(actor: () => void): void; /** * Add {@link Resources} to the transaction. * * If the transaction is exclusive (writing) the transaction will acquire the lock on each {@link ResourceType}, * waiting for other writers to finish if necessary. */ addResources(...resources: Resource[]): Promise; /** * Add {@link ResourceType}s to the transaction synchronously. * * Unlike {@link addResources}, this method will throw an error if the transaction is exclusive and the resources * cannot be locked. */ addResourcesSync(...resources: Resource[]): void; /** * Begin an exclusive transaction. * * Transactions begin automatically on write but there are a few reasons you may want to use this method to start an * exclusive transaction explicitly: * * 1. Automatic transactions are started in a synchronous context so conflicting transactions will throw an error. * If you start a transaction, your code will await any transaction that would otherwise throw an error. * * 2. Transaction isolation means your view of data may become stale if a write occurs in another transaction. * Once you start a transaction you block other writers so can be assured you're dealing with newest state. * * 3. Say transaction A has an exclusive lock on resource 1 and awaits resource 2. Transaction B has an exclusive * lock on resource 2. Transaction B cannot then await resource 1 without causing a deadlock. Matter.js will * detect the deadlock and throw an error. One way to prevent this is to begin a transaction and acquire locks * in a specific order. * * None of the issues above are likely and are probably not a concern for your application. If you do encounter * these issues the error message will suggest solutions. */ begin(): Promise; /** * Begin an exclusive transaction in a synchronous context. * * Unlike {@link begin}, this method will throw an error if any participant has already joined an exclusive * transaction. */ beginSync(): void; /** * Add {@link ParticipantType}s to the transaction. */ addParticipants(...participants: Participant[]): void; /** * Retrieve a participant with a specific role. */ getParticipant(role: {}): Participant | undefined; /** * Commit the transaction. * * Matter.js commits automatically when an interaction completes. You may commit manually to publish your changes * mid-interaction. * * After commit an exclusive transaction becomes shared and data references refresh to the most recent value. */ commit(): MaybePromise; /** * Roll back the transaction. * * Matter.js rolls back automatically when an interaction fails. You may roll back manually to undo your changes * mid-interaction. * * After rollback an exclusive transaction becomes shared and data references refresh to the most recent value. */ rollback(): MaybePromise; /** * Destroy the transaction without proper commit or rollback. * * This guarantees a synchronous resolution but will result in an error if {@link Transaction#status} is not shared, * read-only or destroyed. */ [Symbol.dispose](): void; /** * Wait for a set of transactions to complete. * * @param others the set of transactions to await; cleared on return */ waitFor(others: Set): Promise; } type StatusType = Status; type ResourceType = Resource; type ResourceSetType = ResourceSet; type ParticipantType = Participant; export const Transaction = { /** * Perform a transactional operation. * * This creates a read/write transaction scoped to the life of an optionally async function call. * * The transaction will commit automatically if it is exclusive (write mode) after the actor returns. * * The transaction is destroyed when {@link act} returns. You will receive an error if you access it after it is * destroyed. */ act( via: string, lifetime: Lifetime.Owner, actor: (transaction: Transaction) => MaybePromise, ): MaybePromise { const tx = open(via, lifetime); let result; try { result = actor(tx); } catch (e) { return tx.reject(e); } return tx.resolve(result); }, /** * Create a transaction. * * Transactions must be closed using {@link Finalization#resolve} or {@link Finalization#reject}. * * When closed the transaction commits automatically if exclusive. */ open(via: string, lifetime: Lifetime.Owner, isolation: Transaction.IsolationLevel = "rw") { // This function is replaced below so do not edit return open(via, lifetime, isolation); }, Status, Resource, [Symbol.toStringTag]: "Transaction", }; // This is functionally equivalent to the definition above but removes a stack frame Transaction.open = open; export namespace Transaction { export type Status = StatusType; export type Resource = ResourceType; export type ResourceSet = ResourceSetType; export type Participant = ParticipantType; export interface Disposable extends Transaction, AsyncDisposable { close(): MaybePromise; } export interface Finalization { /** * Finish the transaction. If {@link result} is a promise this may result in commit or rollback. */ resolve(result: T): MaybePromise>; /** * Roll back, close the transaction and throw an error. */ reject(cause: unknown): MaybePromise; } export type IsolationLevel = /** * Transaction reads only committed and may enter exclusive (write) mode. */ | "rw" /** * Transaction is read-only but updates are visible after commit. */ | "ro" /** * Transaction is read-only and data remains at version of first access. */ | "snapshot"; }