Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "tuple-database",
"version": "2.2.3",
"version": "2.3.0",
"description": "",
"repository": "github:ccorcos/tuple-database",
"main": "./main.js",
Expand Down
44 changes: 44 additions & 0 deletions release-git.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Build the package and commit the output to the `release` branch, whose root is
# installable directly from GitHub:
# "tuple-database": "github:tanishqkancharla/tuple-database#<build-commit>"
set -euo pipefail

root="$(git rev-parse --show-toplevel)"
cd "$root"

if [ -n "$(git status --porcelain)" ]; then
echo "Working tree must be clean so the build matches a commit." >&2
exit 1
fi
source_commit="$(git rev-parse HEAD)"

rm -rf build
npm run build
cp README.md build/
node -e '
const pkg = require("./package.json")
delete pkg.scripts
delete pkg.devDependencies
require("fs").writeFileSync("build/package.json", JSON.stringify(pkg, null, 2) + "\n")
'
find build -name "*.test.*" -delete
rm -rf build/test build/tools

worktree="$(mktemp -d)"
trap 'git worktree remove --force "$worktree"' EXIT
if git show-ref --verify --quiet refs/heads/release; then
git worktree add "$worktree" release
else
git worktree add --detach "$worktree"
git -C "$worktree" checkout --orphan release
fi
git -C "$worktree" rm -rf --quiet . 2>/dev/null || true
cp -R build/. "$worktree"/
git -C "$worktree" add -A
if git -C "$worktree" diff --cached --quiet; then
echo "release branch unchanged: $(git rev-parse refs/heads/release)"
exit 0
fi
git -C "$worktree" commit --quiet -m "Build $source_commit"
echo "release branch: $(git rev-parse refs/heads/release)"
10 changes: 6 additions & 4 deletions src/database/async/AsyncReactivityTracker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { maybePromiseAll } from "../../helpers/maybeWaitForPromises"
import { randomId } from "../../helpers/randomId"
import { defaultRng } from "../../helpers/randomId"
import {
Bounds,
getPrefixContainingBounds,
Expand All @@ -8,14 +8,16 @@ import {
import { InMemoryTupleStorage } from "../../storage/InMemoryTupleStorage"
import { MIN, ScanStorageArgs, Tuple, WriteOps } from "../../storage/types"
import { TupleStorageApi } from "../sync/types"
import { TxId } from "../types"
import { RngApi, TxId } from "../types"
import { AsyncCallback } from "./asyncTypes"

export class AsyncReactivityTracker {
private listenersDb = new InMemoryTupleStorage()

constructor(private rng: RngApi = defaultRng) {}

subscribe(args: ScanStorageArgs, callback: AsyncCallback) {
return subscribe(this.listenersDb, args, callback)
return subscribe(this.listenersDb, this.rng.randomId(), args, callback)
}

computeReactivityEmits(writes: WriteOps) {
Expand Down Expand Up @@ -119,14 +121,14 @@ function getReactivityEmits(listenersDb: TupleStorageApi, writes: WriteOps) {

function subscribe(
listenersDb: TupleStorageApi,
id: string,
args: ScanStorageArgs,
callback: AsyncCallback
) {
// this.log("db/subscribe", args)

const prefix = getPrefixContainingBounds(args)

const id = randomId()
const value: AsyncListener = { callback, bounds: args }

listenersDb.commit({ set: [{ key: [prefix, id], value }] })
Expand Down
19 changes: 13 additions & 6 deletions src/database/async/AsyncTupleDatabase.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { iterateWrittenTuples } from "../../helpers/iterateTuples"
import { randomId } from "../../helpers/randomId"
import { defaultRng } from "../../helpers/randomId"
import { KeyValuePair, ScanStorageArgs, WriteOps } from "../../storage/types"
import { ConcurrencyLog } from "../ConcurrencyLog"
import { TupleStorageApi } from "../sync/types"
import { TxId, Unsubscribe } from "../types"
import { RngApi, TupleDatabaseOptions, TxId, Unsubscribe } from "../types"
import { AsyncReactivityTracker } from "./AsyncReactivityTracker"
import {
AsyncCallback,
Expand All @@ -12,10 +12,17 @@ import {
} from "./asyncTypes"

export class AsyncTupleDatabase implements AsyncTupleDatabaseApi {
constructor(private storage: TupleStorageApi | AsyncTupleStorageApi) {}

private rng: RngApi
log = new ConcurrencyLog()
reactivity = new AsyncReactivityTracker()
reactivity: AsyncReactivityTracker

constructor(
private storage: TupleStorageApi | AsyncTupleStorageApi,
options: TupleDatabaseOptions = {}
) {
this.rng = options.rng ?? defaultRng
this.reactivity = new AsyncReactivityTracker(this.rng)
}

async scan(args: ScanStorageArgs = {}, txId?: TxId): Promise<KeyValuePair[]> {
const { reverse, limit, ...bounds } = args
Expand All @@ -40,7 +47,7 @@ export class AsyncTupleDatabase implements AsyncTupleDatabaseApi {
}
await this.storage.commit(writes)

return this.reactivity.emit(emits, txId || randomId())
return this.reactivity.emit(emits, txId || this.rng.randomId())
}

async cancel(txId: string) {
Expand Down
64 changes: 64 additions & 0 deletions src/database/rng.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { strict as assert } from "assert"
import { describe, it } from "mocha"
import { InMemoryTupleStorage } from "../storage/InMemoryTupleStorage"
import { MAX, MIN, ScanStorageArgs } from "../storage/types"
import { AsyncTupleDatabase } from "./async/AsyncTupleDatabase"
import { TupleDatabase } from "./sync/TupleDatabase"
import { RngApi } from "./types"

const todos: ScanStorageArgs = { gt: ["todos", MIN], lt: ["todos", MAX] }

function sequenceRng(ids: string[]): RngApi {
let index = 0
return { randomId: () => ids[index++] }
}

describe("TupleDatabase rng option", () => {
it("orders listeners on the same prefix by the ids the rng returns", () => {
const db = new TupleDatabase(new InMemoryTupleStorage(), {
rng: sequenceRng(["b", "a", "tx"]),
})
const calls: string[] = []
db.subscribe(todos, () => {
calls.push("first")
})
db.subscribe(todos, () => {
calls.push("second")
})

db.commit({ set: [{ key: ["todos", 1], value: null }] })

assert.deepEqual(calls, ["second", "first"])
})

it("uses the rng for a commit made without a transaction id", () => {
const db = new TupleDatabase(new InMemoryTupleStorage(), {
rng: sequenceRng(["listener", "tx-1"]),
})
const txIds: string[] = []
db.subscribe(todos, (_writes, txId) => {
txIds.push(txId)
})

db.commit({ set: [{ key: ["todos", 1], value: null }] })

assert.deepEqual(txIds, ["tx-1"])
})

it("threads the rng through the async database", async () => {
const db = new AsyncTupleDatabase(new InMemoryTupleStorage(), {
rng: sequenceRng(["b", "a", "tx-1"]),
})
const calls: string[] = []
await db.subscribe(todos, (_writes, txId) => {
calls.push(`first:${txId}`)
})
await db.subscribe(todos, (_writes, txId) => {
calls.push(`second:${txId}`)
})

await db.commit({ set: [{ key: ["todos", 1], value: null }] })

assert.deepEqual(calls, ["second:tx-1", "first:tx-1"])
})
})
10 changes: 6 additions & 4 deletions src/database/sync/ReactivityTracker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This file is generated from async/AsyncReactivityTracker.ts
type Identity<T> = T

import { maybePromiseAll } from "../../helpers/maybeWaitForPromises"
import { randomId } from "../../helpers/randomId"
import { defaultRng } from "../../helpers/randomId"
import {
Bounds,
getPrefixContainingBounds,
Expand All @@ -16,14 +16,16 @@ import {
import { InMemoryTupleStorage } from "../../storage/InMemoryTupleStorage"
import { MIN, ScanStorageArgs, Tuple, WriteOps } from "../../storage/types"
import { TupleStorageApi } from "../sync/types"
import { TxId } from "../types"
import { RngApi, TxId } from "../types"
import { Callback } from "./types"

export class ReactivityTracker {
private listenersDb = new InMemoryTupleStorage()

constructor(private rng: RngApi = defaultRng) {}

subscribe(args: ScanStorageArgs, callback: Callback) {
return subscribe(this.listenersDb, args, callback)
return subscribe(this.listenersDb, this.rng.randomId(), args, callback)
}

computeReactivityEmits(writes: WriteOps) {
Expand Down Expand Up @@ -127,14 +129,14 @@ function getReactivityEmits(listenersDb: TupleStorageApi, writes: WriteOps) {

function subscribe(
listenersDb: TupleStorageApi,
id: string,
args: ScanStorageArgs,
callback: Callback
) {
// this.log("db/subscribe", args)

const prefix = getPrefixContainingBounds(args)

const id = randomId()
const value: Listener = { callback, bounds: args }

listenersDb.commit({ set: [{ key: [prefix, id], value }] })
Expand Down
19 changes: 13 additions & 6 deletions src/database/sync/TupleDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,26 @@ This file is generated from async/AsyncTupleDatabase.ts
type Identity<T> = T

import { iterateWrittenTuples } from "../../helpers/iterateTuples"
import { randomId } from "../../helpers/randomId"
import { defaultRng } from "../../helpers/randomId"
import { KeyValuePair, ScanStorageArgs, WriteOps } from "../../storage/types"
import { ConcurrencyLog } from "../ConcurrencyLog"
import { TupleStorageApi } from "../sync/types"
import { TxId, Unsubscribe } from "../types"
import { RngApi, TupleDatabaseOptions, TxId, Unsubscribe } from "../types"
import { ReactivityTracker } from "./ReactivityTracker"
import { Callback, TupleDatabaseApi } from "./types"

export class TupleDatabase implements TupleDatabaseApi {
constructor(private storage: TupleStorageApi) {}

private rng: RngApi
log = new ConcurrencyLog()
reactivity = new ReactivityTracker()
reactivity: ReactivityTracker

constructor(
private storage: TupleStorageApi,
options: TupleDatabaseOptions = {}
) {
this.rng = options.rng ?? defaultRng
this.reactivity = new ReactivityTracker(this.rng)
}

scan(args: ScanStorageArgs = {}, txId?: TxId): Identity<KeyValuePair[]> {
const { reverse, limit, ...bounds } = args
Expand All @@ -41,7 +48,7 @@ export class TupleDatabase implements TupleDatabaseApi {
}
this.storage.commit(writes)

return this.reactivity.emit(emits, txId || randomId())
return this.reactivity.emit(emits, txId || this.rng.randomId())
}

cancel(txId: string) {
Expand Down
9 changes: 9 additions & 0 deletions src/database/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,12 @@ type AllowMinMax<T extends Tuple> = {
export type TxId = string

export type Unsubscribe = () => void

export type RngApi = {
randomId: () => string
}

export type TupleDatabaseOptions = {
/** Source of listener and fallback transaction ids. Defaults to random UUIDs. */
rng?: RngApi
}
Loading
Loading