@supatype/cli 0.1.4 → 0.1.5
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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +98 -96
- package/.turbo/turbo-typecheck.log +1 -1
- package/dist/app-vite-env.d.ts +4 -0
- package/dist/app-vite-env.d.ts.map +1 -0
- package/dist/app-vite-env.js +22 -0
- package/dist/app-vite-env.js.map +1 -0
- package/dist/binary-cache.d.ts +7 -5
- package/dist/binary-cache.d.ts.map +1 -1
- package/dist/binary-cache.js +28 -10
- package/dist/binary-cache.js.map +1 -1
- package/dist/cli-standalone.d.ts +10 -0
- package/dist/cli-standalone.d.ts.map +1 -0
- package/dist/cli-standalone.js +56 -0
- package/dist/cli-standalone.js.map +1 -0
- package/dist/commands/dev.d.ts.map +1 -1
- package/dist/commands/dev.js +34 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +3 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/keys.d.ts.map +1 -1
- package/dist/commands/keys.js +14 -0
- package/dist/commands/keys.js.map +1 -1
- package/dist/commands/self-update.d.ts +1 -1
- package/dist/commands/self-update.d.ts.map +1 -1
- package/dist/commands/self-update.js +33 -9
- package/dist/commands/self-update.js.map +1 -1
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +9 -1
- package/dist/commands/update.js.map +1 -1
- package/dist/components.d.ts +2 -2
- package/dist/components.d.ts.map +1 -1
- package/dist/components.js +2 -2
- package/dist/components.js.map +1 -1
- package/dist/dev-compose.d.ts.map +1 -1
- package/dist/dev-compose.js +120 -12
- package/dist/dev-compose.js.map +1 -1
- package/dist/project-config.d.ts +2 -0
- package/dist/project-config.d.ts.map +1 -1
- package/dist/project-config.js.map +1 -1
- package/dist/realtime-launch.d.ts +11 -0
- package/dist/realtime-launch.d.ts.map +1 -0
- package/dist/realtime-launch.js +39 -0
- package/dist/realtime-launch.js.map +1 -0
- package/dist/required-host-components.js +1 -1
- package/dist/required-host-components.js.map +1 -1
- package/dist/route-manifest.d.ts +3 -0
- package/dist/route-manifest.d.ts.map +1 -0
- package/dist/route-manifest.js +15 -0
- package/dist/route-manifest.js.map +1 -0
- package/dist/schema-push-lock.d.ts +17 -0
- package/dist/schema-push-lock.d.ts.map +1 -0
- package/dist/schema-push-lock.js +106 -0
- package/dist/schema-push-lock.js.map +1 -0
- package/dist/self-host-compose.d.ts.map +1 -1
- package/dist/self-host-compose.js +18 -0
- package/dist/self-host-compose.js.map +1 -1
- package/dist/type-extractor.d.ts.map +1 -1
- package/dist/type-extractor.js +125 -13
- package/dist/type-extractor.js.map +1 -1
- package/package.json +1 -1
- package/scripts/build-release-bundle.mjs +27 -0
- package/src/app-vite-env.ts +27 -0
- package/src/binary-cache.ts +29 -11
- package/src/cli-standalone.ts +62 -0
- package/src/commands/dev.ts +38 -0
- package/src/commands/init.ts +3 -1
- package/src/commands/keys.ts +14 -0
- package/src/commands/self-update.ts +34 -13
- package/src/commands/update.ts +9 -1
- package/src/components.ts +2 -2
- package/src/dev-compose.ts +144 -14
- package/src/project-config.ts +2 -0
- package/src/realtime-launch.ts +52 -0
- package/src/required-host-components.ts +1 -1
- package/src/route-manifest.ts +17 -0
- package/src/schema-push-lock.ts +131 -0
- package/src/self-host-compose.ts +18 -0
- package/src/type-extractor.ts +156 -13
- package/tests/realtime-launch.test.ts +25 -0
- package/tests/required-host-components.test.ts +2 -0
- package/tests/route-manifest.test.ts +29 -0
- package/tests/type-extractor.test.ts +52 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
* supatype self-update — Phase 10.6C8.
|
|
3
3
|
*
|
|
4
4
|
* npm-installed CLI: instruct to use `npm update -g`.
|
|
5
|
-
* Standalone
|
|
5
|
+
* Standalone curl|sh installs: replace binary from releases.supatype.com/cli/.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Command } from "commander"
|
|
9
|
+
import { copyFileSync, renameSync } from "node:fs"
|
|
9
10
|
import { sep } from "node:path"
|
|
10
|
-
import {
|
|
11
|
+
import { downloadStandaloneCli, fetchStandaloneCliLatestVersion } from "../cli-standalone.js"
|
|
12
|
+
import { error, info, plain } from "../ui/messages.js"
|
|
11
13
|
|
|
12
14
|
function looksLikeNpmOrWorkspaceCLI(): boolean {
|
|
13
15
|
const main = process.argv[1] ?? ""
|
|
@@ -23,24 +25,43 @@ function looksLikeNpmOrWorkspaceCLI(): boolean {
|
|
|
23
25
|
export function registerSelfUpdate(program: Command): void {
|
|
24
26
|
program
|
|
25
27
|
.command("self-update")
|
|
26
|
-
.description(
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
.action(() => {
|
|
28
|
+
.description("Update the supatype CLI (npm or standalone CDN binary)")
|
|
29
|
+
.action(async () => {
|
|
30
30
|
if (looksLikeNpmOrWorkspaceCLI()) {
|
|
31
31
|
plain(
|
|
32
32
|
"This CLI was installed via npm (or runs from the package workspace).\n" +
|
|
33
33
|
"Update with:\n\n npm update -g @supatype/cli\n\n" +
|
|
34
|
-
"To bump engine/server/postgres/deno pinned in supatype.config.ts, use:\n\n supatype update\n",
|
|
34
|
+
"To bump engine/server/postgres/deno/realtime pinned in supatype.config.ts, use:\n\n supatype update\n",
|
|
35
35
|
)
|
|
36
36
|
return
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
const currentPath = process.argv[1]
|
|
40
|
+
if (!currentPath) {
|
|
41
|
+
error("Could not determine current CLI path.")
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const latest = await fetchStandaloneCliLatestVersion()
|
|
47
|
+
info(`Downloading supatype CLI v${latest}...`)
|
|
48
|
+
const downloaded = await downloadStandaloneCli(latest)
|
|
49
|
+
const backup = `${currentPath}.bak`
|
|
50
|
+
copyFileSync(currentPath, backup)
|
|
51
|
+
try {
|
|
52
|
+
renameSync(downloaded, currentPath)
|
|
53
|
+
} catch (err) {
|
|
54
|
+
copyFileSync(downloaded, currentPath)
|
|
55
|
+
}
|
|
56
|
+
info(`Updated to v${latest}.`)
|
|
57
|
+
plain("Run `supatype --version` to verify.")
|
|
58
|
+
} catch (err) {
|
|
59
|
+
error(
|
|
60
|
+
`Standalone CLI self-update failed: ${(err as Error).message}\n\n` +
|
|
61
|
+
"Install or upgrade via npm:\n npm install -g @supatype/cli\n\n" +
|
|
62
|
+
"Component binaries (engine, server, postgres, deno, realtime):\n supatype update\n",
|
|
63
|
+
)
|
|
64
|
+
process.exit(1)
|
|
65
|
+
}
|
|
45
66
|
})
|
|
46
67
|
}
|
package/src/commands/update.ts
CHANGED
|
@@ -59,7 +59,7 @@ export function registerUpdate(program: Command): void {
|
|
|
59
59
|
|
|
60
60
|
const platform = currentPlatform()
|
|
61
61
|
|
|
62
|
-
const components: Component[] = ["engine", "server", "postgres", "deno"]
|
|
62
|
+
const components: Component[] = ["engine", "server", "postgres", "deno", "realtime"]
|
|
63
63
|
const updates: Array<{ component: Component; from: string; to: string }> = []
|
|
64
64
|
|
|
65
65
|
info("Fetching latest component versions from CDN...")
|
|
@@ -67,6 +67,14 @@ export function registerUpdate(program: Command): void {
|
|
|
67
67
|
|
|
68
68
|
for (const component of components) {
|
|
69
69
|
const latest = latestVersions[component]
|
|
70
|
+
if (!latest) {
|
|
71
|
+
if (component === "realtime") {
|
|
72
|
+
info(`Skipping ${component}: not published on CDN yet`)
|
|
73
|
+
continue
|
|
74
|
+
}
|
|
75
|
+
error(`No latest version for ${component} on CDN`)
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
70
78
|
const current = pinnedVersion(component, config)
|
|
71
79
|
if (!current) {
|
|
72
80
|
if (opts.check) {
|
package/src/components.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** CDN-cached binaries resolved by the CLI (`engine`, `server`, `postgres`, `deno`). */
|
|
2
|
-
export const BINARY_COMPONENTS = ["engine", "server", "postgres", "deno"] as const
|
|
1
|
+
/** CDN-cached binaries resolved by the CLI (`engine`, `server`, `postgres`, `deno`, `realtime`). */
|
|
2
|
+
export const BINARY_COMPONENTS = ["engine", "server", "postgres", "deno", "realtime"] as const
|
|
3
3
|
|
|
4
4
|
export type Component = (typeof BINARY_COMPONENTS)[number]
|
|
5
5
|
|
package/src/dev-compose.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { dirname, join, resolve } from "node:path"
|
|
|
8
8
|
import { spawnSync } from "node:child_process"
|
|
9
9
|
import { startProxyDevApp, resolveProxyDevScript } from "./app/proxy-dev-app.js"
|
|
10
10
|
import { loadSchemaAst } from "./config.js"
|
|
11
|
+
import { withComposeSchemaPushLock } from "./schema-push-lock.js"
|
|
11
12
|
import {
|
|
12
13
|
COMPOSE_DEV_KONG_PORT,
|
|
13
14
|
connectionString,
|
|
@@ -236,12 +237,69 @@ async function waitComposeHealthy(paths: SelfHostComposePaths, cwd: string, maxM
|
|
|
236
237
|
throw new Error("Compose db service did not become healthy in time")
|
|
237
238
|
}
|
|
238
239
|
|
|
240
|
+
/** True when the named compose service has a running container. */
|
|
241
|
+
function composeServiceIsRunning(
|
|
242
|
+
paths: SelfHostComposePaths,
|
|
243
|
+
cwd: string,
|
|
244
|
+
composeProject: string,
|
|
245
|
+
service: string,
|
|
246
|
+
): boolean {
|
|
247
|
+
const envFile = join(cwd, ".env")
|
|
248
|
+
const args = ["compose", "-p", composeProject, "--project-directory", cwd, "-f", paths.composePath]
|
|
249
|
+
if (existsSync(envFile)) args.push("--env-file", envFile)
|
|
250
|
+
args.push("ps", "-q", "--status", "running", service)
|
|
251
|
+
const result = spawnSync("docker", args, { cwd, encoding: "utf8" })
|
|
252
|
+
return result.status === 0 && typeof result.stdout === "string" && result.stdout.trim() !== ""
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Capture Postgres container logs before compose down destroys them.
|
|
257
|
+
* Writes `.supatype/ci-logs/db-*.log` and prints a tail for CI job logs.
|
|
258
|
+
*/
|
|
259
|
+
function dumpComposeDbLogs(
|
|
260
|
+
paths: SelfHostComposePaths,
|
|
261
|
+
cwd: string,
|
|
262
|
+
composeProject: string,
|
|
263
|
+
reason: string,
|
|
264
|
+
): void {
|
|
265
|
+
const logDir = join(cwd, ".supatype", "ci-logs")
|
|
266
|
+
mkdirSync(logDir, { recursive: true })
|
|
267
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
268
|
+
const logPath = join(logDir, `db-${stamp}.log`)
|
|
269
|
+
|
|
270
|
+
const envFile = join(cwd, ".env")
|
|
271
|
+
const args = ["compose", "-p", composeProject, "--project-directory", cwd, "-f", paths.composePath]
|
|
272
|
+
if (existsSync(envFile)) args.push("--env-file", envFile)
|
|
273
|
+
args.push("logs", "--no-color", "--timestamps", "--tail", "800", "db")
|
|
274
|
+
|
|
275
|
+
const result = spawnSync("docker", args, {
|
|
276
|
+
cwd,
|
|
277
|
+
encoding: "utf8",
|
|
278
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
279
|
+
})
|
|
280
|
+
const body = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim()
|
|
281
|
+
const content = body || `(empty — docker compose logs db exit ${result.status ?? "?"})\n`
|
|
282
|
+
try {
|
|
283
|
+
writeFileSync(logPath, content)
|
|
284
|
+
} catch {
|
|
285
|
+
/* best-effort */
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const lines = content.split("\n")
|
|
289
|
+
const tail = lines.slice(-200).join("\n")
|
|
290
|
+
console.error(`[supatype] Postgres logs after ${reason} (saved ${logPath}):`)
|
|
291
|
+
console.error(tail)
|
|
292
|
+
}
|
|
293
|
+
|
|
239
294
|
async function waitKongReady(kongPort: number, maxSec: number): Promise<void> {
|
|
240
295
|
const base = `http://localhost:${kongPort}`
|
|
241
296
|
for (let i = 0; i < maxSec; i++) {
|
|
242
297
|
try {
|
|
243
|
-
const
|
|
244
|
-
|
|
298
|
+
const [auth, realtime] = await Promise.all([
|
|
299
|
+
fetch(`${base}/auth/v1/health`),
|
|
300
|
+
fetch(`${base}/realtime/v1/health`),
|
|
301
|
+
])
|
|
302
|
+
if (auth.ok && realtime.ok) return
|
|
245
303
|
} catch {
|
|
246
304
|
/* retry */
|
|
247
305
|
}
|
|
@@ -405,12 +463,20 @@ async function runComposeSchemaPush(
|
|
|
405
463
|
|
|
406
464
|
console.log("[supatype] Applying schema via compose schema-engine...")
|
|
407
465
|
const sources = writeSchemaSourcePushArtifacts(cwd)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
466
|
+
const runPush = async () => {
|
|
467
|
+
let result = await runComposeEnginePush(paths, cwd, composeProject, config, sources)
|
|
468
|
+
// Windows Docker bind mounts can lag briefly after the host write.
|
|
469
|
+
if (result.status !== 0) {
|
|
470
|
+
await new Promise((r) => setTimeout(r, 250))
|
|
471
|
+
result = await runComposeEnginePush(paths, cwd, composeProject, config, sources)
|
|
472
|
+
}
|
|
473
|
+
return result
|
|
413
474
|
}
|
|
475
|
+
// B: only hold the advisory lock when realtime is already decoding — first-boot
|
|
476
|
+
// push (db only) must not take the lock; that path crashed under lock+DDL in CI.
|
|
477
|
+
const push = composeServiceIsRunning(paths, cwd, composeProject, "realtime")
|
|
478
|
+
? await withComposeSchemaPushLock(paths, cwd, composeProject, runPush)
|
|
479
|
+
: await runPush()
|
|
414
480
|
if (push.status !== 0) {
|
|
415
481
|
_lastFailedAst = astJson
|
|
416
482
|
const detail = filterComposeNoise(push.output) || push.output
|
|
@@ -750,23 +816,71 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
750
816
|
await recoverStaleDevSession(cwd)
|
|
751
817
|
await handleComposeProjectRename(cwd, config.project.name, paths)
|
|
752
818
|
|
|
753
|
-
console.log("[supatype] Bringing up
|
|
754
|
-
const
|
|
819
|
+
console.log("[supatype] Bringing up Postgres (compose db)...")
|
|
820
|
+
const upDbStatus = runDockerCompose(paths.composePath, ["up", "-d", "db"], cwd, project, {
|
|
755
821
|
quiet: true,
|
|
756
822
|
brand: devBrand,
|
|
757
823
|
})
|
|
758
|
-
if (
|
|
824
|
+
if (upDbStatus !== 0) {
|
|
759
825
|
endDevSession()
|
|
760
|
-
exitComposeFailed(
|
|
826
|
+
exitComposeFailed(upDbStatus, "Could not start Postgres (compose db service).", devBrand)
|
|
761
827
|
}
|
|
762
828
|
|
|
763
829
|
console.log("[supatype] Waiting for Postgres (compose)...")
|
|
764
830
|
await waitComposeHealthy(paths, cwd, 180_000, project)
|
|
831
|
+
// Settle before DDL — pg_isready can pass slightly before the instance is stable.
|
|
832
|
+
await new Promise((r) => setTimeout(r, 3000))
|
|
765
833
|
|
|
834
|
+
// A: apply schema before realtime (and the rest of the stack) starts decoding WAL.
|
|
766
835
|
const schemaPath = schemaPathFromProject(config, cwd)
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
836
|
+
{
|
|
837
|
+
const maxAttempts = 3
|
|
838
|
+
let lastErr: unknown
|
|
839
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
840
|
+
try {
|
|
841
|
+
await runComposeSchemaPush(cwd, config, paths, schemaPath, project)
|
|
842
|
+
lastErr = undefined
|
|
843
|
+
break
|
|
844
|
+
} catch (e: unknown) {
|
|
845
|
+
lastErr = e
|
|
846
|
+
console.error(
|
|
847
|
+
`[supatype] Initial schema push failed (attempt ${attempt}/${maxAttempts}):`,
|
|
848
|
+
(e as Error).message,
|
|
849
|
+
)
|
|
850
|
+
dumpComposeDbLogs(paths, cwd, project, `schema push attempt ${attempt}/${maxAttempts}`)
|
|
851
|
+
if (attempt < maxAttempts) {
|
|
852
|
+
console.log("[supatype] Resetting Postgres after failed schema push...")
|
|
853
|
+
runDockerCompose(paths.composePath, ["down", "-v"], cwd, project, {
|
|
854
|
+
quiet: true,
|
|
855
|
+
brand: devBrand,
|
|
856
|
+
})
|
|
857
|
+
runDockerCompose(paths.composePath, ["up", "-d", "db"], cwd, project, {
|
|
858
|
+
quiet: true,
|
|
859
|
+
brand: devBrand,
|
|
860
|
+
})
|
|
861
|
+
await waitComposeHealthy(paths, cwd, 120_000, project)
|
|
862
|
+
await new Promise((r) => setTimeout(r, 3000 * attempt))
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
if (lastErr) {
|
|
867
|
+
dumpComposeDbLogs(paths, cwd, project, "initial schema push exhausted")
|
|
868
|
+
endDevSession()
|
|
869
|
+
throw new Error(
|
|
870
|
+
`Initial schema push failed after ${maxAttempts} attempts: ${(lastErr as Error).message}`,
|
|
871
|
+
)
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
console.log("[supatype] Bringing up Docker Compose services...")
|
|
876
|
+
const upStatus = runDockerCompose(paths.composePath, ["up", "-d"], cwd, project, {
|
|
877
|
+
quiet: true,
|
|
878
|
+
brand: devBrand,
|
|
879
|
+
})
|
|
880
|
+
if (upStatus !== 0) {
|
|
881
|
+
endDevSession()
|
|
882
|
+
exitComposeFailed(upStatus, "Could not start the local Compose stack.", devBrand)
|
|
883
|
+
}
|
|
770
884
|
|
|
771
885
|
if (localServerImage !== undefined) {
|
|
772
886
|
console.log("[supatype] Recreating server with local image...")
|
|
@@ -783,6 +897,22 @@ export async function runDevCompose(cwd: string, config: SupatypeProjectConfig,
|
|
|
783
897
|
}
|
|
784
898
|
}
|
|
785
899
|
|
|
900
|
+
const pinnedRealtimeImage = readEnvValue(cwd, "SUPATYPE_REALTIME_IMAGE", "").trim()
|
|
901
|
+
if (pinnedRealtimeImage !== "") {
|
|
902
|
+
console.log("[supatype] Recreating realtime with pinned image...")
|
|
903
|
+
const rtStatus = runDockerCompose(
|
|
904
|
+
paths.composePath,
|
|
905
|
+
["up", "-d", "--force-recreate", "--no-deps", "realtime"],
|
|
906
|
+
cwd,
|
|
907
|
+
project,
|
|
908
|
+
{ quiet: true, brand: devBrand },
|
|
909
|
+
)
|
|
910
|
+
if (rtStatus !== 0) {
|
|
911
|
+
endDevSession()
|
|
912
|
+
exitComposeFailed(rtStatus, "Could not recreate the realtime container.", devBrand)
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
786
916
|
console.log("[supatype] Waiting for API gateway...")
|
|
787
917
|
await waitKongReady(kongPort, 120)
|
|
788
918
|
console.log("[supatype] Waiting for storage API...")
|
package/src/project-config.ts
CHANGED
|
@@ -116,6 +116,8 @@ export interface SupatypeProjectConfig {
|
|
|
116
116
|
postgres_dir?: string
|
|
117
117
|
/** Path to a local deno binary. */
|
|
118
118
|
deno?: string
|
|
119
|
+
/** Path to a local supatype-realtime binary (or node entry script). */
|
|
120
|
+
realtime?: string
|
|
119
121
|
/** Path to the @supatype/studio package directory (starts Vite dev server). */
|
|
120
122
|
studio?: string
|
|
121
123
|
/** Path to a local PostgREST binary. */
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { dirname, join, resolve } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import { ensureBinary } from "./ensure-binary.js"
|
|
5
|
+
import type { SupatypeProjectConfig } from "./project-config.js"
|
|
6
|
+
|
|
7
|
+
export interface RealtimeLaunchSpec {
|
|
8
|
+
bin: string
|
|
9
|
+
args: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CLI_PKG_DIR = dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
|
|
14
|
+
/** Monorepo contributor fallback: packages/realtime/dist/index.js relative to CLI package. */
|
|
15
|
+
function monorepoRealtimeEntry(): string | null {
|
|
16
|
+
const candidate = resolve(CLI_PKG_DIR, "../../realtime/dist/index.js")
|
|
17
|
+
return existsSync(candidate) ? candidate : null
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolve how to spawn supatype-realtime for native dev.
|
|
22
|
+
* Order: overrides.realtime → CDN binary → node + monorepo dist.
|
|
23
|
+
*/
|
|
24
|
+
export async function resolveRealtimeLaunch(
|
|
25
|
+
config: SupatypeProjectConfig,
|
|
26
|
+
cwd: string,
|
|
27
|
+
): Promise<RealtimeLaunchSpec> {
|
|
28
|
+
const override = config.overrides?.realtime?.trim()
|
|
29
|
+
if (override) {
|
|
30
|
+
const resolved = resolve(cwd, override)
|
|
31
|
+
if (!existsSync(resolved)) {
|
|
32
|
+
throw new Error(`[overrides] realtime path does not exist: ${resolved}`)
|
|
33
|
+
}
|
|
34
|
+
if (resolved.endsWith(".js") || resolved.endsWith(".mjs") || resolved.endsWith(".cjs")) {
|
|
35
|
+
return { bin: process.execPath, args: [resolved] }
|
|
36
|
+
}
|
|
37
|
+
return { bin: resolved, args: [] }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const bin = await ensureBinary("realtime", config)
|
|
42
|
+
return { bin, args: [] }
|
|
43
|
+
} catch {
|
|
44
|
+
const entry = monorepoRealtimeEntry()
|
|
45
|
+
if (entry) {
|
|
46
|
+
return { bin: process.execPath, args: [entry] }
|
|
47
|
+
}
|
|
48
|
+
throw new Error(
|
|
49
|
+
"realtime binary not available — run `supatype update` or set overrides.realtime in supatype.config.ts",
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -23,7 +23,7 @@ export function requiredHostComponents(
|
|
|
23
23
|
return ["engine"]
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
const components: Component[] = ["engine", "server", "postgres"]
|
|
26
|
+
const components: Component[] = ["engine", "server", "postgres", "realtime"]
|
|
27
27
|
const hasFunctions = functionsPathCandidatesFromProject(config, cwd).some((dir) =>
|
|
28
28
|
existsSync(dir),
|
|
29
29
|
)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs"
|
|
2
|
+
|
|
3
|
+
/** Merge fields into `.supatype/manifest.json` (creates file if missing). */
|
|
4
|
+
export function patchRouteManifest(
|
|
5
|
+
manifestPath: string,
|
|
6
|
+
patch: Record<string, unknown>,
|
|
7
|
+
): void {
|
|
8
|
+
let manifest: Record<string, unknown> = {}
|
|
9
|
+
if (existsSync(manifestPath)) {
|
|
10
|
+
try {
|
|
11
|
+
manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as Record<string, unknown>
|
|
12
|
+
} catch {
|
|
13
|
+
manifest = {}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
writeFileSync(manifestPath, `${JSON.stringify({ ...manifest, ...patch }, null, 2)}\n`, "utf8")
|
|
17
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Advisory lock held while compose schema push runs so realtime can skip WAL
|
|
3
|
+
* decoding without stopping the realtime process (self-host safe).
|
|
4
|
+
*
|
|
5
|
+
* Must stay in sync with:
|
|
6
|
+
* - packages/realtime/src/schema-push-lock.ts
|
|
7
|
+
* - supatype-schema-engine (pg_advisory_xact_lock on the same keys)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync } from "node:fs"
|
|
11
|
+
import { resolve } from "node:path"
|
|
12
|
+
import { spawn, spawnSync, type ChildProcess } from "node:child_process"
|
|
13
|
+
import type { SelfHostComposePaths } from "./self-host-compose.js"
|
|
14
|
+
import { readEnvValue } from "./env-file.js"
|
|
15
|
+
|
|
16
|
+
export const SCHEMA_PUSH_LOCK_CLASSID = 872014
|
|
17
|
+
export const SCHEMA_PUSH_LOCK_OBJID = 1
|
|
18
|
+
|
|
19
|
+
function composeBaseArgs(
|
|
20
|
+
paths: SelfHostComposePaths,
|
|
21
|
+
cwd: string,
|
|
22
|
+
composeProject: string,
|
|
23
|
+
): string[] {
|
|
24
|
+
const envFile = resolve(cwd, ".env")
|
|
25
|
+
const args = ["compose", "-p", composeProject, "--project-directory", cwd, "-f", paths.composePath]
|
|
26
|
+
if (existsSync(envFile)) args.push("--env-file", envFile)
|
|
27
|
+
return args
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function dbExecArgs(
|
|
31
|
+
paths: SelfHostComposePaths,
|
|
32
|
+
cwd: string,
|
|
33
|
+
composeProject: string,
|
|
34
|
+
psqlArgs: string[],
|
|
35
|
+
): string[] {
|
|
36
|
+
const pass = readEnvValue(cwd, "POSTGRES_PASSWORD", "postgres")
|
|
37
|
+
const user = readEnvValue(cwd, "POSTGRES_USER", "supatype_admin")
|
|
38
|
+
const db = readEnvValue(cwd, "POSTGRES_DB", "supatype")
|
|
39
|
+
return [
|
|
40
|
+
...composeBaseArgs(paths, cwd, composeProject),
|
|
41
|
+
"exec",
|
|
42
|
+
"-T",
|
|
43
|
+
"-e",
|
|
44
|
+
`PGPASSWORD=${pass}`,
|
|
45
|
+
"db",
|
|
46
|
+
"psql",
|
|
47
|
+
"-U",
|
|
48
|
+
user,
|
|
49
|
+
"-d",
|
|
50
|
+
db,
|
|
51
|
+
"-v",
|
|
52
|
+
"ON_ERROR_STOP=1",
|
|
53
|
+
...psqlArgs,
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function schemaPushLockIsHeld(
|
|
58
|
+
paths: SelfHostComposePaths,
|
|
59
|
+
cwd: string,
|
|
60
|
+
composeProject: string,
|
|
61
|
+
): boolean {
|
|
62
|
+
const args = dbExecArgs(paths, cwd, composeProject, [
|
|
63
|
+
"-tAc",
|
|
64
|
+
`SELECT EXISTS (
|
|
65
|
+
SELECT 1 FROM pg_locks
|
|
66
|
+
WHERE locktype = 'advisory'
|
|
67
|
+
AND classid = ${SCHEMA_PUSH_LOCK_CLASSID}
|
|
68
|
+
AND objid = ${SCHEMA_PUSH_LOCK_OBJID}
|
|
69
|
+
AND granted
|
|
70
|
+
)`,
|
|
71
|
+
])
|
|
72
|
+
const result = spawnSync("docker", args, { cwd, encoding: "utf8" })
|
|
73
|
+
return result.status === 0 && result.stdout.trim() === "t"
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Hold a session advisory lock for the duration of `fn` via a background
|
|
78
|
+
* `psql` inside the compose db container. Killing the holder releases the lock.
|
|
79
|
+
*/
|
|
80
|
+
export async function withComposeSchemaPushLock<T>(
|
|
81
|
+
paths: SelfHostComposePaths,
|
|
82
|
+
cwd: string,
|
|
83
|
+
composeProject: string,
|
|
84
|
+
fn: () => Promise<T>,
|
|
85
|
+
): Promise<T> {
|
|
86
|
+
const holdSql =
|
|
87
|
+
`SELECT pg_advisory_lock(${SCHEMA_PUSH_LOCK_CLASSID}, ${SCHEMA_PUSH_LOCK_OBJID});` +
|
|
88
|
+
` SELECT pg_sleep(86400);`
|
|
89
|
+
const args = dbExecArgs(paths, cwd, composeProject, ["-c", holdSql])
|
|
90
|
+
|
|
91
|
+
let stderrBuf = ""
|
|
92
|
+
const holder: ChildProcess = spawn("docker", args, {
|
|
93
|
+
cwd,
|
|
94
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
95
|
+
})
|
|
96
|
+
holder.stderr?.on("data", (chunk: Buffer) => {
|
|
97
|
+
stderrBuf += chunk.toString()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const deadline = Date.now() + 30_000
|
|
101
|
+
while (Date.now() < deadline) {
|
|
102
|
+
if (holder.exitCode !== null) {
|
|
103
|
+
const err = stderrBuf.trim() || "lock holder exited"
|
|
104
|
+
throw new Error(`[supatype] Failed to acquire schema-push advisory lock: ${err}`)
|
|
105
|
+
}
|
|
106
|
+
if (schemaPushLockIsHeld(paths, cwd, composeProject)) break
|
|
107
|
+
await new Promise((r) => setTimeout(r, 200))
|
|
108
|
+
}
|
|
109
|
+
if (!schemaPushLockIsHeld(paths, cwd, composeProject)) {
|
|
110
|
+
holder.kill("SIGTERM")
|
|
111
|
+
throw new Error("[supatype] Timed out waiting for schema-push advisory lock")
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
return await fn()
|
|
116
|
+
} finally {
|
|
117
|
+
if (holder.exitCode === null) {
|
|
118
|
+
holder.kill("SIGTERM")
|
|
119
|
+
await new Promise<void>((resolveDone) => {
|
|
120
|
+
const t = setTimeout(() => {
|
|
121
|
+
holder.kill("SIGKILL")
|
|
122
|
+
resolveDone()
|
|
123
|
+
}, 3000)
|
|
124
|
+
holder.once("exit", () => {
|
|
125
|
+
clearTimeout(t)
|
|
126
|
+
resolveDone()
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
package/src/self-host-compose.ts
CHANGED
|
@@ -383,6 +383,20 @@ ${dbPorts} volumes:
|
|
|
383
383
|
db:
|
|
384
384
|
condition: service_healthy
|
|
385
385
|
|
|
386
|
+
realtime:
|
|
387
|
+
image: \${SUPATYPE_REALTIME_IMAGE:-supatype/realtime:latest}
|
|
388
|
+
expose:
|
|
389
|
+
- "4000"
|
|
390
|
+
environment:
|
|
391
|
+
PORT: "4000"
|
|
392
|
+
DATABASE_URL: "postgresql://\${POSTGRES_USER:-supatype_admin}:\${POSTGRES_PASSWORD:-postgres}@db:5432/\${POSTGRES_DB:-supatype}"
|
|
393
|
+
JWT_SECRET: \${JWT_SECRET:-super-secret-jwt-token-change-in-production}
|
|
394
|
+
SLOT_NAME: supatype_realtime
|
|
395
|
+
PUBLICATION_NAME: supatype_realtime_pub
|
|
396
|
+
depends_on:
|
|
397
|
+
db:
|
|
398
|
+
condition: service_healthy
|
|
399
|
+
|
|
386
400
|
control-plane:
|
|
387
401
|
image: \${SUPATYPE_CONTROL_PLANE_IMAGE:-supatype/control-plane:latest}
|
|
388
402
|
expose:
|
|
@@ -423,6 +437,7 @@ ${serverPorts} volumes:
|
|
|
423
437
|
SUPATYPE_SQL_DATABASE_URL: "postgresql://\${POSTGRES_USER:-supatype_admin}:\${POSTGRES_PASSWORD:-postgres}@db:5432/\${POSTGRES_DB:-supatype}"
|
|
424
438
|
SUPATYPE_DENO_FUNCTIONS_DIR: /project/functions
|
|
425
439
|
SUPATYPE_FUNCTIONS_WORKER_URL: http://functions-worker:8001
|
|
440
|
+
SUPATYPE_REALTIME_URL: http://realtime:4000
|
|
426
441
|
SUPATYPE_CONTROL_PLANE_URL: http://control-plane:8080
|
|
427
442
|
SUPATYPE_VALKEY_ADDR: valkey:6379
|
|
428
443
|
${appEnv}
|
|
@@ -452,6 +467,8 @@ ${devLocal ? " STUDIO_OPEN_DEV: \"1\"\n" : ""}
|
|
|
452
467
|
condition: service_started
|
|
453
468
|
functions-worker:
|
|
454
469
|
condition: service_started
|
|
470
|
+
realtime:
|
|
471
|
+
condition: service_started
|
|
455
472
|
control-plane:
|
|
456
473
|
condition: service_started
|
|
457
474
|
|
|
@@ -502,6 +519,7 @@ function ensureComposeManifest(cwd: string): void {
|
|
|
502
519
|
postgrest_url: "http://postgrest:3000",
|
|
503
520
|
storage_url: "http://storage:5000",
|
|
504
521
|
realtime_enabled: true,
|
|
522
|
+
realtime_url: "http://realtime:4000",
|
|
505
523
|
functions_enabled: false,
|
|
506
524
|
}
|
|
507
525
|
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8")
|