@feltdb/core 0.6.2 → 0.6.3

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.
@@ -525,6 +525,7 @@ async function handleDev(args) {
525
525
  if (workspace) {
526
526
  const token = generatePairingToken();
527
527
  token.workspaceId = workspace.workspaceId;
528
+ token.endpoint = process.env.VITE_FELTDB_URL;
528
529
  persistPairingToken(process.cwd(), token);
529
530
  pairingToken = token.token;
530
531
  }
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.2';
26
+ const VERSION = '0.6.3';
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.2';
3
+ export const FELTDB_PACKAGE_VERSION = '0.6.3';
4
4
  export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;
@@ -5856,10 +5856,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5856
5856
  axum::routing::patch(platform_update_application_member),
5857
5857
  )
5858
5858
  .route("/api/tenants/{tenant_id}", get(get_tenant))
5859
- .route(
5860
- "/api/certification/fixtures/{application_id}",
5861
- axum::routing::delete(delete_certification_fixture),
5862
- )
5863
5859
  .route("/api/keys", get(list_keys).post(create_key))
5864
5860
  .route("/api/keys/{id}", axum::routing::delete(revoke_key))
5865
5861
  .route(
@@ -5,6 +5,7 @@
5
5
  * to a FeltDB Development Workspace.
6
6
  */
7
7
  import type { ClientType } from './workspace-types.js';
8
+ import type { WorkspaceEventPayload } from './workspace-types.js';
8
9
  export interface WorkspaceConnectionOptions {
9
10
  pairingCode?: string;
10
11
  workspaceId?: string;
@@ -26,6 +27,7 @@ export interface PairingTokenData {
26
27
  token: string;
27
28
  workspaceId: string;
28
29
  expiresAt: number;
30
+ endpoint?: string;
29
31
  }
30
32
  export interface ClientInfo {
31
33
  clientId: string;
@@ -101,8 +103,9 @@ export declare class DevelopmentWorkspaceConnection {
101
103
  clientType: ClientType;
102
104
  private options;
103
105
  private subscriptions;
104
- private connectedAt;
105
- private connectedClients;
106
+ connectedAt: number;
107
+ private transport;
108
+ private unsubscribeEvents;
106
109
  constructor(workspaceId: string, clientId: string, clientType: ClientType, options: WorkspaceConnectionOptions);
107
110
  /**
108
111
  * Establish connection to workspace
@@ -112,10 +115,6 @@ export declare class DevelopmentWorkspaceConnection {
112
115
  * Close connection to workspace
113
116
  */
114
117
  disconnect(): Promise<void>;
115
- /**
116
- * Register this client as connected
117
- */
118
- private registerClient;
119
118
  /**
120
119
  * Get capabilities for this client type
121
120
  */
@@ -123,7 +122,7 @@ export declare class DevelopmentWorkspaceConnection {
123
122
  /**
124
123
  * List connected clients in workspace
125
124
  */
126
- clients(): ClientInfo[];
125
+ clients(): Promise<ClientInfo[]>;
127
126
  /**
128
127
  * Subscribe to workspace entity changes
129
128
  *
@@ -157,18 +156,7 @@ export declare class DevelopmentWorkspaceConnection {
157
156
  * Notify subscribers of entity changes
158
157
  */
159
158
  private notifySubscribers;
160
- }
161
- /**
162
- * Event payload for workspace changes
163
- */
164
- export interface WorkspaceEventPayload<T = unknown> {
165
- id: string;
166
- workspaceId: string;
167
- collection: string;
168
- entityId: string;
169
- type: 'created' | 'updated' | 'deleted';
170
- value?: T;
171
- timestamp: number;
172
- originClientId: string;
159
+ private requireTransport;
160
+ private resolveEndpoint;
173
161
  }
174
162
  //# sourceMappingURL=workspace-connection.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"workspace-connection.d.ts","sourceRoot":"","sources":["../../src/workspace/workspace-connection.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"workspace-connection.d.ts","sourceRoot":"","sources":["../../src/workspace/workspace-connection.ts"],"names":[],"mappings":"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"}
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import fs from 'fs';
8
8
  import path from 'path';
9
+ import { HttpJsDb } from '../http-db.js';
9
10
  /**
10
11
  * Discover workspace from .feltdb/workspace.json
11
12
  *
@@ -109,7 +110,9 @@ export async function connectDevelopmentWorkspace(options) {
109
110
  }
110
111
  const clientId = options.clientId || generateClientId(options.clientType);
111
112
  const clientType = options.clientType || 'other';
112
- return new DevelopmentWorkspaceConnection(workspaceId, clientId, clientType, options);
113
+ const connection = new DevelopmentWorkspaceConnection(workspaceId, clientId, clientType, options);
114
+ await connection.connect();
115
+ return connection;
113
116
  }
114
117
  /**
115
118
  * Generate a unique client ID based on type
@@ -130,7 +133,8 @@ export class DevelopmentWorkspaceConnection {
130
133
  constructor(workspaceId, clientId, clientType, options) {
131
134
  this.subscriptions = new Map();
132
135
  this.connectedAt = 0;
133
- this.connectedClients = new Map();
136
+ this.transport = null;
137
+ this.unsubscribeEvents = null;
134
138
  this.workspaceId = workspaceId;
135
139
  this.clientId = clientId;
136
140
  this.clientType = clientType;
@@ -140,26 +144,34 @@ export class DevelopmentWorkspaceConnection {
140
144
  * Establish connection to workspace
141
145
  */
142
146
  async connect() {
143
- this.connectedAt = Date.now();
144
- this.registerClient();
147
+ if (this.transport)
148
+ return;
149
+ const endpoint = await this.resolveEndpoint();
150
+ const transport = new HttpWorkspaceTransport(endpoint, this.options.auth?.token ?? '');
151
+ await transport.assertReachable();
152
+ const connectedAt = Date.now();
153
+ await transport.registerClient(this.workspaceId, {
154
+ clientId: this.clientId,
155
+ clientType: this.clientType,
156
+ connectedAt,
157
+ capabilities: this.getCapabilities(),
158
+ });
159
+ this.transport = transport;
160
+ this.unsubscribeEvents = transport.subscribe(this.workspaceId, event => this.notifySubscribers(event));
161
+ this.connectedAt = connectedAt;
145
162
  }
146
163
  /**
147
164
  * Close connection to workspace
148
165
  */
149
166
  async disconnect() {
167
+ this.unsubscribeEvents?.();
168
+ this.unsubscribeEvents = null;
169
+ if (this.transport) {
170
+ await this.transport.unregisterClient(this.workspaceId, this.clientId);
171
+ }
150
172
  this.subscriptions.clear();
151
- this.connectedClients.delete(this.clientId);
152
- }
153
- /**
154
- * Register this client as connected
155
- */
156
- registerClient() {
157
- this.connectedClients.set(this.clientId, {
158
- clientId: this.clientId,
159
- clientType: this.clientType,
160
- connectedAt: this.connectedAt,
161
- capabilities: this.getCapabilities(),
162
- });
173
+ this.transport = null;
174
+ this.connectedAt = 0;
163
175
  }
164
176
  /**
165
177
  * Get capabilities for this client type
@@ -181,8 +193,8 @@ export class DevelopmentWorkspaceConnection {
181
193
  /**
182
194
  * List connected clients in workspace
183
195
  */
184
- clients() {
185
- return Array.from(this.connectedClients.values());
196
+ async clients() {
197
+ return this.requireTransport().clients(this.workspaceId);
186
198
  }
187
199
  /**
188
200
  * Subscribe to workspace entity changes
@@ -214,56 +226,44 @@ export class DevelopmentWorkspaceConnection {
214
226
  */
215
227
  async publish(collection, entity) {
216
228
  const id = generateEntityId();
217
- const event = {
218
- id: generateEventId(),
229
+ await this.requireTransport().publish({
219
230
  workspaceId: this.workspaceId,
220
231
  collection,
221
232
  entityId: id,
222
- type: 'created',
223
233
  value: entity,
224
- timestamp: Date.now(),
225
234
  originClientId: this.clientId,
226
- };
227
- this.notifySubscribers(collection, event);
235
+ });
228
236
  return id;
229
237
  }
230
238
  /**
231
239
  * Get entity from workspace
232
240
  */
233
241
  async get(collection, entityId) {
234
- // In full implementation, would query FeltDB backend
235
- // For now, returns null (placeholder)
236
- return null;
242
+ return this.requireTransport().get({ workspaceId: this.workspaceId, collection, entityId });
237
243
  }
238
244
  /**
239
245
  * Query entities in workspace collection
240
246
  */
241
247
  async query(collection) {
242
- // In full implementation, would query FeltDB backend
243
- // For now, returns empty array (placeholder)
244
- return [];
248
+ return this.requireTransport().query({ workspaceId: this.workspaceId, collection });
245
249
  }
246
250
  /**
247
251
  * Update entity in workspace
248
252
  */
249
253
  async update(collection, entityId, updates) {
250
- const event = {
251
- id: generateEventId(),
254
+ await this.requireTransport().update({
252
255
  workspaceId: this.workspaceId,
253
256
  collection,
254
257
  entityId,
255
- type: 'updated',
256
- value: updates,
257
- timestamp: Date.now(),
258
+ updates,
258
259
  originClientId: this.clientId,
259
- };
260
- this.notifySubscribers(collection, event);
260
+ });
261
261
  }
262
262
  /**
263
263
  * Notify subscribers of entity changes
264
264
  */
265
- notifySubscribers(collection, event) {
266
- const handlers = this.subscriptions.get(collection);
265
+ notifySubscribers(event) {
266
+ const handlers = this.subscriptions.get(event.collection);
267
267
  if (handlers) {
268
268
  for (const handler of handlers) {
269
269
  try {
@@ -275,6 +275,27 @@ export class DevelopmentWorkspaceConnection {
275
275
  }
276
276
  }
277
277
  }
278
+ requireTransport() {
279
+ if (!this.transport)
280
+ throw new Error('Development Workspace is not connected');
281
+ return this.transport;
282
+ }
283
+ async resolveEndpoint() {
284
+ if (this.options.endpoint)
285
+ return this.options.endpoint;
286
+ const environmentEndpoint = typeof process !== 'undefined'
287
+ ? process.env.FELTDB_WORKSPACE_ENDPOINT || process.env.VITE_FELTDB_URL
288
+ : undefined;
289
+ if (environmentEndpoint)
290
+ return environmentEndpoint;
291
+ if (this.options.pairingCode && this.options.projectDir) {
292
+ const pairingPath = path.join(this.options.projectDir, '.feltdb', 'pairing.json');
293
+ const pairing = JSON.parse(fs.readFileSync(pairingPath, 'utf8'));
294
+ if (pairing.endpoint)
295
+ return pairing.endpoint;
296
+ }
297
+ throw new Error('Cannot connect: workspace authority endpoint is required');
298
+ }
278
299
  }
279
300
  /**
280
301
  * Generate unique entity ID
@@ -288,3 +309,108 @@ function generateEntityId() {
288
309
  function generateEventId() {
289
310
  return `event_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
290
311
  }
312
+ const ENTITY_COLLECTION = '_development_workspace_entities';
313
+ const EVENT_COLLECTION = '_development_workspace_events';
314
+ const CLIENT_COLLECTION = '_development_workspace_clients';
315
+ class HttpWorkspaceTransport {
316
+ constructor(endpoint, token) {
317
+ this.seenEvents = new Set();
318
+ this.db = new HttpJsDb({ url: endpoint, token });
319
+ }
320
+ async assertReachable() {
321
+ const result = await this.db.query(ENTITY_COLLECTION);
322
+ if (!result.success)
323
+ throw new Error(`Workspace authority unavailable: ${result.error}`);
324
+ }
325
+ async registerClient(workspaceId, client) {
326
+ const result = await this.db.insert(`${CLIENT_COLLECTION}:${recordId(workspaceId, client.clientId)}`, JSON.stringify({ workspaceId, ...client }));
327
+ if (!result.success) {
328
+ const update = await this.db.update(`${CLIENT_COLLECTION}:${recordId(workspaceId, client.clientId)}`, JSON.stringify({ workspaceId, ...client }));
329
+ if (!update.success)
330
+ throw new Error(`Client registration failed: ${update.error}`);
331
+ }
332
+ }
333
+ async unregisterClient(workspaceId, clientId) {
334
+ const result = await this.db.delete(`${CLIENT_COLLECTION}:${recordId(workspaceId, clientId)}`);
335
+ if (!result.success && !String(result.error).includes('404'))
336
+ throw new Error(`Client cleanup failed: ${result.error}`);
337
+ }
338
+ async clients(workspaceId) {
339
+ return (await this.readCollection(CLIENT_COLLECTION))
340
+ .filter(client => client.workspaceId === workspaceId);
341
+ }
342
+ async publish(input) {
343
+ const envelope = { ...input, updatedAt: Date.now() };
344
+ const stored = await this.db.insert(`${ENTITY_COLLECTION}:${recordId(input.workspaceId, input.collection, input.entityId)}`, JSON.stringify(envelope));
345
+ if (!stored.success)
346
+ throw new Error(`Workspace publish failed: ${stored.error}`);
347
+ await this.appendEvent(input, 'created', input.value);
348
+ }
349
+ async get(input) {
350
+ const result = await this.db.get(`${ENTITY_COLLECTION}:${recordId(input.workspaceId, input.collection, input.entityId)}`);
351
+ if (!result.success)
352
+ throw new Error(`Workspace get failed: ${result.error}`);
353
+ if (!result.data)
354
+ return null;
355
+ const envelope = JSON.parse(result.data);
356
+ return envelope.workspaceId === input.workspaceId && envelope.collection === input.collection ? envelope.value : null;
357
+ }
358
+ async query(input) {
359
+ return (await this.readCollection(ENTITY_COLLECTION))
360
+ .filter(entity => entity.workspaceId === input.workspaceId && entity.collection === input.collection)
361
+ .map(entity => entity.value);
362
+ }
363
+ async update(input) {
364
+ const current = await this.get(input);
365
+ if (current === null)
366
+ throw new Error(`Workspace entity not found: ${input.entityId}`);
367
+ const value = { ...current, ...input.updates };
368
+ const envelope = { workspaceId: input.workspaceId, collection: input.collection, entityId: input.entityId, value, updatedAt: Date.now() };
369
+ const result = await this.db.update(`${ENTITY_COLLECTION}:${recordId(input.workspaceId, input.collection, input.entityId)}`, JSON.stringify(envelope));
370
+ if (!result.success)
371
+ throw new Error(`Workspace update failed: ${result.error}`);
372
+ await this.appendEvent(input, 'updated', value);
373
+ }
374
+ subscribe(workspaceId, handler) {
375
+ let stopped = false;
376
+ const subscribedAt = Date.now();
377
+ const unsubscribe = this.db.subscribe_changes(collection => {
378
+ if (collection !== EVENT_COLLECTION || stopped)
379
+ return;
380
+ void this.deliverEvents(workspaceId, subscribedAt, handler);
381
+ });
382
+ return () => { stopped = true; unsubscribe(); };
383
+ }
384
+ async appendEvent(input, type, value) {
385
+ const event = { id: generateEventId(), ...input, type, value, timestamp: Date.now() };
386
+ const result = await this.db.insert(`${EVENT_COLLECTION}:${event.id}`, JSON.stringify(event));
387
+ if (!result.success)
388
+ throw new Error(`Workspace event persistence failed: ${result.error}`);
389
+ }
390
+ async deliverEvents(workspaceId, subscribedAt, handler) {
391
+ for (const event of await this.readCollection(EVENT_COLLECTION)) {
392
+ if (event.workspaceId !== workspaceId || event.timestamp < subscribedAt || this.seenEvents.has(event.id))
393
+ continue;
394
+ this.seenEvents.add(event.id);
395
+ handler(event);
396
+ }
397
+ }
398
+ async readCollection(collection) {
399
+ const result = await this.db.query(collection);
400
+ if (!result.success)
401
+ throw new Error(`Workspace query failed: ${result.error}`);
402
+ return result.data ? JSON.parse(result.data) : [];
403
+ }
404
+ }
405
+ function recordId(...parts) {
406
+ const value = parts.join('\u0000');
407
+ const hash = (seed) => {
408
+ let current = seed;
409
+ for (const byte of new TextEncoder().encode(value)) {
410
+ current ^= BigInt(byte);
411
+ current = BigInt.asUintN(64, current * 0x100000001b3n);
412
+ }
413
+ return current.toString(16).padStart(16, '0');
414
+ };
415
+ return `workspace_${hash(0xcbf29ce484222325n)}${hash(0x84222325cbf29ce4n)}`;
416
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feltdb/core",
3
- "version": "0.6.2",
3
+ "version": "0.6.3",
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",
@@ -56,6 +56,7 @@
56
56
  "scripts": {
57
57
  "build": "rm -rf dist && tsc && node ../../tools/stage-core-wasm.mjs && npm run build --workspace @feltdb/react && npm run build --workspace @feltdb/migration-sherpa && npm run build --workspace @feltdb/studio && npm run build --workspace @feltdb/cli && npm run build --workspace create-feltdb && node ../../tools/assemble-core-sdk.mjs",
58
58
  "test": "node ../../test-runner.ts",
59
+ "test:workspace-e2e": "node --test test/workspace-connection.e2e.test.mjs",
59
60
  "type-check": "tsc --noEmit"
60
61
  },
61
62
  "keywords": [