@syncular/client 0.21.1 → 0.23.0

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.
package/src/database.ts CHANGED
@@ -9,6 +9,24 @@ export type SqlValue = string | number | bigint | boolean | Uint8Array | null;
9
9
 
10
10
  export type SqlRow = Record<string, SqlValue>;
11
11
 
12
+ /**
13
+ * RFC 0005: a second database file in this database's own storage scope (the
14
+ * same on-disk directory, or the same OPFS SAH pool directory). Used to keep
15
+ * the previous-version container off the connection `query()` and unaware code
16
+ * use. The read guarantee is narrow: an older client's ordinary query
17
+ * connection does not attach this file, so no SQL it runs can reach the
18
+ * container. It is NOT protection against arbitrary same-origin access and NOT
19
+ * protection against native filesystem access.
20
+ */
21
+ export interface SiblingDatabase {
22
+ /** The open connection; the caller closes it before removal. */
23
+ readonly database: ClientDatabase;
24
+ /** Close the handle. A live handle blocks physical removal. */
25
+ close(): void;
26
+ /** Best-effort physical removal of the backing file. Close the handle first. */
27
+ removeFile(): void;
28
+ }
29
+
12
30
  export interface ClientDatabase {
13
31
  /** Execute a single statement (no result rows). */
14
32
  exec(sql: string, params?: readonly SqlValue[]): void;
@@ -32,6 +50,16 @@ export interface ClientDatabase {
32
50
  alias: string,
33
51
  fn: () => T | Promise<T>,
34
52
  ): Promise<T>;
53
+ /**
54
+ * RFC 0005: open (creating on demand) the named sibling database file.
55
+ * `undefined` means the host provides no sibling capability, which makes the
56
+ * previous-version feature unavailable there. The NAME is code-derived and is
57
+ * not persisted in the replica database (data minimisation; not a security
58
+ * control).
59
+ */
60
+ openSibling?(name: string): SiblingDatabase | undefined;
61
+ /** RFC 0005: non-creating existence probe for {@link openSibling}. */
62
+ siblingExists?(name: string): boolean;
35
63
  close(): void;
36
64
  }
37
65
 
package/src/index.ts CHANGED
@@ -28,6 +28,19 @@ export * from './multi-tab';
28
28
  export * from './naming';
29
29
  export * from './outbox';
30
30
  export * from './outcomes';
31
+ // RFC 0005: explicit public surface. Everything else in the module is
32
+ // implementation detail used by `client.ts` (capture, container file, durable
33
+ // refusal/audit codecs), not package API. The feature is opt-in and its
34
+ // consumer-facing contract is exactly the read spec and the two result types.
35
+ export {
36
+ decodePreviousVersionAuditResult,
37
+ decodePreviousVersionDiscardResult,
38
+ decodePreviousVersionSnapshot,
39
+ type PreviousVersionAudit,
40
+ type PreviousVersionReadSpec,
41
+ type PreviousVersionReason,
42
+ type PreviousVersionSnapshot,
43
+ } from './previous-version';
31
44
  export * from './query-guard';
32
45
  export * from './reactive-store';
33
46
  export * from './remote';
@@ -30,6 +30,15 @@ export interface SyncStatusSnapshot {
30
30
  readonly leaseState: LeaseState | undefined;
31
31
  readonly schemaFloor: SchemaFloor | undefined;
32
32
  readonly syncNeeded: boolean;
33
+ /**
34
+ * RFC 0005 A2: whether a previous-version cache is present, so a host's
35
+ * update/rollback path can require its discard. `createdAtMs` is the capture
36
+ * time when a readable cache exists.
37
+ */
38
+ readonly previousVersionContext: {
39
+ readonly present: boolean;
40
+ readonly createdAtMs?: number;
41
+ };
33
42
  }
34
43
 
35
44
  export interface ClientChangeBatch {
@@ -3,13 +3,14 @@
3
3
  * adapter: synchronous queries, nested transactions, and SQLite image attach.
4
4
  */
5
5
  import { DatabaseSync, type SQLInputValue } from 'node:sqlite';
6
- import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
7
7
  import { tmpdir } from 'node:os';
8
8
  import { join } from 'node:path';
9
9
  import {
10
10
  assertImageAlias,
11
11
  type ClientDatabase,
12
12
  runTransaction,
13
+ type SiblingDatabase,
13
14
  type SqlRow,
14
15
  type SqlValue,
15
16
  } from './database';
@@ -36,9 +37,11 @@ function normalizeRow(row: Record<string, unknown>): SqlRow {
36
37
 
37
38
  export class NodeClientDatabase implements ClientDatabase {
38
39
  readonly db: DatabaseSync;
40
+ readonly #path: string;
39
41
  #tx = { depth: 0 };
40
42
 
41
43
  constructor(path = ':memory:') {
44
+ this.#path = path;
42
45
  this.db = new DatabaseSync(path);
43
46
  try {
44
47
  this.db.exec('PRAGMA journal_mode = WAL');
@@ -58,6 +61,28 @@ export class NodeClientDatabase implements ClientDatabase {
58
61
  return rows.map(normalizeRow);
59
62
  }
60
63
 
64
+ /** RFC 0005: a sibling file beside the replica, e.g. `<path>.prev-context`. */
65
+ openSibling(name: string): SiblingDatabase {
66
+ const siblingPath =
67
+ this.#path === ':memory:' ? ':memory:' : `${this.#path}.${name}`;
68
+ const database = new NodeClientDatabase(siblingPath);
69
+ return {
70
+ database,
71
+ close: () => database.close(),
72
+ removeFile: () => {
73
+ if (siblingPath === ':memory:') return;
74
+ rmSync(siblingPath, { force: true });
75
+ rmSync(`${siblingPath}-wal`, { force: true });
76
+ rmSync(`${siblingPath}-shm`, { force: true });
77
+ },
78
+ };
79
+ }
80
+
81
+ siblingExists(name: string): boolean {
82
+ if (this.#path === ':memory:') return false;
83
+ return existsSync(`${this.#path}.${name}`);
84
+ }
85
+
61
86
  transaction<T>(fn: () => T): T {
62
87
  return runTransaction(this.#tx, (sql) => this.db.exec(sql), fn);
63
88
  }