@koolbase/core 10.0.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 +19 -0
- package/dist/cjs/analytics.d.ts +24 -0
- package/dist/cjs/analytics.js +110 -0
- package/dist/cjs/auth-errors.d.ts +117 -0
- package/dist/cjs/auth-errors.js +250 -0
- package/dist/cjs/auth.d.ts +213 -0
- package/dist/cjs/auth.js +813 -0
- package/dist/cjs/cache-store.d.ts +50 -0
- package/dist/cjs/cache-store.js +194 -0
- package/dist/cjs/conflict.d.ts +80 -0
- package/dist/cjs/conflict.js +84 -0
- package/dist/cjs/database-errors.d.ts +101 -0
- package/dist/cjs/database-errors.js +200 -0
- package/dist/cjs/database.d.ts +298 -0
- package/dist/cjs/database.js +852 -0
- package/dist/cjs/device-id.d.ts +1 -0
- package/dist/cjs/device-id.js +57 -0
- package/dist/cjs/device-metadata.d.ts +36 -0
- package/dist/cjs/device-metadata.js +94 -0
- package/dist/cjs/errors.d.ts +64 -0
- package/dist/cjs/errors.js +85 -0
- package/dist/cjs/flags.d.ts +15 -0
- package/dist/cjs/flags.js +76 -0
- package/dist/cjs/function-errors.d.ts +51 -0
- package/dist/cjs/function-errors.js +103 -0
- package/dist/cjs/functions.d.ts +15 -0
- package/dist/cjs/functions.js +83 -0
- package/dist/cjs/index.d.ts +22 -0
- package/dist/cjs/index.js +56 -0
- package/dist/cjs/messaging.d.ts +13 -0
- package/dist/cjs/messaging.js +36 -0
- package/dist/cjs/offline-state.d.ts +97 -0
- package/dist/cjs/offline-state.js +198 -0
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/pending-write.d.ts +47 -0
- package/dist/cjs/pending-write.js +22 -0
- package/dist/cjs/platform.d.ts +51 -0
- package/dist/cjs/platform.js +41 -0
- package/dist/cjs/realtime.d.ts +44 -0
- package/dist/cjs/realtime.js +195 -0
- package/dist/cjs/record.d.ts +2 -0
- package/dist/cjs/record.js +23 -0
- package/dist/cjs/shared.d.ts +9 -0
- package/dist/cjs/shared.js +43 -0
- package/dist/cjs/storage-errors.d.ts +163 -0
- package/dist/cjs/storage-errors.js +253 -0
- package/dist/cjs/storage.d.ts +198 -0
- package/dist/cjs/storage.js +451 -0
- package/dist/cjs/sync-engine.d.ts +30 -0
- package/dist/cjs/sync-engine.js +286 -0
- package/dist/cjs/types.d.ts +487 -0
- package/dist/cjs/types.js +40 -0
- package/dist/esm/analytics.d.ts +24 -0
- package/dist/esm/analytics.js +106 -0
- package/dist/esm/auth-errors.d.ts +117 -0
- package/dist/esm/auth-errors.js +222 -0
- package/dist/esm/auth.d.ts +213 -0
- package/dist/esm/auth.js +809 -0
- package/dist/esm/cache-store.d.ts +50 -0
- package/dist/esm/cache-store.js +179 -0
- package/dist/esm/conflict.d.ts +80 -0
- package/dist/esm/conflict.js +80 -0
- package/dist/esm/database-errors.d.ts +101 -0
- package/dist/esm/database-errors.js +189 -0
- package/dist/esm/database.d.ts +298 -0
- package/dist/esm/database.js +848 -0
- package/dist/esm/device-id.d.ts +1 -0
- package/dist/esm/device-id.js +54 -0
- package/dist/esm/device-metadata.d.ts +36 -0
- package/dist/esm/device-metadata.js +90 -0
- package/dist/esm/errors.d.ts +64 -0
- package/dist/esm/errors.js +79 -0
- package/dist/esm/flags.d.ts +15 -0
- package/dist/esm/flags.js +72 -0
- package/dist/esm/function-errors.d.ts +51 -0
- package/dist/esm/function-errors.js +93 -0
- package/dist/esm/functions.d.ts +15 -0
- package/dist/esm/functions.js +79 -0
- package/dist/esm/index.d.ts +22 -0
- package/dist/esm/index.js +25 -0
- package/dist/esm/messaging.d.ts +13 -0
- package/dist/esm/messaging.js +32 -0
- package/dist/esm/offline-state.d.ts +97 -0
- package/dist/esm/offline-state.js +189 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/pending-write.d.ts +47 -0
- package/dist/esm/pending-write.js +19 -0
- package/dist/esm/platform.d.ts +51 -0
- package/dist/esm/platform.js +36 -0
- package/dist/esm/realtime.d.ts +44 -0
- package/dist/esm/realtime.js +191 -0
- package/dist/esm/record.d.ts +2 -0
- package/dist/esm/record.js +20 -0
- package/dist/esm/shared.d.ts +9 -0
- package/dist/esm/shared.js +39 -0
- package/dist/esm/storage-errors.d.ts +163 -0
- package/dist/esm/storage-errors.js +239 -0
- package/dist/esm/storage.d.ts +198 -0
- package/dist/esm/storage.js +447 -0
- package/dist/esm/sync-engine.d.ts +30 -0
- package/dist/esm/sync-engine.js +282 -0
- package/dist/esm/types.d.ts +487 -0
- package/dist/esm/types.js +37 -0
- package/package.json +45 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { KoolbaseRecord, LegacyPendingWrite, QueryResult } from './types.js';
|
|
2
|
+
export declare function hashQuery(collection: string, options: Record<string, unknown>): string;
|
|
3
|
+
export declare function getCached(userId: string, collection: string, queryHash: string): Promise<QueryResult | null>;
|
|
4
|
+
export declare function setCached(userId: string, collection: string, queryHash: string, result: QueryResult): Promise<void>;
|
|
5
|
+
export declare function invalidateCache(userId: string, collection: string): Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Drops everything cached for a user.
|
|
8
|
+
*
|
|
9
|
+
* Deliberately spares the write queue. The prefix covers every key for this
|
|
10
|
+
* user, and the queue lives under one of them — so clearing the cache used to
|
|
11
|
+
* delete offline writes the user believes are saved, silently and
|
|
12
|
+
* irrecoverably. A cache is what can be refetched; queued writes are not that.
|
|
13
|
+
*/
|
|
14
|
+
export declare function clearUserCache(userId: string): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* A record as the SDK last saw it, with the revision it was read at.
|
|
17
|
+
*
|
|
18
|
+
* Separate from the query cache, which answers "what did this query return".
|
|
19
|
+
* This answers "what is the latest copy of this record" — and an offline
|
|
20
|
+
* mutation composes against the second. Scanning query blobs for one would mean
|
|
21
|
+
* the same record appearing in several snapshots at different revisions, with no
|
|
22
|
+
* principled way to choose.
|
|
23
|
+
*
|
|
24
|
+
* Keyed with `record` where a collection name would sit, so invalidateCache —
|
|
25
|
+
* which scopes to a collection and runs after every write — cannot reach these.
|
|
26
|
+
* A user's own edit must not remove the baseline they need for the next one.
|
|
27
|
+
*/
|
|
28
|
+
export interface CachedRecord {
|
|
29
|
+
collection: string;
|
|
30
|
+
data: Record<string, unknown>;
|
|
31
|
+
revision?: number;
|
|
32
|
+
cachedAt: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function getCachedRecord(userId: string, recordId: string): Promise<CachedRecord | null>;
|
|
35
|
+
/**
|
|
36
|
+
* Stores a record, refusing to move it backwards.
|
|
37
|
+
*
|
|
38
|
+
* Query responses can arrive out of order — a slow request from an earlier
|
|
39
|
+
* screen resolving after a fresh one — and an older copy overwriting a newer
|
|
40
|
+
* would compose the next mutation against a stale revision, producing a conflict
|
|
41
|
+
* the user never caused. False conflicts teach people to force-overwrite, which
|
|
42
|
+
* is worse than none.
|
|
43
|
+
*/
|
|
44
|
+
export declare function cacheRecord(userId: string, collection: string, recordId: string, data: Record<string, unknown>, revision?: number): Promise<void>;
|
|
45
|
+
export declare function removeCachedRecord(userId: string, recordId: string): Promise<void>;
|
|
46
|
+
export declare function getWriteQueue(userId: string): Promise<LegacyPendingWrite[]>;
|
|
47
|
+
export declare function addToWriteQueue(userId: string, write: Omit<LegacyPendingWrite, 'retries' | 'createdAt'>): Promise<void>;
|
|
48
|
+
export declare function removeFromWriteQueue(userId: string, writeId: string): Promise<void>;
|
|
49
|
+
export declare function incrementWriteRetry(userId: string, writeId: string): Promise<void>;
|
|
50
|
+
export declare function optimisticallyInsert(userId: string, collection: string, record: KoolbaseRecord): Promise<void>;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { getPlatform } from './platform.js';
|
|
2
|
+
const CACHE_VERSION = 'v1';
|
|
3
|
+
function cacheKey(userId, collection, queryHash) {
|
|
4
|
+
return `koolbase:${CACHE_VERSION}:${userId}:${collection}:${queryHash}`;
|
|
5
|
+
}
|
|
6
|
+
function writeQueueKey(userId) {
|
|
7
|
+
return `koolbase:${CACHE_VERSION}:${userId}:write_queue`;
|
|
8
|
+
}
|
|
9
|
+
export function hashQuery(collection, options) {
|
|
10
|
+
return `${collection}:${JSON.stringify(options)}`;
|
|
11
|
+
}
|
|
12
|
+
// ─── Cache ──────────────────────────────────────────────────────────────────
|
|
13
|
+
export async function getCached(userId, collection, queryHash) {
|
|
14
|
+
try {
|
|
15
|
+
const raw = await getPlatform().storage.getItem(cacheKey(userId, collection, queryHash));
|
|
16
|
+
if (!raw)
|
|
17
|
+
return null;
|
|
18
|
+
return JSON.parse(raw);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function setCached(userId, collection, queryHash, result) {
|
|
25
|
+
try {
|
|
26
|
+
await getPlatform().storage.setItem(cacheKey(userId, collection, queryHash), JSON.stringify(result));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// ignore storage errors
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function invalidateCache(userId, collection) {
|
|
33
|
+
try {
|
|
34
|
+
const keys = await getPlatform().storage.getAllKeys();
|
|
35
|
+
const prefix = `koolbase:${CACHE_VERSION}:${userId}:${collection}:`;
|
|
36
|
+
const toDelete = keys.filter(k => k.startsWith(prefix));
|
|
37
|
+
for (const key of toDelete) {
|
|
38
|
+
await getPlatform().storage.removeItem(key);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// ignore
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Drops everything cached for a user.
|
|
47
|
+
*
|
|
48
|
+
* Deliberately spares the write queue. The prefix covers every key for this
|
|
49
|
+
* user, and the queue lives under one of them — so clearing the cache used to
|
|
50
|
+
* delete offline writes the user believes are saved, silently and
|
|
51
|
+
* irrecoverably. A cache is what can be refetched; queued writes are not that.
|
|
52
|
+
*/
|
|
53
|
+
export async function clearUserCache(userId) {
|
|
54
|
+
try {
|
|
55
|
+
const keys = await getPlatform().storage.getAllKeys();
|
|
56
|
+
const prefix = `koolbase:${CACHE_VERSION}:${userId}:`;
|
|
57
|
+
const queueKey = writeQueueKey(userId);
|
|
58
|
+
const toDelete = keys.filter(k => k.startsWith(prefix) && k !== queueKey);
|
|
59
|
+
for (const key of toDelete) {
|
|
60
|
+
await getPlatform().storage.removeItem(key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// ignore
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function recordCacheKey(userId, recordId) {
|
|
68
|
+
return `koolbase:${CACHE_VERSION}:${userId}:record:${recordId}`;
|
|
69
|
+
}
|
|
70
|
+
export async function getCachedRecord(userId, recordId) {
|
|
71
|
+
try {
|
|
72
|
+
const raw = await getPlatform().storage.getItem(recordCacheKey(userId, recordId));
|
|
73
|
+
return raw ? JSON.parse(raw) : null;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Stores a record, refusing to move it backwards.
|
|
81
|
+
*
|
|
82
|
+
* Query responses can arrive out of order — a slow request from an earlier
|
|
83
|
+
* screen resolving after a fresh one — and an older copy overwriting a newer
|
|
84
|
+
* would compose the next mutation against a stale revision, producing a conflict
|
|
85
|
+
* the user never caused. False conflicts teach people to force-overwrite, which
|
|
86
|
+
* is worse than none.
|
|
87
|
+
*/
|
|
88
|
+
export async function cacheRecord(userId, collection, recordId, data, revision) {
|
|
89
|
+
try {
|
|
90
|
+
const existing = await getCachedRecord(userId, recordId);
|
|
91
|
+
if (existing?.revision !== undefined &&
|
|
92
|
+
revision !== undefined &&
|
|
93
|
+
revision < existing.revision) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const entry = {
|
|
97
|
+
collection,
|
|
98
|
+
data,
|
|
99
|
+
revision,
|
|
100
|
+
cachedAt: new Date().toISOString(),
|
|
101
|
+
};
|
|
102
|
+
await getPlatform().storage.setItem(recordCacheKey(userId, recordId), JSON.stringify(entry));
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// ignore
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export async function removeCachedRecord(userId, recordId) {
|
|
109
|
+
try {
|
|
110
|
+
await getPlatform().storage.removeItem(recordCacheKey(userId, recordId));
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// ignore
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ─── Write Queue ────────────────────────────────────────────────────────────
|
|
117
|
+
export async function getWriteQueue(userId) {
|
|
118
|
+
try {
|
|
119
|
+
const raw = await getPlatform().storage.getItem(writeQueueKey(userId));
|
|
120
|
+
if (!raw)
|
|
121
|
+
return [];
|
|
122
|
+
return JSON.parse(raw);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
export async function addToWriteQueue(userId, write) {
|
|
129
|
+
try {
|
|
130
|
+
const queue = await getWriteQueue(userId);
|
|
131
|
+
queue.push({ ...write, retries: 0, createdAt: new Date().toISOString() });
|
|
132
|
+
await getPlatform().storage.setItem(writeQueueKey(userId), JSON.stringify(queue));
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// ignore
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
export async function removeFromWriteQueue(userId, writeId) {
|
|
139
|
+
try {
|
|
140
|
+
const queue = await getWriteQueue(userId);
|
|
141
|
+
const updated = queue.filter(w => w.id !== writeId);
|
|
142
|
+
await getPlatform().storage.setItem(writeQueueKey(userId), JSON.stringify(updated));
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
// ignore
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function incrementWriteRetry(userId, writeId) {
|
|
149
|
+
try {
|
|
150
|
+
const queue = await getWriteQueue(userId);
|
|
151
|
+
const updated = queue.map(w => w.id === writeId ? { ...w, retries: w.retries + 1 } : w);
|
|
152
|
+
// Drop writes that have exceeded 3 retries
|
|
153
|
+
const filtered = updated.filter(w => w.retries <= 3);
|
|
154
|
+
await getPlatform().storage.setItem(writeQueueKey(userId), JSON.stringify(filtered));
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// ignore
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
// ─── Optimistic cache update ─────────────────────────────────────────────────
|
|
161
|
+
export async function optimisticallyInsert(userId, collection, record) {
|
|
162
|
+
try {
|
|
163
|
+
const keys = await getPlatform().storage.getAllKeys();
|
|
164
|
+
const prefix = `koolbase:${CACHE_VERSION}:${userId}:${collection}:`;
|
|
165
|
+
const collectionKeys = keys.filter(k => k.startsWith(prefix));
|
|
166
|
+
for (const key of collectionKeys) {
|
|
167
|
+
const raw = await getPlatform().storage.getItem(key);
|
|
168
|
+
if (!raw)
|
|
169
|
+
continue;
|
|
170
|
+
const cached = JSON.parse(raw);
|
|
171
|
+
cached.records = [record, ...cached.records];
|
|
172
|
+
cached.total = cached.total + 1;
|
|
173
|
+
await getPlatform().storage.setItem(key, JSON.stringify(cached));
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// ignore
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { ConflictReason } from './offline-state.js';
|
|
2
|
+
/** Resolves conflicts by id. */
|
|
3
|
+
export interface ConflictResolver {
|
|
4
|
+
resolveWithLocal(conflictId: string): Promise<void>;
|
|
5
|
+
resolveWithServer(conflictId: string): Promise<void>;
|
|
6
|
+
resolveWithMerge(conflictId: string, data: Record<string, unknown>): Promise<void>;
|
|
7
|
+
abandon(conflictId: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A queued offline write that could not be applied, waiting for a decision.
|
|
11
|
+
*
|
|
12
|
+
* Not an error to dismiss and not a write to retry: retrying cannot help, and
|
|
13
|
+
* discarding it would lose a change the user believes is saved. It waits, and
|
|
14
|
+
* keeps waiting across restarts, until the application decides.
|
|
15
|
+
*
|
|
16
|
+
* Only the application can decide. Whether a later edit should win depends on
|
|
17
|
+
* what the data means, and a platform that chooses for everyone is wrong for
|
|
18
|
+
* someone.
|
|
19
|
+
*/
|
|
20
|
+
export declare class KoolbaseConflict {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly reason: ConflictReason;
|
|
23
|
+
readonly operation: 'insert' | 'update' | 'delete';
|
|
24
|
+
readonly collection: string;
|
|
25
|
+
readonly recordId: string;
|
|
26
|
+
/** The change the user made, still unapplied. */
|
|
27
|
+
readonly local: Record<string, unknown> | undefined;
|
|
28
|
+
/** The record as it was when the change was composed, where that is known. */
|
|
29
|
+
readonly baseline: Record<string, unknown> | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* The record as the server held it when the write was refused, captured with
|
|
32
|
+
* the refusal so deciding needs no fetch and cannot race one.
|
|
33
|
+
*
|
|
34
|
+
* Undefined when the reason is `baseline_unavailable` — nothing was ever
|
|
35
|
+
* sent, so the server never answered.
|
|
36
|
+
*/
|
|
37
|
+
readonly server: Record<string, unknown> | undefined;
|
|
38
|
+
readonly baseRevision: number | undefined;
|
|
39
|
+
readonly serverRevision: number | undefined;
|
|
40
|
+
readonly createdAt: string;
|
|
41
|
+
private readonly resolver;
|
|
42
|
+
constructor(id: string, reason: ConflictReason, operation: 'insert' | 'update' | 'delete', collection: string, recordId: string,
|
|
43
|
+
/** The change the user made, still unapplied. */
|
|
44
|
+
local: Record<string, unknown> | undefined,
|
|
45
|
+
/** The record as it was when the change was composed, where that is known. */
|
|
46
|
+
baseline: Record<string, unknown> | undefined,
|
|
47
|
+
/**
|
|
48
|
+
* The record as the server held it when the write was refused, captured with
|
|
49
|
+
* the refusal so deciding needs no fetch and cannot race one.
|
|
50
|
+
*
|
|
51
|
+
* Undefined when the reason is `baseline_unavailable` — nothing was ever
|
|
52
|
+
* sent, so the server never answered.
|
|
53
|
+
*/
|
|
54
|
+
server: Record<string, unknown> | undefined, baseRevision: number | undefined, serverRevision: number | undefined, createdAt: string, resolver: ConflictResolver);
|
|
55
|
+
/**
|
|
56
|
+
* Fields where the user's change and the server's version disagree.
|
|
57
|
+
*
|
|
58
|
+
* Only the fields the change touches: a record accumulates values the write
|
|
59
|
+
* never asserted, and listing those would bury the real disagreement. Empty
|
|
60
|
+
* when there is no server version to compare against.
|
|
61
|
+
*/
|
|
62
|
+
get divergentFields(): string[];
|
|
63
|
+
/** How long this has been waiting. Metadata, not a deletion rule. */
|
|
64
|
+
get ageMs(): number;
|
|
65
|
+
/**
|
|
66
|
+
* Reapplies the user's change to the record as it stands now.
|
|
67
|
+
*
|
|
68
|
+
* An explicit decision to overwrite the server's version of the fields that
|
|
69
|
+
* disagree. Conditional where a revision is known, so a record that moved
|
|
70
|
+
* again while someone was deciding produces a new conflict rather than an
|
|
71
|
+
* unnoticed overwrite.
|
|
72
|
+
*/
|
|
73
|
+
resolveWithLocal(): Promise<void>;
|
|
74
|
+
/** Keeps the server's version and discards the user's change, as a decision. */
|
|
75
|
+
resolveWithServer(): Promise<void>;
|
|
76
|
+
/** Applies something the application composed from both versions. */
|
|
77
|
+
resolveWithMerge(data: Record<string, unknown>): Promise<void>;
|
|
78
|
+
/** Drops the change without claiming either version won. */
|
|
79
|
+
abandon(): Promise<void>;
|
|
80
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A queued offline write that could not be applied, waiting for a decision.
|
|
3
|
+
*
|
|
4
|
+
* Not an error to dismiss and not a write to retry: retrying cannot help, and
|
|
5
|
+
* discarding it would lose a change the user believes is saved. It waits, and
|
|
6
|
+
* keeps waiting across restarts, until the application decides.
|
|
7
|
+
*
|
|
8
|
+
* Only the application can decide. Whether a later edit should win depends on
|
|
9
|
+
* what the data means, and a platform that chooses for everyone is wrong for
|
|
10
|
+
* someone.
|
|
11
|
+
*/
|
|
12
|
+
export class KoolbaseConflict {
|
|
13
|
+
constructor(id, reason, operation, collection, recordId,
|
|
14
|
+
/** The change the user made, still unapplied. */
|
|
15
|
+
local,
|
|
16
|
+
/** The record as it was when the change was composed, where that is known. */
|
|
17
|
+
baseline,
|
|
18
|
+
/**
|
|
19
|
+
* The record as the server held it when the write was refused, captured with
|
|
20
|
+
* the refusal so deciding needs no fetch and cannot race one.
|
|
21
|
+
*
|
|
22
|
+
* Undefined when the reason is `baseline_unavailable` — nothing was ever
|
|
23
|
+
* sent, so the server never answered.
|
|
24
|
+
*/
|
|
25
|
+
server, baseRevision, serverRevision, createdAt, resolver) {
|
|
26
|
+
this.id = id;
|
|
27
|
+
this.reason = reason;
|
|
28
|
+
this.operation = operation;
|
|
29
|
+
this.collection = collection;
|
|
30
|
+
this.recordId = recordId;
|
|
31
|
+
this.local = local;
|
|
32
|
+
this.baseline = baseline;
|
|
33
|
+
this.server = server;
|
|
34
|
+
this.baseRevision = baseRevision;
|
|
35
|
+
this.serverRevision = serverRevision;
|
|
36
|
+
this.createdAt = createdAt;
|
|
37
|
+
this.resolver = resolver;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Fields where the user's change and the server's version disagree.
|
|
41
|
+
*
|
|
42
|
+
* Only the fields the change touches: a record accumulates values the write
|
|
43
|
+
* never asserted, and listing those would bury the real disagreement. Empty
|
|
44
|
+
* when there is no server version to compare against.
|
|
45
|
+
*/
|
|
46
|
+
get divergentFields() {
|
|
47
|
+
if (!this.local)
|
|
48
|
+
return [];
|
|
49
|
+
if (!this.server)
|
|
50
|
+
return Object.keys(this.local);
|
|
51
|
+
return Object.keys(this.local).filter((k) => JSON.stringify(this.server[k]) !== JSON.stringify(this.local[k]));
|
|
52
|
+
}
|
|
53
|
+
/** How long this has been waiting. Metadata, not a deletion rule. */
|
|
54
|
+
get ageMs() {
|
|
55
|
+
return Date.now() - new Date(this.createdAt).getTime();
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Reapplies the user's change to the record as it stands now.
|
|
59
|
+
*
|
|
60
|
+
* An explicit decision to overwrite the server's version of the fields that
|
|
61
|
+
* disagree. Conditional where a revision is known, so a record that moved
|
|
62
|
+
* again while someone was deciding produces a new conflict rather than an
|
|
63
|
+
* unnoticed overwrite.
|
|
64
|
+
*/
|
|
65
|
+
resolveWithLocal() {
|
|
66
|
+
return this.resolver.resolveWithLocal(this.id);
|
|
67
|
+
}
|
|
68
|
+
/** Keeps the server's version and discards the user's change, as a decision. */
|
|
69
|
+
resolveWithServer() {
|
|
70
|
+
return this.resolver.resolveWithServer(this.id);
|
|
71
|
+
}
|
|
72
|
+
/** Applies something the application composed from both versions. */
|
|
73
|
+
resolveWithMerge(data) {
|
|
74
|
+
return this.resolver.resolveWithMerge(this.id, data);
|
|
75
|
+
}
|
|
76
|
+
/** Drops the change without claiming either version won. */
|
|
77
|
+
abandon() {
|
|
78
|
+
return this.resolver.abandon(this.id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { KoolbaseError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Base class for errors surfaced by the Koolbase data layer (database reads
|
|
4
|
+
* and writes). Every data error carries a `message` and, when the server
|
|
5
|
+
* provides one, its stable `code` (e.g. `not_found`, `validation_error`,
|
|
6
|
+
* `unique_violation`).
|
|
7
|
+
*
|
|
8
|
+
* Catch this to handle any data-layer failure generically, or catch a
|
|
9
|
+
* specific subclass to branch on the kind of failure.
|
|
10
|
+
*/
|
|
11
|
+
export declare class KoolbaseDataError extends KoolbaseError {
|
|
12
|
+
/**
|
|
13
|
+
* Structured payload from the server's error body, when it sent one — e.g. a
|
|
14
|
+
* revision_mismatch 409 carries {expected_revision, current_revision,
|
|
15
|
+
* record}. Attached by the factory; absent when the body had none.
|
|
16
|
+
*/
|
|
17
|
+
details?: Record<string, unknown>;
|
|
18
|
+
constructor(message: string, code?: string);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Thrown when a write is rejected because the value would violate a
|
|
22
|
+
* collection's unique constraint — the server responds with 409 Conflict.
|
|
23
|
+
* Catch it to handle duplicates, e.g. an email or username already in use.
|
|
24
|
+
*
|
|
25
|
+
* `field` names the field that collided, when the server reports it
|
|
26
|
+
* (`details.field`) — useful when a collection has more than one unique
|
|
27
|
+
* constraint and you need to know which value clashed.
|
|
28
|
+
*
|
|
29
|
+
* Surfaced by `insert`, `update`, and `upsert` whenever the server is
|
|
30
|
+
* reachable and rejects the write with a 409. These writes are online-first:
|
|
31
|
+
* a server-side conflict throws immediately. Only a genuine network failure
|
|
32
|
+
* falls back to the offline queue, where a conflict that surfaces at sync
|
|
33
|
+
* time is handled by the sync engine rather than thrown here.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* try {
|
|
37
|
+
* await koolbase.db.upsert('users', { email }, { name });
|
|
38
|
+
* } catch (e) {
|
|
39
|
+
* if (e instanceof KoolbaseConflictError) {
|
|
40
|
+
* showError(`That ${e.field ?? 'value'} is already registered.`);
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
*/
|
|
44
|
+
export declare class KoolbaseConflictError extends KoolbaseDataError {
|
|
45
|
+
field?: string;
|
|
46
|
+
constructor(message?: string, field?: string);
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Thrown when the requested record or collection does not exist — the server
|
|
50
|
+
* responds with 404 and code `not_found` / `record_not_found` /
|
|
51
|
+
* `collection_not_found`.
|
|
52
|
+
*/
|
|
53
|
+
export declare class KoolbaseNotFoundError extends KoolbaseDataError {
|
|
54
|
+
constructor(message?: string);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Thrown when the request is rejected as invalid — the server responds with
|
|
58
|
+
* 400 and code `validation_error`.
|
|
59
|
+
*/
|
|
60
|
+
export declare class KoolbaseValidationError extends KoolbaseDataError {
|
|
61
|
+
constructor(message?: string);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Thrown when the caller is authenticated but not allowed to perform the
|
|
65
|
+
* operation — the server responds with 403 and code `permission_denied`
|
|
66
|
+
* (typically a collection access rule rejecting the read/write).
|
|
67
|
+
*/
|
|
68
|
+
export declare class KoolbasePermissionError extends KoolbaseDataError {
|
|
69
|
+
constructor(message?: string);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Thrown when the server is rate-limiting the caller — 429 with code
|
|
73
|
+
* `rate_limit`. Back off and retry after a short delay.
|
|
74
|
+
*/
|
|
75
|
+
export declare class KoolbaseRateLimitError extends KoolbaseDataError {
|
|
76
|
+
constructor(message?: string);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Thrown when the supplied vector's length does not match the dimension
|
|
80
|
+
* declared on the collection's vector field — the server responds with
|
|
81
|
+
* 400 and code `vector_dimension_mismatch`. The message includes both
|
|
82
|
+
* the expected and actual dimensions so you can surface a precise error.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* try {
|
|
86
|
+
* await koolbase.db.setVector(id, 'embedding', [0.1, 0.2]); // 2 dims
|
|
87
|
+
* } catch (e) {
|
|
88
|
+
* if (e instanceof KoolbaseVectorDimensionMismatchError) {
|
|
89
|
+
* showError(e.message); // "expected 1536, got 2"
|
|
90
|
+
* }
|
|
91
|
+
* }
|
|
92
|
+
*/
|
|
93
|
+
export declare class KoolbaseVectorDimensionMismatchError extends KoolbaseDataError {
|
|
94
|
+
constructor(message?: string);
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Maps a non-2xx data-layer response to a typed {@link KoolbaseDataError},
|
|
98
|
+
* preferring the server's stable `code` and falling back to the HTTP status
|
|
99
|
+
* for older or uncoded responses. Always returns an error to throw.
|
|
100
|
+
*/
|
|
101
|
+
export declare function koolbaseDataError(status: number, body: any, fallbackMessage?: string): KoolbaseError;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { KoolbaseError, KoolbaseUnauthenticatedError } from './errors.js';
|
|
2
|
+
/**
|
|
3
|
+
* Base class for errors surfaced by the Koolbase data layer (database reads
|
|
4
|
+
* and writes). Every data error carries a `message` and, when the server
|
|
5
|
+
* provides one, its stable `code` (e.g. `not_found`, `validation_error`,
|
|
6
|
+
* `unique_violation`).
|
|
7
|
+
*
|
|
8
|
+
* Catch this to handle any data-layer failure generically, or catch a
|
|
9
|
+
* specific subclass to branch on the kind of failure.
|
|
10
|
+
*/
|
|
11
|
+
export class KoolbaseDataError extends KoolbaseError {
|
|
12
|
+
constructor(message, code) {
|
|
13
|
+
super(message, code);
|
|
14
|
+
this.name = 'KoolbaseDataError';
|
|
15
|
+
Object.setPrototypeOf(this, KoolbaseDataError.prototype);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Thrown when a write is rejected because the value would violate a
|
|
20
|
+
* collection's unique constraint — the server responds with 409 Conflict.
|
|
21
|
+
* Catch it to handle duplicates, e.g. an email or username already in use.
|
|
22
|
+
*
|
|
23
|
+
* `field` names the field that collided, when the server reports it
|
|
24
|
+
* (`details.field`) — useful when a collection has more than one unique
|
|
25
|
+
* constraint and you need to know which value clashed.
|
|
26
|
+
*
|
|
27
|
+
* Surfaced by `insert`, `update`, and `upsert` whenever the server is
|
|
28
|
+
* reachable and rejects the write with a 409. These writes are online-first:
|
|
29
|
+
* a server-side conflict throws immediately. Only a genuine network failure
|
|
30
|
+
* falls back to the offline queue, where a conflict that surfaces at sync
|
|
31
|
+
* time is handled by the sync engine rather than thrown here.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* try {
|
|
35
|
+
* await koolbase.db.upsert('users', { email }, { name });
|
|
36
|
+
* } catch (e) {
|
|
37
|
+
* if (e instanceof KoolbaseConflictError) {
|
|
38
|
+
* showError(`That ${e.field ?? 'value'} is already registered.`);
|
|
39
|
+
* }
|
|
40
|
+
* }
|
|
41
|
+
*/
|
|
42
|
+
export class KoolbaseConflictError extends KoolbaseDataError {
|
|
43
|
+
constructor(message, field) {
|
|
44
|
+
super(message ?? 'Value violates a unique constraint', 'unique_violation');
|
|
45
|
+
this.field = field;
|
|
46
|
+
this.name = 'KoolbaseConflictError';
|
|
47
|
+
Object.setPrototypeOf(this, KoolbaseConflictError.prototype);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Thrown when the requested record or collection does not exist — the server
|
|
52
|
+
* responds with 404 and code `not_found` / `record_not_found` /
|
|
53
|
+
* `collection_not_found`.
|
|
54
|
+
*/
|
|
55
|
+
export class KoolbaseNotFoundError extends KoolbaseDataError {
|
|
56
|
+
constructor(message) {
|
|
57
|
+
super(message ?? 'The requested resource was not found', 'not_found');
|
|
58
|
+
this.name = 'KoolbaseNotFoundError';
|
|
59
|
+
Object.setPrototypeOf(this, KoolbaseNotFoundError.prototype);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Thrown when the request is rejected as invalid — the server responds with
|
|
64
|
+
* 400 and code `validation_error`.
|
|
65
|
+
*/
|
|
66
|
+
export class KoolbaseValidationError extends KoolbaseDataError {
|
|
67
|
+
constructor(message) {
|
|
68
|
+
super(message ?? 'The request was invalid', 'validation_error');
|
|
69
|
+
this.name = 'KoolbaseValidationError';
|
|
70
|
+
Object.setPrototypeOf(this, KoolbaseValidationError.prototype);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Thrown when the caller is authenticated but not allowed to perform the
|
|
75
|
+
* operation — the server responds with 403 and code `permission_denied`
|
|
76
|
+
* (typically a collection access rule rejecting the read/write).
|
|
77
|
+
*/
|
|
78
|
+
export class KoolbasePermissionError extends KoolbaseDataError {
|
|
79
|
+
constructor(message) {
|
|
80
|
+
super(message ?? 'You do not have permission to perform this action', 'permission_denied');
|
|
81
|
+
this.name = 'KoolbasePermissionError';
|
|
82
|
+
Object.setPrototypeOf(this, KoolbasePermissionError.prototype);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Thrown when the server is rate-limiting the caller — 429 with code
|
|
87
|
+
* `rate_limit`. Back off and retry after a short delay.
|
|
88
|
+
*/
|
|
89
|
+
export class KoolbaseRateLimitError extends KoolbaseDataError {
|
|
90
|
+
constructor(message) {
|
|
91
|
+
super(message ?? 'Too many requests, please slow down', 'rate_limit');
|
|
92
|
+
this.name = 'KoolbaseRateLimitError';
|
|
93
|
+
Object.setPrototypeOf(this, KoolbaseRateLimitError.prototype);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Thrown when the supplied vector's length does not match the dimension
|
|
98
|
+
* declared on the collection's vector field — the server responds with
|
|
99
|
+
* 400 and code `vector_dimension_mismatch`. The message includes both
|
|
100
|
+
* the expected and actual dimensions so you can surface a precise error.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* try {
|
|
104
|
+
* await koolbase.db.setVector(id, 'embedding', [0.1, 0.2]); // 2 dims
|
|
105
|
+
* } catch (e) {
|
|
106
|
+
* if (e instanceof KoolbaseVectorDimensionMismatchError) {
|
|
107
|
+
* showError(e.message); // "expected 1536, got 2"
|
|
108
|
+
* }
|
|
109
|
+
* }
|
|
110
|
+
*/
|
|
111
|
+
export class KoolbaseVectorDimensionMismatchError extends KoolbaseDataError {
|
|
112
|
+
constructor(message) {
|
|
113
|
+
super(message ?? 'Vector dimension does not match field declaration', 'vector_dimension_mismatch');
|
|
114
|
+
this.name = 'KoolbaseVectorDimensionMismatchError';
|
|
115
|
+
Object.setPrototypeOf(this, KoolbaseVectorDimensionMismatchError.prototype);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Maps a non-2xx data-layer response to a typed {@link KoolbaseDataError},
|
|
120
|
+
* preferring the server's stable `code` and falling back to the HTTP status
|
|
121
|
+
* for older or uncoded responses. Always returns an error to throw.
|
|
122
|
+
*/
|
|
123
|
+
export function koolbaseDataError(status, body, fallbackMessage = 'Request failed') {
|
|
124
|
+
const code = body?.code;
|
|
125
|
+
const message = body?.error ?? fallbackMessage;
|
|
126
|
+
const field = body?.details?.field;
|
|
127
|
+
const attach = (err) => {
|
|
128
|
+
// The body's structured details ride along on data errors — a
|
|
129
|
+
// revision_mismatch 409 carries the current revision and record, and
|
|
130
|
+
// discarding them here is how a refused conflict-resolution became
|
|
131
|
+
// permanently unresolvable: the information arrived and died in this file.
|
|
132
|
+
if (err instanceof KoolbaseDataError && body?.details) {
|
|
133
|
+
err.details = body.details;
|
|
134
|
+
}
|
|
135
|
+
return err;
|
|
136
|
+
};
|
|
137
|
+
// Status-first for auth: a 401 means the credentials were not accepted,
|
|
138
|
+
// whatever code the body claims. Trusting a mislabelled body here bypasses
|
|
139
|
+
// session-clearing and strands the app signed-in with dead credentials.
|
|
140
|
+
if (status === 401) {
|
|
141
|
+
return new KoolbaseUnauthenticatedError(message);
|
|
142
|
+
}
|
|
143
|
+
// ─── code-first ───
|
|
144
|
+
switch (code) {
|
|
145
|
+
case 'unique_violation':
|
|
146
|
+
return attach(new KoolbaseConflictError(message, field));
|
|
147
|
+
case 'not_found':
|
|
148
|
+
case 'record_not_found':
|
|
149
|
+
case 'collection_not_found':
|
|
150
|
+
case 'vector_not_found':
|
|
151
|
+
case 'vector_field_not_found':
|
|
152
|
+
return attach(new KoolbaseNotFoundError(message));
|
|
153
|
+
case 'unauthenticated':
|
|
154
|
+
case 'session_expired':
|
|
155
|
+
case 'invalid_token':
|
|
156
|
+
return attach(new KoolbaseUnauthenticatedError(message));
|
|
157
|
+
case 'permission_denied':
|
|
158
|
+
return attach(new KoolbasePermissionError(message));
|
|
159
|
+
case 'rate_limit':
|
|
160
|
+
return attach(new KoolbaseRateLimitError(message));
|
|
161
|
+
case 'validation_error':
|
|
162
|
+
case 'vector_collection_mismatch':
|
|
163
|
+
case 'unsupported_dimension':
|
|
164
|
+
return attach(new KoolbaseValidationError(message));
|
|
165
|
+
case 'vector_dimension_mismatch':
|
|
166
|
+
return attach(new KoolbaseVectorDimensionMismatchError(message));
|
|
167
|
+
}
|
|
168
|
+
// ─── status fallback (pre-code servers) ───
|
|
169
|
+
switch (status) {
|
|
170
|
+
case 409:
|
|
171
|
+
return attach(new KoolbaseConflictError(message));
|
|
172
|
+
case 404:
|
|
173
|
+
return attach(new KoolbaseNotFoundError(message));
|
|
174
|
+
case 401:
|
|
175
|
+
// The status carries the meaning: every 401 from this server reports the
|
|
176
|
+
// same code, so it cannot say whether the session expired, the key was
|
|
177
|
+
// revoked, or the header was malformed. Safe to treat uniformly because a
|
|
178
|
+
// permission failure is 403 — a 401 means the credentials were not
|
|
179
|
+
// accepted, not that this caller may not proceed.
|
|
180
|
+
return attach(new KoolbaseUnauthenticatedError(message));
|
|
181
|
+
case 403:
|
|
182
|
+
return attach(new KoolbasePermissionError(message));
|
|
183
|
+
case 429:
|
|
184
|
+
return attach(new KoolbaseRateLimitError(message));
|
|
185
|
+
case 400:
|
|
186
|
+
return attach(new KoolbaseValidationError(message));
|
|
187
|
+
}
|
|
188
|
+
return attach(new KoolbaseDataError(message, code));
|
|
189
|
+
}
|