@trylight/app 0.3.6 → 0.3.7
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 +8 -6
- package/dist/db/cli.js +12 -9
- package/dist/db/locks.d.ts +13 -0
- package/dist/db/locks.js +35 -0
- package/dist/invoke/contracts.d.ts +1 -1
- package/dist/schema.d.ts +1 -1
- package/package.json +12 -10
package/README.md
CHANGED
|
@@ -91,10 +91,12 @@ drizzle-kit's own layout) and applied, whatever it does to the rows there (a
|
|
|
91
91
|
removed column's values are archived first, below); one the rows refuse is taken back out,
|
|
92
92
|
so the change stays pending rather than blocking every later migrate. Changed
|
|
93
93
|
indexes and constraints are dropped before the columns they cover and enums
|
|
94
|
-
only after, so the two halves apply in order. Syncs and migrates each hold a
|
|
95
|
-
|
|
96
|
-
overlap, and a process that dies releases it.
|
|
97
|
-
|
|
94
|
+
only after, so the two halves apply in order. Syncs and migrates each hold a
|
|
95
|
+
Postgres advisory lock, so the dev server's watcher and an agent's own sync
|
|
96
|
+
never overlap, and a process that dies releases it. A wait for the lock is
|
|
97
|
+
bounded (`LOCK_WAIT_MS` in `src/db/locks.ts`), then fails naming its holder.
|
|
98
|
+
`migrate` applies the journal; in an app's sandbox the agent's shell asks
|
|
99
|
+
the platform's migrate socket to run it. `check` (in `pnpm build`)
|
|
98
100
|
fails while `db/schema` has changes no migration holds.
|
|
99
101
|
|
|
100
102
|
## Recoverable deletes
|
|
@@ -103,13 +105,13 @@ fails while `db/schema` has changes no migration holds.
|
|
|
103
105
|
that records each insert, update, delete and truncate in `light_changes` (the
|
|
104
106
|
new row, `{ column: { from, to } }`, or the old row), whoever wrote it: the
|
|
105
107
|
actor is `light.actor_kind/_id/_name`, which the entities route and the platform's
|
|
106
|
-
`
|
|
108
|
+
`light db write` set per transaction, or else the database role. A migration
|
|
107
109
|
that drops a table or column or changes a column's type first copies what it
|
|
108
110
|
destroys into `light_archive` (`light_archive_values`, called at the top of
|
|
109
111
|
the migration, so in its transaction) and the migrate prints "Archived N
|
|
110
112
|
values from table.column". Both tables refuse UPDATE, DELETE and TRUNCATE (a guard against mistakes:
|
|
111
113
|
the owner role can still drop the trigger). DDL an agent runs itself (`drop
|
|
112
|
-
table` through `
|
|
114
|
+
table` through `light db write`) is not archived.
|
|
113
115
|
`migrate` installs the functions before applying migrations and the triggers
|
|
114
116
|
after; the template's AGENTS.md has the restore SQL.
|
|
115
117
|
|
package/dist/db/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ import { migrate } from "drizzle-orm/postgres-js/migrator";
|
|
|
10
10
|
import postgres from "postgres";
|
|
11
11
|
import { z } from "zod/v4";
|
|
12
12
|
import { archiveNotices, entityTables, prepareAudit, trackTables } from "./audit.js";
|
|
13
|
+
import { LockWaitError, takeLock } from "./locks.js";
|
|
13
14
|
import { MIGRATIONS_DIR, SCHEMA_ENTRY, plan, unwriteMigration, writeMigration } from "./migrations.js";
|
|
14
15
|
import { env } from "../server/env.js";
|
|
15
16
|
/**
|
|
@@ -18,13 +19,9 @@ import { env } from "../server/env.js";
|
|
|
18
19
|
* credential, behind this socket.
|
|
19
20
|
*/
|
|
20
21
|
const PLATFORM_MIGRATE = "/run/mii/migrate.sock";
|
|
21
|
-
/** Postgres advisory locks: one migrate, and one sync, of a database at a time. */
|
|
22
|
-
const MIGRATE_LOCK_KEY = 4_815_162_342;
|
|
23
|
-
const SYNC_LOCK_KEY = 4_815_162_343;
|
|
24
22
|
/** Exit code of a migrate whose migrations committed but whose change tracking (triggers, index) didn't go on. */
|
|
25
23
|
const APPLIED_UNTRACKED = 2;
|
|
26
|
-
//
|
|
27
|
-
// Only on a direct connection: behind a transaction pooler the lock stays on a
|
|
24
|
+
// Session locks (./locks.ts) only on a direct connection: behind a transaction pooler the lock stays on a
|
|
28
25
|
// pooled server connection after the process exits, and every later sync waits.
|
|
29
26
|
const connect = (onnotice = () => undefined) => postgres(unpooledDatabaseUrl(), { max: 1, onnotice });
|
|
30
27
|
const root = process.cwd();
|
|
@@ -50,7 +47,7 @@ function unpooledDatabaseUrl() {
|
|
|
50
47
|
async function sync() {
|
|
51
48
|
const client = connect();
|
|
52
49
|
try {
|
|
53
|
-
await client
|
|
50
|
+
await takeLock(client, "sync");
|
|
54
51
|
const planned = await plan(root, await loadSchema());
|
|
55
52
|
if (planned.statements.length === 0)
|
|
56
53
|
return await applyMigrations();
|
|
@@ -64,7 +61,7 @@ async function sync() {
|
|
|
64
61
|
});
|
|
65
62
|
if (applied !== 0 && applied !== APPLIED_UNTRACKED) {
|
|
66
63
|
unwriteMigration(root, tag);
|
|
67
|
-
console.error(`${MIGRATIONS_DIR}/${tag}.sql didn't apply, so it was taken back;
|
|
64
|
+
console.error(`${MIGRATIONS_DIR}/${tag}.sql didn't apply, so it was taken back; sync again once the error above is dealt with.`);
|
|
68
65
|
}
|
|
69
66
|
return applied;
|
|
70
67
|
}
|
|
@@ -93,7 +90,7 @@ async function applyMigrations() {
|
|
|
93
90
|
const archived = archiveNotices();
|
|
94
91
|
const client = connect(archived.onnotice);
|
|
95
92
|
try {
|
|
96
|
-
await client
|
|
93
|
+
await takeLock(client, "migrate");
|
|
97
94
|
const tables = entityTables(await loadSchema());
|
|
98
95
|
await prepareAudit(client);
|
|
99
96
|
await migrate(drizzle(client), { migrationsFolder: join(root, MIGRATIONS_DIR) });
|
|
@@ -128,4 +125,10 @@ async function migrateThroughPlatform() {
|
|
|
128
125
|
// Last, so every const above is initialized before a command reads it.
|
|
129
126
|
const commands = { sync, migrate: applyMigrations, check };
|
|
130
127
|
const usage = z.enum(["sync", "migrate", "check"], { error: "Usage: light-db sync | migrate | check" });
|
|
131
|
-
|
|
128
|
+
const exitCode = await commands[usage.parse(command)]().catch((error) => {
|
|
129
|
+
if (!(error instanceof LockWaitError))
|
|
130
|
+
throw error;
|
|
131
|
+
console.error(error.message);
|
|
132
|
+
return 1;
|
|
133
|
+
});
|
|
134
|
+
process.exit(exitCode);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import postgres from "postgres";
|
|
2
|
+
/** Postgres advisory locks: one migrate, and one sync, of a database at a time. */
|
|
3
|
+
export declare const LOCKS: {
|
|
4
|
+
migrate: number;
|
|
5
|
+
sync: number;
|
|
6
|
+
};
|
|
7
|
+
/** How long a sync or migrate waits for the one before it, then fails naming who holds the lock. */
|
|
8
|
+
export declare const LOCK_WAIT_MS = 30000;
|
|
9
|
+
/** A sync or migrate gave up waiting for its lock; the message says who holds it. */
|
|
10
|
+
export declare class LockWaitError extends Error {
|
|
11
|
+
}
|
|
12
|
+
/** Takes `command`'s session lock, or throws naming its holder after `LOCK_WAIT_MS`. */
|
|
13
|
+
export declare function takeLock(client: postgres.Sql, command: keyof typeof LOCKS): Promise<void>;
|
package/dist/db/locks.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import postgres from "postgres";
|
|
2
|
+
import { z } from "zod/v4";
|
|
3
|
+
/** Postgres advisory locks: one migrate, and one sync, of a database at a time. */
|
|
4
|
+
export const LOCKS = { migrate: 4_815_162_342, sync: 4_815_162_343 };
|
|
5
|
+
/** How long a sync or migrate waits for the one before it, then fails naming who holds the lock. */
|
|
6
|
+
export const LOCK_WAIT_MS = 30_000;
|
|
7
|
+
const LOCK_NOT_AVAILABLE = "55P03";
|
|
8
|
+
// Exclusive, so at most one backend holds it.
|
|
9
|
+
const holderRows = z.array(z.object({ pid: z.number() })).max(1);
|
|
10
|
+
/** A sync or migrate gave up waiting for its lock; the message says who holds it. */
|
|
11
|
+
export class LockWaitError extends Error {
|
|
12
|
+
}
|
|
13
|
+
/** Takes `command`'s session lock, or throws naming its holder after `LOCK_WAIT_MS`. */
|
|
14
|
+
export async function takeLock(client, command) {
|
|
15
|
+
const key = LOCKS[command];
|
|
16
|
+
try {
|
|
17
|
+
await client.begin(async (tx) => {
|
|
18
|
+
await tx `select set_config('lock_timeout', ${`${LOCK_WAIT_MS}ms`}, true)`;
|
|
19
|
+
await tx `select pg_advisory_lock(${key})`;
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (!(error instanceof postgres.PostgresError) || error.code !== LOCK_NOT_AVAILABLE)
|
|
24
|
+
throw error;
|
|
25
|
+
const [holder] = holderRows.parse(await client `
|
|
26
|
+
select pid from pg_locks
|
|
27
|
+
where locktype = 'advisory' and granted and objsubid = 1
|
|
28
|
+
and database = (select oid from pg_database where datname = current_database())
|
|
29
|
+
and ((classid::bigint << 32) | objid::bigint) = ${key}`);
|
|
30
|
+
const waited = `light-db ${command}: waited ${LOCK_WAIT_MS / 1000}s for the ${command} lock`;
|
|
31
|
+
if (holder === undefined)
|
|
32
|
+
throw new LockWaitError(`${waited}; it came free just now, so run it again.`);
|
|
33
|
+
throw new LockWaitError(`${waited}, held by backend ${holder.pid}; if no other sync or migrate is running, the database's owner ends it with pg_terminate_backend(${holder.pid}).`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { z } from "zod/v4";
|
|
2
2
|
type ObjectSchema = z.ZodType<Record<string, unknown>, Record<string, unknown>>;
|
|
3
3
|
export type Contract = {
|
|
4
|
-
/** The id `
|
|
4
|
+
/** The id `light contract create` printed. */
|
|
5
5
|
readonly id: string;
|
|
6
6
|
readonly input: ObjectSchema;
|
|
7
7
|
readonly output: ObjectSchema;
|
package/dist/schema.d.ts
CHANGED
|
@@ -17,7 +17,7 @@ export type FieldSources = Partial<Record<string, FieldSource>>;
|
|
|
17
17
|
* Who may use a table through `app.table(...)`. Reads always go through the
|
|
18
18
|
* app's door: members only, or anyone with the link on a public app. Writes
|
|
19
19
|
* are either people signed in to the app ("door") or only the org's agents,
|
|
20
|
-
* through `
|
|
20
|
+
* through `light db write` ("agents").
|
|
21
21
|
*/
|
|
22
22
|
export type Access = {
|
|
23
23
|
readonly read: "door";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trylight/app",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.7",
|
|
4
4
|
"description": "The Light SDK every v2 org app depends on: entities (Drizzle tables with a typed client), sign-in, contracts and in-app chat.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"url": "https://github.com/shubh9/mii.git",
|
|
13
13
|
"directory": "packages/app-sdk"
|
|
14
14
|
},
|
|
15
|
+
"packageManager": "pnpm@11.0.9",
|
|
15
16
|
"files": [
|
|
16
17
|
"dist",
|
|
17
18
|
"bin"
|
|
@@ -41,6 +42,15 @@
|
|
|
41
42
|
"default": "./dist/vite.js"
|
|
42
43
|
}
|
|
43
44
|
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"lint": "eslint src test --max-warnings 0",
|
|
49
|
+
"test": "node --test --import tsx \"test/**/*.test.ts\"",
|
|
50
|
+
"check": "tsx scripts/write-brand.ts --check && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build",
|
|
51
|
+
"prepack": "pnpm run build",
|
|
52
|
+
"brand": "tsx scripts/write-brand.ts"
|
|
53
|
+
},
|
|
44
54
|
"peerDependencies": {
|
|
45
55
|
"@tanstack/react-query": ">=5.69",
|
|
46
56
|
"@tanstack/react-router": ">=1.170",
|
|
@@ -74,13 +84,5 @@
|
|
|
74
84
|
"typescript-eslint": "^8.70.1",
|
|
75
85
|
"vite": "^8.2.0",
|
|
76
86
|
"zod": "^4.6.5"
|
|
77
|
-
},
|
|
78
|
-
"scripts": {
|
|
79
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
80
|
-
"typecheck": "tsc --noEmit",
|
|
81
|
-
"lint": "eslint src test --max-warnings 0",
|
|
82
|
-
"test": "node --test --import tsx \"test/**/*.test.ts\"",
|
|
83
|
-
"check": "tsx scripts/write-brand.ts --check && pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build",
|
|
84
|
-
"brand": "tsx scripts/write-brand.ts"
|
|
85
87
|
}
|
|
86
|
-
}
|
|
88
|
+
}
|