@feltdb/core 0.6.9 → 0.6.10
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/cli/commands.js +189 -71
- package/dist/cli/index.js +1 -1
- package/dist/collection.d.ts +110 -2
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +228 -3
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +408 -178
- package/dist/create/server-source/crates/feltdb/src/lib.rs +49 -5
- package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +659 -0
- package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +285 -0
- package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +582 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +141 -0
- package/dist/db.d.ts +59 -0
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +94 -2
- package/dist/embedded-transaction.d.ts +77 -0
- package/dist/embedded-transaction.d.ts.map +1 -0
- package/dist/embedded-transaction.js +102 -0
- package/dist/feltdb.d.ts +49 -1
- package/dist/feltdb.d.ts.map +1 -1
- package/dist/file-db.d.ts +33 -0
- package/dist/file-db.d.ts.map +1 -1
- package/dist/file-db.js +128 -4
- package/dist/freshness.d.ts +119 -0
- package/dist/freshness.d.ts.map +1 -0
- package/dist/freshness.js +115 -0
- package/dist/http-db.d.ts +48 -0
- package/dist/http-db.d.ts.map +1 -1
- package/dist/http-db.js +77 -1
- package/dist/index-backend.d.ts +8 -0
- package/dist/index-backend.d.ts.map +1 -1
- package/dist/index-backend.js +13 -0
- package/dist/index-core.d.ts +78 -0
- package/dist/index-core.d.ts.map +1 -0
- package/dist/index-core.js +72 -0
- package/dist/index-manager.d.ts +11 -0
- package/dist/index-manager.d.ts.map +1 -1
- package/dist/index-manager.js +30 -13
- package/dist/index-types.d.ts +17 -0
- package/dist/index-types.d.ts.map +1 -1
- package/dist/index.browser.d.ts +14 -0
- package/dist/index.browser.d.ts.map +1 -0
- package/dist/index.browser.js +13 -0
- package/dist/index.d.ts +8 -59
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -54
- package/dist/indexeddb-db.d.ts +32 -0
- package/dist/indexeddb-db.d.ts.map +1 -1
- package/dist/indexeddb-db.js +151 -1
- package/dist/memory-db.d.ts +26 -0
- package/dist/memory-db.d.ts.map +1 -1
- package/dist/memory-db.js +86 -0
- package/dist/runtime-registry.d.ts +32 -0
- package/dist/runtime-registry.d.ts.map +1 -0
- package/dist/runtime-registry.js +38 -0
- package/dist/studio-app/assets/{feltdb_wasm-B4wq4mqp.js → feltdb_wasm-CJv3wHzi.js} +1 -1
- package/dist/studio-app/assets/feltdb_wasm_bg-C8TG8r2n.wasm +0 -0
- package/dist/studio-app/assets/index-D3rT0SJi.js +28 -0
- package/dist/studio-app/index.html +1 -1
- package/dist/transaction.d.ts +98 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +86 -0
- package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
- package/dist/workspace/browser.d.ts +22 -0
- package/dist/workspace/browser.d.ts.map +1 -0
- package/dist/workspace/browser.js +17 -0
- package/dist/workspace/git-identity.d.ts +35 -0
- package/dist/workspace/git-identity.d.ts.map +1 -0
- package/dist/workspace/git-identity.js +81 -0
- package/dist/workspace/index.d.ts +11 -1
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +6 -0
- package/dist/workspace/investigation-analysis.d.ts +47 -0
- package/dist/workspace/investigation-analysis.d.ts.map +1 -0
- package/dist/workspace/investigation-analysis.js +181 -0
- package/dist/workspace/investigation-lifecycle-manager.d.ts +86 -0
- package/dist/workspace/investigation-lifecycle-manager.d.ts.map +1 -0
- package/dist/workspace/investigation-lifecycle-manager.js +229 -0
- package/dist/workspace/investigation-supervisor.d.ts +100 -0
- package/dist/workspace/investigation-supervisor.d.ts.map +1 -0
- package/dist/workspace/investigation-supervisor.js +236 -0
- package/dist/workspace/runtime-investigation.d.ts +1 -1
- package/dist/workspace/runtime-investigation.d.ts.map +1 -1
- package/dist/workspace/runtime-investigation.js +46 -21
- package/dist/workspace/runtime-observation.d.ts +79 -0
- package/dist/workspace/runtime-observation.d.ts.map +1 -0
- package/dist/workspace/runtime-observation.js +152 -0
- package/dist/workspace/runtime-observer.d.ts +51 -0
- package/dist/workspace/runtime-observer.d.ts.map +1 -0
- package/dist/workspace/runtime-observer.js +268 -0
- package/dist/workspace/workspace-connection.d.ts.map +1 -1
- package/dist/workspace/workspace-connection.js +14 -5
- package/dist/workspace/workspace-types.d.ts +113 -0
- package/dist/workspace/workspace-types.d.ts.map +1 -1
- package/package.json +12 -4
- package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +0 -523
- package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
- package/dist/studio-app/assets/index-BF45M757.js +0 -28
package/dist/cli/commands.js
CHANGED
|
@@ -7,7 +7,7 @@ import http from 'http';
|
|
|
7
7
|
import net from 'net';
|
|
8
8
|
import { createRequire } from 'module';
|
|
9
9
|
import { spawn, spawnSync } from 'child_process';
|
|
10
|
-
import { createFeltDB, diffFlowSpec, formatFlowSpec, parseFlowSpec, planFlowSpecMigration, startLocalDevelopmentAuthority, validateFlowSpec } from '@feltdb/core';
|
|
10
|
+
import { createFeltDB, diffFlowSpec, formatFlowSpec, InvestigationLifecycleManager, parseFlowSpec, planFlowSpecMigration, startLocalDevelopmentAuthority, validateFlowSpec } from '@feltdb/core';
|
|
11
11
|
import { discoverWorkspace, ensureWorkspaceGitIgnored, generatePairingToken, persistPairingToken, displayWorkspaceStatus, initializeWorkspace, startPairingDiscoveryServer } from './workspace-integration.js';
|
|
12
12
|
import { detectApplication, discoverApplicationUrl } from './application.js';
|
|
13
13
|
function loadProjectEnvironment(file = path.resolve('.env.local')) {
|
|
@@ -55,6 +55,8 @@ export async function handleCommand(command, args) {
|
|
|
55
55
|
return handleWorkspace(args);
|
|
56
56
|
case 'dev':
|
|
57
57
|
return handleDev(args);
|
|
58
|
+
case 'investigations':
|
|
59
|
+
return handleInvestigations(args);
|
|
58
60
|
case 'build':
|
|
59
61
|
return handleBuild(args);
|
|
60
62
|
case 'check':
|
|
@@ -76,6 +78,92 @@ export async function handleCommand(command, args) {
|
|
|
76
78
|
return handleHelp();
|
|
77
79
|
}
|
|
78
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Inspect the durable investigation store.
|
|
83
|
+
*
|
|
84
|
+
* The evidence chain is only useful if a developer can get back to it after
|
|
85
|
+
* the browser is closed, so it is queryable from the command line.
|
|
86
|
+
*/
|
|
87
|
+
function handleInvestigations(args) {
|
|
88
|
+
if (args.includes('--help')) {
|
|
89
|
+
console.log('Usage: feltdb investigations [<investigation-id>] [--json]\n');
|
|
90
|
+
console.log('List durable runtime investigations, or show one in full.\n');
|
|
91
|
+
console.log('Options:');
|
|
92
|
+
console.log(' --json Emit machine-readable JSON');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const store = new InvestigationLifecycleManager(process.cwd());
|
|
96
|
+
const json = args.includes('--json');
|
|
97
|
+
const id = args.find(value => !value.startsWith('--'));
|
|
98
|
+
if (id) {
|
|
99
|
+
const investigation = store.loadInvestigation(id);
|
|
100
|
+
if (!investigation)
|
|
101
|
+
throw new Error(`Investigation not found: ${id}`);
|
|
102
|
+
if (json) {
|
|
103
|
+
console.log(JSON.stringify(investigation, null, 2));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const observation = investigation.originalObservation;
|
|
107
|
+
console.log(`\nInvestigation ${investigation.id}`);
|
|
108
|
+
console.log(` Workspace: ${investigation.workspaceId}`);
|
|
109
|
+
console.log(` Investigation ${investigation.investigationState}`);
|
|
110
|
+
console.log(` Remediation: ${investigation.remediationState}`);
|
|
111
|
+
console.log(` Verification: ${investigation.verificationState}\n`);
|
|
112
|
+
if (observation) {
|
|
113
|
+
console.log('Observation');
|
|
114
|
+
console.log(` ${observation.method} ${observation.url} -> HTTP ${observation.status}`);
|
|
115
|
+
console.log(` ${observation.browser || 'unknown'} / ${observation.runtime || 'unknown'}`
|
|
116
|
+
+ `${observation.durationMs !== undefined ? ` / ${observation.durationMs}ms` : ''}\n`);
|
|
117
|
+
}
|
|
118
|
+
if (investigation.evidence?.length) {
|
|
119
|
+
console.log('Evidence (observed by the runtime)');
|
|
120
|
+
for (const item of investigation.evidence)
|
|
121
|
+
console.log(` [${item.kind}] ${item.statement}`);
|
|
122
|
+
console.log();
|
|
123
|
+
}
|
|
124
|
+
if (investigation.finding) {
|
|
125
|
+
console.log('Finding (FeltDB analysis, not an observation)');
|
|
126
|
+
console.log(` ${investigation.finding.statement}`);
|
|
127
|
+
console.log(` confidence: ${investigation.finding.confidence}\n`);
|
|
128
|
+
}
|
|
129
|
+
if (investigation.remediation?.changeIdentity) {
|
|
130
|
+
console.log('Remediation');
|
|
131
|
+
console.log(` ${investigation.remediation.changeKind}: ${investigation.remediation.changeIdentity}`);
|
|
132
|
+
if (investigation.remediation.changedPaths.length) {
|
|
133
|
+
console.log(` changed: ${investigation.remediation.changedPaths.join(', ')}`);
|
|
134
|
+
}
|
|
135
|
+
console.log();
|
|
136
|
+
}
|
|
137
|
+
if (investigation.verificationAttempt) {
|
|
138
|
+
const attempt = investigation.verificationAttempt;
|
|
139
|
+
console.log('Verification');
|
|
140
|
+
console.log(` ${attempt.result}: ${attempt.summary}`);
|
|
141
|
+
console.log(` ${attempt.originalStatus} -> ${attempt.observedStatus ?? 'no matching observation'}\n`);
|
|
142
|
+
}
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const investigations = store.listInvestigations();
|
|
146
|
+
if (json) {
|
|
147
|
+
console.log(JSON.stringify(investigations, null, 2));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (investigations.length === 0) {
|
|
151
|
+
console.log('\nNo investigations recorded.');
|
|
152
|
+
console.log('Run "feltdb dev", exercise the application, and a runtime defect will open one.\n');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
console.log(`\n${investigations.length} investigation${investigations.length === 1 ? '' : 's'}\n`);
|
|
156
|
+
for (const investigation of investigations) {
|
|
157
|
+
const observation = investigation.originalObservation;
|
|
158
|
+
const summary = observation
|
|
159
|
+
? `${observation.method} ${observation.url} -> ${observation.status}`
|
|
160
|
+
: investigation.observationId;
|
|
161
|
+
console.log(` ${investigation.id} ${investigation.verificationState.padEnd(12)} ${summary}`);
|
|
162
|
+
}
|
|
163
|
+
const metrics = store.metrics();
|
|
164
|
+
console.log(`\n ${metrics.observations} observations, ${metrics.evidenceRecords} evidence records, `
|
|
165
|
+
+ `${metrics.serializedBytes} bytes, loaded in ${metrics.loadMilliseconds}ms\n`);
|
|
166
|
+
}
|
|
79
167
|
function flowFile(args) {
|
|
80
168
|
const candidate = args.find(value => !value.startsWith('--')) || 'feltdb.flow';
|
|
81
169
|
return path.resolve(candidate);
|
|
@@ -414,37 +502,29 @@ async function handleWorkspaceConnect(args) {
|
|
|
414
502
|
console.log(' This feature will be implemented in the next phase');
|
|
415
503
|
console.log(' (when @feltdb/client workspace APIs are available)\n');
|
|
416
504
|
}
|
|
417
|
-
function runLocalVite(args, waitForExit) {
|
|
418
|
-
const executable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
419
|
-
const child = spawn(executable, ['exec', '--', 'vite', ...args], {
|
|
420
|
-
cwd: process.cwd(),
|
|
421
|
-
stdio: 'inherit',
|
|
422
|
-
env: process.env,
|
|
423
|
-
});
|
|
424
|
-
if (!waitForExit)
|
|
425
|
-
return child;
|
|
426
|
-
return new Promise((resolve, reject) => {
|
|
427
|
-
child.once('error', reject);
|
|
428
|
-
child.once('exit', code => code === 0 ? resolve() : reject(new Error(`Vite exited with status ${code ?? 'unknown'}`)));
|
|
429
|
-
});
|
|
430
|
-
}
|
|
431
505
|
async function handleDev(args) {
|
|
432
506
|
var _a, _b, _c;
|
|
507
|
+
const projectDir = process.cwd();
|
|
433
508
|
if (args.includes('--help')) {
|
|
434
|
-
console.log('Usage: feltdb dev [--app-url URL
|
|
509
|
+
console.log('Usage: feltdb dev [--app-url URL] [--studio-port 7701] [--authority-port 7700] [--discovery-port 7799] [--no-open]');
|
|
510
|
+
console.log('\nOptions:');
|
|
511
|
+
console.log(' --app-url <url> Application URL (e.g., http://127.0.0.1:3000)');
|
|
512
|
+
console.log(' --studio-port <port> FeltDB Studio port (default: 7701)');
|
|
513
|
+
console.log(' --authority-port <port> Authority port (default: 7700)');
|
|
514
|
+
console.log(' --discovery-port <port> Pairing discovery port (default: 7799)');
|
|
515
|
+
console.log(' --no-open Do not open Studio in browser');
|
|
435
516
|
return;
|
|
436
517
|
}
|
|
437
518
|
loadProjectEnvironment();
|
|
438
519
|
console.log('🚀 Starting FeltDB development server...\n');
|
|
439
|
-
const projectDir = process.cwd();
|
|
440
|
-
const configPath = path.join(projectDir, 'feltdb.config.json');
|
|
441
520
|
// .feltdb/ is reserved local runtime state. Protect it before discovery,
|
|
442
521
|
// initialization, the authority, or pairing can persist anything there.
|
|
522
|
+
// This runs after `--help` returns, so asking for help never writes anything.
|
|
443
523
|
ensureWorkspaceGitIgnored(projectDir);
|
|
524
|
+
const configPath = path.join(projectDir, 'feltdb.config.json');
|
|
444
525
|
// Initialize workspace if needed
|
|
445
526
|
let workspace = discoverWorkspace(projectDir);
|
|
446
527
|
if (!workspace) {
|
|
447
|
-
// Get project name from package.json
|
|
448
528
|
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
449
529
|
let projectId = path.basename(projectDir);
|
|
450
530
|
if (fs.existsSync(packageJsonPath)) {
|
|
@@ -460,6 +540,8 @@ async function handleDev(args) {
|
|
|
460
540
|
console.log(`✨ Initialized Development Workspace`);
|
|
461
541
|
console.log(` Workspace ID: ${workspace.workspaceId}\n`);
|
|
462
542
|
}
|
|
543
|
+
// Investigation lifecycle manager will be initialized when needed
|
|
544
|
+
// const investigationManager = new InvestigationLifecycleManager(projectDir);
|
|
463
545
|
// Create feltdb.config.json if it doesn't exist
|
|
464
546
|
if (!fs.existsSync(configPath)) {
|
|
465
547
|
const packageJsonPath = path.join(projectDir, 'package.json');
|
|
@@ -478,11 +560,10 @@ async function handleDev(args) {
|
|
|
478
560
|
runtime: 'browser',
|
|
479
561
|
storage: 'indexeddb',
|
|
480
562
|
distributed: true,
|
|
481
|
-
|
|
482
|
-
capabilities: { search: true },
|
|
563
|
+
applicationUrl: null,
|
|
483
564
|
};
|
|
484
565
|
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2) + '\n');
|
|
485
|
-
console.log(`✨ Created feltdb.config.json
|
|
566
|
+
console.log(`✨ Created feltdb.config.json\n`);
|
|
486
567
|
}
|
|
487
568
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
488
569
|
if (config.runtime === 'managed') {
|
|
@@ -495,17 +576,15 @@ async function handleDev(args) {
|
|
|
495
576
|
const runtimeNamespace = config.runtime === 'managed'
|
|
496
577
|
? process.env.VITE_FELTDB_MANAGED_NAMESPACE || config.namespace
|
|
497
578
|
: config.namespace;
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
579
|
+
// Application URL precedence:
|
|
580
|
+
// 1. --app-url (highest priority)
|
|
581
|
+
// 2. feltdb.config.json applicationUrl
|
|
582
|
+
// 3. Deterministically discovered running application
|
|
583
|
+
// 4. No application (lowest priority - allowed)
|
|
584
|
+
const hasAppUrlArg = args.includes('--app-url');
|
|
585
|
+
const appUrlArgument = hasAppUrlArg ? args[args.indexOf('--app-url') + 1] : undefined;
|
|
586
|
+
if (hasAppUrlArg && !appUrlArgument)
|
|
501
587
|
throw new Error('--app-url requires a URL');
|
|
502
|
-
const hasAppPort = args.includes('--app-port') || args.includes('--port');
|
|
503
|
-
const appPortValue = args.includes('--app-port')
|
|
504
|
-
? args[args.indexOf('--app-port') + 1]
|
|
505
|
-
: args.includes('--port') ? args[args.indexOf('--port') + 1] : undefined;
|
|
506
|
-
if (!hasAppUrl && hasAppPort && !appPortValue)
|
|
507
|
-
throw new Error('--app-port requires a port');
|
|
508
|
-
const appPort = appPortValue ? Number(appPortValue) : undefined;
|
|
509
588
|
if (appUrlArgument) {
|
|
510
589
|
try {
|
|
511
590
|
const parsed = new URL(appUrlArgument);
|
|
@@ -516,28 +595,40 @@ async function handleDev(args) {
|
|
|
516
595
|
throw new Error(`Invalid --app-url: ${appUrlArgument}`);
|
|
517
596
|
}
|
|
518
597
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
else {
|
|
598
|
+
let appUrl;
|
|
599
|
+
let detectedApplication;
|
|
600
|
+
// Priority 1: --app-url takes highest priority
|
|
601
|
+
if (appUrlArgument) {
|
|
602
|
+
appUrl = appUrlArgument;
|
|
526
603
|
try {
|
|
527
|
-
|
|
604
|
+
detectedApplication = detectApplication(projectDir);
|
|
528
605
|
}
|
|
529
606
|
catch {
|
|
530
|
-
|
|
607
|
+
detectedApplication = { packageManager: 'npm', framework: 'External' };
|
|
531
608
|
}
|
|
532
609
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
610
|
+
else {
|
|
611
|
+
// Priority 2: Check feltdb.config.json applicationUrl
|
|
612
|
+
if (config.applicationUrl) {
|
|
613
|
+
appUrl = config.applicationUrl;
|
|
614
|
+
try {
|
|
615
|
+
detectedApplication = detectApplication(projectDir);
|
|
616
|
+
}
|
|
617
|
+
catch {
|
|
618
|
+
detectedApplication = { packageManager: 'npm', framework: 'External' };
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
// Priority 3: Try to discover an existing running application
|
|
623
|
+
const discovered = await discoverApplicationUrl(projectDir);
|
|
624
|
+
detectedApplication = discovered.project;
|
|
625
|
+
appUrl = discovered.url;
|
|
626
|
+
}
|
|
536
627
|
}
|
|
537
628
|
const requestedStudioPort = Number(args.includes('--studio-port') ? args[args.indexOf('--studio-port') + 1] || '7701' : '7701');
|
|
538
629
|
const studioPort = String(await availablePort(requestedStudioPort));
|
|
539
630
|
if (studioPort !== String(requestedStudioPort))
|
|
540
|
-
console.log(
|
|
631
|
+
console.log(`⚠ Port ${requestedStudioPort} is busy; using ${studioPort} for Studio.\n`);
|
|
541
632
|
process.env.STUDIO_PORT = studioPort;
|
|
542
633
|
let selfHostedStarted = false;
|
|
543
634
|
let localAuthority = null;
|
|
@@ -588,45 +679,69 @@ async function handleDev(args) {
|
|
|
588
679
|
pairingDiscoveryServer = await startPairingDiscoveryServer(token, discoveryPort);
|
|
589
680
|
pairingToken = token.token;
|
|
590
681
|
}
|
|
591
|
-
|
|
592
|
-
console.log(
|
|
593
|
-
console.log(` Studio: http://127.0.0.1:${studioPort}`);
|
|
594
|
-
console.log(` Pairing: http://127.0.0.1:${(pairingDiscoveryServer?.address()).port}`);
|
|
595
|
-
console.log('Application');
|
|
596
|
-
console.log(` Framework: ${discoveredApplication.project.framework}`);
|
|
597
|
-
console.log(' Dev command: (managed externally)');
|
|
598
|
-
console.log(` URL: ${appUrl}`);
|
|
599
|
-
console.log(` Namespace: ${config.namespace}`);
|
|
600
|
-
console.log(` Runtime: ${config.runtime}`);
|
|
601
|
-
console.log(` Storage: ${config.storage}`);
|
|
602
|
-
console.log(` Distributed: ${config.distributed}`);
|
|
682
|
+
// Output startup information
|
|
683
|
+
console.log('✨ Development Workspace');
|
|
603
684
|
if (workspace) {
|
|
604
|
-
console.log(`
|
|
605
|
-
console.log(`🔗 Development Workspace`);
|
|
606
|
-
if (pairingToken) {
|
|
607
|
-
console.log(` Pairing Code: ${pairingToken}`);
|
|
608
|
-
console.log(` Pairing discovery: http://127.0.0.1:${(pairingDiscoveryServer?.address()).port}`);
|
|
609
|
-
console.log(` Clients can use this code to auto-discover the workspace`);
|
|
610
|
-
}
|
|
685
|
+
console.log(` Workspace ID: ${workspace.workspaceId}`);
|
|
611
686
|
}
|
|
612
687
|
console.log();
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
688
|
+
console.log('🔗 Development Workspace');
|
|
689
|
+
if (pairingToken) {
|
|
690
|
+
console.log(` Pairing Code: ${pairingToken}`);
|
|
691
|
+
console.log(` Pairing discovery: http://127.0.0.1:${(pairingDiscoveryServer?.address()).port}`);
|
|
692
|
+
}
|
|
693
|
+
console.log();
|
|
694
|
+
console.log('FeltDB Dev Server');
|
|
695
|
+
console.log(` Namespace: ${config.namespace}`);
|
|
696
|
+
console.log(` Runtime: ${config.runtime}`);
|
|
697
|
+
console.log(` Storage: ${config.storage}`);
|
|
698
|
+
console.log(` Distributed: ${config.distributed}`);
|
|
699
|
+
console.log();
|
|
700
|
+
if (appUrl) {
|
|
701
|
+
console.log(`Application: ${appUrl}`);
|
|
702
|
+
console.log(`Authority: ${process.env.VITE_FELTDB_URL}`);
|
|
703
|
+
console.log();
|
|
704
|
+
console.log(`Using existing application at ${appUrl}`);
|
|
705
|
+
console.log(`app=${encodeURIComponent(appUrl)}\n`);
|
|
706
|
+
}
|
|
707
|
+
else {
|
|
708
|
+
console.log('❌ No application URL is configured.\n');
|
|
709
|
+
console.log('Start your application, then run:\n');
|
|
710
|
+
console.log(' feltdb dev --app-url http://127.0.0.1:<port>\n');
|
|
711
|
+
console.log('Or set applicationUrl in feltdb.config.json\n');
|
|
712
|
+
console.log('FeltDB workspace is ready at:');
|
|
713
|
+
console.log('Authority: ' + process.env.VITE_FELTDB_URL);
|
|
714
|
+
console.log('Studio: http://127.0.0.1:' + studioPort);
|
|
715
|
+
console.log();
|
|
716
|
+
}
|
|
617
717
|
const stopAll = () => { pairingDiscoveryServer?.close(); void localAuthority?.close(); stopSelfHosted(); };
|
|
618
718
|
process.once('exit', stopAll);
|
|
619
719
|
process.once('SIGINT', () => { stopAll(); process.exit(130); });
|
|
620
720
|
process.once('SIGTERM', () => { stopAll(); process.exit(143); });
|
|
721
|
+
const open = !args.includes('--no-open');
|
|
621
722
|
await handleStudio([
|
|
622
723
|
'--port', studioPort,
|
|
623
724
|
'--namespace', runtimeNamespace || 'default',
|
|
624
725
|
'--runtime', config.runtime || 'browser',
|
|
625
|
-
'--app-url', appUrl,
|
|
726
|
+
...(appUrl ? ['--app-url', appUrl] : []),
|
|
626
727
|
...((config.runtime === 'self-hosted' || config.runtime === 'managed') && process.env.VITE_FELTDB_URL ? ['--connect', process.env.VITE_FELTDB_URL] : []),
|
|
627
728
|
...(open ? [] : ['--no-open']),
|
|
628
729
|
]);
|
|
629
730
|
}
|
|
731
|
+
function runLocalVite(args, waitForExit) {
|
|
732
|
+
const executable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
733
|
+
const child = spawn(executable, ['exec', '--', 'vite', ...args], {
|
|
734
|
+
cwd: process.cwd(),
|
|
735
|
+
stdio: 'inherit',
|
|
736
|
+
env: process.env,
|
|
737
|
+
});
|
|
738
|
+
if (!waitForExit)
|
|
739
|
+
return child;
|
|
740
|
+
return new Promise((resolve, reject) => {
|
|
741
|
+
child.once('error', reject);
|
|
742
|
+
child.once('exit', code => code === 0 ? resolve() : reject(new Error(`Vite exited with status ${code ?? 'unknown'}`)));
|
|
743
|
+
});
|
|
744
|
+
}
|
|
630
745
|
async function handleBuild(args) {
|
|
631
746
|
console.log('🔨 Building FeltDB application...\n');
|
|
632
747
|
const configPath = path.join(process.cwd(), 'feltdb.config.json');
|
|
@@ -897,11 +1012,13 @@ async function handleStudio(args) {
|
|
|
897
1012
|
const require = createRequire(import.meta.url);
|
|
898
1013
|
const packageFile = require.resolve('@feltdb/core/package.json');
|
|
899
1014
|
const root = path.join(path.dirname(packageFile), 'dist', 'studio-app');
|
|
1015
|
+
if (!fs.existsSync(path.join(root, 'index.html'))) {
|
|
1016
|
+
console.log('⚠️ Studio application artifacts not found.');
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
900
1019
|
const projectFlow = ['feltdb.flow', ...fs.readdirSync(process.cwd()).filter(value => value.endsWith('.flow'))]
|
|
901
1020
|
.map(value => path.resolve(process.cwd(), value))
|
|
902
1021
|
.find((value, index, values) => values.indexOf(value) === index && fs.existsSync(value));
|
|
903
|
-
if (!fs.existsSync(path.join(root, 'index.html')))
|
|
904
|
-
throw new Error('Studio application artifact is missing; reinstall @feltdb/core');
|
|
905
1022
|
const mime = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8', '.svg': 'image/svg+xml', '.json': 'application/json' };
|
|
906
1023
|
const server = http.createServer((request, response) => {
|
|
907
1024
|
const pathname = decodeURIComponent(new URL(request.url || '/', 'http://localhost').pathname);
|
|
@@ -964,6 +1081,7 @@ Commands:
|
|
|
964
1081
|
connect Connect to remote FeltDB
|
|
965
1082
|
status Show connection status
|
|
966
1083
|
dev Start development server
|
|
1084
|
+
investigations List or show durable runtime investigations
|
|
967
1085
|
build Build FeltDB application
|
|
968
1086
|
check Validate configuration
|
|
969
1087
|
inspect Inspect runtime state
|
package/dist/cli/index.js
CHANGED
|
@@ -23,7 +23,7 @@ import * as path from 'path';
|
|
|
23
23
|
import * as readline from 'readline';
|
|
24
24
|
import { getClient } from './api-client.js';
|
|
25
25
|
import { loadFeltDBConfig, createDefaultConfig, validateModel, } from './config.js';
|
|
26
|
-
const VERSION = '0.6.
|
|
26
|
+
const VERSION = '0.6.10';
|
|
27
27
|
function prompt(question) {
|
|
28
28
|
const rl = readline.createInterface({
|
|
29
29
|
input: process.stdin,
|
package/dist/collection.d.ts
CHANGED
|
@@ -8,9 +8,27 @@
|
|
|
8
8
|
* for efficient, real-time state propagation.
|
|
9
9
|
*/
|
|
10
10
|
import type { JsDb } from './feltdb.js';
|
|
11
|
-
import type
|
|
11
|
+
import { type FreshnessCapability, type Revision } from './freshness.js';
|
|
12
|
+
import type { IndexConfig, CollectionQueryPlan } from './index-types.js';
|
|
12
13
|
export type Predicate<T> = (item: T) => boolean;
|
|
13
14
|
export type Subscriber<T> = (items: T[]) => void;
|
|
15
|
+
/**
|
|
16
|
+
* How a refresh decided whether the local cache could be reused.
|
|
17
|
+
*
|
|
18
|
+
* Reporting only, and deliberately explicit about *why* the runtime was
|
|
19
|
+
* queried, so a benchmark or a caller can tell a validated cache hit from a
|
|
20
|
+
* fallback that happens to be fast.
|
|
21
|
+
*/
|
|
22
|
+
export interface FreshnessCheck {
|
|
23
|
+
/** `revision` when a revision decided it; `refresh` when the runtime was asked for the data. */
|
|
24
|
+
validation: 'revision' | 'refresh';
|
|
25
|
+
/** True only when the runtime was not queried for the collection at all. */
|
|
26
|
+
servedFromCache: boolean;
|
|
27
|
+
/** The revision the cache now corresponds to, when there is one. */
|
|
28
|
+
revision?: Revision;
|
|
29
|
+
/** Why a revision was not used. */
|
|
30
|
+
reason?: string;
|
|
31
|
+
}
|
|
14
32
|
/**
|
|
15
33
|
* Result of an atomic version-checked update operation.
|
|
16
34
|
* Represents either a successful commit or a version conflict.
|
|
@@ -49,6 +67,26 @@ export declare class Collection<T> {
|
|
|
49
67
|
private indexBackend;
|
|
50
68
|
private indexStore;
|
|
51
69
|
private indexesLoaded;
|
|
70
|
+
/** Incremented whenever `cache` is replaced, so the index can tell it is stale. */
|
|
71
|
+
private cacheGeneration;
|
|
72
|
+
/** The cache generation the index entries currently reflect, or -1 for none. */
|
|
73
|
+
private indexedGeneration;
|
|
74
|
+
private lastPlan;
|
|
75
|
+
/** Cache records addressed by id, valid for `indexedGeneration`. */
|
|
76
|
+
private recordsById;
|
|
77
|
+
/** Raw payload the cache was parsed from, used to detect that nothing changed. */
|
|
78
|
+
private lastRawSnapshot;
|
|
79
|
+
/**
|
|
80
|
+
* The runtime revision the cache was built from, when the runtime offers one.
|
|
81
|
+
*
|
|
82
|
+
* Captured *before* the query that produced the cache, never after. A write
|
|
83
|
+
* landing between the two then makes this older than the data it labels,
|
|
84
|
+
* which costs one unnecessary refresh. Capturing it after would make it
|
|
85
|
+
* newer than the data, and the cache would look current forever.
|
|
86
|
+
*/
|
|
87
|
+
private cachedRevision;
|
|
88
|
+
/** How the last refresh decided the cache was usable. Reporting only. */
|
|
89
|
+
private lastFreshness;
|
|
52
90
|
constructor(db: JsDb, name: string, predicate?: Predicate<T>, parent?: Collection<T>, loadIndexes?: boolean);
|
|
53
91
|
/**
|
|
54
92
|
* Load indexes that were persisted from a previous session.
|
|
@@ -59,8 +97,46 @@ export declare class Collection<T> {
|
|
|
59
97
|
* Returns cached results (live-updated).
|
|
60
98
|
*/
|
|
61
99
|
all(): Promise<T[]>;
|
|
62
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Find records whose fields match the supplied query.
|
|
102
|
+
*
|
|
103
|
+
* When a hash index covers one of the queried fields, the index supplies the
|
|
104
|
+
* candidate records and the remaining fields are checked on those candidates
|
|
105
|
+
* only. Otherwise every record is examined.
|
|
106
|
+
*
|
|
107
|
+
* The result is identical either way. The index is a projection of the same
|
|
108
|
+
* cached array a scan would walk, rebuilt whenever that array is replaced,
|
|
109
|
+
* so an indexed lookup cannot return a record a scan would miss or resolve
|
|
110
|
+
* an id the cache no longer holds.
|
|
111
|
+
*
|
|
112
|
+
* `lastQueryPlan()` reports which path ran.
|
|
113
|
+
*/
|
|
63
114
|
find(query?: Partial<T>): Promise<T[]>;
|
|
115
|
+
/**
|
|
116
|
+
* How the most recent `find` was answered.
|
|
117
|
+
*
|
|
118
|
+
* Exposed so an application — and the benchmark — can confirm that the index
|
|
119
|
+
* was actually consulted rather than inferring it from timing.
|
|
120
|
+
*/
|
|
121
|
+
lastQueryPlan(): CollectionQueryPlan | null;
|
|
122
|
+
/** A hash index covering one of the queried fields, if there is one. */
|
|
123
|
+
private indexFor;
|
|
124
|
+
/**
|
|
125
|
+
* Candidate records from an index, or null when the index cannot be trusted.
|
|
126
|
+
*
|
|
127
|
+
* Returning null makes the caller fall back to a scan, which is always
|
|
128
|
+
* correct. That is the safe direction: a wrong answer is far worse than a
|
|
129
|
+
* slow one.
|
|
130
|
+
*/
|
|
131
|
+
private indexCandidates;
|
|
132
|
+
/**
|
|
133
|
+
* Rebuild the index from the current cache when the cache has moved on.
|
|
134
|
+
*
|
|
135
|
+
* The index is only ever a projection of `this.cache`. Deriving it from the
|
|
136
|
+
* same array a scan walks is what makes the two paths agree by construction,
|
|
137
|
+
* rather than by hoping every write path remembered to update the index.
|
|
138
|
+
*/
|
|
139
|
+
private ensureIndexReflectsCache;
|
|
64
140
|
/**
|
|
65
141
|
* Create an index on this collection for faster queries.
|
|
66
142
|
* @example
|
|
@@ -159,6 +235,38 @@ export declare class Collection<T> {
|
|
|
159
235
|
subscribe(subscriber: Subscriber<T>, _pollInterval?: number): () => void;
|
|
160
236
|
/** Release runtime subscriptions owned by this live collection. */
|
|
161
237
|
close(): void;
|
|
238
|
+
/**
|
|
239
|
+
* What this collection's runtime can tell it about staleness.
|
|
240
|
+
*
|
|
241
|
+
* This is a *validation mechanism*, not a guarantee that the cache is fresh.
|
|
242
|
+
* `validation: 'revision'` means a cheap staleness check exists; it says
|
|
243
|
+
* nothing about whether this collection's cache currently passes it.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* const capability = await people.freshness();
|
|
247
|
+
* // 'revision' -> a cheap staleness check exists
|
|
248
|
+
* // 'refresh' -> re-read is the only honest answer, and `reason` says why
|
|
249
|
+
*/
|
|
250
|
+
freshness(): Promise<FreshnessCapability>;
|
|
251
|
+
/**
|
|
252
|
+
* How the last refresh decided the cache was usable.
|
|
253
|
+
*
|
|
254
|
+
* `servedFromCache` is true only when the runtime was never asked for this
|
|
255
|
+
* collection's data. Reporting only; it exists so a benchmark can tell a
|
|
256
|
+
* validated cache hit from a fallback that merely happened to be fast.
|
|
257
|
+
*/
|
|
258
|
+
lastFreshnessCheck(): FreshnessCheck | null;
|
|
259
|
+
/**
|
|
260
|
+
* Read the runtime's current revision, or explain why there isn't one.
|
|
261
|
+
*
|
|
262
|
+
* The capability is re-checked on every call rather than remembered, because
|
|
263
|
+
* a deployment can change underneath a live collection: switching on
|
|
264
|
+
* replication revokes revision authority, and a collection holding a cached
|
|
265
|
+
* capability would keep trusting a number that stopped meaning anything.
|
|
266
|
+
* Any refusal — no capability, a topology change, a transport failure —
|
|
267
|
+
* lands here as a reason, and the caller falls back to a full refresh.
|
|
268
|
+
*/
|
|
269
|
+
private currentRevision;
|
|
162
270
|
/**
|
|
163
271
|
* Refresh data from the database and notify subscribers.
|
|
164
272
|
*/
|
package/dist/collection.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"collection.d.ts","sourceRoot":"","sources":["../src/collection.ts"],"names":[],"mappings":"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"}
|