@serfab/quereus-plugin-sereus 1.16.0 → 1.17.0

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.
@@ -0,0 +1,89 @@
1
+ import debug from 'debug';
2
+ import type { Database, TableSchema } from '@quereus/quereus';
3
+ import type { Collection, CollectionId } from '@optimystic/db-core';
4
+ import { defaultCollectionUri } from '@optimystic/quereus-plugin-optimystic';
5
+
6
+ const log = debug('sereus:plugin:strand');
7
+
8
+ /** The schemas whose tables a founder gives a collection: strand membership, then the sApp's. */
9
+ const FOUNDED_SCHEMAS = ['Strand', 'App'] as const;
10
+
11
+ /**
12
+ * The part of the Optimystic plugin's transaction bridge this module reads: the live registry of
13
+ * every collection a table opened (its main tree, index trees and unique-enforcement trees),
14
+ * filled as each table initializes.
15
+ */
16
+ export interface CollectionRegistrySource {
17
+ getCollectionRegistry(): Map<CollectionId, Collection<unknown>>;
18
+ }
19
+
20
+ /**
21
+ * Commit an empty collection for every table declared in the `Strand` and `App` schemas that
22
+ * has none yet, and resolve to how many collections were committed.
23
+ *
24
+ * Opening a table whose collection was never committed stages a fresh empty one in this node's
25
+ * tracker and writes nothing (`Collection.createOrOpen`'s invented branch), and a SQL commit
26
+ * flushes only the trees it wrote to. So the step opens every table, which registers each of its
27
+ * trees with the bridge, then syncs every registered collection still in that invented state.
28
+ *
29
+ * Commits outside any SQL transaction whatever is staged in those collections, so it is only
30
+ * sound while nothing else writes through `db`.
31
+ */
32
+ export async function createEmptyCollections(db: Database, bridge: CollectionRegistrySource): Promise<number> {
33
+ const tables = foundedTables(db);
34
+ await openTables(db, tables);
35
+ const registry = bridge.getCollectionRegistry();
36
+ assertMainCollectionsRegistered(registry, tables);
37
+ return await syncInventedCollections(registry);
38
+ }
39
+
40
+ function foundedTables(db: Database): TableSchema[] {
41
+ return FOUNDED_SCHEMAS.flatMap(name => {
42
+ const schema = db.schemaManager.getSchema(name);
43
+ return schema
44
+ ? Array.from(schema.getAllTables()).filter(table => !table.isView && table.vtabModuleName === 'optimystic')
45
+ : [];
46
+ });
47
+ }
48
+
49
+ /** A read initializes the table's vtab, which is what opens and registers its collections. */
50
+ async function openTables(db: Database, tables: readonly TableSchema[]): Promise<void> {
51
+ for (const table of tables) {
52
+ for await (const _row of db.eval(`select 1 from ${table.schemaName}."${table.name}" limit 1`)) {
53
+ break;
54
+ }
55
+ }
56
+ }
57
+
58
+ /**
59
+ * A declared table whose main collection is not registered means the plugin's open or
60
+ * registration path has changed under this module; founding on would leave that table with no
61
+ * collection while appearing to have given every table one.
62
+ */
63
+ function assertMainCollectionsRegistered(registry: ReadonlyMap<CollectionId, unknown>, tables: readonly TableSchema[]): void {
64
+ const unregistered = tables.filter(table => !registry.has(mainCollectionId(table)));
65
+ if (unregistered.length > 0) {
66
+ const names = unregistered.map(table => `${table.schemaName}.${table.name} (${mainCollectionId(table)})`).join(', ');
67
+ throw new Error(`Opening the strand's tables registered no collection for ${names}; cannot give those tables an empty collection`);
68
+ }
69
+ }
70
+
71
+ /** The collection id the Optimystic vtab stores `table` under: its URI without the `tree://` scheme. */
72
+ function mainCollectionId(table: TableSchema): CollectionId {
73
+ const explicitUri = table.vtabArgs?.['0'];
74
+ const uri = typeof explicitUri === 'string' && explicitUri !== '' ? explicitUri : defaultCollectionUri(table.schemaName, table.name);
75
+ return (uri.startsWith('tree://') ? uri.slice('tree://'.length) : uri) as CollectionId;
76
+ }
77
+
78
+ async function syncInventedCollections(registry: ReadonlyMap<CollectionId, Collection<unknown>>): Promise<number> {
79
+ let committed = 0;
80
+ for (const [id, collection] of registry) {
81
+ // `committedRevision() === undefined` is the invented STATE, which its doc says is safe to branch on.
82
+ if (collection.committedRevision() === undefined && collection.hasUnsyncedChanges()) {
83
+ await collection.sync();
84
+ log('Committed empty collection %s', id);
85
+ committed++;
86
+ }
87
+ }
88
+ return committed;
89
+ }
package/src/types.ts CHANGED
@@ -108,6 +108,16 @@ export interface SereusPluginResult {
108
108
  * fires for a forgotten one. Passed through like {@link onCommitDurability}.
109
109
  */
110
110
  onCommitFullyHeld: (listener: CommitFullyHeldListener) => () => void;
111
+ /**
112
+ * Commit an empty collection for every table declared in the `Strand` and `App` schemas that
113
+ * has none yet (main, index and unique-enforcement trees), and resolve to how many were
114
+ * committed. A founder calls it before writing `Strand.Header`, so a read of a table nobody
115
+ * has written is answered from local storage rather than by the cohort proving absence.
116
+ *
117
+ * Run it only while no other writer can reach this connection's `Database`: each collection's
118
+ * `sync()` commits whatever is staged in it, outside any SQL transaction.
119
+ */
120
+ createEmptyCollections: () => Promise<number>;
111
121
  /** Shuts down the libp2p node and collection factory. Call when done. */
112
122
  shutdown: () => Promise<void>;
113
123
  }