Repository navigation
Expand file tree
/
Copy pathfactory.ts
More file actions
104 lines (91 loc) · 3.54 KB
/
Copy pathfactory.ts
File metadata and controls
104 lines (91 loc) · 3.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
/**
* Storage Provider Factory
* Creates the appropriate server storage provider based on STORAGE_PROVIDER env var.
* Uses singleton pattern — one provider instance per process.
*/
import type { ServerStorageProvider, StorageConfigResponse } from "./types";
import { withCredentialEncryption } from "./encrypting-provider";
import { getStorageProviderType } from "./provider-type";
let _provider: ServerStorageProvider | null = null;
let _initialized = false;
/**
* The in-flight initialization, if any. The first caller to arrive when no
* provider exists starts exactly one build+initialize and every concurrent
* caller awaits the SAME promise — otherwise two overlapping first requests
* (e.g. the page-load `GET /api/storage` racing a `PUT /api/storage/[collection]`)
* each construct and initialize a provider, and the second assignment silently
* overwrites the first mid-initialize, leaking its connections.
*/
let _initPromise: Promise<ServerStorageProvider | null> | null = null;
/**
* Check if server-side storage is enabled.
*/
export function isServerStorageEnabled(): boolean {
return getStorageProviderType() !== "local";
}
/**
* Get the storage configuration for the /api/storage/config endpoint.
*/
export function getStorageConfig(): StorageConfigResponse {
const provider = getStorageProviderType();
return {
provider,
serverMode: provider !== "local",
};
}
/**
* Get or create the singleton server storage provider.
* Returns null if STORAGE_PROVIDER is 'local' or not set.
* The provider is automatically initialized on first call.
*/
export async function getStorageProvider(): Promise<ServerStorageProvider | null> {
const providerType = getStorageProviderType();
if (providerType === "local") return null;
if (_provider && _initialized) return _provider;
// Concurrent first callers share one build+initialize: the promise is
// memoized the moment the first caller starts it, so there is no window in
// which a second caller can fall through and build a duplicate provider.
if (_initPromise) return _initPromise;
_initPromise = (async () => {
// Credential encryption is installed HERE, at the one choke point every storage route goes
// through, rather than inside the providers. Three call sites obtain a provider, all under
// src/app/api/storage/, and all of them call this function - so there is no route-level way to
// reach an unencrypted store, and a provider added later inherits the control by construction.
switch (providerType) {
case "sqlite": {
const { SQLiteStorageProvider } = await import("./providers/sqlite");
_provider = withCredentialEncryption(new SQLiteStorageProvider());
break;
}
case "postgres": {
const { PostgresStorageProvider } = await import("./providers/postgres");
_provider = withCredentialEncryption(new PostgresStorageProvider());
break;
}
}
if (_provider && !_initialized) {
await _provider.initialize();
_initialized = true;
}
return _provider;
})();
// A failed initialization must not be memoized: the next request should
// retry from scratch rather than await a rejected promise forever.
try {
return await _initPromise;
} catch (error) {
_initPromise = null;
throw error;
}
}
/**
* Close and reset the singleton provider. Used for testing/cleanup.
*/
export async function closeStorageProvider(): Promise<void> {
_initPromise = null;
if (_provider) {
await _provider.close();
_provider = null;
_initialized = false;
}
}