@siax/mobile-sync (0.1.0)
Installation
@siax:registry=https://git.cloud.siax.io/api/packages/sax3l/npm/npm install @siax/mobile-sync@0.1.0"@siax/mobile-sync": "0.1.0"About this package
@siax/mobile-sync
Offline-first lokalt lager och synkkö för K6-appar. Konfliktmodell per entitet krävs (OFFL-05).
Status: implementerad och testad. 69 tester, 69 gröna (node --test), plus strikt
typkontroll (tsc --noEmit, TypeScript 6). Noll runtime-beroenden.
Ingen tjänst är driftsatt och paketet är ännu inte publicerat till Giteas npm-registry.
Kärnbussgränsen (MOB-02)
Detta paket är en yta, inte en tjänst. Om du står i begrepp att implementera
något som ID0, INF0, ST0RE, N0D, AUD0, N0TIFY, B00K, CL0UD
eller 1P redan gör — bygg ytan, inte tjänsten.
Konkret i det här paketet:
| Beroende | Hur det konsumeras |
|---|---|
| Identitet (ID0) | getAuthToken() injiceras i transporten. Paketet läser aldrig en token själv. |
| Objektlager (ST0RE) | UploadTransport injiceras. Paketet talar inte med MinIO. |
| Audit (AUD0), analys (PostHog), fel (GlitchTip) | Paketet avger händelser via SyncObserver. Värdappen vidarebefordrar. |
| Synk-endpoint | Ägs av appens eget API (ARCHITECTURE.md §5). SyncTransport injiceras. |
Inga hemligheter i koden (SEC-01/MSEC-01): all konfiguration kommer in som parametrar.
Vad som FAKTISKT är byggt
| Kontroll | Status | Var |
|---|---|---|
| OFFL-01 lokalt först, fungerar i flygplansläge | ✅ | src/engine.ts — put/patch/remove/get/list rör aldrig nätet |
OFFL-02 SQLite bakom injicerbart SqliteDriver |
✅ | src/sqlite.ts, src/adapters/expo-sqlite.ts |
| OFFL-03 persistent kö som överlever krasch och återupptas | ✅ | src/queue.ts — lease + recoverAbandoned() |
| OFFL-04 klientgenererad idempotensnyckel per operation | ✅ | src/clock.ts, unik kolumn i siax_sync_queue |
| OFFL-05 konfliktmodell per entitet, typframtvingad | ✅ | src/conflict.ts — ConflictPolicyMap<TMap> med -? |
| OFFL-06 konflikter presenteras, aldrig tyst förlust | ✅ | src/store.ts konfliktjournal med discarded_doc |
| OFFL-07 status pending/syncing/done/failed + orsak | ✅ | engine.status() |
| OFFL-10 versionerade migrationer, klarar versionshopp | ✅ | src/migrations.ts, src/schema.ts (v1→v4) |
| OFFL-11 delta-synk med markör | ✅ | engine.pull(), CursorStore |
| OFFL-13 återupptagbara uppladdningar | ✅ | src/upload.ts |
Utöver kraven: FIFO med head-of-line blocking per post, exponentiell backoff med jitter, dead-letter med explicit återupptagning, och en HTTP-transport som översätter statuskoder till konflikt / retrybart / permanent avslag.
Vad som INTE är byggt
- Ingen synkserver. Endpointen ägs av respektive apps API. Kontraktet står i
docs/synk.md§5, klientsidan finns somcreateHttpSyncTransport(). - Ingen bakgrundsschemaläggning. Appen anropar
flush()/pull()själv (t.ex. vidAppState-övergång eller frånexpo-background-task). - Ingen kryptering av den lokala databasen. SQLCipher är driverns ansvar; nyckeln ska komma från enhetens keystore.
- Ingen exempelapp (MOB-13 kräver en per paket — den saknas fortfarande).
- Inte publicerat till
https://git.siax.io/api/packages/sax3l/npm/. - Ingen
op-sqlite-adapter. Bara expo-formen finns;op-sqlitegår att koppla in viawithSavepointTransactions()men det är inte gjort och inte testat. - Ingen komprimering eller batchning av push — en operation per HTTP-anrop.
Kom igång
import * as SQLite from 'expo-sqlite';
import { createSyncEngine, createExpoSqliteDriver, createHttpSyncTransport, defineConflictPolicies } from '@siax/mobile-sync';
type Note = { readonly id: string; readonly title: string; readonly body: string; readonly updatedAt: number };
type AppEntities = { readonly note: Note }; // OBS: `type`, inte `interface`
const engine = await createSyncEngine<AppEntities>({
driver: createExpoSqliteDriver(await SQLite.openDatabaseAsync('app.db')),
transport: createHttpSyncTransport({
baseUrl: process.env.EXPO_PUBLIC_API_URL ?? '', // NAMN, aldrig ett värde i koden
getAuthToken: () => auth.getAccessToken(), // från ID0 via @siax/mobile-auth
}),
conflicts: defineConflictPolicies<AppEntities>({
note: { kind: 'user-choice' }, // utelämna raden ⇒ kompileringsfel
}),
observer: { onEvent: (event) => telemetry.forward(event) }, // vidare till AUD0/PostHog
});
await engine.put('note', 'n1', { id: 'n1', title: 'Utkast', body: '', updatedAt: Date.now() });
await engine.flush(); // gör inget destruktivt om nätet är nere
await engine.pull('note');
const status = await engine.status();
for (const conflict of await engine.pendingConflicts()) {
// visa conflict.summary + conflict.fields, låt användaren välja
await engine.resolveConflict(conflict.conflictId, { kind: 'keep-local' });
}
Konfliktmodellerna, trådformatet, statusarna och migrationsreglerna dokumenteras i
docs/synk.md.
Tester
node --test "packages/mobile-sync/test/*.test.ts" # 69 tester
cd packages/mobile-sync && npm test # samma sak
cd packages/mobile-sync && npm run typecheck # tsc --noEmit, strict
Inga testberoenden: node:test + node:assert, Node 22+ kör .ts direkt via
type stripping. node:sqlite används i test/helpers/ som riktig SQLite-motor, så
testerna kör samma SQL som telefonen kommer att köra — inte en attrapp som håller med.
Node-fälla i den här miljön:
node --test <katalog>fungerar INTE på Node 24.14 här — katalogargumentet tolkas som en modul att köra (med enpackage.jsoni katalogen körsmainoch 1 tomt "test" rapporteras som grönt). Använd glob-formen ovan, ellernode --testutan argument från paketkatalogen (då räknas även de tre hjälpfilerna in och summan blir 72).
Testerna täcker bland annat: flygplansläge (skrivning + läsning utan nät), omstart mitt i en flush, tappat serversvar utan dubbel effekt, alla tre konfliktmodellerna genom hela motorn, användarval med bevarad förlorare, versionshopp 1→4 med datatransformationer, delta mot full hämtning, utgången markör, och avbruten uppladdning som återupptas av en ny instans från serverns offset.
test/types.check.ts är ett kompileringstest för OFFL-05: fyra @ts-expect-error-fall
som slutar kompilera om typskyddet försvinner. Det körs av npm run typecheck, inte av
testkörningen.