@feltdb/core 0.12.2 → 0.15.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/dist/agent-runtime.d.ts +64 -2
- package/dist/agent-runtime.d.ts.map +1 -1
- package/dist/agent-runtime.js +121 -11
- package/dist/agent-state.d.ts +48 -0
- package/dist/agent-state.d.ts.map +1 -1
- package/dist/agent-state.js +174 -12
- package/dist/agent.d.ts +10 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -1
- package/dist/build-identity.json +4 -4
- package/dist/cli/commands.js +34 -7
- package/dist/cli/workspace-integration.js +8 -0
- package/dist/collection.d.ts +3 -1
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +30 -8
- package/dist/create/build-identity.json +4 -4
- package/dist/create/cli.js +47 -38
- package/dist/create/create.js +21 -8
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +6 -6
- package/dist/create/server-source/Cargo.toml +1 -1
- package/dist/create/server-source/crates/feltdb/src/lib.rs +126 -35
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +162 -53
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +341 -2
- package/dist/create/server-source/crates/feltdb/src/workload.rs +2094 -126
- package/dist/create/server-source/crates/feltdb/tests/production_readiness_contract.rs +18 -12
- package/dist/create/server-source/crates/feltdb/tests/workload_envelope_contract.rs +17 -5
- package/dist/create/server-source/crates/feltdb-client/Cargo.toml +1 -1
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +1 -1
- package/dist/create/server-source/crates/feltdb-server/README.md +6 -1
- package/dist/create/server-source/crates/feltdb-server/src/agent_execution.rs +511 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +9 -1
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +113 -25
- package/dist/create/server-source/crates/feltdb-server/src/backup.rs +4 -0
- package/dist/create/server-source/crates/feltdb-server/src/console_accounts.rs +1009 -0
- package/dist/create/server-source/crates/feltdb-server/src/control_authorization.rs +441 -0
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +68 -6
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +355 -23
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +4 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +1740 -236
- package/dist/create/server-source/crates/feltdb-server/src/redaction.rs +208 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +198 -19
- package/dist/create/server-source/crates/feltdb-server/tests/transaction_precondition_contract.rs +403 -0
- package/dist/create/setup-summary.js +40 -0
- package/dist/db.d.ts +91 -10
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +170 -31
- package/dist/deployment.d.ts +19 -0
- package/dist/deployment.d.ts.map +1 -1
- package/dist/deployment.js +38 -2
- package/dist/development-runtime-bridge.d.ts +20 -0
- package/dist/development-runtime-bridge.d.ts.map +1 -1
- package/dist/development-runtime-bridge.js +61 -12
- package/dist/embedded-query.d.ts.map +1 -1
- package/dist/embedded-query.js +5 -4
- package/dist/embedded-transaction.d.ts +8 -4
- package/dist/embedded-transaction.d.ts.map +1 -1
- package/dist/embedded-transaction.js +55 -19
- package/dist/error-codes.d.ts +31 -2
- package/dist/error-codes.d.ts.map +1 -1
- package/dist/error-codes.js +41 -2
- package/dist/feltdb.d.ts +7 -0
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts +27 -0
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +145 -7
- package/dist/http-client.d.ts +11 -3
- package/dist/http-client.d.ts.map +1 -1
- package/dist/http-client.js +27 -14
- package/dist/http-db.d.ts +40 -1
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +285 -58
- package/dist/index.js +1 -1
- package/dist/local-state-lock.d.ts +34 -0
- package/dist/local-state-lock.d.ts.map +1 -0
- package/dist/local-state-lock.js +225 -0
- package/dist/reactive-graph.d.ts +0 -3
- package/dist/reactive-graph.d.ts.map +1 -1
- package/dist/reactive-graph.js +11 -1
- package/dist/runtime-registry.d.ts +7 -2
- package/dist/runtime-registry.d.ts.map +1 -1
- package/dist/runtime-registry.js +2 -2
- package/dist/studio/{KeyManagementPanel-D5FMvvU9.js → KeyManagementPanel-skUNnKRw.js} +36 -29
- package/dist/studio/app.d.ts +8 -1
- package/dist/studio/app.d.ts.map +1 -1
- package/dist/studio/components/KeyManagementPanel.d.ts.map +1 -1
- package/dist/studio/components/KeyManagementPanel.js +1 -1
- package/dist/studio/components/index.js +2 -2
- package/dist/studio/{components-B4eKIUN5.js → components-CJmzntBi.js} +8 -7
- package/dist/studio/control-plane/Data.d.ts.map +1 -1
- package/dist/studio/control-plane/Instance.d.ts +4 -1
- package/dist/studio/control-plane/Instance.d.ts.map +1 -1
- package/dist/studio/control-plane/Operations.d.ts +4 -1
- package/dist/studio/control-plane/Operations.d.ts.map +1 -1
- package/dist/studio/control-plane/index.js +2 -2
- package/dist/studio/control-plane/primitives.d.ts +16 -0
- package/dist/studio/control-plane/primitives.d.ts.map +1 -1
- package/dist/studio/{control-plane-fxMcJn74.js → control-plane-CaBA7Rgv.js} +448 -424
- package/dist/studio/index.d.ts +1 -0
- package/dist/studio/index.d.ts.map +1 -1
- package/dist/studio/index.js +384 -247
- package/dist/studio/navigation.d.ts +57 -0
- package/dist/studio/navigation.d.ts.map +1 -0
- package/dist/studio/studio.css +1 -1
- package/dist/studio/utils/control-plane-api.d.ts +2 -0
- package/dist/studio/utils/control-plane-api.d.ts.map +1 -1
- package/dist/studio/utils/control-plane-api.js +5 -1
- package/dist/studio-app/assets/{feltdb_wasm-BatwwogY.js → feltdb_wasm-BjM1ylL3.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-TseJn2Ax.wasm +0 -0
- package/dist/studio-app/assets/index-1wd0uKua.js +30 -0
- package/dist/studio-app/assets/{index-CiC8VY-a.css → index-vi21oEQa.css} +1 -1
- package/dist/studio-app/index.html +2 -2
- package/dist/transaction.d.ts +27 -11
- package/dist/transaction.d.ts.map +1 -1
- package/dist/transaction.js +20 -14
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/wasm/package.json +1 -1
- package/dist/workload.d.ts +31 -3
- package/dist/workload.d.ts.map +1 -1
- package/dist/workload.js +28 -3
- package/dist/workspace/local-development-authority.d.ts +19 -0
- package/dist/workspace/local-development-authority.d.ts.map +1 -1
- package/dist/workspace/local-development-authority.js +335 -15
- package/package.json +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-BX0newOr.wasm +0 -0
- package/dist/studio-app/assets/index-DoVvbIay.js +0 -30
package/dist/collection.js
CHANGED
|
@@ -129,7 +129,9 @@ export class Collection {
|
|
|
129
129
|
* Returns cached results (live-updated).
|
|
130
130
|
*/
|
|
131
131
|
async all() {
|
|
132
|
-
|
|
132
|
+
// A read the authority refused, or could not answer, is raised typed --
|
|
133
|
+
// never answered from whatever this handle last cached.
|
|
134
|
+
await this.refresh({ strict: true });
|
|
133
135
|
return [...this.cache];
|
|
134
136
|
}
|
|
135
137
|
/** List records through the canonical application collection API. */
|
|
@@ -375,7 +377,7 @@ export class Collection {
|
|
|
375
377
|
const key = `${this.name}:${id}`;
|
|
376
378
|
const result = await this.db.get(key);
|
|
377
379
|
if (!result.success) {
|
|
378
|
-
throw
|
|
380
|
+
throw runtimeFailure(result, result.error || `Failed to read record ${id}`);
|
|
379
381
|
}
|
|
380
382
|
if (result.data) {
|
|
381
383
|
return JSON.parse(result.data);
|
|
@@ -421,7 +423,7 @@ export class Collection {
|
|
|
421
423
|
const stored = { ...data, id: recordId, __version: 1 };
|
|
422
424
|
const result = await this.db.insert(key, JSON.stringify(stored));
|
|
423
425
|
if (!result.success) {
|
|
424
|
-
throw
|
|
426
|
+
throw runtimeFailure(result, `Insert failed: ${result.error}`);
|
|
425
427
|
}
|
|
426
428
|
await this.publish('insert', key, String(recordId), null, stored);
|
|
427
429
|
return String(recordId);
|
|
@@ -614,9 +616,11 @@ export class Collection {
|
|
|
614
616
|
const result = await this.db.delete(key);
|
|
615
617
|
if (!result.success) {
|
|
616
618
|
const message = `Delete failed: ${result.error}`;
|
|
617
|
-
throw
|
|
618
|
-
?
|
|
619
|
-
:
|
|
619
|
+
throw result.cause
|
|
620
|
+
? runtimeFailure(result, message)
|
|
621
|
+
: /not found/i.test(String(result.error))
|
|
622
|
+
? new FeltDBServiceError(message, FeltDBErrorCode.NOT_FOUND)
|
|
623
|
+
: new Error(message);
|
|
620
624
|
}
|
|
621
625
|
// Update indexes - find and remove the deleted record
|
|
622
626
|
const deleted = await this.get(id);
|
|
@@ -686,7 +690,7 @@ export class Collection {
|
|
|
686
690
|
throw error;
|
|
687
691
|
const existing = await this.db.get(key);
|
|
688
692
|
if (!existing.success) {
|
|
689
|
-
throw
|
|
693
|
+
throw runtimeFailure(existing, existing.error || `could not read the record that won ${key}`);
|
|
690
694
|
}
|
|
691
695
|
if (!existing.data) {
|
|
692
696
|
throw new Error(`${key} was created and then removed while resolving a conditional create`);
|
|
@@ -914,7 +918,7 @@ export class Collection {
|
|
|
914
918
|
/**
|
|
915
919
|
* Refresh data from the database and notify subscribers.
|
|
916
920
|
*/
|
|
917
|
-
async refresh() {
|
|
921
|
+
async refresh(options = {}) {
|
|
918
922
|
try {
|
|
919
923
|
// The revision is read *before* the query, so it can only ever be older
|
|
920
924
|
// than the data it ends up labelling. The reverse ordering would let a
|
|
@@ -980,14 +984,32 @@ export class Collection {
|
|
|
980
984
|
}
|
|
981
985
|
}
|
|
982
986
|
else if (!result.success) {
|
|
987
|
+
if (options.strict)
|
|
988
|
+
throw runtimeFailure(result, result.error || `Failed to read ${this.name}`);
|
|
983
989
|
console.error('Query failed:', result.error);
|
|
984
990
|
}
|
|
985
991
|
}
|
|
986
992
|
catch (err) {
|
|
993
|
+
if (options.strict)
|
|
994
|
+
throw err;
|
|
987
995
|
console.error('Refresh error:', err);
|
|
988
996
|
}
|
|
989
997
|
}
|
|
990
998
|
}
|
|
999
|
+
/**
|
|
1000
|
+
* The error a failed runtime result is raised as. A remote runtime attaches
|
|
1001
|
+
* the authority's typed refusal (`cause`): its stable code, status, request id
|
|
1002
|
+
* and body travel on, so an unauthenticated, cross-application or interrupted
|
|
1003
|
+
* operation is never a bare `Error` with the code only in its message.
|
|
1004
|
+
*/
|
|
1005
|
+
function runtimeFailure(result, message) {
|
|
1006
|
+
const cause = result.cause;
|
|
1007
|
+
if (!(cause instanceof FeltDBServiceError))
|
|
1008
|
+
return new Error(message);
|
|
1009
|
+
const error = new FeltDBServiceError(message, cause.code, cause.requestId, cause.status, cause.body);
|
|
1010
|
+
error.cause = cause;
|
|
1011
|
+
return error;
|
|
1012
|
+
}
|
|
991
1013
|
/**
|
|
992
1014
|
* Relationship helper for loading related data.
|
|
993
1015
|
*/
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "feltdb.build-identity.v2",
|
|
3
3
|
"package": "create-feltdb",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.15.0",
|
|
5
5
|
"git": {
|
|
6
|
-
"sha1": "
|
|
6
|
+
"sha1": "5ad54972a9e31a5459cadbb471428111624ec9c6",
|
|
7
7
|
"dirty": false
|
|
8
8
|
},
|
|
9
9
|
"path_in_vcs": "tools/create-feltdb",
|
|
10
|
-
"source_digest": "sha256:
|
|
10
|
+
"source_digest": "sha256:01efc5737aae1fef37c976e5841df3bb9b615d034f31d27b038f7fd4ca202428",
|
|
11
11
|
"inputs": [
|
|
12
12
|
"Cargo.lock",
|
|
13
13
|
"Cargo.toml",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"tools/cli",
|
|
21
21
|
"tools/create-feltdb"
|
|
22
22
|
],
|
|
23
|
-
"files":
|
|
23
|
+
"files": 789,
|
|
24
24
|
"build": {
|
|
25
25
|
"command": "npm run build",
|
|
26
26
|
"node": "v24.9.0"
|
package/dist/create/cli.js
CHANGED
|
@@ -11,15 +11,18 @@ import { spawn, spawnSync } from 'child_process';
|
|
|
11
11
|
import { createProject } from './create.js';
|
|
12
12
|
import { FELTDB_PACKAGE_VERSION } from './package-versions.js';
|
|
13
13
|
import { configureManagedAccount, finalizeManagedProvisioning } from './managed-account.js';
|
|
14
|
+
import { RUNTIME_CHOICES, agentsIncluded, describeSetup } from './setup-summary.js';
|
|
14
15
|
import { localFeltdbExecutable } from './development-handoff.js';
|
|
15
16
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
17
|
function parseArgs(args) {
|
|
17
18
|
const options = {
|
|
18
19
|
runtime: 'browser',
|
|
19
20
|
framework: 'react',
|
|
20
|
-
|
|
21
|
+
// Distribution and the browser AI builder add services and downloads a
|
|
22
|
+
// first project does not need; each is one flag away.
|
|
23
|
+
distributed: false,
|
|
21
24
|
agents: true,
|
|
22
|
-
webllm:
|
|
25
|
+
webllm: false,
|
|
23
26
|
capabilities: 'search',
|
|
24
27
|
};
|
|
25
28
|
for (let i = 0; i < args.length; i++) {
|
|
@@ -42,6 +45,9 @@ function parseArgs(args) {
|
|
|
42
45
|
case '--framework':
|
|
43
46
|
options.framework = args[++i];
|
|
44
47
|
break;
|
|
48
|
+
case '--distributed':
|
|
49
|
+
options.distributed = true;
|
|
50
|
+
break;
|
|
45
51
|
case '--no-distributed':
|
|
46
52
|
options.distributed = false;
|
|
47
53
|
break;
|
|
@@ -164,29 +170,28 @@ async function promptForOptions(defaults) {
|
|
|
164
170
|
{ label: 'Yes', value: true },
|
|
165
171
|
{ label: 'No', value: false },
|
|
166
172
|
];
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
distributed: await select('Enable distributed operation?', yesNo, defaults.distributed),
|
|
179
|
-
agents: await select('Include an autonomous agent example?', yesNo, defaults.agents),
|
|
180
|
-
webllm: await select('Install WebLLM for private natural-language app building?', [
|
|
181
|
-
{ label: 'Yes', value: true, description: 'build FeltDB schemas, capabilities, workflows, and UI locally with AI' },
|
|
182
|
-
{ label: 'No', value: false, description: 'skip the browser model and AI builder' },
|
|
183
|
-
], defaults.webllm),
|
|
184
|
-
capabilities: await select('Choose starter capabilities:', [
|
|
173
|
+
const runtime = await select('Where does your application\'s data live?', RUNTIME_CHOICES.map(choice => ({ label: choice.label, value: choice.value, description: choice.summary })), defaults.runtime);
|
|
174
|
+
const framework = await select('Choose an application framework:', [
|
|
175
|
+
{ label: 'React', value: 'react', description: 'a complete starter: sign-up, projects, invitations, activity, assistants' },
|
|
176
|
+
{ label: 'Vanilla TypeScript', value: 'vanilla', description: 'a minimal page over the same database' },
|
|
177
|
+
], defaults.framework);
|
|
178
|
+
// The React starter's assistants are part of its tour and need no model, so
|
|
179
|
+
// only the vanilla template asks; its capabilities are chosen here too.
|
|
180
|
+
const agents = framework === 'react' ? true
|
|
181
|
+
: await select('Include an agent example?', yesNo, defaults.agents);
|
|
182
|
+
const capabilities = framework === 'react' ? defaults.capabilities
|
|
183
|
+
: await select('Choose starter capabilities:', [
|
|
185
184
|
{ label: 'Search', value: 'search' },
|
|
186
185
|
{ label: 'Vector search', value: 'vector' },
|
|
187
186
|
{ label: 'Search + vector search', value: 'search,vector' },
|
|
188
|
-
], defaults.capabilities)
|
|
189
|
-
|
|
187
|
+
], defaults.capabilities);
|
|
188
|
+
const webllm = await select('Add the private AI app builder (WebLLM)?', [
|
|
189
|
+
{ label: 'No', value: false, description: 'add it later with npm install @feltdb/webllm' },
|
|
190
|
+
{ label: 'Yes', value: true, description: 'builds schemas and UI with a model that runs in your browser: a download of a few hundred MB on first use, and WebGPU' },
|
|
191
|
+
], defaults.webllm);
|
|
192
|
+
// Distribution is a deployment decision, not a first-run one: `--distributed`
|
|
193
|
+
// records it, and development always runs one local authority.
|
|
194
|
+
return { runtime, framework, distributed: defaults.distributed, agents, webllm, capabilities };
|
|
190
195
|
}
|
|
191
196
|
async function main() {
|
|
192
197
|
const args = process.argv.slice(2);
|
|
@@ -211,9 +216,9 @@ Deployment Target (Explicit):
|
|
|
211
216
|
Best for: Client-side apps, offline-first experiences
|
|
212
217
|
No server required, full offline capability
|
|
213
218
|
|
|
214
|
-
--server
|
|
215
|
-
Best for:
|
|
216
|
-
|
|
219
|
+
--server Data lives in a FeltDB authority the application connects to
|
|
220
|
+
Best for: shared state, server-side work, a path to self-hosting
|
|
221
|
+
\`feltdb dev\` runs a local authority; deploy to feltdb-server
|
|
217
222
|
|
|
218
223
|
--self-host Dedicated FeltDB server with Docker Compose
|
|
219
224
|
Best for: Staging first, then explicit production promotion
|
|
@@ -221,7 +226,7 @@ Deployment Target (Explicit):
|
|
|
221
226
|
|
|
222
227
|
--managed Managed FeltDB runtime
|
|
223
228
|
Best for: Production without operating servers
|
|
224
|
-
Requires VITE_FELTDB_URL
|
|
229
|
+
Requires VITE_FELTDB_URL; server-side FELTDB_TOKEN only
|
|
225
230
|
|
|
226
231
|
Options:
|
|
227
232
|
--browser Explicit: Browser runtime with IndexedDB
|
|
@@ -230,11 +235,15 @@ Options:
|
|
|
230
235
|
--managed Explicit: Managed FeltDB service
|
|
231
236
|
--runtime <runtime> Alternative: Set runtime (browser, node, self-hosted, managed)
|
|
232
237
|
--framework <framework> Choose framework: react or vanilla (default: react)
|
|
233
|
-
--
|
|
234
|
-
|
|
235
|
-
--
|
|
236
|
-
--no-
|
|
237
|
-
|
|
238
|
+
--distributed Record that the application will be deployed to several
|
|
239
|
+
feltdb-server nodes (development always runs one)
|
|
240
|
+
--no-distributed Do not record distributed deployment (default)
|
|
241
|
+
--no-agents Exclude the agent example (vanilla template; the React
|
|
242
|
+
starter always includes its assistants)
|
|
243
|
+
--webllm Include the private natural-language app builder; downloads
|
|
244
|
+
a model into the browser on first use and needs WebGPU
|
|
245
|
+
--no-webllm Do not install WebLLM (default)
|
|
246
|
+
--capabilities <list> Vanilla template capabilities: search, vector, or search,vector
|
|
238
247
|
--no-install Skip npm install
|
|
239
248
|
--no-start Skip automatic feltdb dev startup
|
|
240
249
|
-y, --yes Use all defaults (non-interactive mode)
|
|
@@ -294,13 +303,13 @@ Learn more: https://github.com/rkendel1/feltdb`);
|
|
|
294
303
|
console.log('\n═══════════════════════════════════════════════');
|
|
295
304
|
console.log('📦 DEPLOYMENT TARGET');
|
|
296
305
|
console.log('═══════════════════════════════════════════════');
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
console.log(
|
|
302
|
-
|
|
303
|
-
|
|
306
|
+
if (options.framework === 'react' && !options.agents) {
|
|
307
|
+
console.log('\nNote: the React starter always includes its assistants; --no-agents applies to the vanilla template.');
|
|
308
|
+
}
|
|
309
|
+
options.agents = agentsIncluded(options);
|
|
310
|
+
console.log('');
|
|
311
|
+
for (const line of describeSetup(options))
|
|
312
|
+
console.log(line);
|
|
304
313
|
console.log('\n═══════════════════════════════════════════════\n');
|
|
305
314
|
const projectDir = path.resolve(process.cwd(), projectName);
|
|
306
315
|
let projectCreated = false;
|
package/dist/create/create.js
CHANGED
|
@@ -196,12 +196,25 @@ ${hasAgents ? ` agent WorkspaceAssistant {
|
|
|
196
196
|
tsconfig.compilerOptions.types = ['vite/client'];
|
|
197
197
|
}
|
|
198
198
|
fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
|
|
199
|
-
// Browsers do not expose VITE_* through process.env, so
|
|
199
|
+
// Browsers do not expose VITE_* through process.env, so the database must be handed its
|
|
200
200
|
// namespace explicitly; otherwise it opens the shared `default` store and `feltdb dev` cannot
|
|
201
201
|
// recognise the application. A managed (remote) runtime resolves its target from its URL.
|
|
202
|
+
//
|
|
203
|
+
// The generated application runs in the browser whatever the deployment target. For a
|
|
204
|
+
// browser target its state is the browser's own (IndexedDB). For a Node.js or self-hosted
|
|
205
|
+
// target the state lives in a FeltDB authority, so the browser application connects to it:
|
|
206
|
+
// the URL the developer configured, else the authority `feltdb dev` started. Opening a
|
|
207
|
+
// browser store there instead would leave the application writing data Studio and the
|
|
208
|
+
// authority never see.
|
|
209
|
+
const namespaceExpression = `import.meta.env.VITE_FELTDB_NAMESPACE || ${JSON.stringify(applicationName)}`;
|
|
202
210
|
const createDatabaseCall = runtime === 'managed'
|
|
203
211
|
? 'createFeltDB()'
|
|
204
|
-
:
|
|
212
|
+
: runtime === 'browser'
|
|
213
|
+
? `createFeltDB({ namespace: ${namespaceExpression} })`
|
|
214
|
+
: `createFeltDB({
|
|
215
|
+
namespace: ${namespaceExpression},
|
|
216
|
+
server: { url: import.meta.env.VITE_FELTDB_URL || import.meta.env.VITE_FELTDB_AUTHORITY_URL || 'http://127.0.0.1:7700' },
|
|
217
|
+
})`;
|
|
205
218
|
// Create main application files
|
|
206
219
|
const feltdbTs = `import { configureDevelopmentRuntimeBridge, createFeltDB } from '@feltdb/core';
|
|
207
220
|
|
|
@@ -1222,6 +1235,8 @@ main().catch(console.error);
|
|
|
1222
1235
|
<head>
|
|
1223
1236
|
<meta charset="UTF-8">
|
|
1224
1237
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1238
|
+
<!-- An inline icon: without one the browser requests /favicon.ico and logs a 404. -->
|
|
1239
|
+
<link rel="icon" href="data:,">
|
|
1225
1240
|
<title>${applicationName}</title>
|
|
1226
1241
|
</head>
|
|
1227
1242
|
<body>
|
|
@@ -1244,10 +1259,9 @@ main().catch(console.error);
|
|
|
1244
1259
|
# FeltDB Server URL (self-hosted or managed runtime)
|
|
1245
1260
|
VITE_FELTDB_URL=http://localhost:7700
|
|
1246
1261
|
|
|
1247
|
-
#
|
|
1248
|
-
|
|
1249
|
-
#
|
|
1250
|
-
VITE_FELTDB_API_KEY=
|
|
1262
|
+
# Browser code authenticates users with db.auth.signIn() (end-user sessions).
|
|
1263
|
+
# Never put a FeltDB service key (fdb_live_…) in a VITE_ variable: Vite
|
|
1264
|
+
# inlines every VITE_* value into the browser bundle.
|
|
1251
1265
|
|
|
1252
1266
|
# Managed example: https://runtime.your-app.feltdb.com
|
|
1253
1267
|
VITE_FELTDB_MANAGED_URL=
|
|
@@ -1493,7 +1507,7 @@ npm run feltdb:import
|
|
|
1493
1507
|
|
|
1494
1508
|
### Self-Hosted
|
|
1495
1509
|
- \`VITE_FELTDB_URL\`: Docker container URL (default: http://localhost:7700)
|
|
1496
|
-
- \`
|
|
1510
|
+
- \`FELTDB_TOKEN\`: API key for server-side code only (never a \`VITE_\` variable; browsers use \`db.auth.signIn()\`)
|
|
1497
1511
|
- \`FELTDB_IMAGE\`: Docker image to use (optional override)
|
|
1498
1512
|
- \`DOCKER_NETWORK\`: Custom Docker network (optional)
|
|
1499
1513
|
|
|
@@ -1686,7 +1700,6 @@ Configuration is in \`feltdb.config.json\`:
|
|
|
1686
1700
|
Create a \`.env.local\` file (copy from \`.env.example\`):
|
|
1687
1701
|
|
|
1688
1702
|
\`\`\`
|
|
1689
|
-
VITE_FELTDB_API_KEY=your_api_key_here
|
|
1690
1703
|
VITE_FELTDB_URL=http://localhost:7700
|
|
1691
1704
|
VITE_FELTDB_MANAGED_URL=https://api.feltdb.com
|
|
1692
1705
|
VITE_FELTDB_MANAGED_APPLICATION_ID=your_application_id
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// One release train keeps generated applications installable. The repository
|
|
2
2
|
// validation script checks these values against every workspace manifest.
|
|
3
|
-
export const FELTDB_PACKAGE_VERSION = '0.
|
|
3
|
+
export const FELTDB_PACKAGE_VERSION = '0.15.0';
|
|
4
4
|
export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
|
|
@@ -219,9 +219,9 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
|
|
219
219
|
|
|
220
220
|
[[package]]
|
|
221
221
|
name = "chacha20"
|
|
222
|
-
version = "0.10.
|
|
222
|
+
version = "0.10.2"
|
|
223
223
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
224
|
-
checksum = "
|
|
224
|
+
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
|
|
225
225
|
dependencies = [
|
|
226
226
|
"cfg-if",
|
|
227
227
|
"cpufeatures 0.3.0",
|
|
@@ -494,7 +494,7 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
|
|
494
494
|
|
|
495
495
|
[[package]]
|
|
496
496
|
name = "feltdb"
|
|
497
|
-
version = "0.
|
|
497
|
+
version = "0.3.0"
|
|
498
498
|
dependencies = [
|
|
499
499
|
"async-stream",
|
|
500
500
|
"async-trait",
|
|
@@ -513,7 +513,7 @@ dependencies = [
|
|
|
513
513
|
|
|
514
514
|
[[package]]
|
|
515
515
|
name = "feltdb-client"
|
|
516
|
-
version = "0.
|
|
516
|
+
version = "0.3.0"
|
|
517
517
|
dependencies = [
|
|
518
518
|
"feltdb",
|
|
519
519
|
"reqwest",
|
|
@@ -526,7 +526,7 @@ dependencies = [
|
|
|
526
526
|
|
|
527
527
|
[[package]]
|
|
528
528
|
name = "feltdb-server"
|
|
529
|
-
version = "0.
|
|
529
|
+
version = "0.3.0"
|
|
530
530
|
dependencies = [
|
|
531
531
|
"argon2",
|
|
532
532
|
"async-stream",
|
|
@@ -552,7 +552,7 @@ dependencies = [
|
|
|
552
552
|
|
|
553
553
|
[[package]]
|
|
554
554
|
name = "feltdb-wasm"
|
|
555
|
-
version = "0.
|
|
555
|
+
version = "0.3.0"
|
|
556
556
|
dependencies = [
|
|
557
557
|
"feltdb",
|
|
558
558
|
"getrandom 0.2.17",
|
|
@@ -1133,6 +1133,48 @@ impl PreconditionFailure {
|
|
|
1133
1133
|
}
|
|
1134
1134
|
}
|
|
1135
1135
|
|
|
1136
|
+
impl PreconditionFailure {
|
|
1137
|
+
/// The refusal as every transport serializes it: `predicate`, the
|
|
1138
|
+
/// application's `collection` (never a storage namespace), the record `id`,
|
|
1139
|
+
/// its `key` as `collection:id`, and `expected` / `actual` where the
|
|
1140
|
+
/// predicate has them. A fence names the first fenced collection and
|
|
1141
|
+
/// `key: "read-basis"`, with no `id` and no tokens.
|
|
1142
|
+
///
|
|
1143
|
+
/// `layout` is where the authority addressed the record: the direct route
|
|
1144
|
+
/// keys rows `collection:id` under `collection`; an application namespace
|
|
1145
|
+
/// keys them by id under `namespace:collection`.
|
|
1146
|
+
pub fn public_json(&self, layout: &ReadLayout) -> Value {
|
|
1147
|
+
let mut value = serde_json::to_value(self).unwrap_or(Value::Null);
|
|
1148
|
+
if matches!(self, Self::Fence { .. }) {
|
|
1149
|
+
return value;
|
|
1150
|
+
}
|
|
1151
|
+
let (collection, id) = match layout {
|
|
1152
|
+
ReadLayout::Direct => {
|
|
1153
|
+
let collection = self.collection().to_string();
|
|
1154
|
+
let id = self
|
|
1155
|
+
.key()
|
|
1156
|
+
.strip_prefix(&format!("{collection}:"))
|
|
1157
|
+
.unwrap_or(self.key())
|
|
1158
|
+
.to_string();
|
|
1159
|
+
(collection, id)
|
|
1160
|
+
}
|
|
1161
|
+
ReadLayout::Namespace(namespace) => (
|
|
1162
|
+
self.collection()
|
|
1163
|
+
.strip_prefix(&format!("{namespace}:"))
|
|
1164
|
+
.unwrap_or(self.collection())
|
|
1165
|
+
.to_string(),
|
|
1166
|
+
self.key().to_string(),
|
|
1167
|
+
),
|
|
1168
|
+
};
|
|
1169
|
+
if let Some(object) = value.as_object_mut() {
|
|
1170
|
+
object.insert("key".into(), Value::String(format!("{collection}:{id}")));
|
|
1171
|
+
object.insert("collection".into(), Value::String(collection));
|
|
1172
|
+
object.insert("id".into(), Value::String(id));
|
|
1173
|
+
}
|
|
1174
|
+
value
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1136
1178
|
impl Display for PreconditionFailure {
|
|
1137
1179
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
1138
1180
|
match self {
|
|
@@ -3225,6 +3267,20 @@ impl FeltDb {
|
|
|
3225
3267
|
.collect()
|
|
3226
3268
|
}
|
|
3227
3269
|
|
|
3270
|
+
/// The original commit of a transaction id, if it committed, exactly as a
|
|
3271
|
+
/// retry of it through `apply_atomic_transaction_*` would report it: the
|
|
3272
|
+
/// original revisions and positions (`duplicate: true`), or
|
|
3273
|
+
/// `TransactionIdPayloadMismatch` when `payload_hash` differs from the one
|
|
3274
|
+
/// the id was committed with. Read-only.
|
|
3275
|
+
pub fn committed_transaction(
|
|
3276
|
+
&self,
|
|
3277
|
+
transaction_id: &str,
|
|
3278
|
+
payload_hash: Option<&str>,
|
|
3279
|
+
) -> Result<Option<AtomicCommit>> {
|
|
3280
|
+
let inner = self.state();
|
|
3281
|
+
committed_transaction_locked(&inner, transaction_id, payload_hash).transpose()
|
|
3282
|
+
}
|
|
3283
|
+
|
|
3228
3284
|
pub fn has_applied_transaction(&self, transaction_id: &str) -> Result<bool> {
|
|
3229
3285
|
Ok(self
|
|
3230
3286
|
.inner
|
|
@@ -3385,41 +3441,10 @@ impl FeltDb {
|
|
|
3385
3441
|
) -> Result<AtomicCommit> {
|
|
3386
3442
|
let (commit, events) = {
|
|
3387
3443
|
let mut inner = self.state();
|
|
3388
|
-
if
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
.get(transaction_id)
|
|
3393
|
-
.map(String::as_str)
|
|
3394
|
-
!= Some(expected_hash)
|
|
3395
|
-
{
|
|
3396
|
-
return Err(FlowError::TransactionIdPayloadMismatch {
|
|
3397
|
-
transaction_id: transaction_id.to_string(),
|
|
3398
|
-
});
|
|
3399
|
-
}
|
|
3400
|
-
}
|
|
3401
|
-
let (base_revision, commit_revision) = inner
|
|
3402
|
-
.transaction_revisions
|
|
3403
|
-
.get(transaction_id)
|
|
3404
|
-
.copied()
|
|
3405
|
-
.unwrap_or((inner.authority_revision, inner.authority_revision));
|
|
3406
|
-
let (state_before, state_after) = inner
|
|
3407
|
-
.transaction_history
|
|
3408
|
-
.iter()
|
|
3409
|
-
.find(|attribution| attribution.transaction_id == transaction_id)
|
|
3410
|
-
.map(|attribution| (attribution.state_before, attribution.state_after))
|
|
3411
|
-
.unwrap_or((inner.sequence, inner.sequence));
|
|
3412
|
-
return Ok(AtomicCommit {
|
|
3413
|
-
transaction_id: transaction_id.into(),
|
|
3414
|
-
base_revision,
|
|
3415
|
-
commit_revision,
|
|
3416
|
-
status: TransactionStatus::Committed,
|
|
3417
|
-
state_before,
|
|
3418
|
-
state_after,
|
|
3419
|
-
rows: vec![],
|
|
3420
|
-
duplicate: true,
|
|
3421
|
-
payload_binding: payload_hash.map(|_| PayloadBinding::Verified),
|
|
3422
|
-
});
|
|
3444
|
+
if let Some(duplicate) =
|
|
3445
|
+
committed_transaction_locked(&inner, transaction_id, payload_hash)
|
|
3446
|
+
{
|
|
3447
|
+
return duplicate;
|
|
3423
3448
|
}
|
|
3424
3449
|
// Record preconditions, evaluated before anything is staged and
|
|
3425
3450
|
// under the same lock the writes take. A caller reads state,
|
|
@@ -4167,6 +4192,21 @@ impl FeltDb {
|
|
|
4167
4192
|
self.get::<Value>(key)
|
|
4168
4193
|
}
|
|
4169
4194
|
|
|
4195
|
+
/// One live row of a collection by its key, cloned under the state lock.
|
|
4196
|
+
///
|
|
4197
|
+
/// A keyed lookup, not a scan: callers that know the canonical key of the
|
|
4198
|
+
/// record they need (Durable Work's projection refresh, for one) use this
|
|
4199
|
+
/// rather than a predicate over the whole collection.
|
|
4200
|
+
pub(crate) fn live_row(&self, capability: &str, key: &str) -> Option<StoredRow> {
|
|
4201
|
+
let inner = self.state();
|
|
4202
|
+
inner
|
|
4203
|
+
.rows
|
|
4204
|
+
.get(capability)
|
|
4205
|
+
.and_then(|rows| rows.get(key))
|
|
4206
|
+
.filter(|row| !row.deleted)
|
|
4207
|
+
.cloned()
|
|
4208
|
+
}
|
|
4209
|
+
|
|
4170
4210
|
/// List the canonical values in one collection/capability namespace.
|
|
4171
4211
|
///
|
|
4172
4212
|
/// This clones the whole collection, so its cost is linear in collection
|
|
@@ -6021,6 +6061,57 @@ fn append_event_with(
|
|
|
6021
6061
|
Ok(())
|
|
6022
6062
|
}
|
|
6023
6063
|
|
|
6064
|
+
/// The answer to a retried transaction id, read under the commit lock.
|
|
6065
|
+
///
|
|
6066
|
+
/// `None` when the id has not committed. Otherwise the *original* commit's
|
|
6067
|
+
/// revisions and positions, never the store's current ones, or
|
|
6068
|
+
/// `TransactionIdPayloadMismatch` when a payload hash is given and differs from
|
|
6069
|
+
/// the one the id was committed with. Nothing is evaluated and nothing is
|
|
6070
|
+
/// applied: a duplicate is answered before any precondition is read.
|
|
6071
|
+
fn committed_transaction_locked(
|
|
6072
|
+
inner: &Inner,
|
|
6073
|
+
transaction_id: &str,
|
|
6074
|
+
payload_hash: Option<&str>,
|
|
6075
|
+
) -> Option<Result<AtomicCommit>> {
|
|
6076
|
+
if !inner.applied_transactions.contains(transaction_id) {
|
|
6077
|
+
return None;
|
|
6078
|
+
}
|
|
6079
|
+
if let Some(expected_hash) = payload_hash {
|
|
6080
|
+
if inner
|
|
6081
|
+
.transaction_payload_hashes
|
|
6082
|
+
.get(transaction_id)
|
|
6083
|
+
.map(String::as_str)
|
|
6084
|
+
!= Some(expected_hash)
|
|
6085
|
+
{
|
|
6086
|
+
return Some(Err(FlowError::TransactionIdPayloadMismatch {
|
|
6087
|
+
transaction_id: transaction_id.to_string(),
|
|
6088
|
+
}));
|
|
6089
|
+
}
|
|
6090
|
+
}
|
|
6091
|
+
let (base_revision, commit_revision) = inner
|
|
6092
|
+
.transaction_revisions
|
|
6093
|
+
.get(transaction_id)
|
|
6094
|
+
.copied()
|
|
6095
|
+
.unwrap_or((inner.authority_revision, inner.authority_revision));
|
|
6096
|
+
let (state_before, state_after) = inner
|
|
6097
|
+
.transaction_history
|
|
6098
|
+
.iter()
|
|
6099
|
+
.find(|attribution| attribution.transaction_id == transaction_id)
|
|
6100
|
+
.map(|attribution| (attribution.state_before, attribution.state_after))
|
|
6101
|
+
.unwrap_or((inner.sequence, inner.sequence));
|
|
6102
|
+
Some(Ok(AtomicCommit {
|
|
6103
|
+
transaction_id: transaction_id.into(),
|
|
6104
|
+
base_revision,
|
|
6105
|
+
commit_revision,
|
|
6106
|
+
status: TransactionStatus::Committed,
|
|
6107
|
+
state_before,
|
|
6108
|
+
state_after,
|
|
6109
|
+
rows: vec![],
|
|
6110
|
+
duplicate: true,
|
|
6111
|
+
payload_binding: payload_hash.map(|_| PayloadBinding::Verified),
|
|
6112
|
+
}))
|
|
6113
|
+
}
|
|
6114
|
+
|
|
6024
6115
|
fn append_transaction(path: &Path, transaction: &TransactionLogRecord) -> Result<()> {
|
|
6025
6116
|
// A transaction commit fsyncs. This span therefore includes the fsync, which
|
|
6026
6117
|
// is the point: it is the durability cost of a commit, measured rather than
|