local-lite 0.0.24 → 0.0.25
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 +4 -3
- package/dist/config/index.js +1 -0
- package/dist/config/types.d.ts +7 -2
- package/dist/core/client.d.ts +4 -1
- package/dist/core/client.js +57 -0
- package/dist/core/types.d.ts +4 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -63,9 +63,10 @@ remain configurable; the existing default conflict strategy is last-write-wins.
|
|
|
63
63
|
`inMemoryRemote: true`; it must never silently acknowledge production writes.
|
|
64
64
|
- `autoSchema` now defaults to `false` in the configuration factory as well as the
|
|
65
65
|
core constructor. Set `autoSchema: true` to retain inferred schema behavior.
|
|
66
|
-
-
|
|
67
|
-
is deferred with `autoInitialize: false`.
|
|
68
|
-
|
|
66
|
+
- Versioned `schemaMigrations` run transactionally before sync starts, including
|
|
67
|
+
when initialization is deferred with `autoInitialize: false`. The existing
|
|
68
|
+
`migrations` option remains available for legacy idempotent SQL that runs on
|
|
69
|
+
every initialization.
|
|
69
70
|
- `initialize()` is safe to call repeatedly or concurrently. After `stop()`, call
|
|
70
71
|
`client.syncWorker.start()` to resume. Mutations after stop remain queued.
|
|
71
72
|
- `autoStart: false` prepares storage without automatic sync. Explicit push/pull
|
package/dist/config/index.js
CHANGED
|
@@ -85,6 +85,7 @@ async function createSyncClient(config) {
|
|
|
85
85
|
autoSchema,
|
|
86
86
|
tables: config.tables ?? [],
|
|
87
87
|
migrations: config.migrations,
|
|
88
|
+
schemaMigrations: config.schemaMigrations,
|
|
88
89
|
autoStart: config.autoStart,
|
|
89
90
|
jsonColumns: config.jsonColumns,
|
|
90
91
|
autoHydrateJSON: config.autoHydrateJSON,
|
package/dist/config/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LocalDBAdapter } from '../adapters/local/index.js';
|
|
2
2
|
import { RemoteDBAdapter } from '../adapters/remote/index.js';
|
|
3
|
-
import { ConflictResolver, ConflictStrategy, NetworkStatusProvider, RetryConfig } from '../core/types.js';
|
|
3
|
+
import { ConflictResolver, ConflictStrategy, NetworkStatusProvider, RetryConfig, SchemaMigration } from '../core/types.js';
|
|
4
4
|
import { SyncLogger, CustomLogger, LogLevel } from '../utils/logger.js';
|
|
5
5
|
export interface SupabaseConfigOptions {
|
|
6
6
|
url?: string;
|
|
@@ -84,10 +84,15 @@ export interface LocalLiteConfig {
|
|
|
84
84
|
*/
|
|
85
85
|
tables?: string[];
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
87
|
+
* Legacy idempotent SQL statements executed transactionally on every initialization.
|
|
88
88
|
* e.g. ["CREATE TABLE IF NOT EXISTS todos (id TEXT PRIMARY KEY, title TEXT);"]
|
|
89
89
|
*/
|
|
90
90
|
migrations?: string[];
|
|
91
|
+
/**
|
|
92
|
+
* Versioned application schema migrations. Versions must be consecutive integers
|
|
93
|
+
* starting at 1; each migration is applied once and recorded in schema_migrations.
|
|
94
|
+
*/
|
|
95
|
+
schemaMigrations?: SchemaMigration[];
|
|
91
96
|
/**
|
|
92
97
|
* Automatically create tables and columns dynamically on the fly without manual SQL migrations.
|
|
93
98
|
* Opt-in (default: false). When true, tables in `tables` are created automatically,
|
package/dist/core/client.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { EventEmitter } from '../reactive/event-emitter.js';
|
|
|
4
4
|
import { SyncWorker } from './sync-worker.js';
|
|
5
5
|
import { DatabaseMaintenance } from './maintenance.js';
|
|
6
6
|
import { SchemaManager } from './schema-manager.js';
|
|
7
|
-
import { BatchMutation, ConflictResolver, ConflictStrategy, MutateOptions, NetworkStatusProvider, OutboxRecord, OutboxStatus, QueryOptions, RetryConfig, SyncMetrics, SyncOp, TableChangeEvent, WhereFilter } from './types.js';
|
|
7
|
+
import { BatchMutation, ConflictResolver, ConflictStrategy, MutateOptions, NetworkStatusProvider, OutboxRecord, OutboxStatus, QueryOptions, RetryConfig, SchemaMigration, SyncMetrics, SyncOp, TableChangeEvent, WhereFilter } from './types.js';
|
|
8
8
|
import { SyncLogger, CustomLogger, LogLevel } from '../utils/logger.js';
|
|
9
9
|
import { QueryBuilder, AggregateSpec, AggregateResult } from './query-builder.js';
|
|
10
10
|
import { type ExportJsonOptions, type ImportJsonOptions, type ExportCsvOptions, type ImportCsvOptions, type ImportSummary } from '../utils/portability.js';
|
|
@@ -31,6 +31,7 @@ export interface LocalFirstClientConfig {
|
|
|
31
31
|
autoSchema?: boolean;
|
|
32
32
|
tables?: string[];
|
|
33
33
|
migrations?: string[];
|
|
34
|
+
schemaMigrations?: SchemaMigration[];
|
|
34
35
|
autoStart?: boolean;
|
|
35
36
|
coalesceOutbox?: boolean;
|
|
36
37
|
jsonColumns?: Record<string, string[]>;
|
|
@@ -108,6 +109,7 @@ export declare class LocalFirstClient {
|
|
|
108
109
|
readonly session: SessionManager;
|
|
109
110
|
private primaryKey;
|
|
110
111
|
private migrations;
|
|
112
|
+
private schemaMigrations;
|
|
111
113
|
private autoStart;
|
|
112
114
|
private initialization?;
|
|
113
115
|
constructor(config: LocalFirstClientConfig);
|
|
@@ -139,6 +141,7 @@ export declare class LocalFirstClient {
|
|
|
139
141
|
*/
|
|
140
142
|
initialize(): Promise<void>;
|
|
141
143
|
private initializeStorage;
|
|
144
|
+
private applySchemaMigrations;
|
|
142
145
|
/**
|
|
143
146
|
* Performs an atomic dual-write: mutates the local data table AND writes
|
|
144
147
|
* to sync_outbox within the same database transaction.
|
package/dist/core/client.js
CHANGED
|
@@ -21,6 +21,30 @@ const index_js_5 = require("../search/index.js");
|
|
|
21
21
|
const index_js_6 = require("../storage/index.js");
|
|
22
22
|
const index_js_7 = require("../scheduler/index.js");
|
|
23
23
|
const session_js_1 = require("../auth/session.js");
|
|
24
|
+
function normalizeSchemaMigrations(migrations) {
|
|
25
|
+
if (!Array.isArray(migrations)) {
|
|
26
|
+
throw new TypeError('schemaMigrations must be an array.');
|
|
27
|
+
}
|
|
28
|
+
if (migrations.some((migration) => !migration ||
|
|
29
|
+
typeof migration !== 'object' ||
|
|
30
|
+
!Number.isSafeInteger(migration.version) ||
|
|
31
|
+
migration.version < 1)) {
|
|
32
|
+
throw new TypeError('Each schema migration must have a positive integer version.');
|
|
33
|
+
}
|
|
34
|
+
const ordered = [...migrations].sort((a, b) => a.version - b.version);
|
|
35
|
+
return ordered.map((migration, index) => {
|
|
36
|
+
const expectedVersion = index + 1;
|
|
37
|
+
if (!migration || !Number.isSafeInteger(migration.version) || migration.version !== expectedVersion) {
|
|
38
|
+
throw new TypeError(`schemaMigrations must use consecutive integer versions starting at 1; expected version ${expectedVersion}.`);
|
|
39
|
+
}
|
|
40
|
+
const statements = Array.isArray(migration.up) ? migration.up : [migration.up];
|
|
41
|
+
if (statements.length === 0 ||
|
|
42
|
+
statements.some((statement) => typeof statement !== 'string' || statement.trim().length === 0)) {
|
|
43
|
+
throw new TypeError(`schemaMigrations version ${migration.version} must contain non-empty SQL.`);
|
|
44
|
+
}
|
|
45
|
+
return { version: migration.version, up: statements.map((statement) => statement.trim()) };
|
|
46
|
+
});
|
|
47
|
+
}
|
|
24
48
|
function hasFunctionFilter(value) {
|
|
25
49
|
if (typeof value === 'function')
|
|
26
50
|
return true;
|
|
@@ -182,6 +206,7 @@ class LocalFirstClient {
|
|
|
182
206
|
session;
|
|
183
207
|
primaryKey;
|
|
184
208
|
migrations;
|
|
209
|
+
schemaMigrations;
|
|
185
210
|
autoStart;
|
|
186
211
|
initialization;
|
|
187
212
|
constructor(config) {
|
|
@@ -193,6 +218,7 @@ class LocalFirstClient {
|
|
|
193
218
|
this.coalesceOutbox = config.coalesceOutbox ?? true;
|
|
194
219
|
this.tables = config.tables ?? [];
|
|
195
220
|
this.migrations = config.migrations ?? [];
|
|
221
|
+
this.schemaMigrations = normalizeSchemaMigrations(config.schemaMigrations ?? []);
|
|
196
222
|
this.autoStart = config.autoStart ?? true;
|
|
197
223
|
this.jsonColumns = config.jsonColumns ?? {};
|
|
198
224
|
this.autoHydrateJSON = config.autoHydrateJSON ?? false;
|
|
@@ -416,6 +442,7 @@ class LocalFirstClient {
|
|
|
416
442
|
}
|
|
417
443
|
});
|
|
418
444
|
}
|
|
445
|
+
await this.applySchemaMigrations();
|
|
419
446
|
if (this.autoSchema && this.tables.length > 0) {
|
|
420
447
|
for (const table of this.tables) {
|
|
421
448
|
await this.schemaManager.ensureTable(this.db, table, this.primaryKey);
|
|
@@ -424,6 +451,36 @@ class LocalFirstClient {
|
|
|
424
451
|
if (this.autoStart)
|
|
425
452
|
this.syncWorker.start();
|
|
426
453
|
}
|
|
454
|
+
async applySchemaMigrations() {
|
|
455
|
+
if (this.schemaMigrations.length === 0)
|
|
456
|
+
return;
|
|
457
|
+
await this.db.transaction(async (tx) => {
|
|
458
|
+
await tx.execute(`CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
459
|
+
version INTEGER PRIMARY KEY,
|
|
460
|
+
applied_at INTEGER NOT NULL
|
|
461
|
+
);`);
|
|
462
|
+
const rows = await tx.execute('SELECT version FROM schema_migrations ORDER BY version ASC');
|
|
463
|
+
const appliedVersions = rows.map((row) => Number(row.version));
|
|
464
|
+
for (let index = 0; index < appliedVersions.length; index += 1) {
|
|
465
|
+
if (appliedVersions[index] !== index + 1) {
|
|
466
|
+
throw new Error('The schema_migrations table contains a missing or invalid version.');
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
let currentVersion = appliedVersions.at(-1) ?? 0;
|
|
470
|
+
if (currentVersion > this.schemaMigrations.length) {
|
|
471
|
+
throw new Error(`Database schema version ${currentVersion} is newer than the latest available migration (${this.schemaMigrations.length}).`);
|
|
472
|
+
}
|
|
473
|
+
for (const migration of this.schemaMigrations) {
|
|
474
|
+
if (migration.version <= currentVersion)
|
|
475
|
+
continue;
|
|
476
|
+
for (const statement of migration.up) {
|
|
477
|
+
await tx.execute(statement);
|
|
478
|
+
}
|
|
479
|
+
await tx.execute('INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)', [migration.version, Date.now()]);
|
|
480
|
+
currentVersion = migration.version;
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
427
484
|
/**
|
|
428
485
|
* Performs an atomic dual-write: mutates the local data table AND writes
|
|
429
486
|
* to sync_outbox within the same database transaction.
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export type SyncOp = 'INSERT' | 'UPDATE' | 'DELETE';
|
|
2
2
|
export type OutboxStatus = 'PENDING' | 'PROCESSING' | 'FAILED';
|
|
3
|
+
export interface SchemaMigration {
|
|
4
|
+
version: number;
|
|
5
|
+
up: string | string[];
|
|
6
|
+
}
|
|
3
7
|
export interface SyncOperation {
|
|
4
8
|
id: string;
|
|
5
9
|
tableName: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "local-lite",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"description": "Never let a lost connection break a promise. Local-first sync that faithfully keeps data safe until you're back online.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -141,7 +141,8 @@
|
|
|
141
141
|
"build": "tsc",
|
|
142
142
|
"test": "vitest run",
|
|
143
143
|
"test:watch": "vitest",
|
|
144
|
-
"
|
|
144
|
+
"test:package": "node package-smoke.mjs",
|
|
145
|
+
"prepublishOnly": "npm run test && npm run build && npm run test:package"
|
|
145
146
|
},
|
|
146
147
|
"peerDependencies": {
|
|
147
148
|
"@libsql/client": "^0.6.0",
|