@cavulsqa/create 0.1.2 → 2.2.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.
Files changed (42) hide show
  1. package/README.md +28 -10
  2. package/bin/create.mjs +44 -1
  3. package/lib/scaffold.mjs +15 -1
  4. package/package.json +2 -2
  5. package/templates/f7-app/.claude/rules/database.md +39 -0
  6. package/templates/f7-app/.env.example +23 -0
  7. package/templates/f7-app/CLAUDE.md +8 -3
  8. package/templates/f7-app/auto-imports.d.ts +7 -0
  9. package/templates/f7-app/components.d.ts +2 -0
  10. package/templates/f7-app/package.json +6 -6
  11. package/templates/f7-app/src/app/pragmas.config.ts +51 -0
  12. package/templates/f7-app/src/app/storage.config.ts +55 -0
  13. package/templates/f7-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
  14. package/templates/f7-app/src/domains/benchmark/benchmark.suite.ts +662 -0
  15. package/templates/f7-app/src/domains/sales/sales.repository.ts +1 -1
  16. package/templates/f7-app/src/env.d.ts +15 -0
  17. package/templates/f7-app/src/locales/en.json +37 -2
  18. package/templates/f7-app/src/locales/fr.json +37 -2
  19. package/templates/f7-app/src/main.ts +6 -16
  20. package/templates/f7-app/src/modules/demo/components/DemoBenchmark.vue +112 -0
  21. package/templates/f7-app/src/modules/demo/components/DemoPipelineBenchmark.vue +10 -0
  22. package/templates/f7-app/src/modules/demo/composables/useBenchmark.ts +139 -0
  23. package/templates/f7-app/src/modules/demo/composables/useReactiveDemo.ts +5 -1
  24. package/templates/f7-app/src/modules/demo/views/DemoView.vue +9 -2
  25. package/templates/f7-app/src/modules/settings/views/SettingsView.vue +31 -0
  26. package/templates/f7-app/src/plugins/bootstrapError.ts +60 -0
  27. package/templates/f7-app/src/shared/database/candidates/index.ts +3 -0
  28. package/templates/f7-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
  29. package/templates/f7-app/src/shared/database/candidates/types.ts +41 -0
  30. package/templates/f7-app/src/shared/database/candidates/waSqlite.ts +58 -0
  31. package/templates/f7-app/src/shared/database/database.ts +151 -30
  32. package/templates/f7-app/src/shared/database/migrations.ts +143 -1
  33. package/templates/f7-app/src/shared/database/opfs.worker.ts +5 -0
  34. package/templates/f7-app/src/shared/database/schema.ts +94 -0
  35. package/templates/f7-app/src/shared/database/storage.ts +94 -0
  36. package/templates/f7-app/src/shared/database/wa.worker.ts +5 -0
  37. package/templates/f7-app/src/shared/utils/resolvers/resolvers.ts +1 -2
  38. package/templates/f7-app/tests/benchmark.suite.test.ts +104 -0
  39. package/templates/f7-app/tests/migrations.test.ts +74 -0
  40. package/templates/f7-app/tests/storage.test.ts +108 -0
  41. package/templates/f7-app/vite.config.ts +3 -1
  42. package/templates/f7-app/tsconfig.node.json +0 -14
package/README.md CHANGED
@@ -23,18 +23,20 @@ nothing, pnpm never sees a package name, and you get a 404 for something like `c
23
23
  There is no unscoped `create-cavulsqa`, so `pnpm create cavulsqa` is a 404 as well.
24
24
 
25
25
  ```bash
26
- pnpm create @cavulsqa --name caputa --app-id com.sig.caputa --yes
26
+ pnpm create @cavulsqa --name caputa --app-id com.example.caputa --yes
27
27
  ```
28
28
 
29
- | flag | default | |
30
- | ------------ | ---------------- | ------------------------------------------------------ |
31
- | `--name` | asked | package and directory name |
32
- | `--template` | the only one | which bundled template |
33
- | `--dir` | `./<name>` | where to write it |
34
- | `--app-name` | `Name` | launcher name, window title, Settings screen |
35
- | `--app-id` | `com.ayb.<name>` | Android application id |
36
- | `--from` | — | a template directory on disk, instead of a bundled one |
37
- | `--yes` | — | take the defaults, ask nothing |
29
+ | flag | default | |
30
+ | ------------ | ---------------- | ------------------------------------------------------- |
31
+ | `--name` | asked | package and directory name |
32
+ | `--template` | the only one | which bundled template |
33
+ | `--dir` | `./<name>` | where to write it |
34
+ | `--app-name` | `Name` | launcher name, window title, Settings screen |
35
+ | `--app-id` | `com.ayb.<name>` | Android application id |
36
+ | `--engine` | asked | which storage engine the app prefers, written to `.env` |
37
+ | `--pragmas` | `safe` | `safe` keeps durability, `fast` trades it for speed |
38
+ | `--from` | — | a template directory on disk, instead of a bundled one |
39
+ | `--yes` | — | take the defaults, ask nothing |
38
40
 
39
41
  ## What you get
40
42
 
@@ -51,6 +53,22 @@ nothing in a screen asks for a refresh.
51
53
  repository knows the architecture, the conventions, and the traps that have already cost someone a
52
54
  day.
53
55
 
56
+ ## Choosing an engine
57
+
58
+ `--engine` writes a `.env`, it does not edit the config. The chain in `src/app/storage.config.ts`
59
+ stays intact and the chosen engine is simply promoted to the front, so a device that cannot open it
60
+ still falls back rather than failing.
61
+
62
+ | id | when |
63
+ | ------------------------------ | --------------------------------------------------------------------------------------------- |
64
+ | `sqlite-wasm-opfs-sahpool` | the default. The SQLite team's own build; faster at writes, transactions and seeding |
65
+ | `wa-sqlite-access-handle-pool` | faster at joins and scans on measured hardware; a single-maintainer project |
66
+ | `wa-sqlite-opfs-async` | wa-sqlite over OPFS without the pool, on the Asyncify build |
67
+ | `wa-sqlite-idb-batch-atomic` | SQLite pages in IndexedDB. Slowest, and the only one that needs no synchronous access handles |
68
+
69
+ The ids come from the template itself, so a template that offers a different set is asked about
70
+ correctly rather than validated against a list kept here.
71
+
54
72
  ## Versions
55
73
 
56
74
  The template's dependencies are resolved and pinned when this package is published, so a given
package/bin/create.mjs CHANGED
@@ -42,6 +42,8 @@ Options:
42
42
  --dir PATH where to write it (default: ./<name>)
43
43
  --app-name NAME launcher name and window title (default: Name)
44
44
  --app-id ID android application id (default: com.ayb.<name>)
45
+ --engine ID which storage engine the app prefers, written to .env
46
+ --pragmas PROFILE safe (default) or fast
45
47
  --from PATH use a template directory on disk instead of the bundled ones
46
48
  --yes take the defaults, ask nothing
47
49
  --help this
@@ -65,6 +67,25 @@ function listTemplates() {
65
67
  }));
66
68
  }
67
69
 
70
+ /**
71
+ * The engine ids a template actually offers, read out of its own candidate list rather than kept in
72
+ * a second list here - a generator that knows more engines than the template does is a generator
73
+ * that writes a `.env` the app ignores.
74
+ */
75
+ function listEngines(templateDir) {
76
+ try {
77
+ const source = readFileSync(
78
+ join(templateDir, "src/shared/database/candidates/types.ts"),
79
+ "utf8",
80
+ );
81
+ const union = /export type StorageId =([\s\S]*?);/.exec(source)?.[1] ?? "";
82
+ return [...union.matchAll(/"([a-z0-9-]+)"/g)].map((match) => match[1]);
83
+ } catch {
84
+ // A template without the candidate module simply has no engine choice to offer.
85
+ return [];
86
+ }
87
+ }
88
+
68
89
  /** npm package names: lowercase, no spaces, no leading dot or underscore. */
69
90
  function validName(value) {
70
91
  return /^(?:@[a-z0-9-*~][a-z0-9-*._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/.test(value);
@@ -112,6 +133,20 @@ async function main() {
112
133
  const appId =
113
134
  flags.get("app-id") ?? (await ask("Android application id", `com.ayb.${bareName}`));
114
135
 
136
+ const engines = listEngines(join(BUNDLED, template));
137
+ let engine = flags.get("engine");
138
+ if (typeof engine !== "string" && engines.length > 1) {
139
+ engine = await ask(`Storage engine [${engines.join(", ")}]`, engines[0]);
140
+ }
141
+ if (typeof engine === "string" && engines.length && !engines.includes(engine)) {
142
+ throw new Error(`unknown engine "${engine}"; this template offers: ${engines.join(", ")}`);
143
+ }
144
+
145
+ const pragmas = flags.get("pragmas");
146
+ if (typeof pragmas === "string" && pragmas !== "safe" && pragmas !== "fast") {
147
+ throw new Error(`--pragmas must be "safe" or "fast", not "${pragmas}"`);
148
+ }
149
+
115
150
  const dir = flags.get("dir") ?? (await ask("Directory", `./${name}`));
116
151
  const out = isAbsolute(dir) ? dir : resolve(process.cwd(), dir);
117
152
 
@@ -119,7 +154,15 @@ async function main() {
119
154
  const templateDir =
120
155
  typeof from === "string" ? resolve(process.cwd(), from) : join(BUNDLED, template);
121
156
 
122
- scaffold({ templateDir, out, name, appId, appName: String(appName) });
157
+ scaffold({
158
+ templateDir,
159
+ out,
160
+ name,
161
+ appId,
162
+ appName: String(appName),
163
+ engine: typeof engine === "string" ? engine : undefined,
164
+ pragmas: typeof pragmas === "string" ? pragmas : undefined,
165
+ });
123
166
 
124
167
  console.log(`
125
168
  ${appName} created in ${out}
package/lib/scaffold.mjs CHANGED
@@ -58,7 +58,7 @@ function copyTree(from, to, transform) {
58
58
  * The template's dependencies are already concrete - `bundleTemplates.mjs` resolved them when the
59
59
  * creator was packed - so nothing here has to know about workspaces or catalogs.
60
60
  */
61
- export function scaffold({ templateDir, out, name, appId, appName }) {
61
+ export function scaffold({ templateDir, out, name, appId, appName, engine, pragmas }) {
62
62
  if (!existsSync(templateDir)) throw new Error(`no template at ${templateDir}`);
63
63
  if (existsSync(out)) throw new Error(`${out} already exists`);
64
64
 
@@ -73,6 +73,20 @@ export function scaffold({ templateDir, out, name, appId, appName }) {
73
73
  return personalise(entry, text, { name: identity, appName });
74
74
  });
75
75
 
76
+ /**
77
+ * A real `.env` when the generator was told which engine to prefer.
78
+ *
79
+ * Written rather than editing `storage.config.ts`, because the config is code someone will read
80
+ * and change, while the choice of engine for one deployment is configuration. `.env.example` is
81
+ * copied in by the template and documents every value.
82
+ */
83
+ if (engine || pragmas) {
84
+ const lines = ["# Written by @cavulsqa/create. See .env.example for what these mean.", ""];
85
+ if (engine) lines.push(`VITE_STORAGE_ENGINE=${engine}`);
86
+ if (pragmas) lines.push(`VITE_PRAGMA_PROFILE=${pragmas}`);
87
+ writeFileSync(join(out, ".env"), `${lines.join("\n")}\n`);
88
+ }
89
+
76
90
  // pnpm will not finish an install while a dependency's build script is neither allowed nor
77
91
  // denied, and vite-plus pulls esbuild in. Without this every generated app fails its first
78
92
  // `pnpm install` with ERR_PNPM_IGNORED_BUILDS.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cavulsqa/create",
3
- "version": "0.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "Create a Vue + Framework7 + Capacitor + SQLite app from the cavulsqa templates.",
5
5
  "keywords": [
6
6
  "capacitor",
@@ -52,7 +52,7 @@
52
52
  }
53
53
  ]
54
54
  },
55
- "templatesFingerprint": "sha256-7SiAd2fNFtbBqL3RPdjFpJEM9CeKmmPWj/oet2rfeGk=",
55
+ "templatesFingerprint": "sha256-dj3zUrK7OwVRIwWJcrX/7pxRo/LsGJDBMolhJNIihUs=",
56
56
  "scripts": {
57
57
  "build": "node scripts/bundleTemplates.mjs",
58
58
  "check": "vp check",
@@ -63,3 +63,42 @@ A schema change needs a test in `tests/` that runs the migration and the affecte
63
63
  sql.js. `tests/sales.repository.test.ts` is the pattern: build a Kysely on `createSqlJsDialect()`,
64
64
  migrate, then assert on real rows — including the arithmetic. A total that type-checks can still be
65
65
  computed wrong.
66
+
67
+ ## Writing a lot of rows
68
+
69
+ Measured on a phone, at 100k rows, per row written:
70
+
71
+ | how | per row |
72
+ | ------------------------------------------------ | -------------- |
73
+ | one insert per statement, each its own commit | 8-13 ms |
74
+ | one insert per statement, inside one transaction | ~0.45 ms |
75
+ | multi-row insert, ~150 rows per statement | 0.066-0.115 ms |
76
+
77
+ Roughly a hundredfold between the worst and best way to write the same row. SQLite caps parameters
78
+ per statement, so 150 rows of five columns is about the practical ceiling for one insert - chunk by
79
+ parameter budget, not by a round number.
80
+
81
+ ## Why a big write freezes the screen, and what to do
82
+
83
+ The database runs in one worker, and that worker is serial. A read cannot overtake a write already
84
+ in flight; it waits for everything queued ahead of it. So the cost to the UI is not how fast the
85
+ write is, it is **how much work the write committed to before the read arrived**.
86
+
87
+ Time a screen's read waits when it lands during a 1000-row write:
88
+
89
+ | write strategy | read waits |
90
+ | ---------------------------------------------- | ------------- |
91
+ | 1000 single inserts in one transaction | ~350-690 ms |
92
+ | 150 rows per statement, one transaction | ~36-53 ms |
93
+ | ten transactions of 100, awaited one at a time | **~21-32 ms** |
94
+
95
+ A naive loop stalls the screen for most of a second. Chunked transactions bring it under the
96
+ threshold anyone notices, and the reason is mechanical: awaiting each chunk means only one chunk is
97
+ ever queued, so an arriving read waits for 100 rows instead of 1000.
98
+
99
+ **So: write in chunks of about a hundred rows, use multi-row inserts inside each chunk, and await
100
+ each chunk before starting the next.** Do not wrap a thousand rows in one transaction to be fast -
101
+ it is faster in total and far worse for anyone looking at the screen while it runs.
102
+
103
+ The Diagnostics benchmark measures all three strategies, so this is checkable on any device rather
104
+ than taken on faith.
@@ -0,0 +1,23 @@
1
+ # Which SQLite engine the app opens, and what it falls back to.
2
+ #
3
+ # Copy this file to `.env` and edit. Everything here is baked in at build time - Vite substitutes
4
+ # `import.meta.env` when it bundles - so changing it means rebuilding, not restarting.
5
+
6
+ # The engine tried first. The rest of the chain in src/app/storage.config.ts still follows it as
7
+ # fallback, so this reorders rather than restricts.
8
+ #
9
+ # sqlite-wasm-opfs-sahpool official @sqlite.org/sqlite-wasm, OPFS pool. Faster at writes and
10
+ # transactions, seeds ~1.8x quicker, and it is the SQLite team's own
11
+ # build. The default for those reasons.
12
+ # wa-sqlite-access-handle-pool wa-sqlite, OPFS pool. Faster at joins and scans on measured
13
+ # hardware; a single-maintainer project.
14
+ # wa-sqlite-opfs-async wa-sqlite over OPFS without the pool, on the Asyncify build.
15
+ # wa-sqlite-idb-batch-atomic SQLite pages in IndexedDB. The slowest, and the only one that works
16
+ # without synchronous access handles - the option for a WebView
17
+ # between Chromium 86 and 108.
18
+ VITE_STORAGE_ENGINE=sqlite-wasm-opfs-sahpool
19
+
20
+ # PRAGMA profile. `safe` keeps SQLite's durability guarantee; `fast` trades it for speed by handing
21
+ # the flush decision to the OS, which is right for a benchmark and wrong for data a person would
22
+ # miss. Batched writes cost almost the same either way.
23
+ VITE_PRAGMA_PROFILE=safe
@@ -83,9 +83,14 @@ vp test # the repository and icon tests
83
83
  pnpm type-check # vue-tsc, the gate for anything touching .vue
84
84
  ```
85
85
 
86
- `vue-tsc` and `vp check` disagree by design: `vp check` compiles `vite.config.ts` under a separate
87
- tsconfig because vite-plus bundles its own Vite while the unplugins take it as a peer. Both must
88
- pass.
86
+ `vp check` is the type gate for `vite.config.ts`; `vue-tsc` covers `src`. Both must pass.
87
+
88
+ There is deliberately no `tsconfig.node.json`. A `tsc` project over `vite.config.ts` cannot be
89
+ clean either way: with `skipLibCheck` on, Vite's deeply recursive `PluginOption` union blows the
90
+ comparison depth limit on the plugin array; with it off, vite-plus-core's own declarations fail on
91
+ optional peers it does not ship. Nothing in this repository runs `tsc`, and an editor with no
92
+ project reports the file clean - so the config only ever added a red squiggle and a wrong
93
+ explanation. `vp check` resolves it correctly and is the authority.
89
94
 
90
95
  **Type-checking is not verification for UI.** A screen that compiles can still render an empty
91
96
  box — that is how seven invisible icons and a blank-page bootstrap both shipped. If you changed
@@ -103,6 +103,7 @@ declare global {
103
103
  const refManualReset: typeof import('@vueuse/core').refManualReset
104
104
  const refThrottled: typeof import('@vueuse/core').refThrottled
105
105
  const refWithControl: typeof import('@vueuse/core').refWithControl
106
+ const renderBootstrapError: typeof import('./src/plugins/bootstrapError').renderBootstrapError
106
107
  const request: typeof import('framework7/lite').request
107
108
  const resolveComponent: typeof import('vue').resolveComponent
108
109
  const seedPlugin: typeof import('./src/plugins/seed.plugin').seedPlugin
@@ -152,6 +153,7 @@ declare global {
152
153
  const useAttrs: typeof import('vue').useAttrs
153
154
  const useBase64: typeof import('@vueuse/core').useBase64
154
155
  const useBattery: typeof import('@vueuse/core').useBattery
156
+ const useBenchmark: typeof import('./src/modules/demo/composables/useBenchmark').useBenchmark
155
157
  const useBluetooth: typeof import('@vueuse/core').useBluetooth
156
158
  const useBreakpoints: typeof import('@vueuse/core').useBreakpoints
157
159
  const useBroadcastChannel: typeof import('@vueuse/core').useBroadcastChannel
@@ -336,6 +338,9 @@ declare global {
336
338
  export type { TabbarVisibility } from './src/shared/composables/useTabbarVisibility'
337
339
  import('./src/shared/composables/useTabbarVisibility')
338
340
  // @ts-ignore
341
+ export type { CaseComparison } from './src/modules/demo/composables/useBenchmark'
342
+ import('./src/modules/demo/composables/useBenchmark')
343
+ // @ts-ignore
339
344
  export type { BusEntry, PipelineResult } from './src/modules/demo/composables/useReactiveDemo'
340
345
  import('./src/modules/demo/composables/useReactiveDemo')
341
346
  // @ts-ignore
@@ -443,6 +448,7 @@ declare module 'vue' {
443
448
  readonly refManualReset: UnwrapRef<typeof import('@vueuse/core')['refManualReset']>
444
449
  readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
445
450
  readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
451
+ readonly renderBootstrapError: UnwrapRef<typeof import('./src/plugins/bootstrapError')['renderBootstrapError']>
446
452
  readonly request: UnwrapRef<typeof import('framework7/lite')['request']>
447
453
  readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
448
454
  readonly seedPlugin: UnwrapRef<typeof import('./src/plugins/seed.plugin')['seedPlugin']>
@@ -492,6 +498,7 @@ declare module 'vue' {
492
498
  readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
493
499
  readonly useBase64: UnwrapRef<typeof import('@vueuse/core')['useBase64']>
494
500
  readonly useBattery: UnwrapRef<typeof import('@vueuse/core')['useBattery']>
501
+ readonly useBenchmark: UnwrapRef<typeof import('./src/modules/demo/composables/useBenchmark')['useBenchmark']>
495
502
  readonly useBluetooth: UnwrapRef<typeof import('@vueuse/core')['useBluetooth']>
496
503
  readonly useBreakpoints: UnwrapRef<typeof import('@vueuse/core')['useBreakpoints']>
497
504
  readonly useBroadcastChannel: UnwrapRef<typeof import('@vueuse/core')['useBroadcastChannel']>
@@ -12,6 +12,7 @@ export {}
12
12
  declare module 'vue' {
13
13
  export interface GlobalComponents {
14
14
  404: typeof import('./src/shared/components/error/404.vue')['default']
15
+ DemoBenchmark: typeof import('./src/modules/demo/components/DemoBenchmark.vue')['default']
15
16
  DemoBusLog: typeof import('./src/modules/demo/components/DemoBusLog.vue')['default']
16
17
  DemoCreateOrderSheet: typeof import('./src/modules/demo/components/DemoCreateOrderSheet.vue')['default']
17
18
  DemoOrderList: typeof import('./src/modules/demo/components/DemoOrderList.vue')['default']
@@ -47,6 +48,7 @@ declare module 'vue' {
47
48
  F7Tabs: typeof import('framework7-vue')['f7Tabs']
48
49
  F7Toggle: typeof import('framework7-vue')['f7Toggle']
49
50
  F7Toolbar: typeof import('framework7-vue')['f7Toolbar']
51
+ F7ToolbarPane: typeof import('framework7-vue')['f7ToolbarPane']
50
52
  F7View: typeof import('framework7-vue')['f7View']
51
53
  F7Views: typeof import('framework7-vue')['f7Views']
52
54
  FeatureDetailView: typeof import('./src/modules/home/views/FeatureDetailView.vue')['default']
@@ -12,7 +12,6 @@
12
12
  "check": "vp check"
13
13
  },
14
14
  "dependencies": {
15
- "@capacitor-community/sqlite": "^7.0.2",
16
15
  "@capacitor/android": "^8.5.0",
17
16
  "@capacitor/app": "^8.0.0",
18
17
  "@capacitor/core": "^8.0.0",
@@ -20,24 +19,25 @@
20
19
  "@capacitor/preferences": "^8.0.0",
21
20
  "@capacitor/splash-screen": "^8.0.0",
22
21
  "@capacitor/status-bar": "^8.0.0",
23
- "@cavulsqa/mobile-db": "^0.3.0",
22
+ "@cavulsqa/mobile-db": "^0.5.0",
24
23
  "@cavulsqa/reactive-db": "^0.2.0",
25
24
  "@cavulsqa/reactive-vue": "^0.2.0",
25
+ "@sqlite.org/sqlite-wasm": "3.53.0-build1",
26
26
  "@tailwindcss/vite": "^4.3.3",
27
27
  "@vueuse/core": "^14.4.0",
28
28
  "framework7": "^9.0.2",
29
29
  "framework7-icons": "^5.0.5",
30
- "framework7-vue": "^8.3.4",
30
+ "framework7-vue": "^9.1.2",
31
31
  "kysely": "^0.29.5",
32
- "sql.js": "1.13.0",
33
32
  "tailwindcss": "^4.3.3",
34
33
  "vue": "^3.5.0",
35
- "vue-i18n": "^11.2.2"
34
+ "vue-i18n": "^11.2.2",
35
+ "wa-sqlite": "^1.0.0"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@capacitor/cli": "^8.0.0",
39
39
  "@intlify/unplugin-vue-i18n": "^11.2.5",
40
- "@types/node": "^24",
40
+ "@types/node": "^24.12.2",
41
41
  "@vitejs/plugin-vue": "^6.0.3",
42
42
  "sql.js": "1.13.0",
43
43
  "typescript": "^5.9.3",
@@ -0,0 +1,51 @@
1
+ /**
2
+ * PRAGMAs applied to every engine, identically, right after it opens.
3
+ *
4
+ * They live here rather than inside a candidate for one reason: SQLite's defaults are per-build, and
5
+ * two engines left on their own defaults are not comparable. Measuring them without pinning this
6
+ * produced a 1.47x "ranking" that may have been nothing but a difference in `synchronous`.
7
+ *
8
+ * `fast` is what a benchmark should use - both engines pushed as hard as they go, so the comparison
9
+ * is between engines rather than between journal settings. It is not a safe default for an app that
10
+ * holds a day's orders: `synchronous = OFF` means the OS, not SQLite, decides when bytes reach
11
+ * storage, so a crash or a battery pull can leave the database corrupt rather than merely stale.
12
+ *
13
+ * `safe` keeps SQLite's durability guarantee and pays for it on single writes, which the benchmark
14
+ * measures at 19-57 ms depending on engine. Both settings batch equally well - inside one
15
+ * transaction the cost per row falls under 3 ms either way - so an app that batches its writes gives
16
+ * up very little by staying safe.
17
+ */
18
+ export type PragmaProfile = "fast" | "safe";
19
+
20
+ const PROFILES: Record<PragmaProfile, readonly string[]> = {
21
+ fast: [
22
+ // No journal file to create, write and delete per transaction.
23
+ "PRAGMA journal_mode = MEMORY",
24
+ // The big one for single writes, and the one that trades away crash durability.
25
+ "PRAGMA synchronous = OFF",
26
+ "PRAGMA temp_store = MEMORY",
27
+ // Negative means KiB rather than pages: 16 MB of page cache.
28
+ "PRAGMA cache_size = -16000",
29
+ ],
30
+ safe: [
31
+ "PRAGMA journal_mode = TRUNCATE",
32
+ "PRAGMA synchronous = FULL",
33
+ "PRAGMA temp_store = MEMORY",
34
+ "PRAGMA cache_size = -16000",
35
+ ],
36
+ };
37
+
38
+ /**
39
+ * From `VITE_PRAGMA_PROFILE`, defaulting to `safe`.
40
+ *
41
+ * Safe by default because the cost is small once writes are batched - inside one transaction the
42
+ * per-row difference is under a millisecond - and the failure it prevents is a corrupt database
43
+ * rather than a slow one. Set `fast` when measuring, where the point is to compare engines rather
44
+ * than journal settings.
45
+ */
46
+ export const pragmaProfile: PragmaProfile =
47
+ import.meta.env.VITE_PRAGMA_PROFILE === "fast" ? "fast" : "safe";
48
+
49
+ export function pragmasFor(profile: PragmaProfile): readonly string[] {
50
+ return PROFILES[profile];
51
+ }
@@ -0,0 +1,55 @@
1
+ import {
2
+ opfsSahPool,
3
+ waAccessHandlePool,
4
+ waIdbBatchAtomic,
5
+ waOriginPrivateFileSystem,
6
+ type StorageCandidate,
7
+ } from "@/shared/database/candidates";
8
+
9
+ /**
10
+ * Which SQLite implementation this app uses, and what it falls back to.
11
+ *
12
+ * This is the knob. The list is walked in order: each candidate is asked whether the device supports
13
+ * it, then asked to open; the first that succeeds wins, and the rest are never imported - so a chain
14
+ * that stops at the first entry never downloads wa-sqlite's wasm at all.
15
+ *
16
+ * Ordered fastest-first, and that ordering is a claim worth being precise about. Only
17
+ * `sqlite-wasm-opfs-sahpool` has been measured on a phone; the three below it are placed on the
18
+ * vendor's own description of them and each says so through `evidence`. The Diagnostics benchmark
19
+ * exists to replace that guess with numbers.
20
+ *
21
+ * Reorder freely - this is the supported way to trade speed for reach:
22
+ *
23
+ * - Compatibility first: put `waIdbBatchAtomic` at the top. IndexedDB is the only durable route on a
24
+ * WebView between Chromium 86 and 108, which runs this bundle but has no synchronous access
25
+ * handles. Slowest, works almost everywhere.
26
+ * - One vendor only: drop `opfsSahPool` and keep the wa-sqlite three, or the reverse. Both engines
27
+ * implement the same dialect contract, so nothing above this file changes either way.
28
+ *
29
+ * There is deliberately no in-memory entry. A chain that silently ends somewhere data is not kept is
30
+ * worse than one that fails and says why, and the error screen names every attempt.
31
+ */
32
+ const DEFAULT_ORDER: StorageCandidate[] = [
33
+ opfsSahPool,
34
+ waAccessHandlePool,
35
+ waOriginPrivateFileSystem,
36
+ waIdbBatchAtomic,
37
+ ];
38
+
39
+ /**
40
+ * `VITE_STORAGE_ENGINE` promotes one candidate to the front and leaves the rest as fallback, so the
41
+ * env var reorders the chain rather than replacing it - a device that cannot open the preferred
42
+ * engine still gets a working app.
43
+ *
44
+ * An unknown id is ignored rather than fatal: a typo in a `.env` should not brick the build's
45
+ * output, and the app reports which candidate actually opened in Settings.
46
+ */
47
+ function preferredFirst(candidates: StorageCandidate[]): StorageCandidate[] {
48
+ const preferred = import.meta.env.VITE_STORAGE_ENGINE;
49
+ if (!preferred) return candidates;
50
+
51
+ const match = candidates.find((candidate) => candidate.id === preferred);
52
+ return match ? [match, ...candidates.filter((candidate) => candidate !== match)] : candidates;
53
+ }
54
+
55
+ export const storageChain: StorageCandidate[] = preferredFirst(DEFAULT_ORDER);