@feltdb/core 0.6.9 → 0.6.11

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.
Files changed (122) hide show
  1. package/dist/cli/application-lifecycle.js +112 -0
  2. package/dist/cli/application.js +10 -0
  3. package/dist/cli/commands.js +407 -119
  4. package/dist/cli/development-session.js +54 -0
  5. package/dist/cli/index.js +1 -1
  6. package/dist/cli/workspace-integration.js +146 -20
  7. package/dist/collection.d.ts +110 -2
  8. package/dist/collection.d.ts.map +1 -1
  9. package/dist/collection.js +228 -3
  10. package/dist/create/cli.js +4 -2
  11. package/dist/create/create.js +42 -6
  12. package/dist/create/frameworks.js +18 -0
  13. package/dist/create/package-versions.js +1 -1
  14. package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +408 -178
  15. package/dist/create/server-source/crates/feltdb/src/lib.rs +49 -5
  16. package/dist/create/server-source/crates/feltdb/src/multi_operation_transaction.rs +659 -0
  17. package/dist/create/server-source/crates/feltdb/src/p1_application_atomicity.rs +285 -0
  18. package/dist/create/server-source/crates/feltdb/src/p1_atomicity_acceptance.rs +582 -0
  19. package/dist/create/server-source/crates/feltdb-server/src/main.rs +141 -0
  20. package/dist/create/workspace-initialization.js +4 -5
  21. package/dist/db.d.ts +63 -1
  22. package/dist/db.d.ts.map +1 -1
  23. package/dist/db.js +145 -4
  24. package/dist/development-runtime-bridge.d.ts +60 -0
  25. package/dist/development-runtime-bridge.d.ts.map +1 -0
  26. package/dist/development-runtime-bridge.js +171 -0
  27. package/dist/embedded-transaction.d.ts +77 -0
  28. package/dist/embedded-transaction.d.ts.map +1 -0
  29. package/dist/embedded-transaction.js +102 -0
  30. package/dist/feltdb.d.ts +49 -1
  31. package/dist/feltdb.d.ts.map +1 -1
  32. package/dist/file-db.d.ts +33 -0
  33. package/dist/file-db.d.ts.map +1 -1
  34. package/dist/file-db.js +128 -4
  35. package/dist/freshness.d.ts +119 -0
  36. package/dist/freshness.d.ts.map +1 -0
  37. package/dist/freshness.js +115 -0
  38. package/dist/http-db.d.ts +48 -0
  39. package/dist/http-db.d.ts.map +1 -1
  40. package/dist/http-db.js +77 -1
  41. package/dist/index-backend.d.ts +8 -0
  42. package/dist/index-backend.d.ts.map +1 -1
  43. package/dist/index-backend.js +13 -0
  44. package/dist/index-core.d.ts +80 -0
  45. package/dist/index-core.d.ts.map +1 -0
  46. package/dist/index-core.js +74 -0
  47. package/dist/index-manager.d.ts +11 -0
  48. package/dist/index-manager.d.ts.map +1 -1
  49. package/dist/index-manager.js +30 -13
  50. package/dist/index-types.d.ts +17 -0
  51. package/dist/index-types.d.ts.map +1 -1
  52. package/dist/index.browser.d.ts +14 -0
  53. package/dist/index.browser.d.ts.map +1 -0
  54. package/dist/index.browser.js +13 -0
  55. package/dist/index.d.ts +8 -59
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +11 -54
  58. package/dist/indexeddb-db.d.ts +32 -0
  59. package/dist/indexeddb-db.d.ts.map +1 -1
  60. package/dist/indexeddb-db.js +151 -1
  61. package/dist/memory-db.d.ts +26 -0
  62. package/dist/memory-db.d.ts.map +1 -1
  63. package/dist/memory-db.js +86 -0
  64. package/dist/runtime-registry.d.ts +32 -0
  65. package/dist/runtime-registry.d.ts.map +1 -0
  66. package/dist/runtime-registry.js +38 -0
  67. package/dist/studio/app.d.ts +2 -1
  68. package/dist/studio/app.d.ts.map +1 -1
  69. package/dist/studio/components/StateExplorer.d.ts +2 -1
  70. package/dist/studio/components/StateExplorer.d.ts.map +1 -1
  71. package/dist/studio/components/index.js +1 -1
  72. package/dist/studio/{components-9kDSWiGL.js → components-C5p2TfIU.js} +25 -14
  73. package/dist/studio/index.js +234 -160
  74. package/dist/studio-app/assets/{feltdb_wasm-B4wq4mqp.js → feltdb_wasm-CJv3wHzi.js} +1 -1
  75. package/dist/studio-app/assets/feltdb_wasm_bg-C8TG8r2n.wasm +0 -0
  76. package/dist/studio-app/assets/index-DospFFYE.js +28 -0
  77. package/dist/studio-app/index.html +1 -1
  78. package/dist/transaction.d.ts +98 -0
  79. package/dist/transaction.d.ts.map +1 -0
  80. package/dist/transaction.js +86 -0
  81. package/dist/wasm/feltdb_wasm_bg.wasm +0 -0
  82. package/dist/workspace/browser.d.ts +22 -0
  83. package/dist/workspace/browser.d.ts.map +1 -0
  84. package/dist/workspace/browser.js +17 -0
  85. package/dist/workspace/development-node.d.ts +16 -0
  86. package/dist/workspace/development-node.d.ts.map +1 -1
  87. package/dist/workspace/development-node.js +85 -1
  88. package/dist/workspace/git-identity.d.ts +35 -0
  89. package/dist/workspace/git-identity.d.ts.map +1 -0
  90. package/dist/workspace/git-identity.js +81 -0
  91. package/dist/workspace/index.d.ts +12 -2
  92. package/dist/workspace/index.d.ts.map +1 -1
  93. package/dist/workspace/index.js +7 -1
  94. package/dist/workspace/investigation-analysis.d.ts +47 -0
  95. package/dist/workspace/investigation-analysis.d.ts.map +1 -0
  96. package/dist/workspace/investigation-analysis.js +181 -0
  97. package/dist/workspace/investigation-lifecycle-manager.d.ts +88 -0
  98. package/dist/workspace/investigation-lifecycle-manager.d.ts.map +1 -0
  99. package/dist/workspace/investigation-lifecycle-manager.js +259 -0
  100. package/dist/workspace/investigation-supervisor.d.ts +100 -0
  101. package/dist/workspace/investigation-supervisor.d.ts.map +1 -0
  102. package/dist/workspace/investigation-supervisor.js +238 -0
  103. package/dist/workspace/runtime-investigation.d.ts +5 -1
  104. package/dist/workspace/runtime-investigation.d.ts.map +1 -1
  105. package/dist/workspace/runtime-investigation.js +50 -21
  106. package/dist/workspace/runtime-observation.d.ts +82 -0
  107. package/dist/workspace/runtime-observation.d.ts.map +1 -0
  108. package/dist/workspace/runtime-observation.js +170 -0
  109. package/dist/workspace/runtime-observer.d.ts +57 -0
  110. package/dist/workspace/runtime-observer.d.ts.map +1 -0
  111. package/dist/workspace/runtime-observer.js +272 -0
  112. package/dist/workspace/workspace-connection.d.ts.map +1 -1
  113. package/dist/workspace/workspace-connection.js +14 -5
  114. package/dist/workspace/workspace-identity.d.ts +1 -1
  115. package/dist/workspace/workspace-identity.d.ts.map +1 -1
  116. package/dist/workspace/workspace-identity.js +4 -4
  117. package/dist/workspace/workspace-types.d.ts +208 -0
  118. package/dist/workspace/workspace-types.d.ts.map +1 -1
  119. package/package.json +12 -4
  120. package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +0 -523
  121. package/dist/studio-app/assets/feltdb_wasm_bg-Ceyi7l21.wasm +0 -0
  122. package/dist/studio-app/assets/index-BF45M757.js +0 -28
@@ -7,6 +7,7 @@
7
7
  * Collections now use reactive dependency graphs instead of polling
8
8
  * for efficient, real-time state propagation.
9
9
  */
10
+ import { describeFreshness, isCacheCurrent, readRevision, } from './freshness.js';
10
11
  import { getReactiveDependencyGraph } from './reactive-graph.js';
11
12
  import { IndexBackend } from './index-backend.js';
12
13
  import { IndexStore } from './index-store.js';
@@ -29,6 +30,26 @@ export class Collection {
29
30
  this.runtimeUnsubscribe = null;
30
31
  this.indexBackend = new IndexBackend();
31
32
  this.indexesLoaded = false;
33
+ /** Incremented whenever `cache` is replaced, so the index can tell it is stale. */
34
+ this.cacheGeneration = 0;
35
+ /** The cache generation the index entries currently reflect, or -1 for none. */
36
+ this.indexedGeneration = -1;
37
+ this.lastPlan = null;
38
+ /** Cache records addressed by id, valid for `indexedGeneration`. */
39
+ this.recordsById = null;
40
+ /** Raw payload the cache was parsed from, used to detect that nothing changed. */
41
+ this.lastRawSnapshot = null;
42
+ /**
43
+ * The runtime revision the cache was built from, when the runtime offers one.
44
+ *
45
+ * Captured *before* the query that produced the cache, never after. A write
46
+ * landing between the two then makes this older than the data it labels,
47
+ * which costs one unnecessary refresh. Capturing it after would make it
48
+ * newer than the data, and the cache would look current forever.
49
+ */
50
+ this.cachedRevision = null;
51
+ /** How the last refresh decided the cache was usable. Reporting only. */
52
+ this.lastFreshness = null;
32
53
  this.db = db;
33
54
  this.name = name;
34
55
  this.collectionId = `${name}-${Math.random().toString(36).substr(2, 9)}`;
@@ -71,11 +92,132 @@ export class Collection {
71
92
  await this.refresh();
72
93
  return [...this.cache];
73
94
  }
74
- /** Find records whose fields match the supplied query. */
95
+ /**
96
+ * Find records whose fields match the supplied query.
97
+ *
98
+ * When a hash index covers one of the queried fields, the index supplies the
99
+ * candidate records and the remaining fields are checked on those candidates
100
+ * only. Otherwise every record is examined.
101
+ *
102
+ * The result is identical either way. The index is a projection of the same
103
+ * cached array a scan would walk, rebuilt whenever that array is replaced,
104
+ * so an indexed lookup cannot return a record a scan would miss or resolve
105
+ * an id the cache no longer holds.
106
+ *
107
+ * `lastQueryPlan()` reports which path ran.
108
+ */
75
109
  async find(query = {}) {
76
110
  const items = await this.all();
77
111
  const entries = Object.entries(query);
78
- return items.filter((item) => entries.every(([field, expected]) => item[field] === expected));
112
+ if (entries.length === 0) {
113
+ this.lastPlan = { strategy: 'scan', reason: 'no query fields', candidatesExamined: items.length, recordsReturned: items.length };
114
+ return items;
115
+ }
116
+ const matches = (item) => entries.every(([field, expected]) => item[field] === expected);
117
+ const usable = this.indexFor(entries);
118
+ if (usable) {
119
+ const candidates = this.indexCandidates(usable.indexName, usable.field, usable.value);
120
+ if (candidates) {
121
+ const found = candidates.filter(matches);
122
+ this.lastPlan = {
123
+ strategy: 'index',
124
+ indexName: usable.indexName,
125
+ field: usable.field,
126
+ candidatesExamined: candidates.length,
127
+ recordsReturned: found.length,
128
+ };
129
+ return found;
130
+ }
131
+ }
132
+ const found = items.filter(matches);
133
+ this.lastPlan = {
134
+ strategy: 'scan',
135
+ reason: usable ? 'index could not be trusted for this snapshot' : 'no index covers the queried fields',
136
+ candidatesExamined: items.length,
137
+ recordsReturned: found.length,
138
+ };
139
+ return found;
140
+ }
141
+ /**
142
+ * How the most recent `find` was answered.
143
+ *
144
+ * Exposed so an application — and the benchmark — can confirm that the index
145
+ * was actually consulted rather than inferring it from timing.
146
+ */
147
+ lastQueryPlan() {
148
+ return this.lastPlan;
149
+ }
150
+ /** A hash index covering one of the queried fields, if there is one. */
151
+ indexFor(entries) {
152
+ for (const config of this.indexBackend.listIndexes()) {
153
+ // Only equality on a single-field hash index is planned today. Sorted,
154
+ // compound, and text indexes are maintained but not yet used for reads.
155
+ if (config.type !== 'hash' || !config.field)
156
+ continue;
157
+ const entry = entries.find(([field]) => field === config.field);
158
+ if (entry)
159
+ return { indexName: config.name, field: config.field, value: entry[1] };
160
+ }
161
+ return null;
162
+ }
163
+ /**
164
+ * Candidate records from an index, or null when the index cannot be trusted.
165
+ *
166
+ * Returning null makes the caller fall back to a scan, which is always
167
+ * correct. That is the safe direction: a wrong answer is far worse than a
168
+ * slow one.
169
+ */
170
+ indexCandidates(indexName, field, value) {
171
+ if (!this.ensureIndexReflectsCache())
172
+ return null;
173
+ let ids;
174
+ try {
175
+ ids = this.indexBackend.queryIndex(indexName, { field, operator: 'eq', value });
176
+ }
177
+ catch {
178
+ return null;
179
+ }
180
+ const records = [];
181
+ for (const id of ids) {
182
+ const record = this.recordsById?.get(id);
183
+ if (record === undefined)
184
+ return null; // the index disagrees with the cache
185
+ records.push(record);
186
+ }
187
+ return records;
188
+ }
189
+ /**
190
+ * Rebuild the index from the current cache when the cache has moved on.
191
+ *
192
+ * The index is only ever a projection of `this.cache`. Deriving it from the
193
+ * same array a scan walks is what makes the two paths agree by construction,
194
+ * rather than by hoping every write path remembered to update the index.
195
+ */
196
+ ensureIndexReflectsCache() {
197
+ if (this.indexedGeneration === this.cacheGeneration)
198
+ return this.recordsById !== null;
199
+ if (!this.indexBackend.resetEntries()) {
200
+ this.recordsById = null;
201
+ return false;
202
+ }
203
+ const byId = new Map();
204
+ for (const item of this.cache) {
205
+ const recordId = item?.id;
206
+ // A record without a stable id cannot be resolved from an index result,
207
+ // so the whole snapshot is treated as unindexable rather than partially
208
+ // indexed.
209
+ if (recordId === undefined || recordId === null) {
210
+ this.recordsById = null;
211
+ this.indexedGeneration = this.cacheGeneration;
212
+ return false;
213
+ }
214
+ const key = String(recordId);
215
+ byId.set(key, item);
216
+ this.indexBackend.updateRecord(key, null, item);
217
+ }
218
+ this.recordsById = byId;
219
+ this.indexedGeneration = this.cacheGeneration;
220
+ return true;
79
221
  }
80
222
  /**
81
223
  * Create an index on this collection for faster queries.
@@ -424,13 +566,93 @@ export class Collection {
424
566
  unsubscribe();
425
567
  this.unsubscribeFunctions.clear();
426
568
  }
569
+ /**
570
+ * What this collection's runtime can tell it about staleness.
571
+ *
572
+ * This is a *validation mechanism*, not a guarantee that the cache is fresh.
573
+ * `validation: 'revision'` means a cheap staleness check exists; it says
574
+ * nothing about whether this collection's cache currently passes it.
575
+ *
576
+ * @example
577
+ * const capability = await people.freshness();
578
+ * // 'revision' -> a cheap staleness check exists
579
+ * // 'refresh' -> re-read is the only honest answer, and `reason` says why
580
+ */
581
+ freshness() {
582
+ return describeFreshness(this.db);
583
+ }
584
+ /**
585
+ * How the last refresh decided the cache was usable.
586
+ *
587
+ * `servedFromCache` is true only when the runtime was never asked for this
588
+ * collection's data. Reporting only; it exists so a benchmark can tell a
589
+ * validated cache hit from a fallback that merely happened to be fast.
590
+ */
591
+ lastFreshnessCheck() {
592
+ return this.lastFreshness;
593
+ }
594
+ /**
595
+ * Read the runtime's current revision, or explain why there isn't one.
596
+ *
597
+ * The capability is re-checked on every call rather than remembered, because
598
+ * a deployment can change underneath a live collection: switching on
599
+ * replication revokes revision authority, and a collection holding a cached
600
+ * capability would keep trusting a number that stopped meaning anything.
601
+ * Any refusal — no capability, a topology change, a transport failure —
602
+ * lands here as a reason, and the caller falls back to a full refresh.
603
+ */
604
+ async currentRevision() {
605
+ try {
606
+ return { revision: await readRevision(this.db) };
607
+ }
608
+ catch (error) {
609
+ return { reason: error instanceof Error ? error.message : String(error) };
610
+ }
611
+ }
427
612
  /**
428
613
  * Refresh data from the database and notify subscribers.
429
614
  */
430
615
  async refresh() {
431
616
  try {
617
+ // The revision is read *before* the query, so it can only ever be older
618
+ // than the data it ends up labelling. The reverse ordering would let a
619
+ // write that lands between the query and the revision read produce a
620
+ // cache tagged with a revision it does not contain, and that cache would
621
+ // then validate as current forever.
622
+ const observed = await this.currentRevision();
623
+ if ('revision' in observed) {
624
+ if (this.isInitialized && this.cachedRevision
625
+ && isCacheCurrent(this.cachedRevision, observed.revision)) {
626
+ // The runtime is not queried at all. This is the only path in this
627
+ // method that skips the round trip, and it is reachable only when
628
+ // the runtime advertised an authoritative revision for its domain.
629
+ this.lastFreshness = { validation: 'revision', servedFromCache: true, revision: observed.revision };
630
+ return;
631
+ }
632
+ }
633
+ else {
634
+ // No usable revision: the existing behaviour, unchanged.
635
+ this.cachedRevision = null;
636
+ }
432
637
  const result = await this.db.query(this.name);
638
+ this.lastFreshness = 'revision' in observed
639
+ ? { validation: 'revision', servedFromCache: false, revision: observed.revision }
640
+ : { validation: 'refresh', servedFromCache: false, reason: observed.reason };
433
641
  if (result.success && result.data) {
642
+ // Record the revision only once the data it labels is actually in
643
+ // hand. A failed query must not leave the cache tagged as current.
644
+ this.cachedRevision = 'revision' in observed ? observed.revision : null;
645
+ // The runtime returned exactly what it returned last time, so the data
646
+ // has not changed. Re-parsing it would produce an identical array,
647
+ // discard the cache, and invalidate any index derived from it — paying
648
+ // twice to learn nothing.
649
+ //
650
+ // This remains useful for runtimes with no revision, and as a second
651
+ // line for those that have one: it is what keeps the index valid when
652
+ // some *other* collection's write moved a store-wide revision.
653
+ if (this.isInitialized && result.data === this.lastRawSnapshot) {
654
+ return;
655
+ }
434
656
  try {
435
657
  let items = JSON.parse(result.data);
436
658
  // Apply predicate if this is a derived collection
@@ -444,8 +666,11 @@ export class Collection {
444
666
  }
445
667
  });
446
668
  }
447
- // Update cache with all results
669
+ // Update cache with all results. The index is a projection of this
670
+ // array, so replacing it invalidates the index entries.
448
671
  this.cache = items;
672
+ this.cacheGeneration += 1;
673
+ this.lastRawSnapshot = result.data;
449
674
  this.isInitialized = true;
450
675
  }
451
676
  catch (err) {
@@ -345,17 +345,19 @@ Learn more: https://github.com/rkendel1/feltdb`);
345
345
  console.log('Build the production application image with: docker compose build app');
346
346
  }
347
347
  else {
348
- console.log('\n🚀 Starting the application and FeltDB Studio...\n');
348
+ console.log('\n🚀 Starting the application...\n');
349
349
  await run(npm, ['run', 'dev'], projectDir);
350
350
  }
351
351
  }
352
352
  }
353
353
  else {
354
- console.log('Project is ready. Start everything with:');
354
+ console.log('Project is ready. Start the application with:');
355
355
  console.log(` cd ${projectName}`);
356
356
  if (!shouldInstall)
357
357
  console.log(' npm install');
358
358
  console.log(' npm run dev');
359
+ console.log('Start FeltDB development tooling separately with:');
360
+ console.log(' npm run feltdb:dev');
359
361
  }
360
362
  }
361
363
  catch (error) {
@@ -7,6 +7,7 @@ import { randomBytes } from 'crypto';
7
7
  import { feltdbPackageRange } from './package-versions.js';
8
8
  import { generateDockerCompose, generateDockerfile, generateDockerIgnore, generateDotEnvLocal, generateFeltDBDockerfile, generateStudioDockerfile, } from './docker-compose-generator.js';
9
9
  import { initializeWorkspace, appendWorkspaceGitignore, } from './workspace-initialization.js';
10
+ import { frameworkScaffold } from './frameworks.js';
10
11
  export async function createProject(options) {
11
12
  const { projectName, templatesDir } = options;
12
13
  const projectDir = path.resolve(process.cwd(), projectName);
@@ -29,6 +30,7 @@ export async function createProject(options) {
29
30
  fs.mkdirSync(feltdbConfigDir, { recursive: true });
30
31
  const runtime = options.runtime || 'browser';
31
32
  const framework = options.framework || 'react';
33
+ const applicationFramework = frameworkScaffold(framework);
32
34
  const distributed = options.distributed !== false;
33
35
  const hasAgents = options.agents !== false;
34
36
  const hasWebLLM = options.webllm !== false;
@@ -41,7 +43,8 @@ export async function createProject(options) {
41
43
  main: 'src/index.ts',
42
44
  type: 'module',
43
45
  scripts: {
44
- dev: 'feltdb dev',
46
+ dev: applicationFramework.applicationDevCommand,
47
+ 'feltdb:dev': 'feltdb dev',
45
48
  build: 'feltdb build',
46
49
  test: 'node --test',
47
50
  feltdb: 'feltdb',
@@ -81,6 +84,10 @@ export async function createProject(options) {
81
84
  agents: {
82
85
  enabled: hasAgents,
83
86
  },
87
+ application: {
88
+ lifecycle: 'managed',
89
+ },
90
+ applicationUrl: 'http://127.0.0.1:5173',
84
91
  capabilities: {},
85
92
  };
86
93
  if (capabilities.includes('search')) {
@@ -185,13 +192,23 @@ ${hasAgents ? ` agent WorkspaceAssistant {
185
192
  fs.writeFileSync(path.join(projectDir, 'tsconfig.json'), JSON.stringify(tsconfig, null, 2));
186
193
  // Create main application files
187
194
  const runtimeOptions = runtime === 'browser'
188
- ? "{ namespace: '" + applicationName + "', browser: true }"
195
+ ? "{ namespace: import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', browser: true }"
189
196
  : runtime === 'managed'
190
- ? "{ namespace: import.meta.env.VITE_FELTDB_MANAGED_NAMESPACE || '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_MANAGED_URL || import.meta.env.VITE_FELTDB_URL || '', token: import.meta.env.VITE_FELTDB_MANAGED_API_KEY || import.meta.env.VITE_FELTDB_API_KEY || '' } }"
197
+ ? "{ namespace: import.meta.env.VITE_FELTDB_MANAGED_NAMESPACE || import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_MANAGED_URL || import.meta.env.VITE_FELTDB_URL || '', token: import.meta.env.VITE_FELTDB_MANAGED_API_KEY || import.meta.env.VITE_FELTDB_API_KEY || '' } }"
191
198
  : runtime === 'self-hosted'
192
- ? "{ namespace: '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_URL || 'http://localhost:7700', token: import.meta.env.VITE_FELTDB_API_KEY || '' } }"
193
- : "{ namespace: '" + applicationName + "', memory: true }";
194
- const feltdbTs = `import { createFeltDB } from '@feltdb/core';
199
+ ? "{ namespace: import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', server: { url: import.meta.env.VITE_FELTDB_URL || 'http://localhost:7700', token: import.meta.env.VITE_FELTDB_API_KEY || '' } }"
200
+ : "{ namespace: import.meta.env.VITE_FELTDB_NAMESPACE || '" + applicationName + "', memory: true }";
201
+ const feltdbTs = `import { configureDevelopmentRuntimeBridge, createFeltDB } from '@feltdb/core';
202
+
203
+ configureDevelopmentRuntimeBridge({
204
+ sessionId: import.meta.env.VITE_FELTDB_DEV_SESSION_ID,
205
+ workspaceId: import.meta.env.VITE_FELTDB_WORKSPACE_ID,
206
+ namespace: import.meta.env.VITE_FELTDB_NAMESPACE,
207
+ runtime: import.meta.env.VITE_FELTDB_RUNTIME,
208
+ authorityUrl: import.meta.env.VITE_FELTDB_AUTHORITY_URL,
209
+ bridgeUrl: import.meta.env.VITE_FELTDB_DEV_BRIDGE_URL,
210
+ applicationUrl: import.meta.env.VITE_FELTDB_APPLICATION_URL,
211
+ });
195
212
 
196
213
  export const db = createFeltDB(${runtimeOptions});
197
214
 
@@ -1523,6 +1540,8 @@ A FeltDB Workspace application demonstrating local-first database capabilities.
1523
1540
 
1524
1541
  ## Quick Start
1525
1542
 
1543
+ Start the application:
1544
+
1526
1545
  \`\`\`bash
1527
1546
  npm install
1528
1547
  npm run dev
@@ -1530,6 +1549,13 @@ npm run dev
1530
1549
 
1531
1550
  The application will be available at http://localhost:5173 (or your configured port).
1532
1551
 
1552
+ In a second terminal, start FeltDB authority, pairing, and Studio and attach it
1553
+ to the application explicitly:
1554
+
1555
+ \`\`\`bash
1556
+ npm run feltdb:dev
1557
+ \`\`\`
1558
+
1533
1559
  ## FeltDB Workspace Showcase
1534
1560
 
1535
1561
  This application demonstrates core FeltDB capabilities:
@@ -1628,6 +1654,10 @@ Configuration is in \`feltdb.config.json\`:
1628
1654
  "agents": {
1629
1655
  "enabled": ${hasAgents}
1630
1656
  },
1657
+ "application": {
1658
+ "lifecycle": "managed"
1659
+ },
1660
+ "applicationUrl": "http://127.0.0.1:5173",
1631
1661
  "capabilities": ["${capabilities}"]
1632
1662
  }
1633
1663
  \`\`\`
@@ -1653,6 +1683,12 @@ These are required when connecting to an authenticated self-hosted or managed Fe
1653
1683
  npm run dev
1654
1684
  \`\`\`
1655
1685
 
1686
+ ### Start FeltDB Development Tooling
1687
+
1688
+ \`\`\`bash
1689
+ npm run feltdb:dev
1690
+ \`\`\`
1691
+
1656
1692
  ### Build for Production
1657
1693
  \`\`\`bash
1658
1694
  npm run build
@@ -0,0 +1,18 @@
1
+ const FRAMEWORK_SCAFFOLDS = {
2
+ react: {
3
+ name: 'react',
4
+ applicationDevCommand: 'vite',
5
+ },
6
+ vanilla: {
7
+ name: 'vanilla',
8
+ applicationDevCommand: 'vite',
9
+ },
10
+ };
11
+ /** Resolve framework-owned application lifecycle metadata. */
12
+ export function frameworkScaffold(framework) {
13
+ const scaffold = FRAMEWORK_SCAFFOLDS[framework];
14
+ if (!scaffold) {
15
+ throw new Error(`Unsupported framework "${framework}". Choose react or vanilla.`);
16
+ }
17
+ return scaffold;
18
+ }
@@ -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.9';
3
+ export const FELTDB_PACKAGE_VERSION = '0.6.11';
4
4
  export const feltdbPackageRange = `^${FELTDB_PACKAGE_VERSION}`;