@feltdb/core 0.5.1 → 0.5.2
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/dist/cell.d.ts +233 -0
- package/dist/cell.d.ts.map +1 -0
- package/dist/cell.js +1078 -0
- package/dist/cli/index.js +1 -1
- package/dist/collection.d.ts +4 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +28 -25
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/lib.rs +173 -0
- package/dist/create/server-source/crates/feltdb/src/managed_cas_tests.rs +231 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +108 -1
- package/dist/create/server-source/crates/feltdb-server/src/request_telemetry.rs +1 -1
- package/dist/create/server-source/crates/feltdb-server/tests/revision_recovery_integration_test.rs +219 -0
- package/dist/db.d.ts +16 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +21 -0
- package/dist/feltdb.d.ts +7 -1
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts +5 -0
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +72 -9
- package/dist/http-db.d.ts +13 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +41 -0
- package/dist/http-server.d.ts +42 -0
- package/dist/http-server.d.ts.map +1 -0
- package/dist/http-server.js +182 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/indexeddb-conformance.spec.d.ts +21 -0
- package/dist/indexeddb-conformance.spec.d.ts.map +1 -0
- package/dist/indexeddb-conformance.spec.js +103 -0
- package/dist/indexeddb-db.d.ts +8 -0
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +69 -0
- package/dist/memory-db.d.ts +2 -0
- package/dist/memory-db.d.ts.map +1 -1
- package/dist/memory-db.js +9 -3
- package/dist/studio-app/assets/{feltdb_wasm-DYPuS6Ky.js → feltdb_wasm-h9mxesnH.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-B8U4A1n1.wasm +0 -0
- package/dist/studio-app/assets/index-BMaQv3zF.js +28 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-DEwA82pF.wasm +0 -0
- package/dist/studio-app/assets/index-_qqVLfw1.js +0 -28
package/dist/http-db.d.ts
CHANGED
|
@@ -19,6 +19,19 @@ export declare class HttpJsDb implements JsDb {
|
|
|
19
19
|
query(collection: string): Promise<JsResult>;
|
|
20
20
|
insert(key: string, value: string): Promise<JsResult>;
|
|
21
21
|
update(key: string, value: string): Promise<JsResult>;
|
|
22
|
+
cas(params: {
|
|
23
|
+
key: string;
|
|
24
|
+
expectedVersion: number;
|
|
25
|
+
expectedEpoch?: number;
|
|
26
|
+
expectedLeaseId?: string;
|
|
27
|
+
rejectIfDiverged?: boolean;
|
|
28
|
+
value: string;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
updated: boolean;
|
|
31
|
+
currentVersion: number;
|
|
32
|
+
currentEpoch?: number;
|
|
33
|
+
conflictCode?: string;
|
|
34
|
+
}>;
|
|
22
35
|
delete(key: string): Promise<JsResult>;
|
|
23
36
|
/** Acquire a reference through the server's causal peer fabric. */
|
|
24
37
|
acquire(collection: string, id: string): Promise<unknown | null>;
|
package/dist/http-db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-db.d.ts","sourceRoot":"","sources":["../src/http-db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,UAAU,QAAQ;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,kBAAkB;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,iFAAiF;AACjF,qBAAa,QAAS,YAAW,IAAI;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAEnB,OAAO,EAAE,kBAAkB;IAKvC,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,QAAQ;IAQV,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAenC,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAa5C,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAgBrD,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAerD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAa5C,mEAAmE;IAC7D,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAOtE,qEAAqE;IAC/D,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAQzE,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAMxD,YAAY,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAMxF,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAOxD,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC;IAQxD,iBAAiB,CAAC,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;YAMvD,aAAa;YA+Bb,OAAO;IASrB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAC7D,UAAU,IAAI,QAAQ;IACtB,SAAS,IAAI,QAAQ;IACrB,QAAQ,IAAI,QAAQ;IACpB,aAAa,IAAI,QAAQ;IACzB,gBAAgB,IAAI,QAAQ;IAC5B,oBAAoB,IAAI,QAAQ;IAChC,2BAA2B,IAAI,QAAQ;IACvC,WAAW,IAAI,MAAM;IACrB,YAAY,IAAI,MAAM;CACvB"}
|
|
1
|
+
{"version":3,"file":"http-db.d.ts","sourceRoot":"","sources":["../src/http-db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,UAAU,QAAQ;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,kBAAkB;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;CACf;AAED,iFAAiF;AACjF,qBAAa,QAAS,YAAW,IAAI;IACnC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;gBAEnB,OAAO,EAAE,kBAAkB;IAKvC,OAAO,CAAC,OAAO;IAQf,OAAO,CAAC,QAAQ;IAQV,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAenC,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAa5C,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAgBrD,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAerD,GAAG,CAAC,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IA8CvP,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAa5C,mEAAmE;IAC7D,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC;IAOtE,qEAAqE;IAC/D,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC;IAQzE,UAAU,CAAC,UAAU,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAMxD,YAAY,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,CAAC;IAMxF,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;IAOxD,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC;IAQxD,iBAAiB,CAAC,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;YAMvD,aAAa;YA+Bb,OAAO;IASrB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAC7D,UAAU,IAAI,QAAQ;IACtB,SAAS,IAAI,QAAQ;IACrB,QAAQ,IAAI,QAAQ;IACpB,aAAa,IAAI,QAAQ;IACzB,gBAAgB,IAAI,QAAQ;IAC5B,oBAAoB,IAAI,QAAQ;IAChC,2BAA2B,IAAI,QAAQ;IACvC,WAAW,IAAI,MAAM;IACrB,YAAY,IAAI,MAAM;CACvB"}
|
package/dist/http-db.js
CHANGED
|
@@ -82,6 +82,47 @@ export class HttpJsDb {
|
|
|
82
82
|
return { success: false, error: String(error) };
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
+
async cas(params) {
|
|
86
|
+
try {
|
|
87
|
+
const [collection, id] = this.splitKey(params.key);
|
|
88
|
+
const parsed = JSON.parse(params.value);
|
|
89
|
+
const response = await fetch(`${this.url}/collections/${encodeURIComponent(collection)}/${encodeURIComponent(id)}/cas`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: this.headers(true),
|
|
92
|
+
body: JSON.stringify({
|
|
93
|
+
expectedVersion: params.expectedVersion,
|
|
94
|
+
expectedEpoch: params.expectedEpoch,
|
|
95
|
+
expectedLeaseId: params.expectedLeaseId,
|
|
96
|
+
rejectIfDiverged: params.rejectIfDiverged,
|
|
97
|
+
value: parsed,
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
// Handle conflict response (structured error)
|
|
101
|
+
if (response.status === 409) {
|
|
102
|
+
const body = await response.json();
|
|
103
|
+
if ((body.code === 'VERSION_CONFLICT' || body.code === 'AUTHORITY_CONFLICT' || body.code === 'LEASE_CONFLICT' || body.code === 'REPLICATION_CONFLICT') &&
|
|
104
|
+
body.currentVersion !== undefined) {
|
|
105
|
+
return {
|
|
106
|
+
updated: false,
|
|
107
|
+
currentVersion: body.currentVersion,
|
|
108
|
+
currentEpoch: body.currentEpoch,
|
|
109
|
+
conflictCode: body.code,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Other errors
|
|
114
|
+
if (!response.ok) {
|
|
115
|
+
const failure = await this.failure(response);
|
|
116
|
+
throw new Error(failure.error || 'HTTP CAS failed');
|
|
117
|
+
}
|
|
118
|
+
// Success response
|
|
119
|
+
const result = await response.json();
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
85
126
|
async delete(key) {
|
|
86
127
|
try {
|
|
87
128
|
const [collection, id] = this.splitKey(key);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeltDB HTTP Server
|
|
3
|
+
*
|
|
4
|
+
* Provides HTTP endpoints for FeltDB collection operations including atomic CAS.
|
|
5
|
+
* Can be used as middleware in Express, Hono, or any HTTP framework.
|
|
6
|
+
*
|
|
7
|
+
* Endpoints:
|
|
8
|
+
* - GET /collections/{collection}/{id} - Get a record
|
|
9
|
+
* - POST /collections/{collection} - Insert a record
|
|
10
|
+
* - PATCH /collections/{collection}/{id} - Update a record
|
|
11
|
+
* - POST /collections/{collection}/{id}/cas - Compare-and-set atomic update
|
|
12
|
+
* - DELETE /collections/{collection}/{id} - Delete a record
|
|
13
|
+
*
|
|
14
|
+
* The CAS endpoint preserves atomic semantics across the HTTP boundary by:
|
|
15
|
+
* 1. Receiving expectedVersion and value from client
|
|
16
|
+
* 2. Delegating to Collection.updateIfVersion() inside transaction boundary
|
|
17
|
+
* 3. Returning structured conflict response on version mismatch
|
|
18
|
+
* 4. Ensuring exactly one concurrent writer succeeds on same version
|
|
19
|
+
*/
|
|
20
|
+
import type { StateFirstDB } from './db.js';
|
|
21
|
+
export interface HttpServerOptions {
|
|
22
|
+
db: StateFirstDB;
|
|
23
|
+
}
|
|
24
|
+
export interface HttpRequest {
|
|
25
|
+
method: string;
|
|
26
|
+
path: string;
|
|
27
|
+
headers: Record<string, string>;
|
|
28
|
+
body?: string;
|
|
29
|
+
}
|
|
30
|
+
export interface HttpResponse {
|
|
31
|
+
status: number;
|
|
32
|
+
headers: Record<string, string>;
|
|
33
|
+
body: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* HTTP Server Request Handler
|
|
37
|
+
*
|
|
38
|
+
* This is a framework-agnostic handler that can be adapted to Express, Hono, etc.
|
|
39
|
+
* Returns {status, headers, body} that can be sent to the client.
|
|
40
|
+
*/
|
|
41
|
+
export declare function handleHttpRequest(options: HttpServerOptions, request: HttpRequest): Promise<HttpResponse>;
|
|
42
|
+
//# sourceMappingURL=http-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-server.d.ts","sourceRoot":"","sources":["../src/http-server.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE5C,MAAM,WAAW,iBAAiB;IAChC,EAAE,EAAE,YAAY,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,iBAAiB,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,YAAY,CAAC,CAwC/G"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FeltDB HTTP Server
|
|
3
|
+
*
|
|
4
|
+
* Provides HTTP endpoints for FeltDB collection operations including atomic CAS.
|
|
5
|
+
* Can be used as middleware in Express, Hono, or any HTTP framework.
|
|
6
|
+
*
|
|
7
|
+
* Endpoints:
|
|
8
|
+
* - GET /collections/{collection}/{id} - Get a record
|
|
9
|
+
* - POST /collections/{collection} - Insert a record
|
|
10
|
+
* - PATCH /collections/{collection}/{id} - Update a record
|
|
11
|
+
* - POST /collections/{collection}/{id}/cas - Compare-and-set atomic update
|
|
12
|
+
* - DELETE /collections/{collection}/{id} - Delete a record
|
|
13
|
+
*
|
|
14
|
+
* The CAS endpoint preserves atomic semantics across the HTTP boundary by:
|
|
15
|
+
* 1. Receiving expectedVersion and value from client
|
|
16
|
+
* 2. Delegating to Collection.updateIfVersion() inside transaction boundary
|
|
17
|
+
* 3. Returning structured conflict response on version mismatch
|
|
18
|
+
* 4. Ensuring exactly one concurrent writer succeeds on same version
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* HTTP Server Request Handler
|
|
22
|
+
*
|
|
23
|
+
* This is a framework-agnostic handler that can be adapted to Express, Hono, etc.
|
|
24
|
+
* Returns {status, headers, body} that can be sent to the client.
|
|
25
|
+
*/
|
|
26
|
+
export async function handleHttpRequest(options, request) {
|
|
27
|
+
const { db } = options;
|
|
28
|
+
const { method, path, headers, body } = request;
|
|
29
|
+
// Parse path: /collections/{collection}/{id}[/cas]
|
|
30
|
+
const match = path.match(/^\/collections\/([^/]+)(?:\/([^/]+))?(?:\/(.+))?$/);
|
|
31
|
+
if (!match) {
|
|
32
|
+
return errorResponse(404, 'Not Found', 'Invalid path');
|
|
33
|
+
}
|
|
34
|
+
const collection = decodeURIComponent(match[1]);
|
|
35
|
+
const id = match[2] ? decodeURIComponent(match[2]) : undefined;
|
|
36
|
+
const operation = match[3] || undefined;
|
|
37
|
+
// GET /collections/{collection}/{id}
|
|
38
|
+
if (method === 'GET' && id) {
|
|
39
|
+
return handleGet(db, collection, id);
|
|
40
|
+
}
|
|
41
|
+
// POST /collections/{collection}
|
|
42
|
+
if (method === 'POST' && !id && !operation) {
|
|
43
|
+
return handleInsert(db, collection, body);
|
|
44
|
+
}
|
|
45
|
+
// PATCH /collections/{collection}/{id}
|
|
46
|
+
if (method === 'PATCH' && id && !operation) {
|
|
47
|
+
return handleUpdate(db, collection, id, body);
|
|
48
|
+
}
|
|
49
|
+
// POST /collections/{collection}/{id}/cas - Atomic compare-and-set
|
|
50
|
+
if (method === 'POST' && id && operation === 'cas') {
|
|
51
|
+
return handleCas(db, collection, id, body);
|
|
52
|
+
}
|
|
53
|
+
// DELETE /collections/{collection}/{id}
|
|
54
|
+
if (method === 'DELETE' && id && !operation) {
|
|
55
|
+
return handleDelete(db, collection, id);
|
|
56
|
+
}
|
|
57
|
+
return errorResponse(404, 'Not Found', `No handler for ${method} ${path}`);
|
|
58
|
+
}
|
|
59
|
+
async function handleGet(db, collection, id) {
|
|
60
|
+
try {
|
|
61
|
+
const coll = db.collection(collection);
|
|
62
|
+
const item = await coll.get(id);
|
|
63
|
+
if (!item) {
|
|
64
|
+
return errorResponse(404, 'Not Found', `Record ${collection}:${id} not found`);
|
|
65
|
+
}
|
|
66
|
+
return successResponse(200, { value: item });
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return errorResponse(500, 'Internal Server Error', String(error));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function handleInsert(db, collection, body) {
|
|
73
|
+
try {
|
|
74
|
+
if (!body) {
|
|
75
|
+
return errorResponse(400, 'Bad Request', 'Request body required');
|
|
76
|
+
}
|
|
77
|
+
const value = JSON.parse(body);
|
|
78
|
+
const id = String(value.id ?? '');
|
|
79
|
+
if (!id) {
|
|
80
|
+
return errorResponse(400, 'Bad Request', 'Record must have an id');
|
|
81
|
+
}
|
|
82
|
+
const coll = db.collection(collection);
|
|
83
|
+
await coll.insert(value, id);
|
|
84
|
+
return successResponse(201, { id, value });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
return errorResponse(400, 'Bad Request', String(error));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function handleUpdate(db, collection, id, body) {
|
|
91
|
+
try {
|
|
92
|
+
if (!body) {
|
|
93
|
+
return errorResponse(400, 'Bad Request', 'Request body required');
|
|
94
|
+
}
|
|
95
|
+
const updates = JSON.parse(body);
|
|
96
|
+
const coll = db.collection(collection);
|
|
97
|
+
await coll.update(id, updates);
|
|
98
|
+
const item = await coll.get(id);
|
|
99
|
+
return successResponse(200, { value: item });
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
return errorResponse(400, 'Bad Request', String(error));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Handle atomic Compare-And-Set (CAS) operation via HTTP
|
|
107
|
+
*
|
|
108
|
+
* This endpoint receives expectedVersion and value from the client,
|
|
109
|
+
* delegates to Collection.updateIfVersion() which performs the atomic
|
|
110
|
+
* version check inside the backend transaction boundary, and returns
|
|
111
|
+
* a structured conflict response if the version doesn't match.
|
|
112
|
+
*
|
|
113
|
+
* Path: POST /collections/{collection}/{id}/cas
|
|
114
|
+
* Body: { expectedVersion: number, value: object }
|
|
115
|
+
* Response 200: { updated: true, currentVersion: number, item: object }
|
|
116
|
+
* Response 409: { code: "VERSION_CONFLICT", currentVersion: number }
|
|
117
|
+
*/
|
|
118
|
+
async function handleCas(db, collection, id, body) {
|
|
119
|
+
try {
|
|
120
|
+
if (!body) {
|
|
121
|
+
return errorResponse(400, 'Bad Request', 'Request body required');
|
|
122
|
+
}
|
|
123
|
+
const request = JSON.parse(body);
|
|
124
|
+
if (request.expectedVersion === undefined) {
|
|
125
|
+
return errorResponse(400, 'Bad Request', 'expectedVersion is required');
|
|
126
|
+
}
|
|
127
|
+
if (!request.value) {
|
|
128
|
+
return errorResponse(400, 'Bad Request', 'value is required');
|
|
129
|
+
}
|
|
130
|
+
const coll = db.collection(collection);
|
|
131
|
+
const result = await coll.updateIfVersion(id, request.expectedVersion, request.value);
|
|
132
|
+
if (result.updated) {
|
|
133
|
+
// Success: return the updated item with new version
|
|
134
|
+
return successResponse(200, {
|
|
135
|
+
updated: true,
|
|
136
|
+
currentVersion: result.item?.__version,
|
|
137
|
+
item: result.item,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
// Conflict: return structured error response with actual version
|
|
142
|
+
return errorResponse(409, 'VERSION_CONFLICT', `Version mismatch for ${collection}:${id}`, {
|
|
143
|
+
code: 'VERSION_CONFLICT',
|
|
144
|
+
currentVersion: result.currentVersion,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (String(error).includes('not found')) {
|
|
150
|
+
return errorResponse(404, 'Not Found', `Record ${collection}:${id} not found`);
|
|
151
|
+
}
|
|
152
|
+
return errorResponse(400, 'Bad Request', String(error));
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
async function handleDelete(db, collection, id) {
|
|
156
|
+
try {
|
|
157
|
+
const coll = db.collection(collection);
|
|
158
|
+
await coll.delete(id);
|
|
159
|
+
return successResponse(204, {});
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
return errorResponse(400, 'Bad Request', String(error));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function successResponse(status, data) {
|
|
166
|
+
return {
|
|
167
|
+
status,
|
|
168
|
+
headers: { 'Content-Type': 'application/json' },
|
|
169
|
+
body: JSON.stringify(data),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
function errorResponse(status, code, message, extra) {
|
|
173
|
+
return {
|
|
174
|
+
status,
|
|
175
|
+
headers: { 'Content-Type': 'application/json' },
|
|
176
|
+
body: JSON.stringify({
|
|
177
|
+
code,
|
|
178
|
+
message,
|
|
179
|
+
...extra,
|
|
180
|
+
}),
|
|
181
|
+
};
|
|
182
|
+
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAC1C,OAAO,EACL,eAAe,EACf,KAAK,OAAO,EACZ,KAAK,SAAS,IAAI,iBAAiB,EACnC,KAAK,QAAQ,IAAI,gBAAgB,GAClC,MAAM,eAAe,CAAC;AAEvB;;GAEG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AAEnC;;GAEG;AACH,YAAY,EACV,eAAe,EACf,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,WAAW,CAAC;AAC1B,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,kBAAkB,CAAC;AACjC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAC1C,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,0BAA0B,CAAC;AACzC,cAAc,kBAAkB,CAAC;AACjC,cAAc,oBAAoB,CAAC;AACnC,cAAc,oBAAoB,CAAC;AACnC,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC;AAC1C,OAAO,EACL,eAAe,EACf,KAAK,OAAO,EACZ,KAAK,SAAS,IAAI,iBAAiB,EACnC,KAAK,QAAQ,IAAI,gBAAgB,GAClC,MAAM,eAAe,CAAC;AAEvB;;GAEG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC;AAClC,cAAc,qBAAqB,CAAC;AACpC,cAAc,oBAAoB,CAAC;AAEnC;;GAEG;AACH,YAAY,EACV,eAAe,EACf,cAAc,EACd,cAAc,EACd,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,SAAS,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IndexedDB CAS Conformance Tests - Chromium/Playwright
|
|
3
|
+
*
|
|
4
|
+
* Note: IndexedDB conformance testing requires a browser environment with
|
|
5
|
+
* ES modules support. For now, this file documents the test structure.
|
|
6
|
+
*
|
|
7
|
+
* To run these tests:
|
|
8
|
+
* 1. Create a simple HTML page that imports the compiled FeltDB modules
|
|
9
|
+
* 2. Run `npx playwright test` in this directory
|
|
10
|
+
* 3. Tests will run in Chromium and verify IndexedDB CAS semantics
|
|
11
|
+
*
|
|
12
|
+
* Validation points:
|
|
13
|
+
* - Concurrent transaction isolation
|
|
14
|
+
* - Version persistence across reopen
|
|
15
|
+
* - Stale writer conflict detection
|
|
16
|
+
* - Version increment atomicity
|
|
17
|
+
* - Failed CAS leaves state unchanged
|
|
18
|
+
* - __version cannot be supplied by caller
|
|
19
|
+
*/
|
|
20
|
+
export declare const playwrightTestStructure = "\nRequired test coverage (Playwright + Chromium):\n\nTest 1: Single process CAS increments version\n \u2713 Insert record (version = 1)\n \u2713 CAS update (expectedVersion: 1)\n \u2713 Result: updated=true, currentVersion=2\n \u2713 Verify persisted: version=2\n\nTest 2: Version conflict on mismatch\n \u2713 Insert record (version = 1)\n \u2713 CAS with wrong expectedVersion (999)\n \u2713 Result: updated=false, currentVersion=1\n \u2713 Verify no mutation: version=1, data unchanged\n\nTest 3: Two concurrent writers (Promise.all)\n \u2713 Insert record (version = 1)\n \u2713 Race 2 CAS requests on same version\n \u2713 Result: 1 success, 1 conflict\n \u2713 Verify final version = 2, not 3\n\nTest 4: Three concurrent writers\n \u2713 Insert record (version = 1)\n \u2713 Race 3 CAS requests on same version\n \u2713 Result: 1 success, 2 conflicts\n \u2713 Verify final version = 2\n\nTest 5: 50 concurrent races\n \u2713 For each i in 0..49:\n - Insert record\n - Race 2 writers on version 1\n - Verify: 1 success, 1 conflict\n\nTest 6: 100 sequential updates\n \u2713 Insert counter (version = 1)\n \u2713 For each i in 0..99:\n - CAS with expectedVersion = i+1\n - Verify updated=true, currentVersion = i+2\n \u2713 Final version = 101 (1 + 100 updates)\n\nTest 7: Stale writer conflict after reopen\n \u2713 Insert record, read version N\n \u2713 Other writer updates with expectedVersion=N \u2192 success, version=N+1\n \u2713 Stale writer tries expectedVersion=N \u2192 conflict, currentVersion=N+1\n\nTest 8: Failed CAS leaves state unchanged\n \u2713 Insert record with nested structure\n \u2713 50 failed CAS attempts with expectedVersion=999\n \u2713 Verify: version unchanged, all fields unchanged (including nested)\n\nTest 9: User __version ignored\n \u2713 Insert record (version = 1)\n \u2713 CAS with __version: 999 in updates\n \u2713 Result: currentVersion = 2, not 999\n \u2713 Verify: stored __version = 2\n\nTest 10: Partial updates preserve fields\n \u2713 Insert record with multiple fields\n \u2713 CAS with only some fields in updates\n \u2713 Verify: updated fields changed, others preserved\n\nTest 11: No mutation event on failed CAS\n \u2713 Subscribe to changes for record\n \u2713 50 failed CAS attempts\n \u2713 Verify: no change events fired\n\nTest 12: Exactly one mutation event on success\n \u2713 Subscribe to changes for record\n \u2713 CAS success\n \u2713 Verify: exactly 1 change event fired\n";
|
|
21
|
+
//# sourceMappingURL=indexeddb-conformance.spec.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexeddb-conformance.spec.d.ts","sourceRoot":"","sources":["../src/indexeddb-conformance.spec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AAQH,eAAO,MAAM,uBAAuB,s8EAsEnC,CAAC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IndexedDB CAS Conformance Tests - Chromium/Playwright
|
|
3
|
+
*
|
|
4
|
+
* Note: IndexedDB conformance testing requires a browser environment with
|
|
5
|
+
* ES modules support. For now, this file documents the test structure.
|
|
6
|
+
*
|
|
7
|
+
* To run these tests:
|
|
8
|
+
* 1. Create a simple HTML page that imports the compiled FeltDB modules
|
|
9
|
+
* 2. Run `npx playwright test` in this directory
|
|
10
|
+
* 3. Tests will run in Chromium and verify IndexedDB CAS semantics
|
|
11
|
+
*
|
|
12
|
+
* Validation points:
|
|
13
|
+
* - Concurrent transaction isolation
|
|
14
|
+
* - Version persistence across reopen
|
|
15
|
+
* - Stale writer conflict detection
|
|
16
|
+
* - Version increment atomicity
|
|
17
|
+
* - Failed CAS leaves state unchanged
|
|
18
|
+
* - __version cannot be supplied by caller
|
|
19
|
+
*/
|
|
20
|
+
// This file serves as documentation for the IndexedDB conformance tests.
|
|
21
|
+
// Implementation would require:
|
|
22
|
+
// 1. A test HTML page that loads compiled FeltDB modules
|
|
23
|
+
// 2. Playwright test harness using page.evaluate() for each operation
|
|
24
|
+
// 3. Tests running in real Chromium with IndexedDB available
|
|
25
|
+
export const playwrightTestStructure = `
|
|
26
|
+
Required test coverage (Playwright + Chromium):
|
|
27
|
+
|
|
28
|
+
Test 1: Single process CAS increments version
|
|
29
|
+
✓ Insert record (version = 1)
|
|
30
|
+
✓ CAS update (expectedVersion: 1)
|
|
31
|
+
✓ Result: updated=true, currentVersion=2
|
|
32
|
+
✓ Verify persisted: version=2
|
|
33
|
+
|
|
34
|
+
Test 2: Version conflict on mismatch
|
|
35
|
+
✓ Insert record (version = 1)
|
|
36
|
+
✓ CAS with wrong expectedVersion (999)
|
|
37
|
+
✓ Result: updated=false, currentVersion=1
|
|
38
|
+
✓ Verify no mutation: version=1, data unchanged
|
|
39
|
+
|
|
40
|
+
Test 3: Two concurrent writers (Promise.all)
|
|
41
|
+
✓ Insert record (version = 1)
|
|
42
|
+
✓ Race 2 CAS requests on same version
|
|
43
|
+
✓ Result: 1 success, 1 conflict
|
|
44
|
+
✓ Verify final version = 2, not 3
|
|
45
|
+
|
|
46
|
+
Test 4: Three concurrent writers
|
|
47
|
+
✓ Insert record (version = 1)
|
|
48
|
+
✓ Race 3 CAS requests on same version
|
|
49
|
+
✓ Result: 1 success, 2 conflicts
|
|
50
|
+
✓ Verify final version = 2
|
|
51
|
+
|
|
52
|
+
Test 5: 50 concurrent races
|
|
53
|
+
✓ For each i in 0..49:
|
|
54
|
+
- Insert record
|
|
55
|
+
- Race 2 writers on version 1
|
|
56
|
+
- Verify: 1 success, 1 conflict
|
|
57
|
+
|
|
58
|
+
Test 6: 100 sequential updates
|
|
59
|
+
✓ Insert counter (version = 1)
|
|
60
|
+
✓ For each i in 0..99:
|
|
61
|
+
- CAS with expectedVersion = i+1
|
|
62
|
+
- Verify updated=true, currentVersion = i+2
|
|
63
|
+
✓ Final version = 101 (1 + 100 updates)
|
|
64
|
+
|
|
65
|
+
Test 7: Stale writer conflict after reopen
|
|
66
|
+
✓ Insert record, read version N
|
|
67
|
+
✓ Other writer updates with expectedVersion=N → success, version=N+1
|
|
68
|
+
✓ Stale writer tries expectedVersion=N → conflict, currentVersion=N+1
|
|
69
|
+
|
|
70
|
+
Test 8: Failed CAS leaves state unchanged
|
|
71
|
+
✓ Insert record with nested structure
|
|
72
|
+
✓ 50 failed CAS attempts with expectedVersion=999
|
|
73
|
+
✓ Verify: version unchanged, all fields unchanged (including nested)
|
|
74
|
+
|
|
75
|
+
Test 9: User __version ignored
|
|
76
|
+
✓ Insert record (version = 1)
|
|
77
|
+
✓ CAS with __version: 999 in updates
|
|
78
|
+
✓ Result: currentVersion = 2, not 999
|
|
79
|
+
✓ Verify: stored __version = 2
|
|
80
|
+
|
|
81
|
+
Test 10: Partial updates preserve fields
|
|
82
|
+
✓ Insert record with multiple fields
|
|
83
|
+
✓ CAS with only some fields in updates
|
|
84
|
+
✓ Verify: updated fields changed, others preserved
|
|
85
|
+
|
|
86
|
+
Test 11: No mutation event on failed CAS
|
|
87
|
+
✓ Subscribe to changes for record
|
|
88
|
+
✓ 50 failed CAS attempts
|
|
89
|
+
✓ Verify: no change events fired
|
|
90
|
+
|
|
91
|
+
Test 12: Exactly one mutation event on success
|
|
92
|
+
✓ Subscribe to changes for record
|
|
93
|
+
✓ CAS success
|
|
94
|
+
✓ Verify: exactly 1 change event fired
|
|
95
|
+
`;
|
|
96
|
+
// Implementation note: For production conformance, uncomment and implement
|
|
97
|
+
// the actual Playwright tests once ES module resolution is available in test runner.
|
|
98
|
+
//
|
|
99
|
+
// import { test, expect, Page } from '@playwright/test';
|
|
100
|
+
//
|
|
101
|
+
// test.describe('IndexedDB CAS Conformance', () => {
|
|
102
|
+
// // Test implementations here
|
|
103
|
+
// });
|
package/dist/indexeddb-db.d.ts
CHANGED
|
@@ -38,6 +38,14 @@ export declare class IndexedDbJsDb implements JsDb {
|
|
|
38
38
|
insert(key: string, value: string): Promise<JsResult>;
|
|
39
39
|
update(key: string, value: string): Promise<JsResult>;
|
|
40
40
|
delete(key: string): Promise<JsResult>;
|
|
41
|
+
cas(params: {
|
|
42
|
+
key: string;
|
|
43
|
+
expectedVersion: number;
|
|
44
|
+
value: string;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
updated: boolean;
|
|
47
|
+
currentVersion: number;
|
|
48
|
+
}>;
|
|
41
49
|
get(key: string): Promise<JsResult>;
|
|
42
50
|
query(collection: string): Promise<JsResult>;
|
|
43
51
|
private request;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"indexeddb-db.d.ts","sourceRoot":"","sources":["../src/indexeddb-db.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"indexeddb-db.d.ts","sourceRoot":"","sources":["../src/indexeddb-db.ts"],"names":[],"mappings":"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"}
|
package/dist/indexeddb-db.js
CHANGED
|
@@ -91,6 +91,75 @@ export class IndexedDbJsDb {
|
|
|
91
91
|
insert(key, value) { return this.mutate(key, JSON.parse(value), 'put'); }
|
|
92
92
|
update(key, value) { return this.mutate(key, JSON.parse(value), 'put'); }
|
|
93
93
|
delete(key) { return this.mutate(key, undefined, 'delete'); }
|
|
94
|
+
async cas(params) {
|
|
95
|
+
try {
|
|
96
|
+
const db = await this.database;
|
|
97
|
+
let result = { updated: false, currentVersion: 0 };
|
|
98
|
+
let committedSequence = 0;
|
|
99
|
+
const collection = this.splitKey(params.key);
|
|
100
|
+
await new Promise((resolve, reject) => {
|
|
101
|
+
let transaction;
|
|
102
|
+
try {
|
|
103
|
+
transaction = db.transaction(['rows', 'changes'], 'readwrite', { durability: 'strict' });
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
transaction = db.transaction(['rows', 'changes'], 'readwrite');
|
|
107
|
+
}
|
|
108
|
+
const rowsStore = transaction.objectStore('rows');
|
|
109
|
+
const changesStore = transaction.objectStore('changes');
|
|
110
|
+
// Read current value inside transaction boundary
|
|
111
|
+
const getRequest = rowsStore.get(params.key);
|
|
112
|
+
getRequest.onsuccess = () => {
|
|
113
|
+
const existing = getRequest.result;
|
|
114
|
+
if (existing === undefined) {
|
|
115
|
+
result = { updated: false, currentVersion: 0 };
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const existingRecord = existing;
|
|
119
|
+
const currentVersion = existingRecord.__version ?? 1;
|
|
120
|
+
// Version check happens inside transaction
|
|
121
|
+
if (currentVersion !== params.expectedVersion) {
|
|
122
|
+
result = { updated: false, currentVersion };
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// Version matches - update it
|
|
126
|
+
const parsed = JSON.parse(params.value);
|
|
127
|
+
const newVersion = params.expectedVersion + 1;
|
|
128
|
+
const updatedRecord = { ...parsed, __version: newVersion };
|
|
129
|
+
rowsStore.put(updatedRecord, params.key);
|
|
130
|
+
const timestamp = Date.now();
|
|
131
|
+
const change = changesStore.add({
|
|
132
|
+
collection,
|
|
133
|
+
key: params.key,
|
|
134
|
+
type: 'put',
|
|
135
|
+
value: updatedRecord,
|
|
136
|
+
timestamp,
|
|
137
|
+
origin: this.origin,
|
|
138
|
+
id: `${this.origin}:${timestamp}:${Math.random().toString(36).slice(2)}`
|
|
139
|
+
});
|
|
140
|
+
change.onsuccess = () => {
|
|
141
|
+
committedSequence = Number(change.result);
|
|
142
|
+
result = { updated: true, currentVersion: newVersion };
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
getRequest.onerror = () => {
|
|
146
|
+
transaction.abort();
|
|
147
|
+
};
|
|
148
|
+
transaction.oncomplete = () => resolve();
|
|
149
|
+
transaction.onerror = () => reject(transaction.error ?? new Error('IndexedDB CAS failed'));
|
|
150
|
+
transaction.onabort = () => reject(transaction.error ?? new Error('IndexedDB CAS aborted'));
|
|
151
|
+
});
|
|
152
|
+
if (result.updated && committedSequence > 0) {
|
|
153
|
+
this.sequence += 1;
|
|
154
|
+
this.announceLocalChange(collection, committedSequence);
|
|
155
|
+
this.channel?.postMessage({ collection, sequence: committedSequence });
|
|
156
|
+
}
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
return { updated: false, currentVersion: 0 };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
94
163
|
async get(key) {
|
|
95
164
|
try {
|
|
96
165
|
const value = await this.request('rows', store => store.get(key));
|
package/dist/memory-db.d.ts
CHANGED
|
@@ -34,10 +34,12 @@ export declare class MemoryJsDb implements JsDb {
|
|
|
34
34
|
cas(params: {
|
|
35
35
|
key: string;
|
|
36
36
|
expectedVersion: number;
|
|
37
|
+
expectedEpoch?: number;
|
|
37
38
|
value: string;
|
|
38
39
|
}): Promise<{
|
|
39
40
|
updated: boolean;
|
|
40
41
|
currentVersion: number;
|
|
42
|
+
currentEpoch?: number;
|
|
41
43
|
}>;
|
|
42
44
|
query(capability: string): JsResult;
|
|
43
45
|
get_capability_records(capability: string): JsResult;
|
package/dist/memory-db.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-db.d.ts","sourceRoot":"","sources":["../src/memory-db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,UAAU,QAAQ;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,aAAa;IAAG,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,GAAG,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE;AAIrK,mEAAmE;AACnE,qBAAa,UAAW,YAAW,IAAI;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;gBAE/B,SAAS,EAAE,MAAM;IAU7B,OAAO,CAAC,KAAK;IAMb,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ;IAU5C,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ;IAO5C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ;IAO1B,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ;IAM7B,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAiBtF,GAAG,CAAC,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"memory-db.d.ts","sourceRoot":"","sources":["../src/memory-db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,UAAU,QAAQ;IAChB,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,aAAa;IAAG,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,KAAK,GAAG,QAAQ,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE;AAIrK,mEAAmE;AACnE,qBAAa,UAAW,YAAW,IAAI;IACrC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;IAC5C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;gBAE/B,SAAS,EAAE,MAAM;IAU7B,OAAO,CAAC,KAAK;IAMb,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ;IAU5C,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,QAAQ;IAO5C,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ;IAO1B,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ;IAM7B,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,OAAO,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IAiBtF,GAAG,CAAC,MAAM,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,eAAe,EAAE,MAAM,CAAC;QAAC,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,cAAc,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAiC1K,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ;IAQnC,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,QAAQ;IAIpD,UAAU,IAAI,QAAQ;IAItB,SAAS,IAAI,QAAQ;IAIrB,QAAQ,IAAI,QAAQ;IACpB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IACvC,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IAC1C,2BAA2B,IAAI,QAAQ;IACvC,oBAAoB,IAAI,QAAQ;IAChC,WAAW,IAAI,MAAM;IACrB,YAAY,IAAI,MAAM;IACtB,YAAY;IACZ,iBAAiB,CAAC,aAAa,EAAE,MAAM;IACvC,uBAAuB,CAAC,UAAU,EAAE,aAAa,EAAE;;;;CAapD"}
|
package/dist/memory-db.js
CHANGED
|
@@ -70,16 +70,22 @@ export class MemoryJsDb {
|
|
|
70
70
|
}
|
|
71
71
|
const existingRecord = existing;
|
|
72
72
|
const currentVersion = existingRecord.__version ?? 1;
|
|
73
|
+
const currentEpoch = existingRecord.authority?.epoch ?? 0;
|
|
73
74
|
if (currentVersion !== params.expectedVersion) {
|
|
74
75
|
// Version mismatch - conflict
|
|
75
|
-
return Promise.resolve({ updated: false, currentVersion });
|
|
76
|
+
return Promise.resolve({ updated: false, currentVersion, currentEpoch });
|
|
76
77
|
}
|
|
77
|
-
//
|
|
78
|
+
// Check epoch atomically with version if provided
|
|
79
|
+
if (params.expectedEpoch !== undefined && currentEpoch !== params.expectedEpoch) {
|
|
80
|
+
// Epoch mismatch - conflict
|
|
81
|
+
return Promise.resolve({ updated: false, currentVersion, currentEpoch });
|
|
82
|
+
}
|
|
83
|
+
// Both version and epoch match - update atomically
|
|
78
84
|
const newVersion = params.expectedVersion + 1;
|
|
79
85
|
const updated = { ...parsed, __version: newVersion };
|
|
80
86
|
this.rows.set(params.key, updated);
|
|
81
87
|
this.event(params.key.split(':')[0], params.key, 'put', updated);
|
|
82
|
-
return Promise.resolve({ updated: true, currentVersion: newVersion });
|
|
88
|
+
return Promise.resolve({ updated: true, currentVersion: newVersion, currentEpoch });
|
|
83
89
|
}
|
|
84
90
|
query(capability) {
|
|
85
91
|
const prefix = `${capability}:`;
|