@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.
@@ -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(` Discovery: http://127.0.0.1:${(pairingDiscoveryServer?.address()).port}`);
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.4';
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.4';
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"}
@@ -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 this.deliverEvents(workspaceId, subscribedAt, handler);
435
+ void deliver();
415
436
  });
416
- return () => { stopped = true; unsubscribe(); };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feltdb/core",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "description": "FeltDB Core - Application-facing database with Browser, Local, and Server runtimes. Durable state, reactive APIs.",
5
5
  "license": "MIT",
6
6
  "type": "module",