orez 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/dist/cf-do/tx-journal.d.ts +62 -0
  2. package/dist/cf-do/tx-journal.d.ts.map +1 -0
  3. package/dist/cf-do/tx-journal.js +163 -0
  4. package/dist/cf-do/tx-journal.js.map +1 -0
  5. package/dist/cf-do/worker.d.ts +15 -2
  6. package/dist/cf-do/worker.d.ts.map +1 -1
  7. package/dist/cf-do/worker.js +46 -8
  8. package/dist/cf-do/worker.js.map +1 -1
  9. package/dist/pg-proxy-browser.d.ts.map +1 -1
  10. package/dist/pg-proxy-browser.js +127 -15
  11. package/dist/pg-proxy-browser.js.map +1 -1
  12. package/dist/pg-proxy-do-backend.d.ts +2 -5
  13. package/dist/pg-proxy-do-backend.d.ts.map +1 -1
  14. package/dist/pg-proxy-do-backend.js +36 -63
  15. package/dist/pg-proxy-do-backend.js.map +1 -1
  16. package/dist/pg-proxy.d.ts.map +1 -1
  17. package/dist/pg-proxy.js +73 -17
  18. package/dist/pg-proxy.js.map +1 -1
  19. package/dist/replication/change-tracker.d.ts +28 -0
  20. package/dist/replication/change-tracker.d.ts.map +1 -1
  21. package/dist/replication/change-tracker.js +53 -0
  22. package/dist/replication/change-tracker.js.map +1 -1
  23. package/dist/replication/handler.d.ts +13 -0
  24. package/dist/replication/handler.d.ts.map +1 -1
  25. package/dist/replication/handler.js +145 -36
  26. package/dist/replication/handler.js.map +1 -1
  27. package/dist/worker/cf-patches.d.ts.map +1 -1
  28. package/dist/worker/cf-patches.js +17 -0
  29. package/dist/worker/cf-patches.js.map +1 -1
  30. package/dist/worker/local-sql-backend.d.ts +35 -0
  31. package/dist/worker/local-sql-backend.d.ts.map +1 -0
  32. package/dist/worker/local-sql-backend.js +106 -0
  33. package/dist/worker/local-sql-backend.js.map +1 -0
  34. package/dist/worker/zero-cache-embed-cf.d.ts +1 -11
  35. package/dist/worker/zero-cache-embed-cf.d.ts.map +1 -1
  36. package/dist/worker/zero-cache-embed-cf.js +56 -14
  37. package/dist/worker/zero-cache-embed-cf.js.map +1 -1
  38. package/dist/zero-litestream-patch.d.ts +6 -0
  39. package/dist/zero-litestream-patch.d.ts.map +1 -1
  40. package/dist/zero-litestream-patch.js +15 -7
  41. package/dist/zero-litestream-patch.js.map +1 -1
  42. package/package.json +2 -2
@@ -1,5 +1,6 @@
1
1
  // @ts-nocheck
2
2
  import { deparseSync, loadModule, parseSync } from 'pgsql-parser';
3
+ import { TX_MANIFEST_DDL, TX_MANIFEST_TABLE } from './cf-do/tx-journal.js';
3
4
  import { RETURNING_INTERNAL_PREFIX } from './do-sql-tracking.js';
4
5
  import { signalReplicationChange } from './replication/handler.js';
5
6
  import { markSQLiteKeywordIdentifiers, restoreSQLiteKeywordIdentifierMarkers, } from './sqlite-keyword-identifiers.js';
@@ -4587,10 +4588,17 @@ export class DoBackend {
4587
4588
  // round-trip to durable storage after every DDL statement in a migration.
4588
4589
  txMetadataDirty = false;
4589
4590
  txHasTrackedWrite = false;
4591
+ // identifies the client process generation owning this backend's
4592
+ // transactions in the durable tx journal. a process that knows its previous
4593
+ // generation is dead (e.g. the zero-cache embed at boot) recovers orphaned
4594
+ // transactions for its own owner id only, so it can never roll back another
4595
+ // live client's in-flight transaction.
4596
+ txOwner;
4590
4597
  constructor(doUrl, dbName = 'postgres', namespace = 'default', opts) {
4591
4598
  this.doUrl = doUrl.replace(/\/+$/, '');
4592
4599
  this.dbName = dbName;
4593
4600
  this.namespace = namespace;
4601
+ this.txOwner = opts?.txOwner || 'default';
4594
4602
  this.httpClient = new HttpClient(opts?.fetch);
4595
4603
  this.skippedFunctionNames = getSkippedFunctionNames(dbName, namespace);
4596
4604
  this.triggerFunctions = new Map();
@@ -4630,6 +4638,7 @@ export class DoBackend {
4630
4638
  await this.doExecResult('CREATE TABLE IF NOT EXISTS "_orez___zero_watermark" (dummy INTEGER PRIMARY KEY DEFAULT 1, last_value INTEGER NOT NULL DEFAULT 1, is_called INTEGER NOT NULL DEFAULT 0)');
4631
4639
  await this.doExecResult('INSERT OR IGNORE INTO "_orez___zero_watermark" (dummy, last_value, is_called) VALUES (1, 1, 0)');
4632
4640
  await this.doExecResult("CREATE TABLE IF NOT EXISTS \"_orez__zero_replication_slots\" (slot_name TEXT PRIMARY KEY, restart_lsn TEXT NOT NULL DEFAULT '0/1000000', confirmed_flush_lsn TEXT NOT NULL DEFAULT '0/1000000', wal_status TEXT NOT NULL DEFAULT 'reserved', plugin TEXT NOT NULL DEFAULT 'pgoutput', slot_type TEXT NOT NULL DEFAULT 'logical', active INTEGER NOT NULL DEFAULT 0, active_pid INTEGER DEFAULT NULL, created_at INTEGER NOT NULL DEFAULT (unixepoch()))");
4641
+ await this.doExecResult('CREATE TABLE IF NOT EXISTS "_orez___zero_streamed_batches" (batch_lsn INTEGER PRIMARY KEY, batch_end INTEGER NOT NULL)');
4633
4642
  }
4634
4643
  async ensureMetadataTable() {
4635
4644
  await this.doExecResult(metadataTableDDL());
@@ -4864,9 +4873,12 @@ export class DoBackend {
4864
4873
  const shouldPersist = this.txMetadataDirty;
4865
4874
  const shouldSignal = this.txHasTrackedWrite;
4866
4875
  const txID = this.txID;
4867
- if (shouldSignal && txID)
4868
- await this.commitTrackedTransaction(txID);
4869
- await this.dropTransactionSnapshots();
4876
+ // ONE atomic commit point on the backend: promotes pending tracked
4877
+ // changes and clears the tx journal (snapshots + manifest) in a single
4878
+ // storage transaction. read-only transactions skip the round-trip.
4879
+ if (txID && (shouldSignal || this.txDataSnapshots.size > 0)) {
4880
+ await this.httpClient.post(this.url('/commit-tx'), JSON.stringify({ transactionID: txID }), { 'Content-Type': 'application/json' });
4881
+ }
4870
4882
  this.clearTransactionState();
4871
4883
  if (shouldPersist)
4872
4884
  await this.persistDurableMetadata();
@@ -4881,23 +4893,19 @@ export class DoBackend {
4881
4893
  const snapshot = this.txSnapshot;
4882
4894
  const txID = this.txID;
4883
4895
  try {
4884
- await this.restoreTransactionDataSnapshots();
4896
+ // ONE atomic rollback on the backend: restores snapshotted tables from
4897
+ // the durable tx journal and discards pending tracked changes.
4898
+ if (txID && (this.txHasTrackedWrite || this.txDataSnapshots.size > 0)) {
4899
+ await this.httpClient.post(this.url('/rollback-tx'), JSON.stringify({ transactionID: txID }), { 'Content-Type': 'application/json' });
4900
+ }
4885
4901
  }
4886
4902
  finally {
4887
- if (txID)
4888
- await this.rollbackTrackedTransaction(txID);
4889
4903
  if (snapshot)
4890
4904
  this.restoreTransactionMetadataSnapshot(snapshot);
4891
4905
  await this.persistDurableMetadata();
4892
4906
  this.clearTransactionState();
4893
4907
  }
4894
4908
  }
4895
- async commitTrackedTransaction(transactionID) {
4896
- await this.httpClient.post(this.url('/commit-tracked-tx'), JSON.stringify({ transactionID }), { 'Content-Type': 'application/json' });
4897
- }
4898
- async rollbackTrackedTransaction(transactionID) {
4899
- await this.httpClient.post(this.url('/rollback-tracked-tx'), JSON.stringify({ transactionID }), { 'Content-Type': 'application/json' });
4900
- }
4901
4909
  async execProtocolRaw(message, options) {
4902
4910
  if (!this.ready)
4903
4911
  await this.waitReady;
@@ -5639,19 +5647,6 @@ export class DoBackend {
5639
5647
  const result = await this.doExecResult("SELECT 1 AS ok FROM sqlite_master WHERE type = 'table' AND name = ? LIMIT 1", [table]);
5640
5648
  return result.rows.length > 0;
5641
5649
  }
5642
- async triggerDefinitionsForTables(tables) {
5643
- const uniqueTables = [...new Set(tables)].filter(Boolean);
5644
- if (uniqueTables.length === 0)
5645
- return [];
5646
- const placeholders = uniqueTables.map(() => '?').join(', ');
5647
- const result = await this.doExecResult(`SELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND tbl_name IN (${placeholders}) ORDER BY name`, uniqueTables);
5648
- return result.rows
5649
- .map((row) => ({
5650
- name: String(row.name ?? ''),
5651
- sql: String(row.sql ?? ''),
5652
- }))
5653
- .filter((trigger) => trigger.name && trigger.sql);
5654
- }
5655
5650
  async snapshotTransactionTable(table) {
5656
5651
  if (!this.inTransaction || this.txDataSnapshots.has(table))
5657
5652
  return;
@@ -5661,12 +5656,22 @@ export class DoBackend {
5661
5656
  // the table — registration only happens after a successful CREATE, so its
5662
5657
  // presence is proof the table exists. saves one /exec on the first write
5663
5658
  // to each table per tx, which on chat's hot mutation paths matters.
5664
- if (!this.schemaMetadata.has(table) && !(await this.tableExistsInDo(table))) {
5665
- this.txDataSnapshots.set(table, null);
5666
- return;
5659
+ const exists = this.schemaMetadata.has(table) || (await this.tableExistsInDo(table));
5660
+ // snapshot + manifest row land in one atomic /batch, so a DO kill at any
5661
+ // point leaves either no trace or a journal entry recovery can roll back.
5662
+ // the DDL is idempotent and rides the same batch (no extra round-trip).
5663
+ const snapshot = exists ? this.transactionSnapshotName(table) : null;
5664
+ const statements = [{ sql: TX_MANIFEST_DDL }];
5665
+ if (snapshot) {
5666
+ statements.push({
5667
+ sql: `CREATE TABLE ${quoteIdentifier(snapshot)} AS SELECT * FROM ${quoteIdentifier(table)}`,
5668
+ });
5667
5669
  }
5668
- const snapshot = this.transactionSnapshotName(table);
5669
- await this.doExecResult(`CREATE TABLE ${quoteIdentifier(snapshot)} AS SELECT * FROM ${quoteIdentifier(table)}`);
5670
+ statements.push({
5671
+ sql: `INSERT INTO "${TX_MANIFEST_TABLE}" (tx_id, owner, original, snapshot) VALUES (?, ?, ?, ?)`,
5672
+ params: [this.txID, this.txOwner, table, snapshot],
5673
+ });
5674
+ await this.doRawBatch(statements);
5670
5675
  this.txDataSnapshots.set(table, snapshot);
5671
5676
  }
5672
5677
  async snapshotTransactionChangeTables() {
@@ -5691,40 +5696,8 @@ export class DoBackend {
5691
5696
  if (this.trackingForStatement(statement))
5692
5697
  await this.snapshotTransactionChangeTables();
5693
5698
  }
5694
- async dropTransactionSnapshots() {
5695
- const drops = [];
5696
- for (const snapshot of this.txDataSnapshots.values()) {
5697
- if (snapshot)
5698
- drops.push(`DROP TABLE IF EXISTS ${quoteIdentifier(snapshot)}`);
5699
- }
5700
- if (drops.length > 0)
5701
- await this.doRawBatch(drops);
5702
- }
5703
- async restoreTransactionDataSnapshots() {
5704
- const entries = [...this.txDataSnapshots.entries()].reverse();
5705
- const restoredTables = entries
5706
- .filter(([, snapshot]) => Boolean(snapshot))
5707
- .map(([table]) => table);
5708
- const triggers = await this.triggerDefinitionsForTables(restoredTables);
5709
- const statements = [];
5710
- for (const trigger of triggers) {
5711
- statements.push(`DROP TRIGGER IF EXISTS ${quoteIdentifier(trigger.name)}`);
5712
- }
5713
- for (const [table, snapshot] of entries) {
5714
- const quotedTable = quoteIdentifier(table);
5715
- if (!snapshot) {
5716
- statements.push(`DROP TABLE IF EXISTS ${quotedTable}`);
5717
- continue;
5718
- }
5719
- const quotedSnapshot = quoteIdentifier(snapshot);
5720
- statements.push(`DELETE FROM ${quotedTable}`, `INSERT OR REPLACE INTO ${quotedTable} SELECT * FROM ${quotedSnapshot}`, `DROP TABLE IF EXISTS ${quotedSnapshot}`);
5721
- }
5722
- for (const trigger of triggers)
5723
- statements.push(trigger.sql);
5724
- await this.doRawBatch(statements);
5725
- }
5726
5699
  async doRawBatch(statements) {
5727
- const sqls = statements.filter((sql) => sql.trim());
5700
+ const sqls = statements.filter((statement) => (typeof statement === 'string' ? statement : statement.sql).trim());
5728
5701
  if (sqls.length === 0)
5729
5702
  return;
5730
5703
  await this.httpClient.post(this.url('/batch'), JSON.stringify({ statements: sqls }), {