@feltdb/core 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 +184 -0
- package/dist/agent-decision.d.ts +81 -0
- package/dist/agent-decision.d.ts.map +1 -0
- package/dist/agent-decision.js +18 -0
- package/dist/agent-memory.d.ts +91 -0
- package/dist/agent-memory.d.ts.map +1 -0
- package/dist/agent-memory.js +21 -0
- package/dist/agent-observation.d.ts +61 -0
- package/dist/agent-observation.d.ts.map +1 -0
- package/dist/agent-observation.js +12 -0
- package/dist/agent-registry.d.ts +92 -0
- package/dist/agent-registry.d.ts.map +1 -0
- package/dist/agent-registry.js +134 -0
- package/dist/agent-runtime.d.ts +147 -0
- package/dist/agent-runtime.d.ts.map +1 -0
- package/dist/agent-runtime.js +240 -0
- package/dist/agent.d.ts +132 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +58 -0
- package/dist/capability.d.ts +21 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +23 -0
- package/dist/collection.d.ts +95 -0
- package/dist/collection.d.ts.map +1 -0
- package/dist/collection.js +289 -0
- package/dist/db.d.ts +504 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +700 -0
- package/dist/execution.d.ts +148 -0
- package/dist/execution.d.ts.map +1 -0
- package/dist/execution.js +18 -0
- package/dist/feltdb.d.ts +82 -0
- package/dist/feltdb.d.ts.map +1 -0
- package/dist/feltdb.js +6 -0
- package/dist/flowspec.d.ts +64 -0
- package/dist/flowspec.d.ts.map +1 -0
- package/dist/flowspec.js +272 -0
- package/dist/http-db.d.ts +50 -0
- package/dist/http-db.d.ts.map +1 -0
- package/dist/http-db.js +205 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/indexeddb-db.d.ts +54 -0
- package/dist/indexeddb-db.d.ts.map +1 -0
- package/dist/indexeddb-db.js +175 -0
- package/dist/memory-db.d.ts +49 -0
- package/dist/memory-db.d.ts.map +1 -0
- package/dist/memory-db.js +97 -0
- package/dist/operation.d.ts +25 -0
- package/dist/operation.d.ts.map +1 -0
- package/dist/operation.js +16 -0
- package/dist/reactive-graph.d.ts +67 -0
- package/dist/reactive-graph.d.ts.map +1 -0
- package/dist/reactive-graph.js +118 -0
- package/dist/recovery.d.ts +68 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +104 -0
- package/dist/storage.d.ts +48 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +6 -0
- package/dist/workflow.d.ts +20 -0
- package/dist/workflow.d.ts.map +1 -0
- package/dist/workflow.js +12 -0
- package/package.json +43 -0
package/dist/http-db.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/** Network adapter implementing the same collection runtime contract as WASM. */
|
|
2
|
+
export class HttpJsDb {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
this.url = options.url.replace(/\/$/, '');
|
|
5
|
+
this.token = options.token;
|
|
6
|
+
}
|
|
7
|
+
headers(json = false) {
|
|
8
|
+
return {
|
|
9
|
+
Authorization: `Bearer ${this.token}`,
|
|
10
|
+
'FeltDB-Protocol': '1',
|
|
11
|
+
...(json ? { 'Content-Type': 'application/json' } : {}),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
splitKey(key) {
|
|
15
|
+
const separator = key.indexOf(':');
|
|
16
|
+
if (separator < 1 || separator === key.length - 1) {
|
|
17
|
+
throw new Error(`Invalid FeltDB key: ${key}`);
|
|
18
|
+
}
|
|
19
|
+
return [key.slice(0, separator), key.slice(separator + 1)];
|
|
20
|
+
}
|
|
21
|
+
async get(key) {
|
|
22
|
+
try {
|
|
23
|
+
const [collection, id] = this.splitKey(key);
|
|
24
|
+
const response = await fetch(`${this.url}/collections/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`, {
|
|
25
|
+
headers: this.headers(),
|
|
26
|
+
});
|
|
27
|
+
if (response.status === 404)
|
|
28
|
+
return { success: true };
|
|
29
|
+
if (!response.ok)
|
|
30
|
+
return this.failure(response);
|
|
31
|
+
const record = await response.json();
|
|
32
|
+
return { success: true, data: JSON.stringify(record.value) };
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return { success: false, error: String(error) };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async query(collection) {
|
|
39
|
+
try {
|
|
40
|
+
const response = await fetch(`${this.url}/collections/${encodeURIComponent(collection)}`, {
|
|
41
|
+
headers: this.headers(),
|
|
42
|
+
});
|
|
43
|
+
if (!response.ok)
|
|
44
|
+
return this.failure(response);
|
|
45
|
+
const records = await response.json();
|
|
46
|
+
return { success: true, data: JSON.stringify(records.map(record => record.value)) };
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
return { success: false, error: String(error) };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async insert(key, value) {
|
|
53
|
+
try {
|
|
54
|
+
const [collection, id] = this.splitKey(key);
|
|
55
|
+
const record = JSON.parse(value);
|
|
56
|
+
const response = await fetch(`${this.url}/collections/${encodeURIComponent(collection)}`, {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: this.headers(true),
|
|
59
|
+
body: JSON.stringify({ ...record, id }),
|
|
60
|
+
});
|
|
61
|
+
if (!response.ok)
|
|
62
|
+
return this.failure(response);
|
|
63
|
+
return { success: true, data: key };
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
return { success: false, error: String(error) };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async update(key, value) {
|
|
70
|
+
try {
|
|
71
|
+
const [collection, id] = this.splitKey(key);
|
|
72
|
+
const response = await fetch(`${this.url}/collections/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`, {
|
|
73
|
+
method: 'PATCH',
|
|
74
|
+
headers: this.headers(true),
|
|
75
|
+
body: value,
|
|
76
|
+
});
|
|
77
|
+
if (!response.ok)
|
|
78
|
+
return this.failure(response);
|
|
79
|
+
return { success: true, data: key };
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
return { success: false, error: String(error) };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async delete(key) {
|
|
86
|
+
try {
|
|
87
|
+
const [collection, id] = this.splitKey(key);
|
|
88
|
+
const response = await fetch(`${this.url}/collections/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`, {
|
|
89
|
+
method: 'DELETE',
|
|
90
|
+
headers: this.headers(),
|
|
91
|
+
});
|
|
92
|
+
return response.ok ? { success: true, data: key } : this.failure(response);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
return { success: false, error: String(error) };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
/** Acquire a reference through the server's causal peer fabric. */
|
|
99
|
+
async acquire(collection, id) {
|
|
100
|
+
const response = await fetch(`${this.url}/refs/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`, { headers: this.headers() });
|
|
101
|
+
if (response.status === 404)
|
|
102
|
+
return null;
|
|
103
|
+
if (!response.ok)
|
|
104
|
+
throw new Error((await this.failure(response)).error);
|
|
105
|
+
return (await response.json()).value;
|
|
106
|
+
}
|
|
107
|
+
/** Execute the built-in state-derived search capability remotely. */
|
|
108
|
+
async search(collection, query, limit = 50) {
|
|
109
|
+
const response = await fetch(`${this.url}/capabilities/search/${encodeURIComponent(collection)}`, {
|
|
110
|
+
method: 'POST', headers: this.headers(true), body: JSON.stringify({ query, limit }),
|
|
111
|
+
});
|
|
112
|
+
if (!response.ok)
|
|
113
|
+
throw new Error((await this.failure(response)).error);
|
|
114
|
+
return (await response.json()).map(record => record.value);
|
|
115
|
+
}
|
|
116
|
+
async provenance(collection, id) {
|
|
117
|
+
const response = await fetch(`${this.url}/provenance/${encodeURIComponent(collection)}/${encodeURIComponent(id)}`, { headers: this.headers() });
|
|
118
|
+
if (!response.ok)
|
|
119
|
+
throw new Error((await this.failure(response)).error);
|
|
120
|
+
return response.json();
|
|
121
|
+
}
|
|
122
|
+
async storeContent(content) {
|
|
123
|
+
const response = await fetch(`${this.url}/content`, { method: 'PUT', headers: this.headers(), body: content });
|
|
124
|
+
if (!response.ok)
|
|
125
|
+
throw new Error((await this.failure(response)).error);
|
|
126
|
+
return response.json();
|
|
127
|
+
}
|
|
128
|
+
async acquireContent(hash) {
|
|
129
|
+
const response = await fetch(`${this.url}/content/${encodeURIComponent(hash)}`, { headers: this.headers() });
|
|
130
|
+
if (response.status === 404)
|
|
131
|
+
return null;
|
|
132
|
+
if (!response.ok)
|
|
133
|
+
throw new Error((await this.failure(response)).error);
|
|
134
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
135
|
+
}
|
|
136
|
+
async command(path, body) {
|
|
137
|
+
const response = await fetch(`${this.url}${path}`, {
|
|
138
|
+
method: 'POST', headers: this.headers(true), body: JSON.stringify(body),
|
|
139
|
+
});
|
|
140
|
+
if (!response.ok)
|
|
141
|
+
throw new Error((await this.failure(response)).error);
|
|
142
|
+
return response.status === 204 ? undefined : response.json();
|
|
143
|
+
}
|
|
144
|
+
subscribe_changes(callback) {
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
void this.consumeEvents(controller, callback);
|
|
147
|
+
return () => controller.abort();
|
|
148
|
+
}
|
|
149
|
+
async consumeEvents(controller, callback) {
|
|
150
|
+
while (!controller.signal.aborted) {
|
|
151
|
+
try {
|
|
152
|
+
const response = await fetch(`${this.url}/events`, {
|
|
153
|
+
headers: this.headers(),
|
|
154
|
+
signal: controller.signal,
|
|
155
|
+
});
|
|
156
|
+
if (!response.ok || !response.body)
|
|
157
|
+
throw new Error(`Event stream failed: HTTP ${response.status}`);
|
|
158
|
+
const reader = response.body.getReader();
|
|
159
|
+
const decoder = new TextDecoder();
|
|
160
|
+
let buffer = '';
|
|
161
|
+
while (!controller.signal.aborted) {
|
|
162
|
+
const { done, value } = await reader.read();
|
|
163
|
+
if (done)
|
|
164
|
+
break;
|
|
165
|
+
buffer += decoder.decode(value, { stream: true }).replace(/\r\n/g, '\n');
|
|
166
|
+
const frames = buffer.split('\n\n');
|
|
167
|
+
buffer = frames.pop() ?? '';
|
|
168
|
+
for (const frame of frames) {
|
|
169
|
+
const data = frame.split('\n').filter(line => line.startsWith('data:')).map(line => line.slice(5).trim()).join('\n');
|
|
170
|
+
if (!data)
|
|
171
|
+
continue;
|
|
172
|
+
const event = JSON.parse(data);
|
|
173
|
+
if (event.change?.capability)
|
|
174
|
+
callback(event.change.capability);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
if (controller.signal.aborted)
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
async failure(response) {
|
|
186
|
+
let detail = `HTTP ${response.status}`;
|
|
187
|
+
try {
|
|
188
|
+
const body = await response.json();
|
|
189
|
+
if (body.error)
|
|
190
|
+
detail = body.error;
|
|
191
|
+
}
|
|
192
|
+
catch { /* response was not JSON */ }
|
|
193
|
+
return { success: false, error: detail };
|
|
194
|
+
}
|
|
195
|
+
get_capability_records(capability) { return this.query(capability); }
|
|
196
|
+
execute_op() { return { success: false, error: 'Raw operation execution is internal to the server fabric' }; }
|
|
197
|
+
sync_info() { return { success: false, error: 'Use the server runtime endpoint for diagnostics' }; }
|
|
198
|
+
add_peer() { return { success: false, error: 'Peer topology is server-managed' }; }
|
|
199
|
+
add_sync_peer() { return { success: false, error: 'Peer topology is server-managed' }; }
|
|
200
|
+
remove_sync_peer() { return { success: false, error: 'Peer topology is server-managed' }; }
|
|
201
|
+
get_pending_for_peer() { return { success: false, error: 'Operation transport is server-managed' }; }
|
|
202
|
+
acknowledge_peer_operations() { return { success: false, error: 'Operation transport is server-managed' }; }
|
|
203
|
+
instance_id() { return `remote:${this.url}`; }
|
|
204
|
+
get_sequence() { return 0; }
|
|
205
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @feltdb/core - FeltDB Application-Facing API
|
|
3
|
+
*
|
|
4
|
+
* The primary interface for developers using FeltDB.
|
|
5
|
+
* Provides a simple, state-first API for data persistence,
|
|
6
|
+
* reactivity, and distributed operations.
|
|
7
|
+
*/
|
|
8
|
+
export * from './db.js';
|
|
9
|
+
export * from './collection.js';
|
|
10
|
+
export * from './reactive-graph.js';
|
|
11
|
+
export * from './recovery.js';
|
|
12
|
+
export * from './execution.js';
|
|
13
|
+
export * from './workflow.js';
|
|
14
|
+
export * from './capability.js';
|
|
15
|
+
export * from './memory-db.js';
|
|
16
|
+
export * from './http-db.js';
|
|
17
|
+
export * from './indexeddb-db.js';
|
|
18
|
+
export * from './flowspec.js';
|
|
19
|
+
/**
|
|
20
|
+
* Agent Runtime APIs
|
|
21
|
+
*/
|
|
22
|
+
export * from './agent.js';
|
|
23
|
+
export * from './agent-observation.js';
|
|
24
|
+
export * from './agent-decision.js';
|
|
25
|
+
export * from './agent-memory.js';
|
|
26
|
+
export * from './agent-registry.js';
|
|
27
|
+
export * from './agent-runtime.js';
|
|
28
|
+
/**
|
|
29
|
+
* Observability and inspection APIs
|
|
30
|
+
*/
|
|
31
|
+
export type { ProvenanceGraph, ProvenanceNode, ProvenanceEdge, ProvenanceEdgeType, RuntimeDiagnostics, } from './db.js';
|
|
32
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +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,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,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAE9B;;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
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @feltdb/core - FeltDB Application-Facing API
|
|
3
|
+
*
|
|
4
|
+
* The primary interface for developers using FeltDB.
|
|
5
|
+
* Provides a simple, state-first API for data persistence,
|
|
6
|
+
* reactivity, and distributed operations.
|
|
7
|
+
*/
|
|
8
|
+
export * from './db.js';
|
|
9
|
+
export * from './collection.js';
|
|
10
|
+
export * from './reactive-graph.js';
|
|
11
|
+
export * from './recovery.js';
|
|
12
|
+
export * from './execution.js';
|
|
13
|
+
export * from './workflow.js';
|
|
14
|
+
export * from './capability.js';
|
|
15
|
+
export * from './memory-db.js';
|
|
16
|
+
export * from './http-db.js';
|
|
17
|
+
export * from './indexeddb-db.js';
|
|
18
|
+
export * from './flowspec.js';
|
|
19
|
+
/**
|
|
20
|
+
* Agent Runtime APIs
|
|
21
|
+
*/
|
|
22
|
+
export * from './agent.js';
|
|
23
|
+
export * from './agent-observation.js';
|
|
24
|
+
export * from './agent-decision.js';
|
|
25
|
+
export * from './agent-memory.js';
|
|
26
|
+
export * from './agent-registry.js';
|
|
27
|
+
export * from './agent-runtime.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { JsDb } from './feltdb.js';
|
|
2
|
+
interface JsResult {
|
|
3
|
+
success: boolean;
|
|
4
|
+
data?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
interface ChangeRecord {
|
|
8
|
+
sequence?: number;
|
|
9
|
+
collection: string;
|
|
10
|
+
key: string;
|
|
11
|
+
type: 'put' | 'delete';
|
|
12
|
+
timestamp: number;
|
|
13
|
+
id: string;
|
|
14
|
+
origin: string;
|
|
15
|
+
value?: unknown;
|
|
16
|
+
}
|
|
17
|
+
/** Durable browser runtime. Every mutation resolves only after its IndexedDB transaction commits. */
|
|
18
|
+
export declare class IndexedDbJsDb implements JsDb {
|
|
19
|
+
private readonly database;
|
|
20
|
+
private readonly channel?;
|
|
21
|
+
private sequence;
|
|
22
|
+
private readonly origin;
|
|
23
|
+
private readonly peers;
|
|
24
|
+
constructor(namespace: string);
|
|
25
|
+
private open;
|
|
26
|
+
private splitKey;
|
|
27
|
+
private mutate;
|
|
28
|
+
insert(key: string, value: string): Promise<JsResult>;
|
|
29
|
+
update(key: string, value: string): Promise<JsResult>;
|
|
30
|
+
delete(key: string): Promise<JsResult>;
|
|
31
|
+
get(key: string): Promise<JsResult>;
|
|
32
|
+
query(collection: string): Promise<JsResult>;
|
|
33
|
+
private request;
|
|
34
|
+
subscribe_changes(callback: (collection: string) => void): () => void;
|
|
35
|
+
close(): void;
|
|
36
|
+
get_capability_records(capability: string): Promise<JsResult>;
|
|
37
|
+
execute_op(): JsResult;
|
|
38
|
+
sync_info(): JsResult;
|
|
39
|
+
add_peer(): JsResult;
|
|
40
|
+
add_sync_peer(peerId: string): JsResult;
|
|
41
|
+
remove_sync_peer(peerId: string): JsResult;
|
|
42
|
+
get_pending_for_peer(): JsResult;
|
|
43
|
+
acknowledge_peer_operations(): JsResult;
|
|
44
|
+
instance_id(): string;
|
|
45
|
+
get_sequence(): number;
|
|
46
|
+
audit_events(): Promise<ChangeRecord[]>;
|
|
47
|
+
export_operations(sinceSequence: number): Promise<ChangeRecord[]>;
|
|
48
|
+
apply_remote_operations(operations: ChangeRecord[]): Promise<{
|
|
49
|
+
applied: number;
|
|
50
|
+
ignored: number;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
export {};
|
|
54
|
+
//# sourceMappingURL=indexeddb-db.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"indexeddb-db.d.ts","sourceRoot":"","sources":["../src/indexeddb-db.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,UAAU,QAAQ;IAAG,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE;AACtE,UAAU,YAAY;IAAG,QAAQ,CAAC,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;AAErK,qGAAqG;AACrG,qBAAa,aAAc,YAAW,IAAI;IACxC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAuB;IAChD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAmB;IAC5C,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;gBAE/B,SAAS,EAAE,MAAM;IAO7B,OAAO,CAAC,IAAI;IAcZ,OAAO,CAAC,QAAQ;YAMF,MAAM;IA2BpB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IACrD,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IACrD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAEhC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAKnC,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;YAapC,OAAO;IAKrB,iBAAiB,CAAC,QAAQ,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAuBrE,KAAK,IAAI,IAAI;IACb,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAC7D,UAAU,IAAI,QAAQ;IACtB,SAAS,IAAI,QAAQ;IACrB,QAAQ,IAAI,QAAQ;IACpB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IACvC,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,QAAQ;IAC1C,oBAAoB,IAAI,QAAQ;IAChC,2BAA2B,IAAI,QAAQ;IACvC,WAAW,IAAI,MAAM;IACrB,YAAY,IAAI,MAAM;IAChB,YAAY,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC;IACvC,iBAAiB,CAAC,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IACjE,uBAAuB,CAAC,UAAU,EAAE,YAAY,EAAE,GAAG,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAqBzG"}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/** Durable browser runtime. Every mutation resolves only after its IndexedDB transaction commits. */
|
|
2
|
+
export class IndexedDbJsDb {
|
|
3
|
+
constructor(namespace) {
|
|
4
|
+
this.sequence = 0;
|
|
5
|
+
this.peers = new Set();
|
|
6
|
+
if (typeof indexedDB === 'undefined')
|
|
7
|
+
throw new Error('IndexedDB is unavailable in this browser');
|
|
8
|
+
this.database = this.open(`feltdb:${namespace}`);
|
|
9
|
+
this.origin = `browser:${namespace}`;
|
|
10
|
+
if (typeof BroadcastChannel !== 'undefined')
|
|
11
|
+
this.channel = new BroadcastChannel(`feltdb:${namespace}:changes`);
|
|
12
|
+
}
|
|
13
|
+
open(name) {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
const request = indexedDB.open(name, 1);
|
|
16
|
+
request.onupgradeneeded = () => {
|
|
17
|
+
const db = request.result;
|
|
18
|
+
if (!db.objectStoreNames.contains('rows'))
|
|
19
|
+
db.createObjectStore('rows');
|
|
20
|
+
if (!db.objectStoreNames.contains('changes'))
|
|
21
|
+
db.createObjectStore('changes', { keyPath: 'sequence', autoIncrement: true });
|
|
22
|
+
};
|
|
23
|
+
request.onsuccess = () => resolve(request.result);
|
|
24
|
+
request.onerror = () => reject(request.error ?? new Error('open IndexedDB failed'));
|
|
25
|
+
request.onblocked = () => reject(new Error('IndexedDB upgrade blocked by another tab'));
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
splitKey(key) {
|
|
29
|
+
const separator = key.indexOf(':');
|
|
30
|
+
if (separator < 1)
|
|
31
|
+
throw new Error(`Invalid FeltDB key: ${key}`);
|
|
32
|
+
return key.slice(0, separator);
|
|
33
|
+
}
|
|
34
|
+
async mutate(key, value, type) {
|
|
35
|
+
try {
|
|
36
|
+
const db = await this.database;
|
|
37
|
+
const collection = this.splitKey(key);
|
|
38
|
+
await new Promise((resolve, reject) => {
|
|
39
|
+
let transaction;
|
|
40
|
+
try {
|
|
41
|
+
transaction = db.transaction(['rows', 'changes'], 'readwrite', { durability: 'strict' });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
transaction = db.transaction(['rows', 'changes'], 'readwrite');
|
|
45
|
+
}
|
|
46
|
+
if (type === 'delete')
|
|
47
|
+
transaction.objectStore('rows').delete(key);
|
|
48
|
+
else
|
|
49
|
+
transaction.objectStore('rows').put(value, key);
|
|
50
|
+
const changes = transaction.objectStore('changes');
|
|
51
|
+
const timestamp = Date.now();
|
|
52
|
+
const change = changes.add({ collection, key, type, value, timestamp, origin: this.origin, id: `${this.origin}:${timestamp}:${Math.random().toString(36).slice(2)}` });
|
|
53
|
+
change.onsuccess = () => {
|
|
54
|
+
const sequence = Number(change.result);
|
|
55
|
+
if (sequence > 10000)
|
|
56
|
+
changes.delete(IDBKeyRange.upperBound(sequence - 10000));
|
|
57
|
+
};
|
|
58
|
+
transaction.oncomplete = () => resolve();
|
|
59
|
+
transaction.onerror = () => reject(transaction.error ?? new Error('IndexedDB mutation failed'));
|
|
60
|
+
transaction.onabort = () => reject(transaction.error ?? new Error('IndexedDB mutation aborted'));
|
|
61
|
+
});
|
|
62
|
+
this.sequence += 1;
|
|
63
|
+
this.channel?.postMessage({ collection });
|
|
64
|
+
return { success: true, data: key };
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return { success: false, error: String(error) };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
insert(key, value) { return this.mutate(key, JSON.parse(value), 'put'); }
|
|
71
|
+
update(key, value) { return this.mutate(key, JSON.parse(value), 'put'); }
|
|
72
|
+
delete(key) { return this.mutate(key, undefined, 'delete'); }
|
|
73
|
+
async get(key) {
|
|
74
|
+
try {
|
|
75
|
+
const value = await this.request('rows', store => store.get(key));
|
|
76
|
+
return { success: true, data: value === undefined ? undefined : JSON.stringify(value) };
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
return { success: false, error: String(error) };
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async query(collection) {
|
|
83
|
+
try {
|
|
84
|
+
const db = await this.database;
|
|
85
|
+
const values = await new Promise((resolve, reject) => {
|
|
86
|
+
const request = db.transaction('rows').objectStore('rows').openCursor();
|
|
87
|
+
const output = [];
|
|
88
|
+
request.onsuccess = () => { const cursor = request.result; if (!cursor)
|
|
89
|
+
return resolve(output); if (String(cursor.key).startsWith(`${collection}:`))
|
|
90
|
+
output.push(cursor.value); cursor.continue(); };
|
|
91
|
+
request.onerror = () => reject(request.error);
|
|
92
|
+
});
|
|
93
|
+
return { success: true, data: JSON.stringify(values) };
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return { success: false, error: String(error) };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async request(storeName, create) {
|
|
100
|
+
const db = await this.database;
|
|
101
|
+
return new Promise((resolve, reject) => { const request = create(db.transaction(storeName).objectStore(storeName)); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); });
|
|
102
|
+
}
|
|
103
|
+
subscribe_changes(callback) {
|
|
104
|
+
let stopped = false;
|
|
105
|
+
let cursor = 0;
|
|
106
|
+
const receive = (event) => callback(event.data.collection);
|
|
107
|
+
this.channel?.addEventListener('message', receive);
|
|
108
|
+
const poll = async () => {
|
|
109
|
+
while (!stopped) {
|
|
110
|
+
try {
|
|
111
|
+
const db = await this.database;
|
|
112
|
+
await new Promise((resolve, reject) => {
|
|
113
|
+
const range = cursor ? IDBKeyRange.lowerBound(cursor, true) : undefined;
|
|
114
|
+
const request = db.transaction('changes').objectStore('changes').openCursor(range);
|
|
115
|
+
request.onsuccess = () => { const item = request.result; if (!item)
|
|
116
|
+
return resolve(); cursor = Number(item.key); callback(item.value.collection); item.continue(); };
|
|
117
|
+
request.onerror = () => reject(request.error);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
catch { /* a later poll retries durable events */ }
|
|
121
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
void poll();
|
|
125
|
+
return () => { stopped = true; this.channel?.removeEventListener('message', receive); };
|
|
126
|
+
}
|
|
127
|
+
close() { void this.database.then(db => db.close()); this.channel?.close(); }
|
|
128
|
+
get_capability_records(capability) { return this.query(capability); }
|
|
129
|
+
execute_op() { return { success: false, error: 'Raw operations are unavailable in IndexedDB' }; }
|
|
130
|
+
sync_info() { return { success: true, data: JSON.stringify({ instance_id: this.origin, sequence: this.sequence, connected_peers: [...this.peers], pending_operations: 0, operations_sent: 0, operations_received: 0, conflicts_detected: 0, last_sync_ms: 0, is_connected: this.peers.size > 0 }) }; }
|
|
131
|
+
add_peer() { return { success: false, error: 'Use a remote runtime for peer sync' }; }
|
|
132
|
+
add_sync_peer(peerId) { this.peers.add(peerId); return { success: true }; }
|
|
133
|
+
remove_sync_peer(peerId) { this.peers.delete(peerId); return { success: true }; }
|
|
134
|
+
get_pending_for_peer() { return { success: true, data: '[]' }; }
|
|
135
|
+
acknowledge_peer_operations() { return { success: true }; }
|
|
136
|
+
instance_id() { return this.origin; }
|
|
137
|
+
get_sequence() { return this.sequence; }
|
|
138
|
+
async audit_events() { return (await this.request('changes', store => store.getAll())).map((value, index) => ({ ...value, sequence: value.sequence ?? index + 1 })); }
|
|
139
|
+
async export_operations(sinceSequence) { return (await this.audit_events()).filter(value => (value.sequence ?? 0) > sinceSequence); }
|
|
140
|
+
async apply_remote_operations(operations) {
|
|
141
|
+
const db = await this.database;
|
|
142
|
+
let applied = 0, ignored = 0;
|
|
143
|
+
for (const operation of operations) {
|
|
144
|
+
const history = await this.audit_events();
|
|
145
|
+
if (history.some(event => event.id === operation.id)) {
|
|
146
|
+
ignored++;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const current = history.filter(event => event.key === operation.key).sort((a, b) => (b.sequence ?? 0) - (a.sequence ?? 0))[0];
|
|
150
|
+
const incomingOrder = `${String(operation.timestamp).padStart(16, '0')}:${operation.origin}:${operation.id}`;
|
|
151
|
+
const currentOrder = current ? `${String(current.timestamp).padStart(16, '0')}:${current.origin}:${current.id}` : '';
|
|
152
|
+
const accepted = incomingOrder >= currentOrder;
|
|
153
|
+
await new Promise((resolve, reject) => {
|
|
154
|
+
const transaction = db.transaction(['rows', 'changes'], 'readwrite');
|
|
155
|
+
if (accepted) {
|
|
156
|
+
if (operation.type === 'delete')
|
|
157
|
+
transaction.objectStore('rows').delete(operation.key);
|
|
158
|
+
else
|
|
159
|
+
transaction.objectStore('rows').put(operation.value, operation.key);
|
|
160
|
+
}
|
|
161
|
+
const { sequence: _sequence, ...portable } = operation;
|
|
162
|
+
transaction.objectStore('changes').add(portable);
|
|
163
|
+
transaction.oncomplete = () => resolve();
|
|
164
|
+
transaction.onerror = () => reject(transaction.error);
|
|
165
|
+
transaction.onabort = () => reject(transaction.error);
|
|
166
|
+
});
|
|
167
|
+
this.channel?.postMessage({ collection: operation.collection });
|
|
168
|
+
if (accepted)
|
|
169
|
+
applied++;
|
|
170
|
+
else
|
|
171
|
+
ignored++;
|
|
172
|
+
}
|
|
173
|
+
return { applied, ignored };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { JsDb } from './feltdb.js';
|
|
2
|
+
interface JsResult {
|
|
3
|
+
success: boolean;
|
|
4
|
+
data?: string;
|
|
5
|
+
error?: string;
|
|
6
|
+
}
|
|
7
|
+
interface PortableEvent {
|
|
8
|
+
sequence: number;
|
|
9
|
+
collection: string;
|
|
10
|
+
key: string;
|
|
11
|
+
type: 'put' | 'delete';
|
|
12
|
+
timestamp: number;
|
|
13
|
+
id: string;
|
|
14
|
+
origin: string;
|
|
15
|
+
value?: unknown;
|
|
16
|
+
}
|
|
17
|
+
/** In-process runtime used by the synchronous createFeltDB API. */
|
|
18
|
+
export declare class MemoryJsDb implements JsDb {
|
|
19
|
+
private readonly rows;
|
|
20
|
+
private readonly events;
|
|
21
|
+
private sequence;
|
|
22
|
+
private readonly origin;
|
|
23
|
+
private readonly peers;
|
|
24
|
+
constructor(namespace: string);
|
|
25
|
+
private event;
|
|
26
|
+
insert(key: string, value: string): JsResult;
|
|
27
|
+
update(key: string, value: string): JsResult;
|
|
28
|
+
get(key: string): JsResult;
|
|
29
|
+
delete(key: string): JsResult;
|
|
30
|
+
query(capability: string): JsResult;
|
|
31
|
+
get_capability_records(capability: string): JsResult;
|
|
32
|
+
execute_op(): JsResult;
|
|
33
|
+
sync_info(): JsResult;
|
|
34
|
+
add_peer(): JsResult;
|
|
35
|
+
add_sync_peer(peerId: string): JsResult;
|
|
36
|
+
remove_sync_peer(peerId: string): JsResult;
|
|
37
|
+
acknowledge_peer_operations(): JsResult;
|
|
38
|
+
get_pending_for_peer(): JsResult;
|
|
39
|
+
instance_id(): string;
|
|
40
|
+
get_sequence(): number;
|
|
41
|
+
audit_events(): PortableEvent[];
|
|
42
|
+
export_operations(sinceSequence: number): PortableEvent[];
|
|
43
|
+
apply_remote_operations(operations: PortableEvent[]): {
|
|
44
|
+
applied: number;
|
|
45
|
+
ignored: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
49
|
+
//# sourceMappingURL=memory-db.d.ts.map
|
|
@@ -0,0 +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,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"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const namespaces = new Map();
|
|
2
|
+
/** In-process runtime used by the synchronous createFeltDB API. */
|
|
3
|
+
export class MemoryJsDb {
|
|
4
|
+
constructor(namespace) {
|
|
5
|
+
this.events = [];
|
|
6
|
+
this.sequence = 0;
|
|
7
|
+
this.peers = new Set();
|
|
8
|
+
let rows = namespaces.get(namespace);
|
|
9
|
+
if (!rows) {
|
|
10
|
+
rows = new Map();
|
|
11
|
+
namespaces.set(namespace, rows);
|
|
12
|
+
}
|
|
13
|
+
this.rows = rows;
|
|
14
|
+
this.origin = `memory:${namespace}`;
|
|
15
|
+
}
|
|
16
|
+
event(collection, key, type, value) {
|
|
17
|
+
this.sequence += 1;
|
|
18
|
+
const timestamp = Date.now();
|
|
19
|
+
this.events.push({ sequence: this.sequence, collection, key, type, value, timestamp, origin: this.origin, id: `${this.origin}:${timestamp}:${this.sequence}` });
|
|
20
|
+
}
|
|
21
|
+
insert(key, value) {
|
|
22
|
+
try {
|
|
23
|
+
this.rows.set(key, JSON.parse(value));
|
|
24
|
+
this.event(key.split(':')[0], key, 'put', JSON.parse(value));
|
|
25
|
+
return { success: true, data: key };
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return { success: false, error: String(error) };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
update(key, value) {
|
|
32
|
+
if (!this.rows.has(key)) {
|
|
33
|
+
return { success: false, error: `Record ${key} not found` };
|
|
34
|
+
}
|
|
35
|
+
return this.insert(key, value);
|
|
36
|
+
}
|
|
37
|
+
get(key) {
|
|
38
|
+
const value = this.rows.get(key);
|
|
39
|
+
return value === undefined
|
|
40
|
+
? { success: true }
|
|
41
|
+
: { success: true, data: JSON.stringify(value) };
|
|
42
|
+
}
|
|
43
|
+
delete(key) {
|
|
44
|
+
this.rows.delete(key);
|
|
45
|
+
this.event(key.split(':')[0], key, 'delete');
|
|
46
|
+
return { success: true, data: key };
|
|
47
|
+
}
|
|
48
|
+
query(capability) {
|
|
49
|
+
const prefix = `${capability}:`;
|
|
50
|
+
const values = [...this.rows]
|
|
51
|
+
.filter(([key]) => key.startsWith(prefix))
|
|
52
|
+
.map(([, value]) => value);
|
|
53
|
+
return { success: true, data: JSON.stringify(values) };
|
|
54
|
+
}
|
|
55
|
+
get_capability_records(capability) {
|
|
56
|
+
return this.query(capability);
|
|
57
|
+
}
|
|
58
|
+
execute_op() {
|
|
59
|
+
return { success: false, error: 'Raw operations are unavailable in the memory runtime' };
|
|
60
|
+
}
|
|
61
|
+
sync_info() {
|
|
62
|
+
return { success: true, data: JSON.stringify({ instance_id: this.origin, sequence: this.sequence, connected_peers: [...this.peers], pending_operations: 0, operations_sent: 0, operations_received: 0, conflicts_detected: 0, last_sync_ms: 0, is_connected: this.peers.size > 0 }) };
|
|
63
|
+
}
|
|
64
|
+
add_peer() { return { success: true }; }
|
|
65
|
+
add_sync_peer(peerId) { this.peers.add(peerId); return { success: true }; }
|
|
66
|
+
remove_sync_peer(peerId) { this.peers.delete(peerId); return { success: true }; }
|
|
67
|
+
acknowledge_peer_operations() { return { success: true }; }
|
|
68
|
+
get_pending_for_peer() { return { success: true, data: '[]' }; }
|
|
69
|
+
instance_id() { return this.origin; }
|
|
70
|
+
get_sequence() { return this.sequence; }
|
|
71
|
+
audit_events() { return [...this.events]; }
|
|
72
|
+
export_operations(sinceSequence) { return this.events.filter(event => event.sequence > sinceSequence); }
|
|
73
|
+
apply_remote_operations(operations) {
|
|
74
|
+
let applied = 0, ignored = 0;
|
|
75
|
+
for (const operation of operations) {
|
|
76
|
+
if (this.events.some(event => event.id === operation.id)) {
|
|
77
|
+
ignored++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const current = [...this.events].reverse().find(event => event.key === operation.key);
|
|
81
|
+
const incomingOrder = `${String(operation.timestamp).padStart(16, '0')}:${operation.origin}:${operation.id}`;
|
|
82
|
+
const currentOrder = current ? `${String(current.timestamp).padStart(16, '0')}:${current.origin}:${current.id}` : '';
|
|
83
|
+
this.sequence += 1;
|
|
84
|
+
this.events.push({ ...operation, sequence: this.sequence });
|
|
85
|
+
if (incomingOrder >= currentOrder) {
|
|
86
|
+
if (operation.type === 'delete')
|
|
87
|
+
this.rows.delete(operation.key);
|
|
88
|
+
else
|
|
89
|
+
this.rows.set(operation.key, operation.value);
|
|
90
|
+
applied++;
|
|
91
|
+
}
|
|
92
|
+
else
|
|
93
|
+
ignored++;
|
|
94
|
+
}
|
|
95
|
+
return { applied, ignored };
|
|
96
|
+
}
|
|
97
|
+
}
|