@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.
- package/README.md +28 -10
- package/bin/create.mjs +44 -1
- package/lib/scaffold.mjs +15 -1
- package/package.json +2 -2
- package/templates/f7-app/.claude/rules/database.md +39 -0
- package/templates/f7-app/.env.example +23 -0
- package/templates/f7-app/CLAUDE.md +8 -3
- package/templates/f7-app/auto-imports.d.ts +7 -0
- package/templates/f7-app/components.d.ts +2 -0
- package/templates/f7-app/package.json +6 -6
- package/templates/f7-app/src/app/pragmas.config.ts +51 -0
- package/templates/f7-app/src/app/storage.config.ts +55 -0
- package/templates/f7-app/src/domains/benchmark/benchmark.dataset.ts +209 -0
- package/templates/f7-app/src/domains/benchmark/benchmark.suite.ts +662 -0
- package/templates/f7-app/src/domains/sales/sales.repository.ts +1 -1
- package/templates/f7-app/src/env.d.ts +15 -0
- package/templates/f7-app/src/locales/en.json +37 -2
- package/templates/f7-app/src/locales/fr.json +37 -2
- package/templates/f7-app/src/main.ts +6 -16
- package/templates/f7-app/src/modules/demo/components/DemoBenchmark.vue +112 -0
- package/templates/f7-app/src/modules/demo/components/DemoPipelineBenchmark.vue +10 -0
- package/templates/f7-app/src/modules/demo/composables/useBenchmark.ts +139 -0
- package/templates/f7-app/src/modules/demo/composables/useReactiveDemo.ts +5 -1
- package/templates/f7-app/src/modules/demo/views/DemoView.vue +9 -2
- package/templates/f7-app/src/modules/settings/views/SettingsView.vue +31 -0
- package/templates/f7-app/src/plugins/bootstrapError.ts +60 -0
- package/templates/f7-app/src/shared/database/candidates/index.ts +3 -0
- package/templates/f7-app/src/shared/database/candidates/opfsSahPool.ts +25 -0
- package/templates/f7-app/src/shared/database/candidates/types.ts +41 -0
- package/templates/f7-app/src/shared/database/candidates/waSqlite.ts +58 -0
- package/templates/f7-app/src/shared/database/database.ts +151 -30
- package/templates/f7-app/src/shared/database/migrations.ts +143 -1
- package/templates/f7-app/src/shared/database/opfs.worker.ts +5 -0
- package/templates/f7-app/src/shared/database/schema.ts +94 -0
- package/templates/f7-app/src/shared/database/storage.ts +94 -0
- package/templates/f7-app/src/shared/database/wa.worker.ts +5 -0
- package/templates/f7-app/src/shared/utils/resolvers/resolvers.ts +1 -2
- package/templates/f7-app/tests/benchmark.suite.test.ts +104 -0
- package/templates/f7-app/tests/migrations.test.ts +74 -0
- package/templates/f7-app/tests/storage.test.ts +108 -0
- package/templates/f7-app/vite.config.ts +3 -1
- 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.
|
|
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
|
-
| `--
|
|
37
|
-
| `--
|
|
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({
|
|
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": "
|
|
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-
|
|
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
|
-
`
|
|
87
|
-
|
|
88
|
-
|
|
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.
|
|
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": "^
|
|
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);
|