@syncular/client 0.1.2 → 0.2.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/README.md +227 -709
- package/dist/apply.d.ts +80 -0
- package/dist/apply.js +301 -0
- package/dist/blob.d.ts +127 -0
- package/dist/blob.js +201 -0
- package/dist/bun-database.d.ts +22 -0
- package/dist/bun-database.js +61 -0
- package/dist/client.d.ts +320 -69
- package/dist/client.js +1627 -163
- package/dist/content-type.d.ts +2 -0
- package/dist/content-type.js +2 -0
- package/dist/database.d.ts +35 -80
- package/dist/database.js +32 -947
- package/dist/encryption.d.ts +47 -0
- package/dist/encryption.js +75 -0
- package/dist/errors.d.ts +8 -22
- package/dist/errors.js +10 -207
- package/dist/http.d.ts +32 -0
- package/dist/http.js +196 -0
- package/dist/index.d.ts +28 -16
- package/dist/index.js +25 -12
- package/dist/invalidation.d.ts +69 -0
- package/dist/invalidation.js +84 -0
- package/dist/leader-lock.d.ts +28 -0
- package/dist/leader-lock.js +38 -0
- package/dist/multi-tab.d.ts +134 -0
- package/dist/multi-tab.js +399 -0
- package/dist/node-database.d.ts +41 -0
- package/dist/node-database.js +133 -0
- package/dist/outbox.d.ts +56 -0
- package/dist/outbox.js +140 -0
- package/dist/schema.d.ts +107 -0
- package/dist/schema.js +267 -0
- package/dist/state.d.ts +40 -0
- package/dist/state.js +77 -0
- package/dist/transport.d.ts +44 -0
- package/dist/transport.js +7 -0
- package/dist/wasm-database.d.ts +30 -0
- package/dist/wasm-database.js +170 -0
- package/dist/window.d.ts +64 -0
- package/dist/window.js +0 -0
- package/dist/worker-entry.d.ts +16 -2
- package/dist/worker-entry.js +300 -456
- package/dist/worker-host.d.ts +174 -0
- package/dist/worker-host.js +536 -0
- package/dist/worker-protocol.d.ts +173 -61
- package/dist/worker-protocol.js +7 -16
- package/package.json +45 -112
- package/src/apply.ts +435 -0
- package/src/blob.ts +333 -0
- package/src/bun-database.ts +75 -0
- package/src/client.ts +2247 -277
- package/src/content-type.ts +2 -0
- package/src/database.ts +63 -1389
- package/src/encryption.ts +123 -0
- package/src/errors.ts +11 -265
- package/src/http.ts +254 -0
- package/src/index.ts +25 -20
- package/src/invalidation.ts +128 -0
- package/src/leader-lock.ts +68 -0
- package/src/multi-tab.ts +550 -0
- package/src/node-database.ts +180 -0
- package/src/outbox.ts +206 -0
- package/src/schema.ts +397 -0
- package/src/state.ts +126 -0
- package/src/transport.ts +52 -0
- package/src/wasm-database.ts +279 -0
- package/src/window.ts +0 -0
- package/src/worker-entry.ts +391 -545
- package/src/worker-host.ts +800 -0
- package/src/worker-protocol.ts +204 -99
- package/dist/auth-leases.d.ts +0 -11
- package/dist/auth-leases.d.ts.map +0 -1
- package/dist/auth-leases.js +0 -140
- package/dist/auth-leases.js.map +0 -1
- package/dist/blob-limits.d.ts +0 -15
- package/dist/blob-limits.d.ts.map +0 -1
- package/dist/blob-limits.js +0 -66
- package/dist/blob-limits.js.map +0 -1
- package/dist/bridge-client.d.ts +0 -69
- package/dist/bridge-client.d.ts.map +0 -1
- package/dist/bridge-client.js +0 -282
- package/dist/bridge-client.js.map +0 -1
- package/dist/client-config.d.ts +0 -6
- package/dist/client-config.d.ts.map +0 -1
- package/dist/client-config.js +0 -18
- package/dist/client-config.js.map +0 -1
- package/dist/client.d.ts.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/command-history.d.ts +0 -35
- package/dist/command-history.d.ts.map +0 -1
- package/dist/command-history.js +0 -378
- package/dist/command-history.js.map +0 -1
- package/dist/console-diagnostics.d.ts +0 -33
- package/dist/console-diagnostics.d.ts.map +0 -1
- package/dist/console-diagnostics.js +0 -496
- package/dist/console-diagnostics.js.map +0 -1
- package/dist/crdt-yjs/index.d.ts +0 -4
- package/dist/crdt-yjs/index.d.ts.map +0 -1
- package/dist/crdt-yjs/index.js +0 -4
- package/dist/crdt-yjs/index.js.map +0 -1
- package/dist/crdt-yjs/webview-host-facade.d.ts +0 -126
- package/dist/crdt-yjs/webview-host-facade.d.ts.map +0 -1
- package/dist/crdt-yjs/webview-host-facade.js +0 -284
- package/dist/crdt-yjs/webview-host-facade.js.map +0 -1
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts +0 -153
- package/dist/crdt-yjs/yjs-document-field-adapter.d.ts.map +0 -1
- package/dist/crdt-yjs/yjs-document-field-adapter.js +0 -406
- package/dist/crdt-yjs/yjs-document-field-adapter.js.map +0 -1
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts +0 -73
- package/dist/crdt-yjs/yjs-prosemirror-bridge.d.ts.map +0 -1
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js +0 -169
- package/dist/crdt-yjs/yjs-prosemirror-bridge.js.map +0 -1
- package/dist/database.d.ts.map +0 -1
- package/dist/database.js.map +0 -1
- package/dist/diagnostics.d.ts +0 -10
- package/dist/diagnostics.d.ts.map +0 -1
- package/dist/diagnostics.js +0 -83
- package/dist/diagnostics.js.map +0 -1
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/generated-bridge.d.ts +0 -365
- package/dist/generated-bridge.d.ts.map +0 -1
- package/dist/generated-bridge.js +0 -250
- package/dist/generated-bridge.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/mutations.d.ts +0 -72
- package/dist/mutations.d.ts.map +0 -1
- package/dist/mutations.js +0 -63
- package/dist/mutations.js.map +0 -1
- package/dist/network.d.ts +0 -3
- package/dist/network.d.ts.map +0 -1
- package/dist/network.js +0 -17
- package/dist/network.js.map +0 -1
- package/dist/react/index.d.ts +0 -169
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/index.js +0 -628
- package/dist/react/index.js.map +0 -1
- package/dist/react-native/index.d.ts +0 -35
- package/dist/react-native/index.d.ts.map +0 -1
- package/dist/react-native/index.js +0 -49
- package/dist/react-native/index.js.map +0 -1
- package/dist/runtime-contract.d.ts +0 -13
- package/dist/runtime-contract.d.ts.map +0 -1
- package/dist/runtime-contract.js +0 -24
- package/dist/runtime-contract.js.map +0 -1
- package/dist/rust-client.d.ts +0 -106
- package/dist/rust-client.d.ts.map +0 -1
- package/dist/rust-client.js +0 -938
- package/dist/rust-client.js.map +0 -1
- package/dist/sentry.d.ts +0 -35
- package/dist/sentry.d.ts.map +0 -1
- package/dist/sentry.js +0 -155
- package/dist/sentry.js.map +0 -1
- package/dist/sql-safety.d.ts +0 -3
- package/dist/sql-safety.d.ts.map +0 -1
- package/dist/sql-safety.js +0 -62
- package/dist/sql-safety.js.map +0 -1
- package/dist/syncular-runtime-artifacts.json +0 -61
- package/dist/tauri/index.d.ts +0 -35
- package/dist/tauri/index.d.ts.map +0 -1
- package/dist/tauri/index.js +0 -114
- package/dist/tauri/index.js.map +0 -1
- package/dist/types.d.ts +0 -1096
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/wasm/.syncular-wasm-profile +0 -1
- package/dist/wasm/syncular-runtime-artifact.json +0 -21
- package/dist/wasm/syncular.d.ts +0 -207
- package/dist/wasm/syncular.js +0 -2341
- package/dist/wasm/syncular_bg.wasm +0 -0
- package/dist/wasm/syncular_bg.wasm.d.ts +0 -97
- package/dist/wasm-bindings/runtime-contract.d.ts +0 -22
- package/dist/wasm-bindings/runtime-contract.d.ts.map +0 -1
- package/dist/wasm-bindings/runtime-contract.js +0 -112
- package/dist/wasm-bindings/runtime-contract.js.map +0 -1
- package/dist/wasm-core/.syncular-wasm-profile +0 -1
- package/dist/wasm-core/syncular-runtime-artifact.json +0 -17
- package/dist/wasm-core/syncular.d.ts +0 -162
- package/dist/wasm-core/syncular.js +0 -1847
- package/dist/wasm-core/syncular_bg.wasm +0 -0
- package/dist/wasm-core/syncular_bg.wasm.d.ts +0 -77
- package/dist/wasm-perf/.syncular-wasm-profile +0 -1
- package/dist/wasm-perf/syncular-runtime-artifact.json +0 -21
- package/dist/wasm-perf/syncular.d.ts +0 -207
- package/dist/wasm-perf/syncular.js +0 -2341
- package/dist/wasm-perf/syncular_bg.wasm +0 -0
- package/dist/wasm-perf/syncular_bg.wasm.d.ts +0 -97
- package/dist/wasm-runtime.d.ts +0 -23
- package/dist/wasm-runtime.d.ts.map +0 -1
- package/dist/wasm-runtime.js +0 -69
- package/dist/wasm-runtime.js.map +0 -1
- package/dist/worker-client.d.ts +0 -123
- package/dist/worker-client.d.ts.map +0 -1
- package/dist/worker-client.js +0 -1735
- package/dist/worker-client.js.map +0 -1
- package/dist/worker-entry.d.ts.map +0 -1
- package/dist/worker-entry.js.map +0 -1
- package/dist/worker-protocol.d.ts.map +0 -1
- package/dist/worker-protocol.js.map +0 -1
- package/dist/worker-realtime.d.ts +0 -39
- package/dist/worker-realtime.d.ts.map +0 -1
- package/dist/worker-realtime.js +0 -677
- package/dist/worker-realtime.js.map +0 -1
- package/src/auth-leases.ts +0 -251
- package/src/blob-limits.ts +0 -98
- package/src/bridge-client.ts +0 -512
- package/src/client-config.ts +0 -29
- package/src/command-history.ts +0 -623
- package/src/console-diagnostics.ts +0 -617
- package/src/crdt-yjs/index.ts +0 -3
- package/src/crdt-yjs/webview-host-facade.ts +0 -477
- package/src/crdt-yjs/yjs-document-field-adapter.ts +0 -733
- package/src/crdt-yjs/yjs-prosemirror-bridge.ts +0 -272
- package/src/diagnostics.ts +0 -116
- package/src/generated-bridge.ts +0 -741
- package/src/mutations.ts +0 -168
- package/src/network.ts +0 -32
- package/src/react/index.ts +0 -1036
- package/src/react-native/index.ts +0 -152
- package/src/runtime-contract.ts +0 -48
- package/src/rust-client.ts +0 -1491
- package/src/sentry.ts +0 -215
- package/src/sql-safety.ts +0 -61
- package/src/tauri/index.ts +0 -211
- package/src/types.ts +0 -1397
- package/src/wasm-bindings/generated-wasm-bindings.d.ts +0 -70
- package/src/wasm-bindings/runtime-contract.ts +0 -158
- package/src/wasm-runtime.ts +0 -145
- package/src/worker-client.ts +0 -2289
- package/src/worker-realtime.ts +0 -843
package/dist/blob.js
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side blob cache + transport (SPEC.md §5.9.7).
|
|
3
|
+
*
|
|
4
|
+
* Blob bytes are cached content-addressed by `blobId` and refcounted by the
|
|
5
|
+
* local rows whose `blob_ref` columns reference them (B1). The cache is
|
|
6
|
+
* derived from live-row references: after any apply/purge, refcounts are
|
|
7
|
+
* reconciled from the current `blob_ref` column contents, and a body whose
|
|
8
|
+
* only referencing rows were revocation-purged is deleted (B2, evicted ≠
|
|
9
|
+
* revoked). BlobRefs stay resolvable at any time (B3): the `blobId` in the
|
|
10
|
+
* row value is the whole download key. Pending uploads are tracked in the
|
|
11
|
+
* outbox-adjacent uploads table (B4) and flushed before push.
|
|
12
|
+
*/
|
|
13
|
+
import { parseBlobRef, serializeBlobRef } from '@syncular/core';
|
|
14
|
+
import { quoteIdent } from './schema.js';
|
|
15
|
+
/** `"sha256:" + hex` of the bytes — the content address (§5.9.1). */
|
|
16
|
+
export async function computeBlobId(bytes) {
|
|
17
|
+
const digest = await crypto.subtle.digest('SHA-256', bytes.slice().buffer);
|
|
18
|
+
const hex = [...new Uint8Array(digest)]
|
|
19
|
+
.map((b) => b.toString(16).padStart(2, '0'))
|
|
20
|
+
.join('');
|
|
21
|
+
return `sha256:${hex}`;
|
|
22
|
+
}
|
|
23
|
+
export function ensureBlobSchema(db) {
|
|
24
|
+
db.exec(`CREATE TABLE IF NOT EXISTS _syncular_blobs(
|
|
25
|
+
blob_id TEXT PRIMARY KEY,
|
|
26
|
+
bytes BLOB NOT NULL,
|
|
27
|
+
byte_length INTEGER NOT NULL,
|
|
28
|
+
media_type TEXT,
|
|
29
|
+
refcount INTEGER NOT NULL DEFAULT 0,
|
|
30
|
+
created_at_ms INTEGER NOT NULL,
|
|
31
|
+
last_used_ms INTEGER NOT NULL DEFAULT 0)`);
|
|
32
|
+
// Migrate a cache created before the §5.9.7 B1 LRU column: additive,
|
|
33
|
+
// idempotent (a duplicate-column error on an already-migrated DB is
|
|
34
|
+
// swallowed). last_used_ms drives cap eviction (LRU of zero-ref bodies).
|
|
35
|
+
try {
|
|
36
|
+
db.exec('ALTER TABLE _syncular_blobs ADD COLUMN last_used_ms INTEGER NOT NULL DEFAULT 0');
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// column already exists — the CREATE above included it
|
|
40
|
+
}
|
|
41
|
+
db.exec(`CREATE TABLE IF NOT EXISTS _syncular_blob_uploads(
|
|
42
|
+
blob_id TEXT PRIMARY KEY,
|
|
43
|
+
media_type TEXT,
|
|
44
|
+
created_at_ms INTEGER NOT NULL)`);
|
|
45
|
+
}
|
|
46
|
+
/** Put bytes into the content-addressed cache (idempotent); touches LRU. */
|
|
47
|
+
export function putCachedBlob(db, blobId, bytes, nowMs, mediaType) {
|
|
48
|
+
db.exec(`INSERT INTO _syncular_blobs(
|
|
49
|
+
blob_id, bytes, byte_length, media_type, refcount, created_at_ms, last_used_ms)
|
|
50
|
+
VALUES (?,?,?,?,0,?,?)
|
|
51
|
+
ON CONFLICT(blob_id) DO UPDATE SET last_used_ms = excluded.last_used_ms`, [blobId, bytes, bytes.length, mediaType ?? null, nowMs, nowMs]);
|
|
52
|
+
}
|
|
53
|
+
export function getCachedBlob(db, blobId, nowMs) {
|
|
54
|
+
const rows = db.query('SELECT bytes, byte_length, media_type FROM _syncular_blobs WHERE blob_id = ?', [blobId]);
|
|
55
|
+
const row = rows[0];
|
|
56
|
+
if (row === undefined)
|
|
57
|
+
return undefined;
|
|
58
|
+
// §5.9.7 B1 LRU: a cache-hit read touches "recently used" so a hot image
|
|
59
|
+
// survives a cap trim. Skipped when no clock is supplied (pure read).
|
|
60
|
+
if (nowMs !== undefined) {
|
|
61
|
+
db.exec('UPDATE _syncular_blobs SET last_used_ms = ? WHERE blob_id = ?', [
|
|
62
|
+
nowMs,
|
|
63
|
+
blobId,
|
|
64
|
+
]);
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
blobId,
|
|
68
|
+
bytes: row.bytes,
|
|
69
|
+
byteLength: Number(row.byte_length),
|
|
70
|
+
...(row.media_type !== null ? { mediaType: row.media_type } : {}),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* §5.9.7 B1 size cap + LRU eviction. When the sum of cached body sizes exceeds
|
|
75
|
+
* `maxBytes`, evict **zero-ref, non-pinned** bodies in least-recently-used
|
|
76
|
+
* order until back under the cap. NEVER evicts a referenced body (refcount > 0
|
|
77
|
+
* — it must stay resolvable without a re-download) nor a pending-upload-pinned
|
|
78
|
+
* body (its bytes are the only copy until push, B4). If every over-cap body is
|
|
79
|
+
* referenced or pinned, the cache stays over the cap (correctness beats the
|
|
80
|
+
* cap). Evicting a zero-ref body is always safe: B3 re-enables the fetch from
|
|
81
|
+
* any surviving `blob_ref` value. Returns the evicted blobIds.
|
|
82
|
+
*/
|
|
83
|
+
export function enforceBlobCacheCap(db, maxBytes) {
|
|
84
|
+
const totalRow = db.query('SELECT COALESCE(SUM(byte_length), 0) AS total FROM _syncular_blobs')[0];
|
|
85
|
+
let total = Number(totalRow?.total ?? 0);
|
|
86
|
+
if (total <= maxBytes)
|
|
87
|
+
return [];
|
|
88
|
+
// Eviction candidates: zero-ref AND not pinned by a pending upload, oldest
|
|
89
|
+
// (LRU) first, then oldest created as a stable tiebreak.
|
|
90
|
+
const candidates = db.query(`SELECT blob_id, byte_length FROM _syncular_blobs
|
|
91
|
+
WHERE refcount = 0
|
|
92
|
+
AND blob_id NOT IN (SELECT blob_id FROM _syncular_blob_uploads)
|
|
93
|
+
ORDER BY last_used_ms ASC, created_at_ms ASC`);
|
|
94
|
+
const evicted = [];
|
|
95
|
+
db.transaction(() => {
|
|
96
|
+
for (const row of candidates) {
|
|
97
|
+
if (total <= maxBytes)
|
|
98
|
+
break;
|
|
99
|
+
const blobId = row.blob_id;
|
|
100
|
+
db.exec('DELETE FROM _syncular_blobs WHERE blob_id = ?', [blobId]);
|
|
101
|
+
total -= Number(row.byte_length);
|
|
102
|
+
evicted.push(blobId);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
return evicted;
|
|
106
|
+
}
|
|
107
|
+
/** Record a pending upload (§5.9.7 B4); flushed before the next push. */
|
|
108
|
+
export function recordPendingUpload(db, blobId, nowMs, mediaType) {
|
|
109
|
+
db.exec(`INSERT OR IGNORE INTO _syncular_blob_uploads(blob_id, media_type, created_at_ms)
|
|
110
|
+
VALUES (?,?,?)`, [blobId, mediaType ?? null, nowMs]);
|
|
111
|
+
}
|
|
112
|
+
export function listPendingUploads(db) {
|
|
113
|
+
return db
|
|
114
|
+
.query('SELECT blob_id, media_type FROM _syncular_blob_uploads ORDER BY created_at_ms')
|
|
115
|
+
.map((row) => ({
|
|
116
|
+
blobId: row.blob_id,
|
|
117
|
+
...(row.media_type !== null
|
|
118
|
+
? { mediaType: row.media_type }
|
|
119
|
+
: {}),
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
export function clearPendingUpload(db, blobId) {
|
|
123
|
+
db.exec('DELETE FROM _syncular_blob_uploads WHERE blob_id = ?', [blobId]);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* All `blob_ref` column names per table (for refcount reconciliation).
|
|
127
|
+
* Blank result ⇒ the schema has no attachments; callers skip reconciliation.
|
|
128
|
+
*/
|
|
129
|
+
export function blobRefColumnsBySchema(schema) {
|
|
130
|
+
const out = new Map();
|
|
131
|
+
for (const table of schema.tables.values()) {
|
|
132
|
+
const cols = table.columns
|
|
133
|
+
.filter((c) => c.type === 'blob_ref')
|
|
134
|
+
.map((c) => c.name);
|
|
135
|
+
if (cols.length > 0)
|
|
136
|
+
out.set(table.name, cols);
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
export function schemaHasBlobs(schema) {
|
|
141
|
+
for (const table of schema.tables.values()) {
|
|
142
|
+
if (table.columns.some((c) => c.type === 'blob_ref'))
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* §5.9.7 B1/B2: recompute cache refcounts from the current `blob_ref` column
|
|
149
|
+
* contents across all synced tables, then delete cache bodies that dropped
|
|
150
|
+
* to zero refs *and* have no pending upload (a pending upload pins its body,
|
|
151
|
+
* B4). Called after every apply/purge that may add or remove references.
|
|
152
|
+
*
|
|
153
|
+
* `deleteOrphans` distinguishes the two B2 transitions: revocation purge
|
|
154
|
+
* passes `true` (drop the now-unauthorized body); a benign apply passes
|
|
155
|
+
* `false` (retain zero-ref bodies as LRU cache entries — the shipped
|
|
156
|
+
* default). Bodies pinned by a pending upload are always retained.
|
|
157
|
+
*/
|
|
158
|
+
export function reconcileBlobRefcounts(db, schema, options) {
|
|
159
|
+
const byTable = blobRefColumnsBySchema(schema);
|
|
160
|
+
if (byTable.size === 0)
|
|
161
|
+
return;
|
|
162
|
+
// Count references to each blobId across every blob_ref column.
|
|
163
|
+
const counts = new Map();
|
|
164
|
+
for (const [tableName, columns] of byTable) {
|
|
165
|
+
for (const column of columns) {
|
|
166
|
+
const rows = db.query(`SELECT ${quoteIdent(column)} AS v FROM ${quoteIdent(tableName)}
|
|
167
|
+
WHERE ${quoteIdent(column)} IS NOT NULL`);
|
|
168
|
+
for (const row of rows) {
|
|
169
|
+
const raw = row.v;
|
|
170
|
+
if (typeof raw !== 'string')
|
|
171
|
+
continue;
|
|
172
|
+
let ref;
|
|
173
|
+
try {
|
|
174
|
+
ref = parseBlobRef(raw);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
counts.set(ref.blobId, (counts.get(ref.blobId) ?? 0) + 1);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
db.transaction(() => {
|
|
184
|
+
// Reset all refcounts, then apply the recomputed counts.
|
|
185
|
+
db.exec('UPDATE _syncular_blobs SET refcount = 0');
|
|
186
|
+
for (const [blobId, count] of counts) {
|
|
187
|
+
db.exec('UPDATE _syncular_blobs SET refcount = ? WHERE blob_id = ?', [
|
|
188
|
+
count,
|
|
189
|
+
blobId,
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
if (options?.deleteOrphans === true) {
|
|
193
|
+
// §5.9.7 B2 revocation side: delete zero-ref bodies not pinned by a
|
|
194
|
+
// pending upload.
|
|
195
|
+
db.exec(`DELETE FROM _syncular_blobs
|
|
196
|
+
WHERE refcount = 0
|
|
197
|
+
AND blob_id NOT IN (SELECT blob_id FROM _syncular_blob_uploads)`);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
export { parseBlobRef, serializeBlobRef };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ClientDatabase` on bun:sqlite — the test backend (REVISE B3: the core
|
|
3
|
+
* must run without a browser). Not exported from the package root so the
|
|
4
|
+
* browser entry never touches `bun:sqlite`.
|
|
5
|
+
*/
|
|
6
|
+
import { Database } from 'bun:sqlite';
|
|
7
|
+
import { type ClientDatabase, type SqlRow, type SqlValue } from './database.js';
|
|
8
|
+
export declare class BunClientDatabase implements ClientDatabase {
|
|
9
|
+
#private;
|
|
10
|
+
readonly db: Database;
|
|
11
|
+
constructor(path?: string);
|
|
12
|
+
exec(sql: string, params?: readonly SqlValue[]): void;
|
|
13
|
+
query(sql: string, params?: readonly SqlValue[]): SqlRow[];
|
|
14
|
+
transaction<T>(fn: () => T): T;
|
|
15
|
+
/**
|
|
16
|
+
* §5.3 image import: bun:sqlite attaches files, not buffers, so the
|
|
17
|
+
* image lands in a private temp file for the duration of the ATTACH.
|
|
18
|
+
*/
|
|
19
|
+
withSqliteImage<T>(bytes: Uint8Array, alias: string, fn: () => T): T;
|
|
20
|
+
close(): void;
|
|
21
|
+
}
|
|
22
|
+
export declare function openBunDatabase(path?: string): ClientDatabase;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ClientDatabase` on bun:sqlite — the test backend (REVISE B3: the core
|
|
3
|
+
* must run without a browser). Not exported from the package root so the
|
|
4
|
+
* browser entry never touches `bun:sqlite`.
|
|
5
|
+
*/
|
|
6
|
+
import { Database } from 'bun:sqlite';
|
|
7
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { assertImageAlias, runTransaction, } from './database.js';
|
|
11
|
+
function coerceParams(params) {
|
|
12
|
+
return params.map((value) => {
|
|
13
|
+
if (typeof value === 'boolean')
|
|
14
|
+
return value ? 1 : 0;
|
|
15
|
+
return value;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export class BunClientDatabase {
|
|
19
|
+
db;
|
|
20
|
+
#tx = { depth: 0 };
|
|
21
|
+
constructor(path = ':memory:') {
|
|
22
|
+
this.db = new Database(path);
|
|
23
|
+
}
|
|
24
|
+
exec(sql, params = []) {
|
|
25
|
+
this.db.query(sql).run(...coerceParams(params));
|
|
26
|
+
}
|
|
27
|
+
query(sql, params = []) {
|
|
28
|
+
return this.db.query(sql).all(...coerceParams(params));
|
|
29
|
+
}
|
|
30
|
+
transaction(fn) {
|
|
31
|
+
return runTransaction(this.#tx, (sql) => this.db.run(sql), fn);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* §5.3 image import: bun:sqlite attaches files, not buffers, so the
|
|
35
|
+
* image lands in a private temp file for the duration of the ATTACH.
|
|
36
|
+
*/
|
|
37
|
+
withSqliteImage(bytes, alias, fn) {
|
|
38
|
+
assertImageAlias(alias);
|
|
39
|
+
const dir = mkdtempSync(join(tmpdir(), 'syncular-image-'));
|
|
40
|
+
const path = join(dir, 'segment.db');
|
|
41
|
+
try {
|
|
42
|
+
writeFileSync(path, bytes);
|
|
43
|
+
this.db.run(`ATTACH DATABASE ? AS ${alias}`, [path]);
|
|
44
|
+
try {
|
|
45
|
+
return fn();
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
this.db.run(`DETACH DATABASE ${alias}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
rmSync(dir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
close() {
|
|
56
|
+
this.db.close();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function openBunDatabase(path = ':memory:') {
|
|
60
|
+
return new BunClientDatabase(path);
|
|
61
|
+
}
|