Skip to content
Merged
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
17 changes: 17 additions & 0 deletions .changeset/forget-ledger-first.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
"@bounda-dev/core": minor
"@bounda-dev/sqlite": minor
"@bounda-dev/postgresql": minor
"@bounda-dev/cloudflare": minor
"@bounda-dev/cli": minor
---

With a forget ledger, every forgetting is in the ledger before it commits. `forget`, `forgetAgain`, a policy's or process's `forget` and the retention write the forgetting's intention first, its event's id, time and reason set before the transaction, which appends the event with them; a reconciliation commits again, under the same id, time and reason, every forgetting the database does not hold whose person still holds a key it names, so an intention whose commit never happened, because the process stopped or the transaction failed, is applied at the next start, after a restore too. A `forget` that rejects after its intention was written still forgets the person then. A forgetting the retention made is committed again only while the retention would still forget the person. A reconciliation leaves an entry younger than `FORGET_LEDGER_GRACE_MS` to a later one, due once it is old enough, but for the one a restore found, which the copy cannot be committing.

Ledger entries and `PersonalDataForgotten` name the keys a forgetting destroys by their fingerprint: `ForgetLedgerEntry` gains `reason` and `fingerprints`, the payload gains `fingerprints`, and a reconciliation leaves a key the entry does not name, given to the person after it. The reason `"restored"` is gone: a forgetting committed again keeps its own. The SQL ledger table gains `reason` and `fingerprints` in version 2 of its `forget-ledger` scope, and the KV ledger keeps them in each key's metadata; an entry written before reads as a request that names no keys.

`forgetLedger.strict` now also means nothing is forgotten that the ledger has not recorded: a forgetting whose intention the ledger does not take fails with `ForgetLedgerUnavailableError` (`FORGET_LEDGER_UNAVAILABLE`) and forgets nobody, so trying again is safe. `forgetLedger.timeout` (`DEFAULT_FORGET_LEDGER_TIMEOUT_MS`, ten seconds) bounds each call to the ledger, given up with `ForgetLedgerTimeoutError` (`FORGET_LEDGER_TIMEOUT`), and how long a start that is not strict waits for the reconciliation; a strict start fails once a read runs past it. `parseDuration` is exported, and `isAdapter` from `@bounda-dev/core/adapter`.

`bounda forgotten` reads the event store without starting the app, so a strict ledger that is down does not stop it, and prints each forgetting's reason and fingerprints; `bounda forget --from` commits each whole line again under its own id, time and reason. `bounda forget-ledger expire --binding <name> --keep-for <duration>` gives every key of a Workers KV ledger the expiration `keepFor` sets, those written before it was set included, through the project's wrangler; `expireKvForgetLedger` and `planKvForgetLedgerExpiry` from `@bounda-dev/cloudflare`, and its new `./forget-ledger` subpath, do the same in code.

`sqlite()` closes its libSQL connection only once the statements and transactions it started have settled, so closing an app or a ledger under a call still running, as one given up for its timeout, no longer frees what libSQL's native code is using.
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ jobs:
if: needs.changes.outputs.packages != '[]'
runs-on: windows-latest
timeout-minutes: 30
env:
# A worker that dies in native code, as libSQL can there, names the test it was running.
BOUNDA_TEST_TRACE: "1"
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0
Expand All @@ -112,7 +115,7 @@ jobs:
run: pnpm --filter @bounda-dev/core test
- name: sqlite
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: pnpm --filter @bounda-dev/sqlite exec vitest run --exclude src/libsql-server.test.ts
run: pnpm --filter @bounda-dev/sqlite exec vitest run --exclude src/libsql-server.test.ts --reporter=verbose
- name: cli
if: ${{ !cancelled() && steps.build.outcome == 'success' }}
run: pnpm --filter @bounda-dev/cli test
Expand Down
6 changes: 5 additions & 1 deletion docs/src/content/docs/adapters/cloudflare.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,11 @@ in `wrangler.jsonc` and name it in `runtime.personalData.forgetLedger.store`; an
`ConfigurationError`. Each object keeps its entries under its own name, and reads the ledger once
more a minute after it starts, since KV may take that long to list a key. The periodic
reconciliation never wakes an object on its own, and with `keepFor` each key expires in KV by
itself. See [backups](/guides/personal-data/#backups).
itself; `bounda forget-ledger expire` gives the keys written before `keepFor` was set theirs.
Every forgetting writes its intention to KV before the object commits it, so one the object did not
get to commit, because it was evicted or its transaction failed, is committed at its next start;
with `strict`, a forgetting KV does not take fails and forgets nobody. See
[backups](/guides/personal-data/#backups).

## One object per tenant

Expand Down
Loading
Loading