@feltdb/core 0.6.4 → 0.6.6
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 +17 -3
- package/dist/cli/index.js +1 -1
- package/dist/create/package-versions.js +1 -1
- package/dist/workspace/index.d.ts +2 -0
- package/dist/workspace/index.d.ts.map +1 -1
- package/dist/workspace/index.js +1 -0
- package/dist/workspace/local-development-authority.d.ts +12 -0
- package/dist/workspace/local-development-authority.d.ts.map +1 -0
- package/dist/workspace/local-development-authority.js +137 -0
- package/dist/workspace/workspace-connection.js +27 -2
- package/package.json +1 -1
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, validateFlowSpec } from '@feltdb/core';
|
|
10
|
+
import { createFeltDB, diffFlowSpec, formatFlowSpec, parseFlowSpec, planFlowSpecMigration, startLocalDevelopmentAuthority, validateFlowSpec } from '@feltdb/core';
|
|
11
11
|
import { discoverWorkspace, generatePairingToken, persistPairingToken, displayWorkspaceStatus, initializeWorkspace, startPairingDiscoveryServer } from './workspace-integration.js';
|
|
12
12
|
function loadProjectEnvironment(file = path.resolve('.env.local')) {
|
|
13
13
|
if (!fs.existsSync(file))
|
|
@@ -429,6 +429,10 @@ function runLocalVite(args, waitForExit) {
|
|
|
429
429
|
}
|
|
430
430
|
async function handleDev(args) {
|
|
431
431
|
var _a, _b, _c;
|
|
432
|
+
if (args.includes('--help')) {
|
|
433
|
+
console.log('Usage: feltdb dev [--port 5173] [--studio-port 7701] [--authority-port 7700] [--discovery-port 7799] [--no-open]');
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
432
436
|
loadProjectEnvironment();
|
|
433
437
|
console.log('🚀 Starting FeltDB development server...\n');
|
|
434
438
|
const projectDir = process.cwd();
|
|
@@ -498,6 +502,7 @@ async function handleDev(args) {
|
|
|
498
502
|
process.env.APP_PORT = appPort;
|
|
499
503
|
process.env.STUDIO_PORT = studioPort;
|
|
500
504
|
let selfHostedStarted = false;
|
|
505
|
+
let localAuthority = null;
|
|
501
506
|
const stopSelfHosted = () => {
|
|
502
507
|
if (selfHostedStarted) {
|
|
503
508
|
spawnSync('docker', ['compose', 'stop', 'feltdb'], { cwd: process.cwd(), stdio: 'ignore' });
|
|
@@ -519,6 +524,14 @@ async function handleDev(args) {
|
|
|
519
524
|
selfHostedStarted = true;
|
|
520
525
|
(_c = process.env).VITE_FELTDB_URL || (_c.VITE_FELTDB_URL = 'http://127.0.0.1:7700');
|
|
521
526
|
}
|
|
527
|
+
if (!process.env.VITE_FELTDB_URL) {
|
|
528
|
+
const authorityPort = Number(args.includes('--authority-port') ? args[args.indexOf('--authority-port') + 1] || '7700' : '7700');
|
|
529
|
+
localAuthority = await startLocalDevelopmentAuthority({
|
|
530
|
+
dataDir: path.join(projectDir, '.feltdb', 'authority'),
|
|
531
|
+
port: authorityPort,
|
|
532
|
+
});
|
|
533
|
+
process.env.VITE_FELTDB_URL = localAuthority.endpoint;
|
|
534
|
+
}
|
|
522
535
|
// Generate pairing token for the workspace
|
|
523
536
|
// This enables browsers and IDEs to discover and connect to the workspace
|
|
524
537
|
let pairingToken = null;
|
|
@@ -538,6 +551,7 @@ async function handleDev(args) {
|
|
|
538
551
|
pairingToken = token.token;
|
|
539
552
|
}
|
|
540
553
|
console.log('FeltDB Dev Server');
|
|
554
|
+
console.log(` Authority: ${process.env.VITE_FELTDB_URL}`);
|
|
541
555
|
console.log(` Namespace: ${config.namespace}`);
|
|
542
556
|
console.log(` Runtime: ${config.runtime}`);
|
|
543
557
|
console.log(` Storage: ${config.storage}`);
|
|
@@ -547,7 +561,7 @@ async function handleDev(args) {
|
|
|
547
561
|
console.log(`🔗 Development Workspace`);
|
|
548
562
|
if (pairingToken) {
|
|
549
563
|
console.log(` Pairing Code: ${pairingToken}`);
|
|
550
|
-
console.log(`
|
|
564
|
+
console.log(` Pairing discovery: http://127.0.0.1:${(pairingDiscoveryServer?.address()).port}`);
|
|
551
565
|
console.log(` Clients can use this code to auto-discover the workspace`);
|
|
552
566
|
}
|
|
553
567
|
}
|
|
@@ -560,7 +574,7 @@ async function handleDev(args) {
|
|
|
560
574
|
let shuttingDown = false;
|
|
561
575
|
const stopVite = () => { if (!vite.killed)
|
|
562
576
|
vite.kill('SIGTERM'); };
|
|
563
|
-
const stopAll = () => { shuttingDown = true; stopVite(); pairingDiscoveryServer?.close(); stopSelfHosted(); };
|
|
577
|
+
const stopAll = () => { shuttingDown = true; stopVite(); pairingDiscoveryServer?.close(); void localAuthority?.close(); stopSelfHosted(); };
|
|
564
578
|
// In an inherited terminal Ctrl-C can reach Vite before this parent process.
|
|
565
579
|
// Never leave Studio (or its port) running after the application exits.
|
|
566
580
|
vite.once('exit', code => {
|
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.6';
|
|
27
27
|
function prompt(question) {
|
|
28
28
|
const rl = readline.createInterface({
|
|
29
29
|
input: process.stdin,
|
|
@@ -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.6.
|
|
3
|
+
export const FELTDB_PACKAGE_VERSION = '0.6.6';
|
|
4
4
|
export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
|
|
@@ -17,4 +17,6 @@ export { connectDevelopmentWorkspace, discoverDevelopmentWorkspace, resolvePairi
|
|
|
17
17
|
export { createWorkspaceId, isValidWorkspaceId, createDevelopmentWorkspace, updateWorkspaceTimestamp, discoverWorkspace, persistWorkspaceDiscovery, } from './workspace-identity.js';
|
|
18
18
|
export { DevelopmentNode, getDevelopmentNode, shutdownDevelopmentNode, } from './development-node.js';
|
|
19
19
|
export type { DevelopmentNodeConfig } from './development-node.js';
|
|
20
|
+
export { startLocalDevelopmentAuthority } from './local-development-authority.js';
|
|
21
|
+
export type { LocalDevelopmentAuthority, LocalDevelopmentAuthorityOptions } from './local-development-authority.js';
|
|
20
22
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/workspace/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,YAAY,EACV,oBAAoB,EACpB,UAAU,EACV,qBAAqB,EACrB,eAAe,EACf,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EACV,0BAA0B,EAC1B,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,EACxB,uBAAuB,EACvB,UAAU,GACX,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,2BAA2B,EAC3B,4BAA4B,EAC5B,kBAAkB,EAClB,8BAA8B,GAC/B,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,0BAA0B,EAC1B,wBAAwB,EACxB,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/workspace/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,YAAY,EACV,oBAAoB,EACpB,UAAU,EACV,qBAAqB,EACrB,eAAe,EACf,UAAU,EACV,kBAAkB,EAClB,cAAc,EACd,eAAe,EACf,kBAAkB,GACnB,MAAM,sBAAsB,CAAC;AAE9B,YAAY,EACV,0BAA0B,EAC1B,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,EACxB,uBAAuB,EACvB,UAAU,GACX,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,2BAA2B,EAC3B,4BAA4B,EAC5B,kBAAkB,EAClB,8BAA8B,GAC/B,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,iBAAiB,EACjB,kBAAkB,EAClB,0BAA0B,EAC1B,wBAAwB,EACxB,iBAAiB,EACjB,yBAAyB,GAC1B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,uBAAuB,GACxB,MAAM,uBAAuB,CAAC;AAE/B,YAAY,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAEnE,OAAO,EAAE,8BAA8B,EAAE,MAAM,kCAAkC,CAAC;AAClF,YAAY,EAAE,yBAAyB,EAAE,gCAAgC,EAAE,MAAM,kCAAkC,CAAC"}
|
package/dist/workspace/index.js
CHANGED
|
@@ -14,3 +14,4 @@
|
|
|
14
14
|
export { connectDevelopmentWorkspace, discoverDevelopmentWorkspace, resolvePairingCode, DevelopmentWorkspaceConnection, } from './workspace-connection.js';
|
|
15
15
|
export { createWorkspaceId, isValidWorkspaceId, createDevelopmentWorkspace, updateWorkspaceTimestamp, discoverWorkspace, persistWorkspaceDiscovery, } from './workspace-identity.js';
|
|
16
16
|
export { DevelopmentNode, getDevelopmentNode, shutdownDevelopmentNode, } from './development-node.js';
|
|
17
|
+
export { startLocalDevelopmentAuthority } from './local-development-authority.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface LocalDevelopmentAuthority {
|
|
2
|
+
endpoint: string;
|
|
3
|
+
close(): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export interface LocalDevelopmentAuthorityOptions {
|
|
6
|
+
dataDir: string;
|
|
7
|
+
host?: string;
|
|
8
|
+
port?: number;
|
|
9
|
+
}
|
|
10
|
+
/** Start the durable, unauthenticated loopback authority used by `feltdb dev`. */
|
|
11
|
+
export declare function startLocalDevelopmentAuthority(options: LocalDevelopmentAuthorityOptions): Promise<LocalDevelopmentAuthority>;
|
|
12
|
+
//# sourceMappingURL=local-development-authority.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-development-authority.d.ts","sourceRoot":"","sources":["../../src/workspace/local-development-authority.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,gCAAgC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,kFAAkF;AAClF,wBAAsB,8BAA8B,CAClD,OAAO,EAAE,gCAAgC,GACxC,OAAO,CAAC,yBAAyB,CAAC,CAyFpC"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { FileJsDb } from '../file-db.js';
|
|
4
|
+
/** Start the durable, unauthenticated loopback authority used by `feltdb dev`. */
|
|
5
|
+
export async function startLocalDevelopmentAuthority(options) {
|
|
6
|
+
const host = options.host || '127.0.0.1';
|
|
7
|
+
const port = options.port ?? 7700;
|
|
8
|
+
if (host !== '127.0.0.1' && host !== 'localhost' && host !== '::1') {
|
|
9
|
+
throw new Error('The development authority may only bind to a loopback address');
|
|
10
|
+
}
|
|
11
|
+
const db = new FileJsDb(path.resolve(options.dataDir));
|
|
12
|
+
const streams = new Set();
|
|
13
|
+
const server = http.createServer(async (request, response) => {
|
|
14
|
+
try {
|
|
15
|
+
response.setHeader('Access-Control-Allow-Origin', '*');
|
|
16
|
+
response.setHeader('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, OPTIONS');
|
|
17
|
+
response.setHeader('Access-Control-Allow-Headers', 'Authorization, Content-Type, FeltDB-Protocol');
|
|
18
|
+
response.setHeader('Cache-Control', 'no-store');
|
|
19
|
+
if (request.method === 'OPTIONS') {
|
|
20
|
+
response.statusCode = 204;
|
|
21
|
+
response.end();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const pathname = new URL(request.url || '/', `http://${host}`).pathname;
|
|
25
|
+
if (request.method === 'GET' && (pathname === '/health' || pathname === '/ready')) {
|
|
26
|
+
json(response, 200, { status: 'ready', storage: 'writable', authority: 'local-development' });
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (request.method === 'GET' && pathname === '/events') {
|
|
30
|
+
response.writeHead(200, {
|
|
31
|
+
'Content-Type': 'text/event-stream',
|
|
32
|
+
Connection: 'keep-alive',
|
|
33
|
+
'Cache-Control': 'no-cache',
|
|
34
|
+
'Access-Control-Allow-Origin': '*',
|
|
35
|
+
});
|
|
36
|
+
response.write(': connected\n\n');
|
|
37
|
+
streams.add(response);
|
|
38
|
+
request.once('close', () => streams.delete(response));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const match = pathname.match(/^\/collections\/([^/]+)(?:\/([^/]+))?$/);
|
|
42
|
+
if (!match) {
|
|
43
|
+
json(response, 404, { error: 'NOT_FOUND' });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const collection = decodeURIComponent(match[1]);
|
|
47
|
+
const id = match[2] ? decodeURIComponent(match[2]) : undefined;
|
|
48
|
+
const body = await readBody(request);
|
|
49
|
+
let result;
|
|
50
|
+
if (request.method === 'GET' && !id) {
|
|
51
|
+
result = db.query(collection);
|
|
52
|
+
json(response, result.success ? 200 : 500, result.success
|
|
53
|
+
? JSON.parse(result.data || '[]').map(value => ({ value }))
|
|
54
|
+
: { error: result.error });
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (request.method === 'GET' && id) {
|
|
58
|
+
result = db.get(`${collection}:${id}`);
|
|
59
|
+
if (!result.success) {
|
|
60
|
+
json(response, 500, { error: result.error });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (!result.data) {
|
|
64
|
+
json(response, 404, { error: 'NOT_FOUND' });
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
json(response, 200, { value: JSON.parse(result.data) });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (request.method === 'POST' && !id) {
|
|
71
|
+
const value = parseObject(body);
|
|
72
|
+
const recordId = typeof value.id === 'string' ? value.id : '';
|
|
73
|
+
if (!recordId) {
|
|
74
|
+
json(response, 400, { error: 'Record must have an id' });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
result = db.insert(`${collection}:${recordId}`, JSON.stringify(value));
|
|
78
|
+
}
|
|
79
|
+
else if (request.method === 'PATCH' && id) {
|
|
80
|
+
result = db.update(`${collection}:${id}`, JSON.stringify(parseObject(body)));
|
|
81
|
+
}
|
|
82
|
+
else if (request.method === 'DELETE' && id) {
|
|
83
|
+
result = db.delete(`${collection}:${id}`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
json(response, 404, { error: 'NOT_FOUND' });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (!result.success) {
|
|
90
|
+
json(response, 400, { error: result.error });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
json(response, request.method === 'POST' ? 201 : 200, { ok: true });
|
|
94
|
+
const frame = `data: ${JSON.stringify({ change: { capability: collection } })}\n\n`;
|
|
95
|
+
for (const stream of streams)
|
|
96
|
+
stream.write(frame);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (!response.headersSent)
|
|
100
|
+
json(response, 400, { error: error instanceof Error ? error.message : String(error) });
|
|
101
|
+
else
|
|
102
|
+
response.end();
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
await new Promise((resolve, reject) => {
|
|
106
|
+
server.once('error', reject);
|
|
107
|
+
server.listen(port, host, resolve);
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
endpoint: `http://${host}:${port}`,
|
|
111
|
+
close: () => new Promise((resolve, reject) => {
|
|
112
|
+
for (const stream of streams)
|
|
113
|
+
stream.end();
|
|
114
|
+
server.close(error => error ? reject(error) : resolve());
|
|
115
|
+
}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function json(response, status, value) {
|
|
119
|
+
response.statusCode = status;
|
|
120
|
+
response.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
121
|
+
response.end(JSON.stringify(value));
|
|
122
|
+
}
|
|
123
|
+
function readBody(request) {
|
|
124
|
+
return new Promise((resolve, reject) => {
|
|
125
|
+
let body = '';
|
|
126
|
+
request.setEncoding('utf8');
|
|
127
|
+
request.on('data', chunk => { body += chunk; });
|
|
128
|
+
request.once('end', () => resolve(body));
|
|
129
|
+
request.once('error', reject);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function parseObject(body) {
|
|
133
|
+
const value = JSON.parse(body || '{}');
|
|
134
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
135
|
+
throw new Error('Request body must be an object');
|
|
136
|
+
return value;
|
|
137
|
+
}
|
|
@@ -407,13 +407,38 @@ class HttpWorkspaceTransport {
|
|
|
407
407
|
}
|
|
408
408
|
subscribe(workspaceId, handler) {
|
|
409
409
|
let stopped = false;
|
|
410
|
+
let delivering = false;
|
|
411
|
+
let deliveryPending = false;
|
|
410
412
|
const subscribedAt = Date.now();
|
|
413
|
+
const deliver = async () => {
|
|
414
|
+
if (stopped)
|
|
415
|
+
return;
|
|
416
|
+
if (delivering) {
|
|
417
|
+
deliveryPending = true;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
delivering = true;
|
|
421
|
+
try {
|
|
422
|
+
await this.deliverEvents(workspaceId, subscribedAt, handler);
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
delivering = false;
|
|
426
|
+
if (deliveryPending) {
|
|
427
|
+
deliveryPending = false;
|
|
428
|
+
void deliver();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
};
|
|
411
432
|
const unsubscribe = this.db.subscribe_changes(collection => {
|
|
412
433
|
if (collection !== EVENT_COLLECTION || stopped)
|
|
413
434
|
return;
|
|
414
|
-
void
|
|
435
|
+
void deliver();
|
|
415
436
|
});
|
|
416
|
-
|
|
437
|
+
// The event log is authoritative. Reconcile immediately and periodically so
|
|
438
|
+
// an event cannot be stranded in the small window before SSE is established.
|
|
439
|
+
void deliver();
|
|
440
|
+
const reconciliation = setInterval(() => void deliver(), 250);
|
|
441
|
+
return () => { stopped = true; clearInterval(reconciliation); unsubscribe(); };
|
|
417
442
|
}
|
|
418
443
|
async appendEvent(input, type, value) {
|
|
419
444
|
const event = { id: generateEventId(), ...input, type, value, timestamp: Date.now() };
|
package/package.json
CHANGED