@serfab/quereus-plugin-sereus 1.7.0 → 1.8.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.
@@ -1,70 +1,189 @@
1
- import type { SqlValue } from '@quereus/quereus';
2
- import type { StrandConnectionOptions, StrandTransactor } from './types.js';
3
-
4
- const STRAND_TRANSACTORS: readonly StrandTransactor[] = ['local', 'network', 'test'];
5
-
6
- /**
7
- * Read the optional `transactor` key. Unlike `fret_profile` (whose unknown
8
- * values fall back to the default), an unrecognised transactor throws: silently
9
- * running on the network when the caller asked for local storage is the kind of
10
- * mistake that only surfaces as a mysterious hang on a machine with no peers.
11
- */
12
- function parseTransactor(raw: SqlValue): StrandTransactor | undefined {
13
- if (raw === undefined || raw === null || raw === '') return undefined;
14
- if (typeof raw !== 'string' || !STRAND_TRANSACTORS.includes(raw as StrandTransactor)) {
15
- throw new Error(
16
- `quereus-plugin-sereus: transactor must be one of ${STRAND_TRANSACTORS.join(', ')} (got ${JSON.stringify(raw)})`,
17
- );
18
- }
19
- return raw as StrandTransactor;
20
- }
21
-
22
- /**
23
- * Parse the plugin-loader SqlValue config into typed StrandConnectionOptions.
24
- * Shared by the Node (`plugin.ts`) and browser (`plugin-browser.ts`) entries.
25
- */
26
- export function parseConfig(config: Record<string, SqlValue>): StrandConnectionOptions {
27
- const strandId = config.strand_id;
28
- if (typeof strandId !== 'string' || !strandId) {
29
- throw new Error('quereus-plugin-sereus: strand_id is required');
30
- }
31
-
32
- const bootstrapNodesRaw = config.bootstrap_nodes;
33
- const bootstrapNodes = typeof bootstrapNodesRaw === 'string' && bootstrapNodesRaw
34
- ? bootstrapNodesRaw.split(',').map(s => s.trim()).filter(Boolean)
35
- : [];
36
-
37
- const schema = typeof config.schema === 'string' ? config.schema : undefined;
38
- const sAppId = typeof config.sapp_id === 'string' ? config.sapp_id : 'unknown';
39
- const sAppVersion = typeof config.sapp_version === 'string' ? config.sapp_version : '1.0.0';
40
- const port = typeof config.port === 'number' ? config.port : 0;
41
- const enableCache = config.enable_cache !== false && config.enable_cache !== 0;
42
- const fretProfile = config.fret_profile === 'core' ? 'core' as const : 'edge' as const;
43
-
44
- // Storage engine — see `StrandConnectionOptions.transactor`. Left out entirely
45
- // when unset so `composeStrand` applies its own default.
46
- const transactor = parseTransactor(config.transactor);
47
-
48
- // NOTE: `parseConfig` reads the keys it knows and ignores every other key in
49
- // the map — there is no allowlist, so a misspelled or retired setting (e.g.
50
- // the `mode` this package used to carry) is silently dropped rather than
51
- // rejected. Deliberate: the plugin loader hands through whatever the host's
52
- // settings file holds.
53
-
54
- // NOTE: persistent storage cannot ride a `Record<string, SqlValue>` — an
55
- // `IRawStorage` is not an `SqlValue`. The Node loader (`plugin.ts`) reads the
56
- // platform-only `storage_path` key directly and resolves it to a concrete
57
- // `FileRawStorage`; it is intentionally NOT parsed into the typed options here.
58
-
59
- return {
60
- strandId,
61
- bootstrapNodes,
62
- schema,
63
- sAppId,
64
- sAppVersion,
65
- port,
66
- enableCache,
67
- fretProfile,
68
- ...(transactor && { transactor }),
69
- };
70
- }
1
+ import type { SqlValue } from '@quereus/quereus';
2
+ import type { StrandConnectionOptions, StrandTransactor } from './types.js';
3
+
4
+ type FretProfile = NonNullable<StrandConnectionOptions['fretProfile']>;
5
+
6
+ export interface ParsedPluginConfig {
7
+ options: StrandConnectionOptions;
8
+ /**
9
+ * Node-only: resolved to a `FileRawStorage` by `plugin.ts`, rejected by
10
+ * `plugin-browser.ts`. It travels beside the options rather than in them
11
+ * because `StrandConnectionOptions.storage` is an `IRawStorage`, which no
12
+ * `SqlValue` can carry.
13
+ */
14
+ storagePath?: string;
15
+ }
16
+
17
+ /** What each setting reads to once it is supplied and accepted. */
18
+ interface SettingTypes {
19
+ strand_id: string;
20
+ bootstrap_nodes: string[];
21
+ schema: string;
22
+ sapp_id: string;
23
+ sapp_version: string;
24
+ port: number;
25
+ enable_cache: boolean;
26
+ fret_profile: FretProfile;
27
+ transactor: StrandTransactor;
28
+ storage_path: string;
29
+ }
30
+
31
+ type SettingKey = keyof SettingTypes;
32
+
33
+ interface Setting<T> {
34
+ /** What the setting accepts, as a problem line states it. */
35
+ readonly expected: string;
36
+ /** The value as typed, or undefined when `raw` is not acceptable. */
37
+ readonly read: (raw: NonNullable<SqlValue>) => T | undefined;
38
+ /** `''` means "not set" — Quoomb Web's text inputs send it for a cleared field. */
39
+ readonly emptyIsAbsent?: boolean;
40
+ readonly required?: boolean;
41
+ }
42
+
43
+ const TEXT: Setting<string> = {
44
+ expected: 'a string',
45
+ read: raw => (typeof raw === 'string' ? raw : undefined),
46
+ emptyIsAbsent: true,
47
+ };
48
+
49
+ const COMMA_LIST: Setting<string[]> = {
50
+ ...TEXT,
51
+ expected: 'a comma-separated string',
52
+ read: raw => (typeof raw === 'string' ? raw.split(',').map(s => s.trim()).filter(Boolean) : undefined),
53
+ };
54
+
55
+ // NOTE: no numeric strings here or in FLAG, so a `${VAR}` in quoomb.config.json
56
+ // (whose env interpolation always yields a string) cannot supply port or
57
+ // enable_cache; if that is ever needed, accept decimal-integer strings here.
58
+ const PORT: Setting<number> = {
59
+ expected: 'an integer from 0 to 65535',
60
+ read: raw => (typeof raw === 'number' && Number.isInteger(raw) && raw >= 0 && raw <= 65535 ? raw : undefined),
61
+ };
62
+
63
+ /** 0 and 1 are accepted because SQL has no boolean type. */
64
+ const FLAG: Setting<boolean> = {
65
+ expected: 'true, false, 1 or 0',
66
+ read: raw => {
67
+ if (raw === true || raw === 1) return true;
68
+ if (raw === false || raw === 0) return false;
69
+ return undefined;
70
+ },
71
+ };
72
+
73
+ /**
74
+ * One of a closed set of strings. The set is written as an object with every
75
+ * member as a key so it is checked against the union: `oneOf<StrandTransactor>`
76
+ * stops compiling when the union gains or loses a member.
77
+ */
78
+ function oneOf<T extends string>(members: Record<T, true>): Setting<T> {
79
+ const values = Object.keys(members) as T[];
80
+ return {
81
+ expected: `one of ${values.join(', ')}`,
82
+ read: raw => values.find(v => v === raw),
83
+ emptyIsAbsent: true,
84
+ };
85
+ }
86
+
87
+ const SETTINGS: { readonly [K in SettingKey]: Setting<SettingTypes[K]> } = {
88
+ strand_id: { ...TEXT, required: true },
89
+ bootstrap_nodes: COMMA_LIST,
90
+ schema: TEXT,
91
+ sapp_id: TEXT,
92
+ sapp_version: TEXT,
93
+ port: PORT,
94
+ enable_cache: FLAG,
95
+ fret_profile: oneOf<FretProfile>({ edge: true, core: true }),
96
+ transactor: oneOf<StrandTransactor>({ local: true, network: true, test: true }),
97
+ storage_path: TEXT,
98
+ };
99
+
100
+ /** Every key the plugin accepts; the package manifest's `quereus.settings` must list the same. */
101
+ export const PLUGIN_SETTING_KEYS = Object.keys(SETTINGS) as readonly SettingKey[];
102
+
103
+ const PROBLEM_HEADER = 'quereus-plugin-sereus: invalid plugin settings';
104
+
105
+ /** A string echoed in a problem is cut here: enough to recognise a typo, not a pasted schema. */
106
+ const MAX_ECHOED_CHARS = 120;
107
+
108
+ /**
109
+ * Parse the plugin-loader SqlValue config into typed StrandConnectionOptions.
110
+ * Shared by the Node (`plugin.ts`) and browser (`plugin-browser.ts`) entries.
111
+ *
112
+ * Strict: an unknown key or a value of the wrong type throws one error listing
113
+ * every problem, rather than falling back to a default the user never chose.
114
+ */
115
+ export function parseConfig(config: Record<string, SqlValue>): ParsedPluginConfig {
116
+ const problems: string[] = [];
117
+ const values = readSettings(config, problems);
118
+ problems.push(...unknownKeyProblems(config));
119
+ // `strand_id` is undefined only alongside its own "required" problem.
120
+ const strandId = values.strand_id;
121
+ if (problems.length > 0 || strandId === undefined) {
122
+ throw new Error([PROBLEM_HEADER, ...problems.map(p => ` - ${p}`)].join('\n'));
123
+ }
124
+ return toParsedConfig(strandId, values);
125
+ }
126
+
127
+ function readSettings(config: Record<string, SqlValue>, problems: string[]): Partial<SettingTypes> {
128
+ const values: Partial<SettingTypes> = {};
129
+ for (const key of PLUGIN_SETTING_KEYS) readSettingInto(values, key, config[key], problems);
130
+ return values;
131
+ }
132
+
133
+ function readSettingInto<K extends SettingKey>(
134
+ values: Partial<SettingTypes>,
135
+ key: K,
136
+ raw: SqlValue | undefined,
137
+ problems: string[],
138
+ ): void {
139
+ const setting: Setting<SettingTypes[K]> = SETTINGS[key];
140
+ if (raw === undefined || raw === null || (raw === '' && setting.emptyIsAbsent)) {
141
+ if (setting.required) problems.push(`${key} is required`);
142
+ return;
143
+ }
144
+ const value = setting.read(raw);
145
+ if (value === undefined) {
146
+ problems.push(`${key} must be ${setting.expected} (got ${describeValue(raw)})`);
147
+ return;
148
+ }
149
+ values[key] = value;
150
+ }
151
+
152
+ function unknownKeyProblems(config: Record<string, SqlValue>): string[] {
153
+ return Object.keys(config)
154
+ .filter(key => !Object.hasOwn(SETTINGS, key))
155
+ .map(key => key === 'mode'
156
+ ? 'mode was removed: a lone node now coordinates for itself, and transactor selects the storage engine'
157
+ : `unknown setting ${JSON.stringify(key)}; accepted settings are ${PLUGIN_SETTING_KEYS.join(', ')}`);
158
+ }
159
+
160
+ function describeValue(raw: NonNullable<SqlValue>): string {
161
+ if (typeof raw === 'string') {
162
+ return raw.length > MAX_ECHOED_CHARS
163
+ ? `${JSON.stringify(raw.slice(0, MAX_ECHOED_CHARS))}… (${raw.length} characters)`
164
+ : JSON.stringify(raw);
165
+ }
166
+ if (typeof raw === 'bigint') return `${raw}n`;
167
+ if (typeof raw === 'number' || typeof raw === 'boolean') return String(raw);
168
+ if (raw instanceof Uint8Array) return `a blob of ${raw.length} bytes`;
169
+ return 'a JSON value';
170
+ }
171
+
172
+ function toParsedConfig(strandId: string, values: Partial<SettingTypes>): ParsedPluginConfig {
173
+ const { transactor } = values;
174
+ return {
175
+ options: {
176
+ strandId,
177
+ bootstrapNodes: values.bootstrap_nodes ?? [],
178
+ schema: values.schema,
179
+ sAppId: values.sapp_id ?? 'unknown',
180
+ sAppVersion: values.sapp_version ?? '1.0.0',
181
+ port: values.port ?? 0,
182
+ enableCache: values.enable_cache ?? true,
183
+ fretProfile: values.fret_profile ?? 'edge',
184
+ // Left out entirely when unset so `composeStrand` applies its own default.
185
+ ...(transactor && { transactor }),
186
+ },
187
+ storagePath: values.storage_path,
188
+ };
189
+ }
@@ -1,29 +1,32 @@
1
- /**
2
- * Quereus plugin entry point for Sereus strand connections (browser/worker).
3
- *
4
- * This is the ESM-bundled counterpart to `./plugin`. Load via Quoomb-web's
5
- * plugin URL field or `plugins[].source` in `quoomb.config.json`. Defaults
6
- * storage to IndexedDB and uses WebSockets + circuit-relay transports.
7
- */
8
-
9
- import type { Database, SqlValue } from '@quereus/quereus';
10
- import { connectToStrandBrowser } from './connect-browser.js';
11
- import { parseConfig } from './parse-config.js';
12
- import type { SereusPluginResult } from './types.js';
13
-
14
- export { parseConfig };
15
-
16
- /**
17
- * Default export: Quereus plugin registration function (browser).
18
- *
19
- * Note: the `storage_path` setting is Node-only (it resolves to a filesystem
20
- * `FileRawStorage` in `./plugin`). The browser entry ignores it and defaults
21
- * storage to IndexedDB via `connectToStrandBrowser`.
22
- */
23
- export default async function register(
24
- db: Database,
25
- config: Record<string, SqlValue> = {},
26
- ): Promise<SereusPluginResult> {
27
- const options = parseConfig(config);
28
- return connectToStrandBrowser(db, options);
29
- }
1
+ /**
2
+ * Quereus plugin entry point for Sereus strand connections (browser/worker).
3
+ *
4
+ * This is the ESM-bundled counterpart to `./plugin`. Load via Quoomb-web's
5
+ * plugin URL field or `plugins[].source` in `quoomb.config.json`. Defaults
6
+ * storage to IndexedDB and uses WebSockets + circuit-relay transports.
7
+ */
8
+
9
+ import type { Database, SqlValue } from '@quereus/quereus';
10
+ import { connectToStrandBrowser } from './connect-browser.js';
11
+ import { parseConfig } from './parse-config.js';
12
+ import type { SereusPluginResult } from './types.js';
13
+
14
+ export { parseConfig };
15
+
16
+ /**
17
+ * Default export: Quereus plugin registration function (browser).
18
+ *
19
+ * The `storage_path` setting is Node-only (it resolves to a filesystem
20
+ * `FileRawStorage` in `./plugin`), so it is rejected here rather than ignored;
21
+ * storage defaults to IndexedDB via `connectToStrandBrowser`.
22
+ */
23
+ export default async function register(
24
+ db: Database,
25
+ config: Record<string, SqlValue> = {},
26
+ ): Promise<SereusPluginResult> {
27
+ const { options, storagePath } = parseConfig(config);
28
+ if (storagePath) {
29
+ throw new Error('quereus-plugin-sereus: storage_path is Node-only; the browser entry stores in IndexedDB');
30
+ }
31
+ return connectToStrandBrowser(db, options);
32
+ }
package/src/plugin.ts CHANGED
@@ -1,39 +1,35 @@
1
- /**
2
- * Quereus plugin entry point for Sereus strand connections (Node).
3
- *
4
- * Loaded via Quoomb's `.plugin install` or config file, or via `registerPlugin()`.
5
- * Parses SqlValue config and delegates to `connectToStrand`.
6
- *
7
- * Browser/Worker consumers should load `./plugin-browser` instead, which uses
8
- * the TCP-free libp2p entry and IndexedDB-backed default storage.
9
- */
10
-
11
- import type { Database, SqlValue } from '@quereus/quereus';
12
- import { connectToStrand } from './connect.js';
13
- import { parseConfig } from './parse-config.js';
14
- import type { SereusPluginResult } from './types.js';
15
-
16
- export { parseConfig };
17
-
18
- /**
19
- * Default export: Quereus plugin registration function.
20
- */
21
- export default async function register(
22
- db: Database,
23
- config: Record<string, SqlValue> = {},
24
- ): Promise<SereusPluginResult> {
25
- const options = parseConfig(config);
26
-
27
- // `storage_path` is a Node-only loader convenience: a `Record<string, SqlValue>`
28
- // cannot carry an `IRawStorage`, so the path is resolved to a concrete
29
- // `FileRawStorage` here (not in the platform-agnostic `parseConfig`). The
30
- // dynamic import keeps `@optimystic/db-p2p-storage-fs` (and its `node:fs`
31
- // dependency) out of the browser/RN module graph.
32
- const storagePath = config.storage_path;
33
- if (typeof storagePath === 'string' && storagePath) {
34
- const { FileRawStorage } = await import('@optimystic/db-p2p-storage-fs');
35
- options.storage = new FileRawStorage(storagePath);
36
- }
37
-
38
- return connectToStrand(db, options);
39
- }
1
+ /**
2
+ * Quereus plugin entry point for Sereus strand connections (Node).
3
+ *
4
+ * Loaded via Quoomb's `.plugin install` or config file, or via `registerPlugin()`.
5
+ * Parses SqlValue config and delegates to `connectToStrand`.
6
+ *
7
+ * Browser/Worker consumers should load `./plugin-browser` instead, which uses
8
+ * the TCP-free libp2p entry and IndexedDB-backed default storage.
9
+ */
10
+
11
+ import type { Database, SqlValue } from '@quereus/quereus';
12
+ import { connectToStrand } from './connect.js';
13
+ import { parseConfig } from './parse-config.js';
14
+ import type { SereusPluginResult } from './types.js';
15
+
16
+ export { parseConfig };
17
+
18
+ /**
19
+ * Default export: Quereus plugin registration function.
20
+ */
21
+ export default async function register(
22
+ db: Database,
23
+ config: Record<string, SqlValue> = {},
24
+ ): Promise<SereusPluginResult> {
25
+ const { options, storagePath } = parseConfig(config);
26
+
27
+ // The dynamic import keeps `@optimystic/db-p2p-storage-fs` (and its `node:fs`
28
+ // dependency) out of the browser/RN module graph.
29
+ if (storagePath) {
30
+ const { FileRawStorage } = await import('@optimystic/db-p2p-storage-fs');
31
+ options.storage = new FileRawStorage(storagePath);
32
+ }
33
+
34
+ return connectToStrand(db, options);
35
+ }