ras-stack 0.11.0 → 0.13.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.
package/README.md CHANGED
@@ -121,6 +121,22 @@ const database = openDrizzleSqlite({
121
121
 
122
122
  Applications retain their schema, migrations, database path, repositories, transactions, and driver selection. Dual-database applications can use `openSqliteClient()` and `configureSqlite()` beneath their own wrapper without forcing PostgreSQL through a shared database interface.
123
123
 
124
+ The PostgreSQL entrypoint applies the same boundary to Postgres.js and Drizzle while returning both native objects:
125
+
126
+ ```ts
127
+ import { databaseTarget } from 'ras-stack/database'
128
+ import { closeDrizzlePostgres, migrateDrizzlePostgres, openDrizzlePostgres } from 'ras-stack/database/postgres'
129
+
130
+ const target = databaseTarget({ databaseUrl: process.env.DATABASE_URL, sqliteFile })
131
+ if (target.provider === 'postgres') {
132
+ const connection = openDrizzlePostgres({ url: target.url, schema })
133
+ await migrateDrizzlePostgres(connection, postgresMigrationsFolder)
134
+ await closeDrizzlePostgres(connection)
135
+ }
136
+ ```
137
+
138
+ The package does not make SQLite and PostgreSQL queries look identical. Applications keep driver-specific transaction and compatibility behavior while sharing target validation, pool defaults, numeric parsing, migrations, credential-safe display URLs, and shutdown.
139
+
124
140
  ## Realtime updates
125
141
 
126
142
  Applications choose their channel names, authorize subscriptions, and define payloads. `ras-stack` handles Centrifugo's HTTP publication, signed tokens, and repeated browser lifecycle mechanics:
@@ -192,6 +208,17 @@ await startTusUpload(upload)
192
208
 
193
209
  Applications retain ownership of email templates, missing-email behavior, upload metadata, authorization, quotas, and completion processing.
194
210
 
211
+ Stateful development servers can reuse one typed resource across HMR without application-specific `globalThis` casts:
212
+
213
+ ```ts
214
+ import { clearGlobalSingleton, globalAsyncSingleton } from 'ras-stack/server'
215
+
216
+ export const app = () => globalAsyncSingleton('my-app.instance', createApp)
217
+ export const resetApp = () => clearGlobalSingleton('my-app.instance', (instance) => instance.close())
218
+ ```
219
+
220
+ Rejected async initialization removes only its own pending value so a later request can retry. Clearing deletes the key before awaiting initialization or disposal, allowing replacement startup without reusing a closing resource.
221
+
195
222
  ## Shared project configuration
196
223
 
197
224
  Extend the supplied configuration and override anything specific to the application:
@@ -1,4 +1,4 @@
1
- import { type URL } from 'node:url';
1
+ import { URL } from 'node:url';
2
2
  export type BundledDirectoryOptions = {
3
3
  developmentUrl: URL;
4
4
  production: boolean;
@@ -6,3 +6,14 @@ export type BundledDirectoryOptions = {
6
6
  name: string;
7
7
  };
8
8
  export declare function bundledDirectory(options: BundledDirectoryOptions): string;
9
+ export type DatabaseTarget = {
10
+ provider: 'sqlite';
11
+ file: string;
12
+ } | {
13
+ provider: 'postgres';
14
+ url: string;
15
+ };
16
+ export declare function databaseTarget(options: {
17
+ databaseUrl?: string;
18
+ sqliteFile: string;
19
+ }): DatabaseTarget;
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { fileURLToPath } from 'node:url';
2
+ import { fileURLToPath, URL } from 'node:url';
3
3
  export function bundledDirectory(options) {
4
4
  if (!options.production)
5
5
  return fileURLToPath(options.developmentUrl);
@@ -8,4 +8,14 @@ export function bundledDirectory(options) {
8
8
  throw new Error('production entrypoint is required to resolve a bundled directory');
9
9
  return path.join(path.dirname(entry), options.name);
10
10
  }
11
+ export function databaseTarget(options) {
12
+ const configured = options.databaseUrl?.trim();
13
+ if (!configured)
14
+ return { provider: 'sqlite', file: options.sqliteFile };
15
+ const url = new URL(configured);
16
+ if (url.protocol !== 'postgres:' && url.protocol !== 'postgresql:') {
17
+ throw new Error('database URL must use a postgres:// or postgresql:// URL');
18
+ }
19
+ return { provider: 'postgres', url: configured };
20
+ }
11
21
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAY,MAAM,UAAU,CAAA;AASlD,MAAM,UAAU,gBAAgB,CAAC,OAAgC;IAC/D,IAAI,CAAC,OAAO,CAAC,UAAU;QAAE,OAAO,aAAa,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;IACrE,MAAM,KAAK,GAAG,OAAO,CAAC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACxD,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAA;IAC/F,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;AACrD,CAAC","sourcesContent":["import path from 'node:path'\nimport { fileURLToPath, type URL } from 'node:url'\n\nexport type BundledDirectoryOptions = {\n developmentUrl: URL\n production: boolean\n productionEntry?: string\n name: string\n}\n\nexport function bundledDirectory(options: BundledDirectoryOptions) {\n if (!options.production) return fileURLToPath(options.developmentUrl)\n const entry = options.productionEntry ?? process.argv[1]\n if (!entry) throw new Error('production entrypoint is required to resolve a bundled directory')\n return path.join(path.dirname(entry), options.name)\n}\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/database/index.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,UAAU,CAAA;AAS7C,MAAM,UAAU,gBAAgB,CAAC,OAAgC;IAC/D,IAAI,CAAC,OAAO,CAAC,UAAU;QAAE,OAAO,aAAa,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;IACrE,MAAM,KAAK,GAAG,OAAO,CAAC,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACxD,IAAI,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAA;IAC/F,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;AACrD,CAAC;AAID,MAAM,UAAU,cAAc,CAAC,OAAqD;IAClF,MAAM,UAAU,GAAG,OAAO,CAAC,WAAW,EAAE,IAAI,EAAE,CAAA;IAC9C,IAAI,CAAC,UAAU;QAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,CAAC,UAAU,EAAE,CAAA;IACxE,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAA;IAC/B,IAAI,GAAG,CAAC,QAAQ,KAAK,WAAW,IAAI,GAAG,CAAC,QAAQ,KAAK,aAAa,EAAE,CAAC;QACnE,MAAM,IAAI,KAAK,CAAC,0DAA0D,CAAC,CAAA;IAC7E,CAAC;IACD,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,EAAE,UAAU,EAAE,CAAA;AAClD,CAAC","sourcesContent":["import path from 'node:path'\nimport { fileURLToPath, URL } from 'node:url'\n\nexport type BundledDirectoryOptions = {\n developmentUrl: URL\n production: boolean\n productionEntry?: string\n name: string\n}\n\nexport function bundledDirectory(options: BundledDirectoryOptions) {\n if (!options.production) return fileURLToPath(options.developmentUrl)\n const entry = options.productionEntry ?? process.argv[1]\n if (!entry) throw new Error('production entrypoint is required to resolve a bundled directory')\n return path.join(path.dirname(entry), options.name)\n}\n\nexport type DatabaseTarget = { provider: 'sqlite'; file: string } | { provider: 'postgres'; url: string }\n\nexport function databaseTarget(options: { databaseUrl?: string; sqliteFile: string }): DatabaseTarget {\n const configured = options.databaseUrl?.trim()\n if (!configured) return { provider: 'sqlite', file: options.sqliteFile }\n const url = new URL(configured)\n if (url.protocol !== 'postgres:' && url.protocol !== 'postgresql:') {\n throw new Error('database URL must use a postgres:// or postgresql:// URL')\n }\n return { provider: 'postgres', url: configured }\n}\n"]}
@@ -0,0 +1,16 @@
1
+ import { type PostgresJsDatabase } from 'drizzle-orm/postgres-js';
2
+ import postgres, { type Sql } from 'postgres';
3
+ export type PostgresClientOptions = NonNullable<Parameters<typeof postgres>[1]>;
4
+ export type OpenDrizzlePostgresOptions<TSchema extends Record<string, unknown>> = {
5
+ url: string;
6
+ schema: TSchema;
7
+ client?: PostgresClientOptions;
8
+ };
9
+ export type DrizzlePostgresConnection<TSchema extends Record<string, unknown>> = {
10
+ database: PostgresJsDatabase<TSchema>;
11
+ client: Sql;
12
+ };
13
+ export declare function openDrizzlePostgres<TSchema extends Record<string, unknown>>(options: OpenDrizzlePostgresOptions<TSchema>): DrizzlePostgresConnection<TSchema>;
14
+ export declare function migrateDrizzlePostgres<TSchema extends Record<string, unknown>>(connection: DrizzlePostgresConnection<TSchema>, migrationsFolder: string): Promise<void>;
15
+ export declare function closeDrizzlePostgres(connection: Pick<DrizzlePostgresConnection<Record<string, unknown>>, 'client'>, timeout?: number): Promise<void>;
16
+ export declare function redactedPostgresUrl(source: string): string;
@@ -0,0 +1,29 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js';
2
+ import { migrate } from 'drizzle-orm/postgres-js/migrator';
3
+ import postgres, {} from 'postgres';
4
+ export function openDrizzlePostgres(options) {
5
+ const client = postgres(options.url, {
6
+ max: 10,
7
+ onnotice: () => undefined,
8
+ types: {
9
+ bigint: { to: 20, from: [20], serialize: (value) => String(value), parse: Number },
10
+ numeric: { to: 1700, from: [1700], serialize: (value) => String(value), parse: Number },
11
+ },
12
+ ...options.client,
13
+ });
14
+ const database = drizzle(client, { schema: options.schema });
15
+ return { database, client };
16
+ }
17
+ export async function migrateDrizzlePostgres(connection, migrationsFolder) {
18
+ await migrate(connection.database, { migrationsFolder });
19
+ }
20
+ export async function closeDrizzlePostgres(connection, timeout = 5) {
21
+ await connection.client.end({ timeout });
22
+ }
23
+ export function redactedPostgresUrl(source) {
24
+ const url = new URL(source);
25
+ url.username = '';
26
+ url.password = '';
27
+ return url.toString();
28
+ }
29
+ //# sourceMappingURL=postgres.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../src/database/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAA2B,MAAM,yBAAyB,CAAA;AAC1E,OAAO,EAAE,OAAO,EAAE,MAAM,kCAAkC,CAAA;AAC1D,OAAO,QAAQ,EAAE,EAAY,MAAM,UAAU,CAAA;AAe7C,MAAM,UAAU,mBAAmB,CACjC,OAA4C;IAE5C,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,EAAE;QACnC,GAAG,EAAE,EAAE;QACP,QAAQ,EAAE,GAAG,EAAE,CAAC,SAAS;QACzB,KAAK,EAAE;YACL,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;YAC1F,OAAO,EAAE,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE;SAChG;QACD,GAAG,OAAO,CAAC,MAAM;KAClB,CAAC,CAAA;IACF,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;IAC5D,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAA;AAC7B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,UAA8C,EAC9C,gBAAwB;IAExB,MAAM,OAAO,CAAC,UAAU,CAAC,QAAQ,EAAE,EAAE,gBAAgB,EAAE,CAAC,CAAA;AAC1D,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,UAA8E,EAAE,OAAO,GAAG,CAAC;IACpI,MAAM,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC,CAAA;AAC1C,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,MAAc;IAChD,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,CAAA;IAC3B,GAAG,CAAC,QAAQ,GAAG,EAAE,CAAA;IACjB,GAAG,CAAC,QAAQ,GAAG,EAAE,CAAA;IACjB,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAA;AACvB,CAAC","sourcesContent":["import { drizzle, type PostgresJsDatabase } from 'drizzle-orm/postgres-js'\nimport { migrate } from 'drizzle-orm/postgres-js/migrator'\nimport postgres, { type Sql } from 'postgres'\n\nexport type PostgresClientOptions = NonNullable<Parameters<typeof postgres>[1]>\n\nexport type OpenDrizzlePostgresOptions<TSchema extends Record<string, unknown>> = {\n url: string\n schema: TSchema\n client?: PostgresClientOptions\n}\n\nexport type DrizzlePostgresConnection<TSchema extends Record<string, unknown>> = {\n database: PostgresJsDatabase<TSchema>\n client: Sql\n}\n\nexport function openDrizzlePostgres<TSchema extends Record<string, unknown>>(\n options: OpenDrizzlePostgresOptions<TSchema>,\n): DrizzlePostgresConnection<TSchema> {\n const client = postgres(options.url, {\n max: 10,\n onnotice: () => undefined,\n types: {\n bigint: { to: 20, from: [20], serialize: (value: number) => String(value), parse: Number },\n numeric: { to: 1700, from: [1700], serialize: (value: number) => String(value), parse: Number },\n },\n ...options.client,\n })\n const database = drizzle(client, { schema: options.schema })\n return { database, client }\n}\n\nexport async function migrateDrizzlePostgres<TSchema extends Record<string, unknown>>(\n connection: DrizzlePostgresConnection<TSchema>,\n migrationsFolder: string,\n) {\n await migrate(connection.database, { migrationsFolder })\n}\n\nexport async function closeDrizzlePostgres(connection: Pick<DrizzlePostgresConnection<Record<string, unknown>>, 'client'>, timeout = 5) {\n await connection.client.end({ timeout })\n}\n\nexport function redactedPostgresUrl(source: string) {\n const url = new URL(source)\n url.username = ''\n url.password = ''\n return url.toString()\n}\n"]}
@@ -1,3 +1,4 @@
1
1
  export * from './canonical-host.js';
2
2
  export * from './health.js';
3
3
  export * from './rpc.js';
4
+ export * from './singleton.js';
@@ -1,4 +1,5 @@
1
1
  export * from './canonical-host.js';
2
2
  export * from './health.js';
3
3
  export * from './rpc.js';
4
+ export * from './singleton.js';
4
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA","sourcesContent":["export * from './canonical-host.js'\nexport * from './health.js'\nexport * from './rpc.js'\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/server/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAA;AACnC,cAAc,aAAa,CAAA;AAC3B,cAAc,UAAU,CAAA;AACxB,cAAc,gBAAgB,CAAA","sourcesContent":["export * from './canonical-host.js'\nexport * from './health.js'\nexport * from './rpc.js'\nexport * from './singleton.js'\n"]}
@@ -0,0 +1,4 @@
1
+ export declare function globalSingleton<T>(key: string, create: () => T): T;
2
+ export declare function globalAsyncSingleton<T>(key: string, create: () => T | Promise<T>): Promise<T>;
3
+ export declare function peekGlobalSingleton(key: string): unknown;
4
+ export declare function clearGlobalSingleton<T>(key: string, dispose?: (value: Awaited<T>) => void | Promise<void>): Promise<boolean>;
@@ -0,0 +1,48 @@
1
+ const storeKey = Symbol.for('ras-stack.server.singletons');
2
+ function store() {
3
+ const target = globalThis;
4
+ return (target[storeKey] ??= new Map());
5
+ }
6
+ export function globalSingleton(key, create) {
7
+ const singletons = store();
8
+ const existing = singletons.get(key);
9
+ if (existing) {
10
+ if (existing.kind !== 'sync')
11
+ throw new Error(`singleton ${key} is asynchronous`);
12
+ return existing.value;
13
+ }
14
+ const value = create();
15
+ singletons.set(key, { kind: 'sync', value });
16
+ return value;
17
+ }
18
+ export function globalAsyncSingleton(key, create) {
19
+ const singletons = store();
20
+ const existing = singletons.get(key);
21
+ if (existing) {
22
+ if (existing.kind !== 'async')
23
+ throw new Error(`singleton ${key} is synchronous`);
24
+ return existing.value;
25
+ }
26
+ const pending = Promise.resolve().then(create);
27
+ const guarded = pending.catch((error) => {
28
+ if (singletons.get(key)?.value === guarded)
29
+ singletons.delete(key);
30
+ throw error;
31
+ });
32
+ singletons.set(key, { kind: 'async', value: guarded });
33
+ return guarded;
34
+ }
35
+ export function peekGlobalSingleton(key) {
36
+ return store().get(key)?.value;
37
+ }
38
+ export async function clearGlobalSingleton(key, dispose) {
39
+ const singletons = store();
40
+ if (!singletons.has(key))
41
+ return false;
42
+ const value = singletons.get(key)?.value;
43
+ singletons.delete(key);
44
+ if (dispose)
45
+ await dispose(await value);
46
+ return true;
47
+ }
48
+ //# sourceMappingURL=singleton.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"singleton.js","sourceRoot":"","sources":["../../src/server/singleton.ts"],"names":[],"mappings":"AAAA,MAAM,QAAQ,GAAG,MAAM,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAA;AAI1D,SAAS,KAAK;IACZ,MAAM,MAAM,GAAG,UAA6B,CAAA;IAC5C,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,IAAI,GAAG,EAAE,CAAC,CAAA;AACzC,CAAC;AAED,MAAM,UAAU,eAAe,CAAI,GAAW,EAAE,MAAe;IAC7D,MAAM,UAAU,GAAG,KAAK,EAAE,CAAA;IAC1B,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACpC,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,QAAQ,CAAC,IAAI,KAAK,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,aAAa,GAAG,kBAAkB,CAAC,CAAA;QACjF,OAAO,QAAQ,CAAC,KAAU,CAAA;IAC5B,CAAC;IACD,MAAM,KAAK,GAAG,MAAM,EAAE,CAAA;IACtB,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;IAC5C,OAAO,KAAK,CAAA;AACd,CAAC;AAED,MAAM,UAAU,oBAAoB,CAAI,GAAW,EAAE,MAA4B;IAC/E,MAAM,UAAU,GAAG,KAAK,EAAE,CAAA;IAC1B,MAAM,QAAQ,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;IACpC,IAAI,QAAQ,EAAE,CAAC;QACb,IAAI,QAAQ,CAAC,IAAI,KAAK,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,aAAa,GAAG,iBAAiB,CAAC,CAAA;QACjF,OAAO,QAAQ,CAAC,KAAmB,CAAA;IACrC,CAAC;IACD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC9C,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE;QAC/C,IAAI,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,KAAK,OAAO;YAAE,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAClE,MAAM,KAAK,CAAA;IACb,CAAC,CAAC,CAAA;IACF,UAAU,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAA;IACtD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,OAAO,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,KAAK,CAAA;AAChC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAI,GAAW,EAAE,OAAqD;IAC9G,MAAM,UAAU,GAAG,KAAK,EAAE,CAAA;IAC1B,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,CAAA;IACtC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,KAAU,CAAA;IAC7C,UAAU,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACtB,IAAI,OAAO;QAAE,MAAM,OAAO,CAAC,MAAM,KAAK,CAAC,CAAA;IACvC,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["const storeKey = Symbol.for('ras-stack.server.singletons')\ntype SingletonEntry = { kind: 'sync' | 'async'; value: unknown }\ntype SingletonGlobal = typeof globalThis & { [storeKey]?: Map<string, SingletonEntry> }\n\nfunction store() {\n const target = globalThis as SingletonGlobal\n return (target[storeKey] ??= new Map())\n}\n\nexport function globalSingleton<T>(key: string, create: () => T): T {\n const singletons = store()\n const existing = singletons.get(key)\n if (existing) {\n if (existing.kind !== 'sync') throw new Error(`singleton ${key} is asynchronous`)\n return existing.value as T\n }\n const value = create()\n singletons.set(key, { kind: 'sync', value })\n return value\n}\n\nexport function globalAsyncSingleton<T>(key: string, create: () => T | Promise<T>): Promise<T> {\n const singletons = store()\n const existing = singletons.get(key)\n if (existing) {\n if (existing.kind !== 'async') throw new Error(`singleton ${key} is synchronous`)\n return existing.value as Promise<T>\n }\n const pending = Promise.resolve().then(create)\n const guarded = pending.catch((error: unknown) => {\n if (singletons.get(key)?.value === guarded) singletons.delete(key)\n throw error\n })\n singletons.set(key, { kind: 'async', value: guarded })\n return guarded\n}\n\nexport function peekGlobalSingleton(key: string): unknown {\n return store().get(key)?.value\n}\n\nexport async function clearGlobalSingleton<T>(key: string, dispose?: (value: Awaited<T>) => void | Promise<void>) {\n const singletons = store()\n if (!singletons.has(key)) return false\n const value = singletons.get(key)?.value as T\n singletons.delete(key)\n if (dispose) await dispose(await value)\n return true\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ras-stack",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Composable full-stack primitives shared across Richard Solomou's applications.",
5
5
  "keywords": [
6
6
  "authentication",
@@ -42,6 +42,10 @@
42
42
  "types": "./dist/database/sqlite.d.ts",
43
43
  "default": "./dist/database/sqlite.js"
44
44
  },
45
+ "./database/postgres": {
46
+ "types": "./dist/database/postgres.d.ts",
47
+ "default": "./dist/database/postgres.js"
48
+ },
45
49
  "./policy": {
46
50
  "types": "./dist/policy/index.d.ts",
47
51
  "default": "./dist/policy/index.js"
@@ -112,6 +116,7 @@
112
116
  "oxfmt": "^0.61.0",
113
117
  "oxlint": "^1.74.0",
114
118
  "oxlint-tsgolint": "^7.0.2001",
119
+ "postgres": "3.4.9",
115
120
  "tus-js-client": "^4.3.1",
116
121
  "typescript": "^7.0.2",
117
122
  "vite": "8.2.1",
@@ -124,6 +129,7 @@
124
129
  "centrifuge": ">=5 <6",
125
130
  "drizzle-orm": ">=0.45 <1",
126
131
  "nodemailer": ">=9 <10",
132
+ "postgres": ">=3.4 <4",
127
133
  "tus-js-client": ">=4 <5"
128
134
  },
129
135
  "peerDependenciesMeta": {
@@ -145,6 +151,9 @@
145
151
  "nodemailer": {
146
152
  "optional": true
147
153
  },
154
+ "postgres": {
155
+ "optional": true
156
+ },
148
157
  "tus-js-client": {
149
158
  "optional": true
150
159
  }