@shivaedev/server 0.4.0 → 0.6.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.
Files changed (45) hide show
  1. package/README.md +5 -3
  2. package/dist/AppHostFactory.d.ts +3 -3
  3. package/dist/AppHostFactory.d.ts.map +1 -1
  4. package/dist/AppHostInput.d.ts +2 -2
  5. package/dist/AppHostInput.d.ts.map +1 -1
  6. package/dist/MigrationOptions.d.ts +2 -2
  7. package/dist/MigrationOptions.d.ts.map +1 -1
  8. package/dist/TrustedProxyRequired.d.ts +8 -0
  9. package/dist/TrustedProxyRequired.d.ts.map +1 -0
  10. package/dist/TrustedProxyRequired.js +5 -0
  11. package/dist/TrustedProxyRequired.js.map +1 -0
  12. package/dist/appHost.d.ts +8 -8
  13. package/dist/appHost.d.ts.map +1 -1
  14. package/dist/appHost.js.map +1 -1
  15. package/dist/appHostHttp.d.ts +29 -29
  16. package/dist/appHostHttp.d.ts.map +1 -1
  17. package/dist/appHostHttp.js.map +1 -1
  18. package/dist/databaseLayer.d.ts +6 -6
  19. package/dist/databaseLayer.d.ts.map +1 -1
  20. package/dist/databaseLayer.js +15 -33
  21. package/dist/databaseLayer.js.map +1 -1
  22. package/dist/hostRpcRoutes.js +1 -4
  23. package/dist/hostRpcRoutes.js.map +1 -1
  24. package/dist/hostTransport.d.ts +2 -1
  25. package/dist/hostTransport.d.ts.map +1 -1
  26. package/dist/hostTransport.js +2 -1
  27. package/dist/hostTransport.js.map +1 -1
  28. package/dist/node-http/subscriptionSignal.js +1 -1
  29. package/dist/node-http/subscriptionSignal.js.map +1 -1
  30. package/package.json +20 -20
  31. package/src/AppHostFactory.ts +6 -3
  32. package/src/AppHostInput.ts +6 -3
  33. package/src/MigrationOptions.ts +2 -2
  34. package/src/TrustedProxyRequired.ts +6 -0
  35. package/src/appHost.ts +10 -6
  36. package/src/appHostHttp.ts +5 -3
  37. package/src/databaseLayer.ts +27 -43
  38. package/src/hostRpcRoutes.ts +5 -5
  39. package/src/hostTransport.ts +2 -3
  40. package/src/node-http/subscriptionSignal.ts +1 -1
  41. package/dist/databaseAddress.d.ts +0 -4
  42. package/dist/databaseAddress.d.ts.map +0 -1
  43. package/dist/databaseAddress.js +0 -25
  44. package/dist/databaseAddress.js.map +0 -1
  45. package/src/databaseAddress.ts +0 -25
@@ -1,57 +1,29 @@
1
1
  import { createHash } from "node:crypto";
2
- import { createConnection } from "node:net";
3
2
  import * as PgClient from "@effect/sql-pg/PgClient";
4
- import { Config, Duration, Effect, Fiber, Layer, Redacted, Schedule } from "effect";
3
+ import { Config, Context, Duration, Effect, Fiber, Layer, Option, Redacted, Schedule } from "effect";
5
4
  import * as Reactivity from "effect/reactivity/Reactivity";
5
+ import { Migrator } from "effect/sql";
6
6
  import { makeLiveServer } from "@shivaedev/contract/makeLiveServer.ts";
7
7
  import { assertPostgresMigrations, postgresMigrationError } from "@shivaedev/sql/assertPostgresMigrations.ts";
8
- import type { SqlNameTransforms } from "@shivaedev/sql/clientOptions.ts";
9
8
  import { coordinateWorkerRelease } from "@shivaedev/sql/coordinateWorkerRelease.ts";
10
9
  import { migratePostgres } from "@shivaedev/sql/migrations.ts";
11
10
  import { type PendingPostgresMigrations, pendingPostgresMigrations } from "@shivaedev/sql/pendingPostgresMigrations.ts";
12
11
  import { type PostgresChangesOptions, postgresChanges } from "@shivaedev/sql/postgresChanges.ts";
12
+ import { type PostgresOptions, postgresLayer } from "@shivaedev/sql/postgresLayer.ts";
13
13
  import { WorkerRelease } from "@shivaedev/sql/WorkerRelease.ts";
14
14
  import type { AppChanges, AppDatabaseOptions } from "#appHost.ts";
15
- import { databaseAddress } from "./databaseAddress.ts";
16
15
 
17
- export interface DatabaseConfig extends SqlNameTransforms {
18
- readonly applicationName?: string;
19
- readonly changes?: Partial<Omit<PostgresChangesOptions, "client" | "resync">>;
16
+ export interface DatabaseConfig extends PostgresOptions {
17
+ readonly changes?: Partial<Omit<PostgresChangesOptions, "client" | "listenerClient" | "connectionSetting" | "resync">>;
20
18
  readonly databaseUrl?: Redacted.Redacted<string>;
21
- readonly maxConnections?: number;
19
+ readonly listenDatabaseUrl?: Redacted.Redacted<string>;
22
20
  }
23
21
 
24
- export function databaseLayer<R>(options: AppDatabaseOptions<R>, changesService: typeof AppChanges) {
22
+ export function databaseLayer<R, E>(options: AppDatabaseOptions<R, E>, changesService: typeof AppChanges) {
25
23
  const pool = Layer.unwrap(
26
- Effect.gen(function* () {
27
- const url = options.config?.databaseUrl ?? (yield* Config.Redacted("DATABASE_URL"));
28
- const address = databaseAddress(url);
29
- const disconnected = new Set<Promise<void>>();
30
- // Native pool shutdown sends Terminate without waiting for its sockets to close.
31
- yield* Effect.addFinalizer(() => Effect.promise(() => Promise.all([...disconnected])));
32
- return PgClient.layer({
33
- stream: () => {
34
- const target = new URL(Redacted.value(url));
35
- const host = address.host ?? target.searchParams.getAll("host").at(-1) ?? (decodeURIComponent(target.hostname) || "localhost");
36
- const port = address.port ?? 5432;
37
- const socket = createConnection(host.startsWith("/") ? { path: `${host}/.s.PGSQL.${port}` } : { host, noDelay: true, port });
38
- const closed = new Promise<void>((resolve) =>
39
- socket.once("close", () => {
40
- disconnected.delete(closed);
41
- resolve();
42
- }),
43
- );
44
- disconnected.add(closed);
45
- return socket;
46
- },
47
- transformQueryNames: options.config?.transformQueryNames,
48
- transformResultNames: options.config?.transformResultNames,
49
- url,
50
- ...address,
51
- ...(options.config?.maxConnections === undefined ? {} : { maxConnections: options.config.maxConnections }),
52
- ...(options.config?.applicationName === undefined ? {} : { applicationName: options.config.applicationName }),
53
- });
54
- }),
24
+ Effect.map(options.config?.databaseUrl === undefined ? Config.Redacted("DATABASE_URL") : Effect.succeed(options.config.databaseUrl), (url) =>
25
+ postgresLayer(url, options.config),
26
+ ),
55
27
  );
56
28
  const prepared = Layer.effectDiscard(prepareMigrations(options.migrations)).pipe(Layer.provideMerge(pool));
57
29
  const live = Layer.effect(changesService, makeLiveServer).pipe(Layer.provideMerge(Reactivity.layer));
@@ -69,12 +41,24 @@ export function databaseLayer<R>(options: AppDatabaseOptions<R>, changesService:
69
41
  const url = options.config?.databaseUrl ?? (yield* Config.Redacted("DATABASE_URL"));
70
42
  const name = yield* Config.String("APP_NAME").pipe(Config.withDefault(new URL(Redacted.value(url)).pathname));
71
43
  const channel = changes?.channel ?? `changes_${createHash("sha256").update(name).digest("hex").slice(0, 32)}`;
72
- return postgresChanges({ ...changes, channel, client, resync: hints.resync });
44
+ const listenUrl = options.config?.listenDatabaseUrl ?? Option.getOrUndefined(yield* Config.Redacted("LISTEN_DATABASE_URL").pipe(Config.option));
45
+ const listenerClient =
46
+ listenUrl === undefined
47
+ ? client
48
+ : yield* Effect.map(
49
+ Layer.build(postgresLayer(listenUrl, { applicationName: options.config?.applicationName, maxConnections: 1 })),
50
+ (context) => Context.get(context, PgClient.PgClient),
51
+ );
52
+ let connectionSetting = options.config?.databaseUrl === undefined ? "DATABASE_URL" : "config.databaseUrl";
53
+ if (listenUrl !== undefined) {
54
+ connectionSetting = options.config?.listenDatabaseUrl === undefined ? "LISTEN_DATABASE_URL" : "config.listenDatabaseUrl";
55
+ }
56
+ return postgresChanges({ ...changes, channel, client, connectionSetting, listenerClient, resync: hints.resync });
73
57
  }),
74
58
  ).pipe(Layer.provideMerge(base));
75
59
  }
76
60
 
77
- function prepareMigrations<R>(migrations: AppDatabaseOptions<R>["migrations"]) {
61
+ function prepareMigrations<R, E>(migrations: AppDatabaseOptions<R, E>["migrations"]) {
78
62
  switch (migrations.mode) {
79
63
  case "check": {
80
64
  const schema = { ...migrations };
@@ -88,7 +72,7 @@ function prepareMigrations<R>(migrations: AppDatabaseOptions<R>["migrations"]) {
88
72
  }
89
73
  }
90
74
 
91
- function waitForMigrations<R>(migrations: AppDatabaseOptions<R>["migrations"]) {
75
+ function waitForMigrations<R, E>(migrations: AppDatabaseOptions<R, E>["migrations"]) {
92
76
  return Effect.gen(function* () {
93
77
  const progressInterval = Duration.fromInputUnsafe(migrations.progressInterval ?? "30 seconds");
94
78
  let pending: PendingPostgresMigrations = { components: [], migrations: [] };
@@ -113,10 +97,10 @@ function waitForMigrations<R>(migrations: AppDatabaseOptions<R>["migrations"]) {
113
97
  }).pipe(
114
98
  Effect.retry({
115
99
  schedule: Schedule.max([Schedule.spaced(migrations.pollInterval ?? "1 second"), Schedule.during(migrations.waitTimeout ?? "5 minutes")]),
116
- while: (error) => error._tag === "MigrationError" && error.kind === "BadState",
100
+ while: (error) => error instanceof Migrator.MigrationError && error.kind === "BadState",
117
101
  }),
118
102
  Effect.tapError((error) =>
119
- error._tag === "MigrationError" && error.kind === "BadState" ? log("Timed out waiting for database migrations") : Effect.void,
103
+ error instanceof Migrator.MigrationError && error.kind === "BadState" ? log("Timed out waiting for database migrations") : Effect.void,
120
104
  ),
121
105
  );
122
106
  if (progress !== undefined) {
@@ -49,14 +49,14 @@ export function hostRpcRoutes<TRpcs extends Rpc.Any, THandlers, TError, TRequire
49
49
  options.rpcPath ?? "/rpc",
50
50
  socketRpc(websocket, options, trustedProxy, nativeOrigins).pipe(Effect.provideService(RpcSerialization.RpcSerialization, wire)),
51
51
  );
52
- yield* router.add("POST", options.rpcPath ?? "/rpc", (request) => {
53
- HttpEffect.appendPreResponseHandlerUnsafe(request, rpcCacheControl);
54
- return guarded.pipe(
52
+ yield* router.add("POST", options.rpcPath ?? "/rpc", (request) =>
53
+ guarded.pipe(
55
54
  Effect.provideService(HttpIncomingMessage.MaxBodySize, ByteSize.bytes(bodySize)),
56
55
  Effect.provideService(HttpServerRequest.HttpServerRequest, boundedRpcRequest(request)),
57
56
  Effect.provideService(RpcSerialization.RpcSerialization, wire),
58
- );
59
- });
57
+ HttpEffect.withPreResponseHandler(rpcCacheControl),
58
+ ),
59
+ );
60
60
  }),
61
61
  ).pipe(
62
62
  Layer.provide(
@@ -2,6 +2,7 @@ import { Config, Effect, Option, Result, Schema } from "effect";
2
2
  import * as NetAddress from "effect/net/NetAddress";
3
3
  import { TrustedProxy } from "@shivaedev/identity/server/trustedProxy.ts";
4
4
  import { NativeOrigins } from "#nativeClientOrigins.ts";
5
+ import { TrustedProxyRequired } from "#TrustedProxyRequired.ts";
5
6
 
6
7
  interface Options {
7
8
  readonly config?: { readonly host?: string; readonly maxBodySize?: number; readonly trustedProxy?: string };
@@ -28,9 +29,7 @@ export const hostTransport = Effect.fn("Platform.hostTransport")(function* (opti
28
29
  onSuccess: (address) => NetAddress.isLoopback(NetAddress.toCanonical(address)),
29
30
  });
30
31
  if (options.policy !== "loopback" && !loopback && trustedProxy === undefined) {
31
- yield* Effect.logWarning(
32
- "RPC forwarding is refused without a trusted proxy. Set TRUSTED_PROXY or config.trustedProxy for this non-loopback HOST.",
33
- );
32
+ return yield* Effect.fail(new TrustedProxyRequired());
34
33
  }
35
34
  const nativeOrigins = yield* Schema.decodeUnknownEffect(NativeOrigins)(options.nativeOrigins ?? []).pipe(
36
35
  Effect.mapError((cause) => new Config.ConfigError(cause)),
@@ -46,7 +46,7 @@ export function nodeSubscriptionSignal(options: NodeSubscriptionSignalOptions):
46
46
  }
47
47
  nodeRequest.once("close", onRequestClose);
48
48
  socket.once("close", onSocketClose);
49
- if (nodeRequest.destroyed || socket.destroyed) {
49
+ if ((nodeRequest.destroyed && !nodeRequest.complete) || socket.destroyed) {
50
50
  controller.abort();
51
51
  }
52
52
  let disposed = false;
@@ -1,4 +0,0 @@
1
- import type { PgPoolConfig } from "@effect/sql-pg/PgClient";
2
- import { Redacted } from "effect";
3
- export declare function databaseAddress(value: Redacted.Redacted<string>): Pick<PgPoolConfig, "host" | "port">;
4
- //# sourceMappingURL=databaseAddress.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"databaseAddress.d.ts","sourceRoot":"","sources":["../src/databaseAddress.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAElC,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,CAqBrG"}
@@ -1,25 +0,0 @@
1
- import { Redacted } from "effect";
2
- export function databaseAddress(value) {
3
- try {
4
- const url = new URL(Redacted.value(value));
5
- if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
6
- return {};
7
- }
8
- const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : decodeURIComponent(url.hostname);
9
- // PgConnection applies query parameters in order, after the authority.
10
- const host = url.searchParams.getAll("host").at(-1) ?? hostname;
11
- const rawPort = url.searchParams.getAll("port").at(-1) ?? url.port;
12
- const port = rawPort === "" ? undefined : Number(rawPort);
13
- if (host.includes(",") || (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535))) {
14
- return {};
15
- }
16
- return {
17
- ...(host === "" && !url.searchParams.has("host") ? {} : { host }),
18
- ...(port === undefined ? {} : { port }),
19
- };
20
- }
21
- catch {
22
- return {};
23
- }
24
- }
25
- //# sourceMappingURL=databaseAddress.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"databaseAddress.js","sourceRoot":"","sources":["../src/databaseAddress.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AAElC,MAAM,UAAU,eAAe,CAAC,KAAgC;IAC/D,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3C,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,IAAI,GAAG,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;YACpE,OAAO,EAAE,CAAC;QACX,CAAC;QACD,MAAM,QAAQ,GAAG,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC7G,uEAAuE;QACvE,MAAM,IAAI,GAAG,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC;QAChE,MAAM,OAAO,GAAG,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,IAAI,CAAC;QACnE,MAAM,IAAI,GAAG,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC1D,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,MAAM,CAAC,CAAC,EAAE,CAAC;YAC1G,OAAO,EAAE,CAAC;QACX,CAAC;QACD,OAAO;YACN,GAAG,CAAC,IAAI,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;YACjE,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;SACvC,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAC;IACX,CAAC;AACF,CAAC"}
@@ -1,25 +0,0 @@
1
- import type { PgPoolConfig } from "@effect/sql-pg/PgClient";
2
- import { Redacted } from "effect";
3
-
4
- export function databaseAddress(value: Redacted.Redacted<string>): Pick<PgPoolConfig, "host" | "port"> {
5
- try {
6
- const url = new URL(Redacted.value(value));
7
- if (url.protocol !== "postgres:" && url.protocol !== "postgresql:") {
8
- return {};
9
- }
10
- const hostname = url.hostname.startsWith("[") ? url.hostname.slice(1, -1) : decodeURIComponent(url.hostname);
11
- // PgConnection applies query parameters in order, after the authority.
12
- const host = url.searchParams.getAll("host").at(-1) ?? hostname;
13
- const rawPort = url.searchParams.getAll("port").at(-1) ?? url.port;
14
- const port = rawPort === "" ? undefined : Number(rawPort);
15
- if (host.includes(",") || (port !== undefined && (!Number.isInteger(port) || port < 1 || port > 65_535))) {
16
- return {};
17
- }
18
- return {
19
- ...(host === "" && !url.searchParams.has("host") ? {} : { host }),
20
- ...(port === undefined ? {} : { port }),
21
- };
22
- } catch {
23
- return {};
24
- }
25
- }