@spfn/core 0.2.0-beta.4 → 0.2.0-beta.42
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 +260 -1175
- package/dist/{boss-BO8ty33K.d.ts → boss-DI1r4kTS.d.ts} +24 -7
- package/dist/cache/index.js +32 -29
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +55 -8
- package/dist/codegen/index.js +179 -5
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +168 -6
- package/dist/config/index.js +29 -5
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +128 -4
- package/dist/db/index.js +177 -50
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +55 -1
- package/dist/env/index.js +71 -3
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +27 -19
- package/dist/env/loader.js +33 -25
- package/dist/env/loader.js.map +1 -1
- package/dist/event/index.d.ts +27 -1
- package/dist/event/index.js +6 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.d.ts +77 -2
- package/dist/event/sse/client.js +87 -24
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.d.ts +10 -4
- package/dist/event/sse/index.js +158 -12
- package/dist/event/sse/index.js.map +1 -1
- package/dist/job/index.d.ts +23 -8
- package/dist/job/index.js +96 -20
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.d.ts +5 -0
- package/dist/logger/index.js +14 -0
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +23 -1
- package/dist/middleware/index.js +58 -5
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +2 -2
- package/dist/nextjs/index.js +77 -31
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +44 -23
- package/dist/nextjs/server.js +83 -65
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.d.ts +158 -4
- package/dist/route/index.js +253 -17
- package/dist/route/index.js.map +1 -1
- package/dist/server/index.d.ts +251 -16
- package/dist/server/index.js +774 -228
- package/dist/server/index.js.map +1 -1
- package/dist/{types-D_N_U-Py.d.ts → types-7Mhoxnnt.d.ts} +21 -1
- package/dist/types-DKQ90YL7.d.ts +372 -0
- package/docs/cache.md +133 -0
- package/docs/codegen.md +74 -0
- package/docs/database.md +370 -0
- package/docs/entity.md +539 -0
- package/docs/env.md +499 -0
- package/docs/errors.md +319 -0
- package/docs/event.md +443 -0
- package/docs/file-upload.md +717 -0
- package/docs/job.md +131 -0
- package/docs/logger.md +108 -0
- package/docs/middleware.md +337 -0
- package/docs/nextjs.md +247 -0
- package/docs/repository.md +496 -0
- package/docs/route.md +497 -0
- package/docs/server.md +429 -0
- package/package.json +3 -2
- package/dist/types-B-e_f2dQ.d.ts +0 -121
|
@@ -61,11 +61,15 @@ interface JobSendOptions {
|
|
|
61
61
|
/**
|
|
62
62
|
* Job handler function type
|
|
63
63
|
*/
|
|
64
|
-
type JobHandler<TInput> = TInput extends void ? () => Promise<
|
|
64
|
+
type JobHandler<TInput, TOutput = void> = TInput extends void ? () => Promise<TOutput> : (input: TInput) => Promise<TOutput>;
|
|
65
|
+
/**
|
|
66
|
+
* Compensate handler function type (for rollback)
|
|
67
|
+
*/
|
|
68
|
+
type CompensateHandler<TInput, TOutput> = (input: TInput, output: TOutput) => Promise<void>;
|
|
65
69
|
/**
|
|
66
70
|
* Job definition interface
|
|
67
71
|
*/
|
|
68
|
-
interface JobDef<TInput = void> {
|
|
72
|
+
interface JobDef<TInput = void, TOutput = void> {
|
|
69
73
|
/**
|
|
70
74
|
* Unique job name
|
|
71
75
|
*/
|
|
@@ -74,6 +78,10 @@ interface JobDef<TInput = void> {
|
|
|
74
78
|
* TypeBox input schema (optional)
|
|
75
79
|
*/
|
|
76
80
|
readonly inputSchema?: TSchema;
|
|
81
|
+
/**
|
|
82
|
+
* TypeBox output schema (optional, for workflow integration)
|
|
83
|
+
*/
|
|
84
|
+
readonly outputSchema?: TSchema;
|
|
77
85
|
/**
|
|
78
86
|
* Cron expression for scheduled jobs
|
|
79
87
|
*/
|
|
@@ -97,7 +105,11 @@ interface JobDef<TInput = void> {
|
|
|
97
105
|
/**
|
|
98
106
|
* Job handler
|
|
99
107
|
*/
|
|
100
|
-
readonly handler: JobHandler<TInput>;
|
|
108
|
+
readonly handler: JobHandler<TInput, TOutput>;
|
|
109
|
+
/**
|
|
110
|
+
* Compensate handler for rollback (optional, for workflow integration)
|
|
111
|
+
*/
|
|
112
|
+
readonly compensate?: CompensateHandler<TInput, TOutput>;
|
|
101
113
|
/**
|
|
102
114
|
* Send job to queue (returns immediately, executes in background)
|
|
103
115
|
*/
|
|
@@ -105,16 +117,17 @@ interface JobDef<TInput = void> {
|
|
|
105
117
|
/**
|
|
106
118
|
* Run job synchronously (for testing/debugging)
|
|
107
119
|
*/
|
|
108
|
-
run: TInput extends void ? () => Promise<
|
|
120
|
+
run: TInput extends void ? () => Promise<TOutput> : (input: TInput) => Promise<TOutput>;
|
|
109
121
|
/**
|
|
110
122
|
* Type inference helpers
|
|
111
123
|
*/
|
|
112
124
|
_input: TInput;
|
|
125
|
+
_output: TOutput;
|
|
113
126
|
}
|
|
114
127
|
/**
|
|
115
128
|
* Job router entry - can be a job or nested router
|
|
116
129
|
*/
|
|
117
|
-
type JobRouterEntry = JobDef<any> | JobRouter<any>;
|
|
130
|
+
type JobRouterEntry = JobDef<any, any> | JobRouter<any>;
|
|
118
131
|
/**
|
|
119
132
|
* Job router interface
|
|
120
133
|
*/
|
|
@@ -125,7 +138,11 @@ interface JobRouter<TJobs extends Record<string, JobRouterEntry> = Record<string
|
|
|
125
138
|
/**
|
|
126
139
|
* Infer input type from JobDef
|
|
127
140
|
*/
|
|
128
|
-
type InferJobInput<TJob> = TJob extends JobDef<infer TInput> ? TInput : never;
|
|
141
|
+
type InferJobInput<TJob> = TJob extends JobDef<infer TInput, any> ? TInput : never;
|
|
142
|
+
/**
|
|
143
|
+
* Infer output type from JobDef
|
|
144
|
+
*/
|
|
145
|
+
type InferJobOutput<TJob> = TJob extends JobDef<any, infer TOutput> ? TOutput : never;
|
|
129
146
|
|
|
130
147
|
/**
|
|
131
148
|
* pg-boss Wrapper
|
|
@@ -224,4 +241,4 @@ declare function isBossRunning(): boolean;
|
|
|
224
241
|
*/
|
|
225
242
|
declare function shouldClearOnStart(): boolean;
|
|
226
243
|
|
|
227
|
-
export { type BossOptions as B, type InferJobInput as I, type JobRouter as J, type JobOptions as a, type JobHandler as b, type JobDef as c, type JobRouterEntry as d, type JobSendOptions as e,
|
|
244
|
+
export { type BossOptions as B, type CompensateHandler as C, type InferJobInput as I, type JobRouter as J, type JobOptions as a, type JobHandler as b, type JobDef as c, type JobRouterEntry as d, type JobSendOptions as e, type InferJobOutput as f, getBoss as g, isBossRunning as h, initBoss as i, shouldClearOnStart as j, type BossConfig as k, stopBoss as s };
|
package/dist/cache/index.js
CHANGED
|
@@ -107,26 +107,29 @@ async function createSingleCacheFromEnv() {
|
|
|
107
107
|
return write;
|
|
108
108
|
}
|
|
109
109
|
var cacheLogger2 = logger.child("@spfn/core:cache");
|
|
110
|
-
var
|
|
111
|
-
var
|
|
112
|
-
|
|
110
|
+
var CACHE_KEY = Symbol.for("@spfn/core:cache");
|
|
111
|
+
var state = globalThis[CACHE_KEY] ??= {
|
|
112
|
+
write: void 0,
|
|
113
|
+
read: void 0,
|
|
114
|
+
disabled: false
|
|
115
|
+
};
|
|
113
116
|
function getCache() {
|
|
114
|
-
return
|
|
117
|
+
return state.write;
|
|
115
118
|
}
|
|
116
119
|
function getCacheRead() {
|
|
117
|
-
return
|
|
120
|
+
return state.read ?? state.write;
|
|
118
121
|
}
|
|
119
122
|
function isCacheDisabled() {
|
|
120
|
-
return
|
|
123
|
+
return state.disabled;
|
|
121
124
|
}
|
|
122
125
|
function setCache(write, read) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
+
state.write = write;
|
|
127
|
+
state.read = read ?? write;
|
|
128
|
+
state.disabled = !write;
|
|
126
129
|
}
|
|
127
130
|
async function initCache() {
|
|
128
|
-
if (
|
|
129
|
-
return { write:
|
|
131
|
+
if (state.write) {
|
|
132
|
+
return { write: state.write, read: state.read, disabled: state.disabled };
|
|
130
133
|
}
|
|
131
134
|
const { write, read } = await createCacheFromEnv();
|
|
132
135
|
if (write) {
|
|
@@ -135,15 +138,15 @@ async function initCache() {
|
|
|
135
138
|
if (read && read !== write) {
|
|
136
139
|
await read.ping();
|
|
137
140
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
state.write = write;
|
|
142
|
+
state.read = read;
|
|
143
|
+
state.disabled = false;
|
|
141
144
|
const hasReplica = read && read !== write;
|
|
142
145
|
cacheLogger2.info(
|
|
143
146
|
hasReplica ? "Cache connected (Master-Replica)" : "Cache connected",
|
|
144
147
|
{ mode: "enabled" }
|
|
145
148
|
);
|
|
146
|
-
return { write:
|
|
149
|
+
return { write: state.write, read: state.read, disabled: false };
|
|
147
150
|
} catch (error) {
|
|
148
151
|
cacheLogger2.error(
|
|
149
152
|
"Cache connection failed - running in disabled mode",
|
|
@@ -157,46 +160,46 @@ async function initCache() {
|
|
|
157
160
|
}
|
|
158
161
|
} catch {
|
|
159
162
|
}
|
|
160
|
-
|
|
163
|
+
state.disabled = true;
|
|
161
164
|
return { write: void 0, read: void 0, disabled: true };
|
|
162
165
|
}
|
|
163
166
|
}
|
|
164
|
-
|
|
167
|
+
state.disabled = true;
|
|
165
168
|
cacheLogger2.info("Cache disabled - no configuration or library not installed", { mode: "disabled" });
|
|
166
169
|
return { write: void 0, read: void 0, disabled: true };
|
|
167
170
|
}
|
|
168
171
|
async function closeCache() {
|
|
169
|
-
if (
|
|
172
|
+
if (state.disabled) {
|
|
170
173
|
cacheLogger2.debug("Cache already disabled, nothing to close");
|
|
171
174
|
return;
|
|
172
175
|
}
|
|
173
176
|
const closePromises = [];
|
|
174
|
-
if (
|
|
177
|
+
if (state.write) {
|
|
175
178
|
closePromises.push(
|
|
176
|
-
|
|
179
|
+
state.write.quit().catch((err) => {
|
|
177
180
|
cacheLogger2.error("Error closing cache write instance", err);
|
|
178
181
|
})
|
|
179
182
|
);
|
|
180
183
|
}
|
|
181
|
-
if (
|
|
184
|
+
if (state.read && state.read !== state.write) {
|
|
182
185
|
closePromises.push(
|
|
183
|
-
|
|
186
|
+
state.read.quit().catch((err) => {
|
|
184
187
|
cacheLogger2.error("Error closing cache read instance", err);
|
|
185
188
|
})
|
|
186
189
|
);
|
|
187
190
|
}
|
|
188
191
|
await Promise.all(closePromises);
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
+
state.write = void 0;
|
|
193
|
+
state.read = void 0;
|
|
194
|
+
state.disabled = true;
|
|
192
195
|
cacheLogger2.info("Cache connections closed", { mode: "disabled" });
|
|
193
196
|
}
|
|
194
197
|
function getCacheInfo() {
|
|
195
198
|
return {
|
|
196
|
-
hasWrite: !!
|
|
197
|
-
hasRead: !!
|
|
198
|
-
isReplica: !!(
|
|
199
|
-
disabled:
|
|
199
|
+
hasWrite: !!state.write,
|
|
200
|
+
hasRead: !!state.read,
|
|
201
|
+
isReplica: !!(state.read && state.read !== state.write),
|
|
202
|
+
disabled: state.disabled
|
|
200
203
|
};
|
|
201
204
|
}
|
|
202
205
|
|
package/dist/cache/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/cache/cache-factory.ts","../../src/cache/cache-manager.ts"],"names":["cacheLogger","logger"],"mappings":";;;AAOA,IAAM,WAAA,GAAc,MAAA,CAAO,KAAA,CAAM,kBAAkB,CAAA;AAYnD,SAAS,cAAA,GACT;AACI,EAAA,OAAO,CAAC,EACJ,OAAA,CAAQ,GAAA,CAAI,aACZ,OAAA,CAAQ,GAAA,CAAI,eAAA,IACZ,OAAA,CAAQ,IAAI,cAAA,IACZ,OAAA,CAAQ,GAAA,CAAI,oBAAA,IACZ,QAAQ,GAAA,CAAI,mBAAA,CAAA;AAEpB;AAKA,SAAS,YAAA,CACL,aACA,GAAA,EAEJ;AACI,EAAA,MAAM,UAAwB,EAAC;AAG/B,EAAA,IAAI,GAAA,CAAI,UAAA,CAAW,WAAW,CAAA,EAC9B;AACI,IAAA,OAAA,CAAQ,GAAA,GAAM;AAAA,MACV,kBAAA,EAAoB,OAAA,CAAQ,GAAA,CAAI,6BAAA,KAAkC;AAAA,KACtE;AAAA,EACJ;AAEA,EAAA,OAAO,IAAI,WAAA,CAAY,GAAA,EAAK,OAAO,CAAA;AACvC;AAiCA,eAAsB,kBAAA,GACtB;AAEI,EAAA,IAAI,CAAC,gBAAe,EACpB;AACI,IAAA,WAAA,CAAY,KAAK,sDAAsD,CAAA;AACvE,IAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAU;AAAA,EAC/C;AAEA,EAAA,IACA;AAEI,IAAA,MAAM,OAAA,GAAU,MAAM,OAAO,SAAS,CAAA;AACtC,IAAA,MAAM,cAAc,OAAA,CAAQ,OAAA;AAG5B,IAAA,MAAM,SAAA,GAAY,QAAQ,GAAA,CAAI,SAAA;AAC9B,IAAA,MAAM,QAAA,GAAW,QAAQ,GAAA,CAAI,eAAA;AAC7B,IAAA,MAAM,OAAA,GAAU,QAAQ,GAAA,CAAI,cAAA;AAC5B,IAAA,MAAM,YAAA,GAAe,QAAQ,GAAA,CAAI,mBAAA;AACjC,IAAA,MAAM,aAAA,GAAgB,QAAQ,GAAA,CAAI,oBAAA;AAClC,IAAA,MAAM,UAAA,GAAa,QAAQ,GAAA,CAAI,iBAAA;AAC/B,IAAA,MAAM,QAAA,GAAW,QAAQ,GAAA,CAAI,cAAA;AAG7B,IAAA,IAAI,aAAa,CAAC,QAAA,IAAY,CAAC,OAAA,IAAW,CAAC,YAAA,EAC3C;AACI,MAAA,MAAM,MAAA,GAAS,YAAA,CAAa,WAAA,EAAa,SAAS,CAAA;AAClD,MAAA,WAAA,CAAY,KAAA,CAAM,iCAAiC,EAAE,GAAA,EAAK,UAAU,OAAA,CAAQ,UAAA,EAAY,OAAO,CAAA,EAAG,CAAA;AAClG,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAO;AAAA,IACzC;AAGA,IAAA,IAAI,YAAY,OAAA,EAChB;AACI,MAAA,MAAM,KAAA,GAAQ,YAAA,CAAa,WAAA,EAAa,QAAQ,CAAA;AAChD,MAAA,MAAM,IAAA,GAAO,YAAA,CAAa,WAAA,EAAa,OAAO,CAAA;AAC9C,MAAA,WAAA,CAAY,MAAM,wCAAwC,CAAA;AAC1D,MAAA,OAAO,EAAE,OAAO,IAAA,EAAK;AAAA,IACzB;AAGA,IAAA,IAAI,iBAAiB,UAAA,EACrB;AACI,MAAA,MAAM,YAAY,aAAA,CAAc,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,IAAA,KAChD;AACI,QAAA,MAAM,CAAC,UAAU,IAAI,CAAA,GAAI,KAAK,IAAA,EAAK,CAAE,MAAM,GAAG,CAAA;AAC9C,QAAA,OAAO,EAAE,IAAA,EAAM,QAAA,EAAU,MAAM,MAAA,CAAO,IAAI,KAAK,KAAA,EAAM;AAAA,MACzD,CAAC,CAAA;AAED,MAAA,MAAM,OAAA,GAAwB;AAAA,QAC1B,SAAA;AAAA,QACA,IAAA,EAAM,UAAA;AAAA,QACN;AAAA,OACJ;AAEA,MAAA,MAAM,MAAA,GAAS,IAAI,WAAA,CAAY,OAAO,CAAA;AACtC,MAAA,WAAA,CAAY,MAAM,iCAAA,EAAmC,EAAE,YAAY,SAAA,EAAW,SAAA,CAAU,QAAQ,CAAA;AAChG,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAO;AAAA,IACzC;AAGA,IAAA,IAAI,YAAA,EACJ;AACI,MAAA,MAAM,QAAQ,YAAA,CAAa,KAAA,CAAM,GAAG,CAAA,CAAE,GAAA,CAAI,CAAC,IAAA,KAC3C;AACI,QAAA,MAAM,CAAC,MAAM,IAAI,CAAA,GAAI,KAAK,IAAA,EAAK,CAAE,MAAM,GAAG,CAAA;AAC1C,QAAA,OAAO,EAAE,IAAA,EAAM,IAAA,EAAM,MAAA,CAAO,IAAI,KAAK,IAAA,EAAK;AAAA,MAC9C,CAAC,CAAA;AAED,MAAA,MAAM,cAAA,GAAiC;AAAA,QACnC,YAAA,EAAc;AAAA,UACV;AAAA;AACJ,OACJ;AAEA,MAAA,MAAM,OAAA,GAAU,IAAI,WAAA,CAAY,OAAA,CAAQ,OAAO,cAAc,CAAA;AAC7D,MAAA,WAAA,CAAY,MAAM,gCAAA,EAAkC,EAAE,KAAA,EAAO,KAAA,CAAM,QAAQ,CAAA;AAC3E,MAAA,OAAO,EAAE,KAAA,EAAO,OAAA,EAAS,IAAA,EAAM,OAAA,EAAQ;AAAA,IAC3C;AAGA,IAAA,IAAI,SAAA,EACJ;AACI,MAAA,MAAM,MAAA,GAAS,YAAA,CAAa,WAAA,EAAa,SAAS,CAAA;AAClD,MAAA,WAAA,CAAY,KAAA,CAAM,qCAAqC,EAAE,GAAA,EAAK,UAAU,OAAA,CAAQ,UAAA,EAAY,OAAO,CAAA,EAAG,CAAA;AACtG,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAQ,IAAA,EAAM,MAAA,EAAO;AAAA,IACzC;AAGA,IAAA,WAAA,CAAY,KAAK,4DAA4D,CAAA;AAC7E,IAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAU;AAAA,EAC/C,SACO,KAAA,EACP;AACI,IAAA,IAAI,iBAAiB,KAAA,EACrB;AAEI,MAAA,IAAI,KAAA,CAAM,OAAA,CAAQ,QAAA,CAAS,oBAAoB,CAAA,EAC/C;AACI,QAAA,WAAA,CAAY,IAAA;AAAA,UACR,oCAAA;AAAA,UACA,KAAA;AAAA,UACA;AAAA,YACI,UAAA,EAAY,uDAAA;AAAA,YACZ,IAAA,EAAM;AAAA;AACV,SACJ;AAAA,MACJ,CAAA,MAEA;AACI,QAAA,WAAA,CAAY,IAAA;AAAA,UACR,+BAAA;AAAA,UACA,KAAA;AAAA,UACA,EAAE,MAAM,UAAA;AAAW,SACvB;AAAA,MACJ;AAAA,IACJ,CAAA,MAEA;AACI,MAAA,WAAA,CAAY,IAAA;AAAA,QACR,+BAAA;AAAA,QACA,EAAE,KAAA,EAAO,MAAA,CAAO,KAAK,CAAA,EAAG,MAAM,UAAA;AAAW,OAC7C;AAAA,IACJ;AACA,IAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAU;AAAA,EAC/C;AACJ;AAMA,eAAsB,wBAAA,GACtB;AACI,EAAA,MAAM,EAAE,KAAA,EAAM,GAAI,MAAM,kBAAA,EAAmB;AAC3C,EAAA,OAAO,KAAA;AACX;AC9MA,IAAMA,YAAAA,GAAcC,MAAAA,CAAO,KAAA,CAAM,kBAAkB,CAAA;AAEnD,IAAI,aAAA;AACJ,IAAI,YAAA;AACJ,IAAI,UAAA,GAAa,KAAA;AAoBV,SAAS,QAAA,GAChB;AACI,EAAA,OAAO,aAAA;AACX;AAiBO,SAAS,YAAA,GAChB;AACI,EAAA,OAAO,YAAA,IAAgB,aAAA;AAC3B;AAeO,SAAS,eAAA,GAChB;AACI,EAAA,OAAO,UAAA;AACX;AAkBO,SAAS,QAAA,CACZ,OACA,IAAA,EAEJ;AACI,EAAA,aAAA,GAAgB,KAAA;AAChB,EAAA,YAAA,GAAe,IAAA,IAAQ,KAAA;AACvB,EAAA,UAAA,GAAa,CAAC,KAAA;AAClB;AA2BA,eAAsB,SAAA,GAKtB;AAEI,EAAA,IAAI,aAAA,EACJ;AACI,IAAA,OAAO,EAAE,KAAA,EAAO,aAAA,EAAe,IAAA,EAAM,YAAA,EAAc,UAAU,UAAA,EAAW;AAAA,EAC5E;AAGA,EAAA,MAAM,EAAE,KAAA,EAAO,IAAA,EAAK,GAAI,MAAM,kBAAA,EAAmB;AAEjD,EAAA,IAAI,KAAA,EACJ;AACI,IAAA,IACA;AAEI,MAAA,MAAM,MAAM,IAAA,EAAK;AAGjB,MAAA,IAAI,IAAA,IAAQ,SAAS,KAAA,EACrB;AACI,QAAA,MAAM,KAAK,IAAA,EAAK;AAAA,MACpB;AAEA,MAAA,aAAA,GAAgB,KAAA;AAChB,MAAA,YAAA,GAAe,IAAA;AACf,MAAA,UAAA,GAAa,KAAA;AAEb,MAAA,MAAM,UAAA,GAAa,QAAQ,IAAA,KAAS,KAAA;AACpC,MAAAD,YAAAA,CAAY,IAAA;AAAA,QACR,aACM,kCAAA,GACA,iBAAA;AAAA,QACN,EAAE,MAAM,SAAA;AAAU,OACtB;AAEA,MAAA,OAAO,EAAE,KAAA,EAAO,aAAA,EAAe,IAAA,EAAM,YAAA,EAAc,UAAU,KAAA,EAAM;AAAA,IACvE,SACO,KAAA,EACP;AACI,MAAAA,YAAAA,CAAY,KAAA;AAAA,QACR,oDAAA;AAAA,QACA,iBAAiB,KAAA,GAAQ,KAAA,GAAQ,IAAI,KAAA,CAAM,MAAA,CAAO,KAAK,CAAC,CAAA;AAAA,QACxD,EAAE,MAAM,UAAA;AAAW,OACvB;AAGA,MAAA,IACA;AACI,QAAA,MAAM,MAAM,IAAA,EAAK;AACjB,QAAA,IAAI,IAAA,IAAQ,SAAS,KAAA,EACrB;AACI,UAAA,MAAM,KAAK,IAAA,EAAK;AAAA,QACpB;AAAA,MACJ,CAAA,CAAA,MAEA;AAAA,MAEA;AAEA,MAAA,UAAA,GAAa,IAAA;AACb,MAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAW,UAAU,IAAA,EAAK;AAAA,IAC/D;AAAA,EACJ;AAGA,EAAA,UAAA,GAAa,IAAA;AACb,EAAAA,aAAY,IAAA,CAAK,4DAAA,EAA8D,EAAE,IAAA,EAAM,YAAY,CAAA;AACnG,EAAA,OAAO,EAAE,KAAA,EAAO,MAAA,EAAW,IAAA,EAAM,MAAA,EAAW,UAAU,IAAA,EAAK;AAC/D;AAaA,eAAsB,UAAA,GACtB;AACI,EAAA,IAAI,UAAA,EACJ;AACI,IAAAA,YAAAA,CAAY,MAAM,0CAA0C,CAAA;AAC5D,IAAA;AAAA,EACJ;AAEA,EAAA,MAAM,gBAAoC,EAAC;AAE3C,EAAA,IAAI,aAAA,EACJ;AACI,IAAA,aAAA,CAAc,IAAA;AAAA,MACV,aAAA,CAAc,IAAA,EAAK,CAAE,KAAA,CAAM,CAAC,GAAA,KAC5B;AACI,QAAAA,YAAAA,CAAY,KAAA,CAAM,oCAAA,EAAsC,GAAG,CAAA;AAAA,MAC/D,CAAC;AAAA,KACL;AAAA,EACJ;AAEA,EAAA,IAAI,YAAA,IAAgB,iBAAiB,aAAA,EACrC;AACI,IAAA,aAAA,CAAc,IAAA;AAAA,MACV,YAAA,CAAa,IAAA,EAAK,CAAE,KAAA,CAAM,CAAC,GAAA,KAC3B;AACI,QAAAA,YAAAA,CAAY,KAAA,CAAM,mCAAA,EAAqC,GAAG,CAAA;AAAA,MAC9D,CAAC;AAAA,KACL;AAAA,EACJ;AAEA,EAAA,MAAM,OAAA,CAAQ,IAAI,aAAa,CAAA;AAE/B,EAAA,aAAA,GAAgB,MAAA;AAChB,EAAA,YAAA,GAAe,MAAA;AACf,EAAA,UAAA,GAAa,IAAA;AAEb,EAAAA,aAAY,IAAA,CAAK,0BAAA,EAA4B,EAAE,IAAA,EAAM,YAAY,CAAA;AACrE;AAmBO,SAAS,YAAA,GAMhB;AACI,EAAA,OAAO;AAAA,IACH,QAAA,EAAU,CAAC,CAAC,aAAA;AAAA,IACZ,OAAA,EAAS,CAAC,CAAC,YAAA;AAAA,IACX,SAAA,EAAW,CAAC,EAAE,YAAA,IAAgB,YAAA,KAAiB,aAAA,CAAA;AAAA,IAC/C,QAAA,EAAU;AAAA,GACd;AACJ","file":"index.js","sourcesContent":["/**\n * Cache factory with automatic environment variable detection\n */\n\nimport type { Redis, Cluster, RedisOptions, ClusterOptions } from 'ioredis';\nimport { logger } from '@spfn/core/logger';\n\nconst cacheLogger = logger.child('@spfn/core:cache');\n\nexport interface CacheClients {\n /** Primary cache for writes (or both read/write if no replica) */\n write?: Redis | Cluster;\n /** Replica cache for reads (optional, falls back to write) */\n read?: Redis | Cluster;\n}\n\n/**\n * Check if any cache configuration exists in environment\n */\nfunction hasCacheConfig(): boolean\n{\n return !!(\n process.env.CACHE_URL ||\n process.env.CACHE_WRITE_URL ||\n process.env.CACHE_READ_URL ||\n process.env.CACHE_SENTINEL_HOSTS ||\n process.env.CACHE_CLUSTER_NODES\n );\n}\n\n/**\n * Create cache client with TLS support\n */\nfunction createClient(\n RedisClient: new (url: string, options?: RedisOptions) => Redis,\n url: string\n): Redis\n{\n const options: RedisOptions = {};\n\n // TLS support for secure connections\n if (url.startsWith('rediss://'))\n {\n options.tls = {\n rejectUnauthorized: process.env.CACHE_TLS_REJECT_UNAUTHORIZED !== 'false',\n };\n }\n\n return new RedisClient(url, options);\n}\n\n/**\n * Create cache client(s) from environment variables\n *\n * Supported patterns (priority order):\n * 1. Single instance: CACHE_URL\n * 2. Master-Replica: CACHE_WRITE_URL + CACHE_READ_URL\n * 3. Sentinel: CACHE_SENTINEL_HOSTS + CACHE_MASTER_NAME\n * 4. Cluster: CACHE_CLUSTER_NODES\n *\n * @returns Cache client(s) or undefined if no configuration found\n *\n * @example\n * ```bash\n * # Single (most common)\n * CACHE_URL=redis://localhost:6379\n * CACHE_URL=rediss://secure.cache.com:6380 # TLS\n *\n * # Master-Replica\n * CACHE_WRITE_URL=redis://master:6379\n * CACHE_READ_URL=redis://replica:6379\n *\n * # Sentinel\n * CACHE_SENTINEL_HOSTS=sentinel1:26379,sentinel2:26379\n * CACHE_MASTER_NAME=mymaster\n * CACHE_PASSWORD=secret\n *\n * # Cluster\n * CACHE_CLUSTER_NODES=node1:6379,node2:6379,node3:6379\n * CACHE_PASSWORD=secret\n * ```\n */\nexport async function createCacheFromEnv(): Promise<CacheClients>\n{\n // Quick exit if no cache config\n if (!hasCacheConfig())\n {\n cacheLogger.info('No cache configuration found - running without cache');\n return { write: undefined, read: undefined };\n }\n\n try\n {\n // Dynamic import to avoid bundling if not used\n const ioredis = await import('ioredis');\n const RedisClient = ioredis.default;\n\n // Get environment variables\n const singleUrl = process.env.CACHE_URL;\n const writeUrl = process.env.CACHE_WRITE_URL;\n const readUrl = process.env.CACHE_READ_URL;\n const clusterNodes = process.env.CACHE_CLUSTER_NODES;\n const sentinelHosts = process.env.CACHE_SENTINEL_HOSTS;\n const masterName = process.env.CACHE_MASTER_NAME;\n const password = process.env.CACHE_PASSWORD;\n\n // 1. Single instance (most common - highest priority)\n if (singleUrl && !writeUrl && !readUrl && !clusterNodes)\n {\n const client = createClient(RedisClient, singleUrl);\n cacheLogger.debug('Created single cache instance', { url: singleUrl.replace(/:[^:@]+@/, ':***@') });\n return { write: client, read: client };\n }\n\n // 2. Master-Replica pattern (both URLs required)\n if (writeUrl && readUrl)\n {\n const write = createClient(RedisClient, writeUrl);\n const read = createClient(RedisClient, readUrl);\n cacheLogger.debug('Created master-replica cache instances');\n return { write, read };\n }\n\n // 3. Sentinel pattern\n if (sentinelHosts && masterName)\n {\n const sentinels = sentinelHosts.split(',').map((host) =>\n {\n const [hostname, port] = host.trim().split(':');\n return { host: hostname, port: Number(port) || 26379 };\n });\n\n const options: RedisOptions = {\n sentinels,\n name: masterName,\n password,\n };\n\n const client = new RedisClient(options);\n cacheLogger.debug('Created sentinel cache instance', { masterName, sentinels: sentinels.length });\n return { write: client, read: client };\n }\n\n // 4. Cluster pattern\n if (clusterNodes)\n {\n const nodes = clusterNodes.split(',').map((node) =>\n {\n const [host, port] = node.trim().split(':');\n return { host, port: Number(port) || 6379 };\n });\n\n const clusterOptions: ClusterOptions = {\n redisOptions: {\n password,\n },\n };\n\n const cluster = new RedisClient.Cluster(nodes, clusterOptions);\n cacheLogger.debug('Created cluster cache instance', { nodes: nodes.length });\n return { write: cluster, read: cluster };\n }\n\n // 5. Fallback: Single URL with other configs present\n if (singleUrl)\n {\n const client = createClient(RedisClient, singleUrl);\n cacheLogger.debug('Created cache instance (fallback)', { url: singleUrl.replace(/:[^:@]+@/, ':***@') });\n return { write: client, read: client };\n }\n\n // No valid configuration\n cacheLogger.info('No valid cache configuration found - running without cache');\n return { write: undefined, read: undefined };\n }\n catch (error)\n {\n if (error instanceof Error)\n {\n // Check if it's a missing dependency error\n if (error.message.includes('Cannot find module'))\n {\n cacheLogger.warn(\n 'Cache client library not installed',\n error,\n {\n suggestion: 'Install ioredis to enable cache: pnpm install ioredis',\n mode: 'disabled'\n }\n );\n }\n else\n {\n cacheLogger.warn(\n 'Failed to create cache client',\n error,\n { mode: 'disabled' }\n );\n }\n }\n else\n {\n cacheLogger.warn(\n 'Failed to create cache client',\n { error: String(error), mode: 'disabled' }\n );\n }\n return { write: undefined, read: undefined };\n }\n}\n\n/**\n * Create single cache client (backward compatibility)\n * Only returns write instance\n */\nexport async function createSingleCacheFromEnv(): Promise<Redis | Cluster | undefined>\n{\n const { write } = await createCacheFromEnv();\n return write;\n}","/**\n * Global cache instance manager\n * Provides singleton access to cache (Valkey/Redis) across all modules\n * Supports Master-Replica pattern with separate read/write instances\n *\n * When cache is unavailable, falls back to disabled mode gracefully\n */\n\nimport type { Redis, Cluster } from 'ioredis';\n\nimport { createCacheFromEnv } from './cache-factory';\nimport { logger } from '@spfn/core/logger';\n\nconst cacheLogger = logger.child('@spfn/core:cache');\n\nlet writeInstance: Redis | Cluster | undefined;\nlet readInstance: Redis | Cluster | undefined;\nlet isDisabled = false;\n\n/**\n * Get global cache write instance\n *\n * @returns Cache write instance or undefined if disabled/not initialized\n *\n * @example\n * ```typescript\n * import { getCache } from '@spfn/core/cache';\n *\n * const cache = getCache();\n * if (cache) {\n * await cache.set('key', 'value');\n * } else {\n * // Cache disabled - handle gracefully\n * console.log('Cache unavailable, skipping...');\n * }\n * ```\n */\nexport function getCache(): Redis | Cluster | undefined\n{\n return writeInstance;\n}\n\n/**\n * Get global cache read instance (falls back to write if no replica)\n *\n * @returns Cache read instance or write instance as fallback, undefined if disabled\n *\n * @example\n * ```typescript\n * import { getCacheRead } from '@spfn/core/cache';\n *\n * const cache = getCacheRead();\n * if (cache) {\n * const value = await cache.get('key');\n * }\n * ```\n */\nexport function getCacheRead(): Redis | Cluster | undefined\n{\n return readInstance ?? writeInstance;\n}\n\n/**\n * Check if cache is disabled (connection failed or not configured)\n *\n * @example\n * ```typescript\n * import { isCacheDisabled } from '@spfn/core/cache';\n *\n * if (isCacheDisabled()) {\n * // Use alternative strategy (e.g., in-memory cache, database)\n * return await fetchFromDatabase();\n * }\n * ```\n */\nexport function isCacheDisabled(): boolean\n{\n return isDisabled;\n}\n\n/**\n * Set global cache instances (for testing or manual configuration)\n *\n * @param write - Cache write instance\n * @param read - Cache read instance (optional, defaults to write)\n *\n * @example\n * ```typescript\n * import { setCache } from '@spfn/core/cache';\n * import Redis from 'ioredis';\n *\n * const write = new Redis('redis://master:6379');\n * const read = new Redis('redis://replica:6379');\n * setCache(write, read);\n * ```\n */\nexport function setCache(\n write: Redis | Cluster | undefined,\n read?: Redis | Cluster | undefined\n): void\n{\n writeInstance = write;\n readInstance = read ?? write;\n isDisabled = !write;\n}\n\n/**\n * Initialize cache from environment variables\n * Automatically called by startServer()\n *\n * Supported environment variables (priority order):\n * - VALKEY_URL / CACHE_URL (single instance)\n * - VALKEY_WRITE_URL + VALKEY_READ_URL (master-replica)\n * - VALKEY_SENTINEL_HOSTS + VALKEY_MASTER_NAME (sentinel)\n * - VALKEY_CLUSTER_NODES (cluster)\n * - VALKEY_TLS_REJECT_UNAUTHORIZED (TLS config)\n * - Legacy: REDIS_* (backward compatibility)\n *\n * @returns Object with write and read instances, or undefined if disabled\n *\n * @example\n * ```typescript\n * import { initCache } from '@spfn/core/cache';\n *\n * // Manual initialization (not needed if using startServer)\n * const { write, read, disabled } = await initCache();\n * if (!disabled) {\n * console.log('Cache available');\n * }\n * ```\n */\nexport async function initCache(): Promise<{\n write?: Redis | Cluster;\n read?: Redis | Cluster;\n disabled: boolean;\n}>\n{\n // Already initialized\n if (writeInstance)\n {\n return { write: writeInstance, read: readInstance, disabled: isDisabled };\n }\n\n // Auto-detect from environment\n const { write, read } = await createCacheFromEnv();\n\n if (write)\n {\n try\n {\n // Test connection\n await write.ping();\n\n // Test read instance if different\n if (read && read !== write)\n {\n await read.ping();\n }\n\n writeInstance = write;\n readInstance = read;\n isDisabled = false;\n\n const hasReplica = read && read !== write;\n cacheLogger.info(\n hasReplica\n ? 'Cache connected (Master-Replica)'\n : 'Cache connected',\n { mode: 'enabled' }\n );\n\n return { write: writeInstance, read: readInstance, disabled: false };\n }\n catch (error)\n {\n cacheLogger.error(\n 'Cache connection failed - running in disabled mode',\n error instanceof Error ? error : new Error(String(error)),\n { mode: 'disabled' }\n );\n\n // Clean up failed connections\n try\n {\n await write.quit();\n if (read && read !== write)\n {\n await read.quit();\n }\n }\n catch\n {\n // Ignore cleanup errors\n }\n\n isDisabled = true;\n return { write: undefined, read: undefined, disabled: true };\n }\n }\n\n // No configuration or library not installed\n isDisabled = true;\n cacheLogger.info('Cache disabled - no configuration or library not installed', { mode: 'disabled' });\n return { write: undefined, read: undefined, disabled: true };\n}\n\n/**\n * Close all cache connections and cleanup\n *\n * @example\n * ```typescript\n * import { closeCache } from '@spfn/core/cache';\n *\n * // During graceful shutdown\n * await closeCache();\n * ```\n */\nexport async function closeCache(): Promise<void>\n{\n if (isDisabled)\n {\n cacheLogger.debug('Cache already disabled, nothing to close');\n return;\n }\n\n const closePromises: Promise<unknown>[] = [];\n\n if (writeInstance)\n {\n closePromises.push(\n writeInstance.quit().catch((err: Error) =>\n {\n cacheLogger.error('Error closing cache write instance', err);\n })\n );\n }\n\n if (readInstance && readInstance !== writeInstance)\n {\n closePromises.push(\n readInstance.quit().catch((err: Error) =>\n {\n cacheLogger.error('Error closing cache read instance', err);\n })\n );\n }\n\n await Promise.all(closePromises);\n\n writeInstance = undefined;\n readInstance = undefined;\n isDisabled = true;\n\n cacheLogger.info('Cache connections closed', { mode: 'disabled' });\n}\n\n/**\n * Get cache connection info (for debugging)\n *\n * @example\n * ```typescript\n * import { getCacheInfo } from '@spfn/core/cache';\n *\n * const info = getCacheInfo();\n * console.log(info);\n * // {\n * // hasWrite: true,\n * // hasRead: true,\n * // isReplica: true,\n * // disabled: false\n * // }\n * ```\n */\nexport function getCacheInfo(): {\n hasWrite: boolean;\n hasRead: boolean;\n isReplica: boolean;\n disabled: boolean;\n}\n{\n return {\n hasWrite: !!writeInstance,\n hasRead: !!readInstance,\n isReplica: !!(readInstance && readInstance !== writeInstance),\n disabled: isDisabled,\n };\n}"]}
|
|
1
|
+
{"version":3,"sources":["../../src/cache/cache-factory.ts","../../src/cache/cache-manager.ts"],"names":["cacheLogger","logger"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Cache factory with automatic environment variable detection\n */\n\nimport type { Redis, Cluster, RedisOptions, ClusterOptions } from 'ioredis';\nimport { logger } from '@spfn/core/logger';\n\nconst cacheLogger = logger.child('@spfn/core:cache');\n\nexport interface CacheClients {\n /** Primary cache for writes (or both read/write if no replica) */\n write?: Redis | Cluster;\n /** Replica cache for reads (optional, falls back to write) */\n read?: Redis | Cluster;\n}\n\n/**\n * Check if any cache configuration exists in environment\n */\nfunction hasCacheConfig(): boolean\n{\n return !!(\n process.env.CACHE_URL ||\n process.env.CACHE_WRITE_URL ||\n process.env.CACHE_READ_URL ||\n process.env.CACHE_SENTINEL_HOSTS ||\n process.env.CACHE_CLUSTER_NODES\n );\n}\n\n/**\n * Create cache client with TLS support\n */\nfunction createClient(\n RedisClient: new (url: string, options?: RedisOptions) => Redis,\n url: string\n): Redis\n{\n const options: RedisOptions = {};\n\n // TLS support for secure connections\n if (url.startsWith('rediss://'))\n {\n options.tls = {\n rejectUnauthorized: process.env.CACHE_TLS_REJECT_UNAUTHORIZED !== 'false',\n };\n }\n\n return new RedisClient(url, options);\n}\n\n/**\n * Create cache client(s) from environment variables\n *\n * Supported patterns (priority order):\n * 1. Single instance: CACHE_URL\n * 2. Master-Replica: CACHE_WRITE_URL + CACHE_READ_URL\n * 3. Sentinel: CACHE_SENTINEL_HOSTS + CACHE_MASTER_NAME\n * 4. Cluster: CACHE_CLUSTER_NODES\n *\n * @returns Cache client(s) or undefined if no configuration found\n *\n * @example\n * ```bash\n * # Single (most common)\n * CACHE_URL=redis://localhost:6379\n * CACHE_URL=rediss://secure.cache.com:6380 # TLS\n *\n * # Master-Replica\n * CACHE_WRITE_URL=redis://master:6379\n * CACHE_READ_URL=redis://replica:6379\n *\n * # Sentinel\n * CACHE_SENTINEL_HOSTS=sentinel1:26379,sentinel2:26379\n * CACHE_MASTER_NAME=mymaster\n * CACHE_PASSWORD=secret\n *\n * # Cluster\n * CACHE_CLUSTER_NODES=node1:6379,node2:6379,node3:6379\n * CACHE_PASSWORD=secret\n * ```\n */\nexport async function createCacheFromEnv(): Promise<CacheClients>\n{\n // Quick exit if no cache config\n if (!hasCacheConfig())\n {\n cacheLogger.info('No cache configuration found - running without cache');\n return { write: undefined, read: undefined };\n }\n\n try\n {\n // Dynamic import to avoid bundling if not used\n const ioredis = await import('ioredis');\n const RedisClient = ioredis.default;\n\n // Get environment variables\n const singleUrl = process.env.CACHE_URL;\n const writeUrl = process.env.CACHE_WRITE_URL;\n const readUrl = process.env.CACHE_READ_URL;\n const clusterNodes = process.env.CACHE_CLUSTER_NODES;\n const sentinelHosts = process.env.CACHE_SENTINEL_HOSTS;\n const masterName = process.env.CACHE_MASTER_NAME;\n const password = process.env.CACHE_PASSWORD;\n\n // 1. Single instance (most common - highest priority)\n if (singleUrl && !writeUrl && !readUrl && !clusterNodes)\n {\n const client = createClient(RedisClient, singleUrl);\n cacheLogger.debug('Created single cache instance', { url: singleUrl.replace(/:[^:@]+@/, ':***@') });\n return { write: client, read: client };\n }\n\n // 2. Master-Replica pattern (both URLs required)\n if (writeUrl && readUrl)\n {\n const write = createClient(RedisClient, writeUrl);\n const read = createClient(RedisClient, readUrl);\n cacheLogger.debug('Created master-replica cache instances');\n return { write, read };\n }\n\n // 3. Sentinel pattern\n if (sentinelHosts && masterName)\n {\n const sentinels = sentinelHosts.split(',').map((host) =>\n {\n const [hostname, port] = host.trim().split(':');\n return { host: hostname, port: Number(port) || 26379 };\n });\n\n const options: RedisOptions = {\n sentinels,\n name: masterName,\n password,\n };\n\n const client = new RedisClient(options);\n cacheLogger.debug('Created sentinel cache instance', { masterName, sentinels: sentinels.length });\n return { write: client, read: client };\n }\n\n // 4. Cluster pattern\n if (clusterNodes)\n {\n const nodes = clusterNodes.split(',').map((node) =>\n {\n const [host, port] = node.trim().split(':');\n return { host, port: Number(port) || 6379 };\n });\n\n const clusterOptions: ClusterOptions = {\n redisOptions: {\n password,\n },\n };\n\n const cluster = new RedisClient.Cluster(nodes, clusterOptions);\n cacheLogger.debug('Created cluster cache instance', { nodes: nodes.length });\n return { write: cluster, read: cluster };\n }\n\n // 5. Fallback: Single URL with other configs present\n if (singleUrl)\n {\n const client = createClient(RedisClient, singleUrl);\n cacheLogger.debug('Created cache instance (fallback)', { url: singleUrl.replace(/:[^:@]+@/, ':***@') });\n return { write: client, read: client };\n }\n\n // No valid configuration\n cacheLogger.info('No valid cache configuration found - running without cache');\n return { write: undefined, read: undefined };\n }\n catch (error)\n {\n if (error instanceof Error)\n {\n // Check if it's a missing dependency error\n if (error.message.includes('Cannot find module'))\n {\n cacheLogger.warn(\n 'Cache client library not installed',\n error,\n {\n suggestion: 'Install ioredis to enable cache: pnpm install ioredis',\n mode: 'disabled'\n }\n );\n }\n else\n {\n cacheLogger.warn(\n 'Failed to create cache client',\n error,\n { mode: 'disabled' }\n );\n }\n }\n else\n {\n cacheLogger.warn(\n 'Failed to create cache client',\n { error: String(error), mode: 'disabled' }\n );\n }\n return { write: undefined, read: undefined };\n }\n}\n\n/**\n * Create single cache client (backward compatibility)\n * Only returns write instance\n */\nexport async function createSingleCacheFromEnv(): Promise<Redis | Cluster | undefined>\n{\n const { write } = await createCacheFromEnv();\n return write;\n}","/**\n * Global cache instance manager\n * Provides singleton access to cache (Valkey/Redis) across all modules\n * Supports Master-Replica pattern with separate read/write instances\n *\n * When cache is unavailable, falls back to disabled mode gracefully\n */\n\nimport type { Redis, Cluster } from 'ioredis';\n\nimport { createCacheFromEnv } from './cache-factory';\nimport { logger } from '@spfn/core/logger';\n\nconst cacheLogger = logger.child('@spfn/core:cache');\n\n// ── globalThis singleton ──────────────────────────────────────────\n// CJS/ESM dual-package hazard: 모듈이 두 번 로드되어도\n// Symbol.for() + globalThis로 동일 상태를 공유한다.\ninterface CacheState\n{\n write: Redis | Cluster | undefined;\n read: Redis | Cluster | undefined;\n disabled: boolean;\n}\n\nconst CACHE_KEY = Symbol.for('@spfn/core:cache');\n\nconst state: CacheState = ((globalThis as any)[CACHE_KEY] ??= {\n write: undefined,\n read: undefined,\n disabled: false,\n});\n\n/**\n * Get global cache write instance\n *\n * @returns Cache write instance or undefined if disabled/not initialized\n *\n * @example\n * ```typescript\n * import { getCache } from '@spfn/core/cache';\n *\n * const cache = getCache();\n * if (cache) {\n * await cache.set('key', 'value');\n * } else {\n * // Cache disabled - handle gracefully\n * console.log('Cache unavailable, skipping...');\n * }\n * ```\n */\nexport function getCache(): Redis | Cluster | undefined\n{\n return state.write;\n}\n\n/**\n * Get global cache read instance (falls back to write if no replica)\n *\n * @returns Cache read instance or write instance as fallback, undefined if disabled\n *\n * @example\n * ```typescript\n * import { getCacheRead } from '@spfn/core/cache';\n *\n * const cache = getCacheRead();\n * if (cache) {\n * const value = await cache.get('key');\n * }\n * ```\n */\nexport function getCacheRead(): Redis | Cluster | undefined\n{\n return state.read ?? state.write;\n}\n\n/**\n * Check if cache is disabled (connection failed or not configured)\n *\n * @example\n * ```typescript\n * import { isCacheDisabled } from '@spfn/core/cache';\n *\n * if (isCacheDisabled()) {\n * // Use alternative strategy (e.g., in-memory cache, database)\n * return await fetchFromDatabase();\n * }\n * ```\n */\nexport function isCacheDisabled(): boolean\n{\n return state.disabled;\n}\n\n/**\n * Set global cache instances (for testing or manual configuration)\n *\n * @param write - Cache write instance\n * @param read - Cache read instance (optional, defaults to write)\n *\n * @example\n * ```typescript\n * import { setCache } from '@spfn/core/cache';\n * import Redis from 'ioredis';\n *\n * const write = new Redis('redis://master:6379');\n * const read = new Redis('redis://replica:6379');\n * setCache(write, read);\n * ```\n */\nexport function setCache(\n write: Redis | Cluster | undefined,\n read?: Redis | Cluster | undefined\n): void\n{\n state.write = write;\n state.read = read ?? write;\n state.disabled = !write;\n}\n\n/**\n * Initialize cache from environment variables\n * Automatically called by startServer()\n *\n * Supported environment variables (priority order):\n * - VALKEY_URL / CACHE_URL (single instance)\n * - VALKEY_WRITE_URL + VALKEY_READ_URL (master-replica)\n * - VALKEY_SENTINEL_HOSTS + VALKEY_MASTER_NAME (sentinel)\n * - VALKEY_CLUSTER_NODES (cluster)\n * - VALKEY_TLS_REJECT_UNAUTHORIZED (TLS config)\n * - Legacy: REDIS_* (backward compatibility)\n *\n * @returns Object with write and read instances, or undefined if disabled\n *\n * @example\n * ```typescript\n * import { initCache } from '@spfn/core/cache';\n *\n * // Manual initialization (not needed if using startServer)\n * const { write, read, disabled } = await initCache();\n * if (!disabled) {\n * console.log('Cache available');\n * }\n * ```\n */\nexport async function initCache(): Promise<{\n write?: Redis | Cluster;\n read?: Redis | Cluster;\n disabled: boolean;\n}>\n{\n // Already initialized\n if (state.write)\n {\n return { write: state.write, read: state.read, disabled: state.disabled };\n }\n\n // Auto-detect from environment\n const { write, read } = await createCacheFromEnv();\n\n if (write)\n {\n try\n {\n // Test connection\n await write.ping();\n\n // Test read instance if different\n if (read && read !== write)\n {\n await read.ping();\n }\n\n state.write = write;\n state.read = read;\n state.disabled = false;\n\n const hasReplica = read && read !== write;\n cacheLogger.info(\n hasReplica\n ? 'Cache connected (Master-Replica)'\n : 'Cache connected',\n { mode: 'enabled' }\n );\n\n return { write: state.write, read: state.read, disabled: false };\n }\n catch (error)\n {\n cacheLogger.error(\n 'Cache connection failed - running in disabled mode',\n error instanceof Error ? error : new Error(String(error)),\n { mode: 'disabled' }\n );\n\n // Clean up failed connections\n try\n {\n await write.quit();\n if (read && read !== write)\n {\n await read.quit();\n }\n }\n catch\n {\n // Ignore cleanup errors\n }\n\n state.disabled = true;\n return { write: undefined, read: undefined, disabled: true };\n }\n }\n\n // No configuration or library not installed\n state.disabled = true;\n cacheLogger.info('Cache disabled - no configuration or library not installed', { mode: 'disabled' });\n return { write: undefined, read: undefined, disabled: true };\n}\n\n/**\n * Close all cache connections and cleanup\n *\n * @example\n * ```typescript\n * import { closeCache } from '@spfn/core/cache';\n *\n * // During graceful shutdown\n * await closeCache();\n * ```\n */\nexport async function closeCache(): Promise<void>\n{\n if (state.disabled)\n {\n cacheLogger.debug('Cache already disabled, nothing to close');\n return;\n }\n\n const closePromises: Promise<unknown>[] = [];\n\n if (state.write)\n {\n closePromises.push(\n state.write.quit().catch((err: Error) =>\n {\n cacheLogger.error('Error closing cache write instance', err);\n })\n );\n }\n\n if (state.read && state.read !== state.write)\n {\n closePromises.push(\n state.read.quit().catch((err: Error) =>\n {\n cacheLogger.error('Error closing cache read instance', err);\n })\n );\n }\n\n await Promise.all(closePromises);\n\n state.write = undefined;\n state.read = undefined;\n state.disabled = true;\n\n cacheLogger.info('Cache connections closed', { mode: 'disabled' });\n}\n\n/**\n * Get cache connection info (for debugging)\n *\n * @example\n * ```typescript\n * import { getCacheInfo } from '@spfn/core/cache';\n *\n * const info = getCacheInfo();\n * console.log(info);\n * // {\n * // hasWrite: true,\n * // hasRead: true,\n * // isReplica: true,\n * // disabled: false\n * // }\n * ```\n */\nexport function getCacheInfo(): {\n hasWrite: boolean;\n hasRead: boolean;\n isReplica: boolean;\n disabled: boolean;\n}\n{\n return {\n hasWrite: !!state.write,\n hasRead: !!state.read,\n isReplica: !!(state.read && state.read !== state.write),\n disabled: state.disabled,\n };\n}"]}
|
package/dist/codegen/index.d.ts
CHANGED
|
@@ -270,28 +270,75 @@ interface GenerationStats {
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
/**
|
|
273
|
-
*
|
|
273
|
+
* Route Map Generator
|
|
274
|
+
*
|
|
275
|
+
* Generates a route map file containing routeName → {method, path} mappings.
|
|
276
|
+
* This allows RPC proxy to resolve routes without importing the full router.
|
|
277
|
+
*
|
|
278
|
+
* @example
|
|
279
|
+
* ```typescript
|
|
280
|
+
* // .spfnrc.ts
|
|
281
|
+
* import { defineConfig, defineGenerator } from '@spfn/core/codegen';
|
|
274
282
|
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
283
|
+
* export default defineConfig({
|
|
284
|
+
* generators: [
|
|
285
|
+
* defineGenerator({
|
|
286
|
+
* name: '@spfn/core:route-map',
|
|
287
|
+
* routerPath: './src/server/router.ts',
|
|
288
|
+
* outputPath: './src/generated/route-map.ts',
|
|
289
|
+
* })
|
|
290
|
+
* ]
|
|
291
|
+
* });
|
|
292
|
+
* ```
|
|
293
|
+
*/
|
|
294
|
+
|
|
295
|
+
interface RouteMapGeneratorConfig {
|
|
296
|
+
/**
|
|
297
|
+
* Generator name (required for package-based loading)
|
|
298
|
+
*/
|
|
299
|
+
name: '@spfn/core:route-map';
|
|
300
|
+
/**
|
|
301
|
+
* Path to the router file (relative to project root)
|
|
302
|
+
* @example './src/server/router.ts'
|
|
303
|
+
*/
|
|
304
|
+
routerPath: string;
|
|
305
|
+
/**
|
|
306
|
+
* Output path for generated route map (relative to project root)
|
|
307
|
+
* @default './src/generated/route-map.ts'
|
|
308
|
+
*/
|
|
309
|
+
outputPath?: string;
|
|
310
|
+
/**
|
|
311
|
+
* Additional route directories to scan (for package routers)
|
|
312
|
+
*/
|
|
313
|
+
additionalRouteDirs?: string[];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Built-in Generators Export
|
|
277
318
|
*
|
|
278
319
|
* @example
|
|
279
320
|
* ```typescript
|
|
280
321
|
* // .spfnrc.ts
|
|
281
322
|
* import { defineConfig, defineGenerator } from '@spfn/core/codegen';
|
|
323
|
+
* import type { RouteMapGeneratorConfig } from '@spfn/core/codegen';
|
|
282
324
|
*
|
|
283
325
|
* export default defineConfig({
|
|
284
326
|
* generators: [
|
|
285
|
-
* defineGenerator({
|
|
327
|
+
* defineGenerator<RouteMapGeneratorConfig>({
|
|
328
|
+
* name: '@spfn/core:route-map',
|
|
329
|
+
* routerPath: './src/server/router.ts',
|
|
330
|
+
* outputPath: './src/generated/route-map.ts',
|
|
331
|
+
* })
|
|
286
332
|
* ]
|
|
287
333
|
* });
|
|
288
334
|
* ```
|
|
289
335
|
*/
|
|
336
|
+
|
|
290
337
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
338
|
+
* @internal
|
|
339
|
+
* Registry of available generators for package-based loading.
|
|
340
|
+
* DO NOT use directly - use defineGenerator({ name: '@spfn/core:route-map', ... }) instead.
|
|
294
341
|
*/
|
|
295
342
|
declare const generators: Record<string, unknown>;
|
|
296
343
|
|
|
297
|
-
export { type ClientGenerationOptions, type CodegenConfig, CodegenOrchestrator, type GenerationStats, type Generator, type GeneratorConfig, type GeneratorOptions, type GeneratorTrigger, type OrchestratorOptions, type ResourceRoutes, type RouteContractMapping, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
|
|
344
|
+
export { type ClientGenerationOptions, type CodegenConfig, CodegenOrchestrator, type GenerationStats, type Generator, type GeneratorConfig, type GeneratorOptions, type GeneratorTrigger, type OrchestratorOptions, type ResourceRoutes, type RouteContractMapping, type RouteMapGeneratorConfig, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
|
package/dist/codegen/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { watch } from 'chokidar';
|
|
2
|
-
import { join, relative } from 'path';
|
|
2
|
+
import { join, dirname, resolve, relative } from 'path';
|
|
3
3
|
import mm from 'micromatch';
|
|
4
4
|
import { logger } from '@spfn/core/logger';
|
|
5
|
-
import { existsSync, readFileSync } from 'fs';
|
|
5
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
|
|
6
6
|
import { createJiti } from 'jiti';
|
|
7
7
|
|
|
8
8
|
// src/codegen/core/orchestrator.ts
|
|
@@ -172,8 +172,8 @@ var CodegenOrchestrator = class {
|
|
|
172
172
|
}
|
|
173
173
|
};
|
|
174
174
|
this.watcher.on("add", (path) => handleChange(path, "add")).on("change", (path) => handleChange(path, "change")).on("unlink", (path) => handleChange(path, "unlink"));
|
|
175
|
-
return new Promise((
|
|
176
|
-
this.watcherClosePromise = { resolve, reject };
|
|
175
|
+
return new Promise((resolve2, reject) => {
|
|
176
|
+
this.watcherClosePromise = { resolve: resolve2, reject };
|
|
177
177
|
});
|
|
178
178
|
}
|
|
179
179
|
};
|
|
@@ -276,6 +276,11 @@ async function createGeneratorsFromConfig(config, cwd) {
|
|
|
276
276
|
}
|
|
277
277
|
for (const generatorConfig of config.generators) {
|
|
278
278
|
try {
|
|
279
|
+
if ("generate" in generatorConfig && typeof generatorConfig.generate === "function") {
|
|
280
|
+
generators2.push(generatorConfig);
|
|
281
|
+
configLogger.info(`Generator instance added: ${generatorConfig.name}`);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
279
284
|
if ("path" in generatorConfig) {
|
|
280
285
|
const generatorPath = generatorConfig.path.startsWith(".") ? join(cwd, generatorConfig.path) : generatorConfig.path;
|
|
281
286
|
configLogger.info(`Loading custom generator: ${generatorPath}`);
|
|
@@ -321,9 +326,178 @@ async function createGeneratorsFromConfig(config, cwd) {
|
|
|
321
326
|
}
|
|
322
327
|
return generators2;
|
|
323
328
|
}
|
|
329
|
+
var genLogger = logger.child("@spfn/core:route-map-generator");
|
|
330
|
+
function parseRouteFile(filePath) {
|
|
331
|
+
const routes = [];
|
|
332
|
+
try {
|
|
333
|
+
const content = readFileSync(filePath, "utf-8");
|
|
334
|
+
const routePattern = /export\s+const\s+(\w+)\s*=\s*route\.(get|post|put|patch|delete)\s*\(\s*['"`]([^'"`]+)['"`]\s*\)/gi;
|
|
335
|
+
let match;
|
|
336
|
+
while ((match = routePattern.exec(content)) !== null) {
|
|
337
|
+
const [, name, method, path] = match;
|
|
338
|
+
routes.push({
|
|
339
|
+
name,
|
|
340
|
+
method: method.toUpperCase(),
|
|
341
|
+
path,
|
|
342
|
+
file: filePath
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
genLogger.warn(`Failed to parse route file: ${filePath}`, error);
|
|
347
|
+
}
|
|
348
|
+
return routes;
|
|
349
|
+
}
|
|
350
|
+
function parseRouterFile(routerPath) {
|
|
351
|
+
const importPaths = [];
|
|
352
|
+
const routeNames = [];
|
|
353
|
+
try {
|
|
354
|
+
const content = readFileSync(routerPath, "utf-8");
|
|
355
|
+
const importPattern = /import\s+\{[^}]+\}\s+from\s+['"`](\.[^'"`]+)['"`]/g;
|
|
356
|
+
let match;
|
|
357
|
+
while ((match = importPattern.exec(content)) !== null) {
|
|
358
|
+
const importPath = match[1];
|
|
359
|
+
if (importPath.startsWith(".")) {
|
|
360
|
+
importPaths.push(importPath);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const defineRouterStart = content.indexOf("defineRouter(");
|
|
364
|
+
if (defineRouterStart !== -1) {
|
|
365
|
+
const braceStart = content.indexOf("{", defineRouterStart);
|
|
366
|
+
if (braceStart !== -1) {
|
|
367
|
+
let depth = 1;
|
|
368
|
+
let braceEnd = braceStart + 1;
|
|
369
|
+
while (depth > 0 && braceEnd < content.length) {
|
|
370
|
+
if (content[braceEnd] === "{") depth++;
|
|
371
|
+
else if (content[braceEnd] === "}") depth--;
|
|
372
|
+
braceEnd++;
|
|
373
|
+
}
|
|
374
|
+
const routerContent = content.slice(braceStart + 1, braceEnd - 1);
|
|
375
|
+
const withoutComments = routerContent.replace(/\/\/[^\n]*/g, "");
|
|
376
|
+
const namePattern = /^\s*(\w+)\s*[,\n]/gm;
|
|
377
|
+
while ((match = namePattern.exec(withoutComments)) !== null) {
|
|
378
|
+
routeNames.push(match[1]);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
genLogger.warn(`Failed to parse router file: ${routerPath}`, error);
|
|
384
|
+
}
|
|
385
|
+
return { importPaths, routeNames };
|
|
386
|
+
}
|
|
387
|
+
function generateRouteMapContent(routes) {
|
|
388
|
+
const lines = [
|
|
389
|
+
"/**",
|
|
390
|
+
" * Route Map (Auto-generated)",
|
|
391
|
+
" *",
|
|
392
|
+
" * DO NOT EDIT - This file is generated by @spfn/core:route-map generator",
|
|
393
|
+
" */",
|
|
394
|
+
"",
|
|
395
|
+
"import type { HttpMethod } from '@spfn/core/route';",
|
|
396
|
+
"",
|
|
397
|
+
"export interface RouteInfo",
|
|
398
|
+
"{",
|
|
399
|
+
" method: HttpMethod;",
|
|
400
|
+
" path: string;",
|
|
401
|
+
"}",
|
|
402
|
+
"",
|
|
403
|
+
"export const routeMap: Record<string, RouteInfo> = {"
|
|
404
|
+
];
|
|
405
|
+
for (const route of routes) {
|
|
406
|
+
lines.push(` ${route.name}: { method: '${route.method}', path: '${route.path}' },`);
|
|
407
|
+
}
|
|
408
|
+
lines.push("};");
|
|
409
|
+
lines.push("");
|
|
410
|
+
lines.push("export type RouteMap = typeof routeMap;");
|
|
411
|
+
lines.push("");
|
|
412
|
+
lines.push("export type RouteName = keyof RouteMap;");
|
|
413
|
+
lines.push("");
|
|
414
|
+
return lines.join("\n");
|
|
415
|
+
}
|
|
416
|
+
function createRouteMapGenerator(config) {
|
|
417
|
+
const {
|
|
418
|
+
routerPath,
|
|
419
|
+
outputPath = "./src/generated/route-map.ts",
|
|
420
|
+
additionalRouteDirs = []
|
|
421
|
+
} = config;
|
|
422
|
+
if (!routerPath) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`[@spfn/core:route-map] Missing required "routerPath" option.
|
|
425
|
+
|
|
426
|
+
Usage:
|
|
427
|
+
defineGenerator<RouteMapGeneratorConfig>({
|
|
428
|
+
name: '@spfn/core:route-map',
|
|
429
|
+
routerPath: './src/server/router.ts',
|
|
430
|
+
})`
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
name: "@spfn/core:route-map",
|
|
435
|
+
watchPatterns: [
|
|
436
|
+
routerPath,
|
|
437
|
+
// Watch route directories derived from router imports
|
|
438
|
+
"src/server/routes/**/*.ts",
|
|
439
|
+
...additionalRouteDirs.map((dir) => `${dir}/**/*.ts`)
|
|
440
|
+
],
|
|
441
|
+
runOn: ["watch", "build", "start", "manual"],
|
|
442
|
+
async generate(options) {
|
|
443
|
+
const { cwd, debug } = options;
|
|
444
|
+
const absoluteRouterPath = join(cwd, routerPath);
|
|
445
|
+
const absoluteOutputPath = join(cwd, outputPath);
|
|
446
|
+
if (!existsSync(absoluteRouterPath)) {
|
|
447
|
+
genLogger.warn(`Router file not found: ${absoluteRouterPath}`);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (debug) {
|
|
451
|
+
genLogger.info("Parsing router file", { path: absoluteRouterPath });
|
|
452
|
+
}
|
|
453
|
+
const { importPaths, routeNames } = parseRouterFile(absoluteRouterPath);
|
|
454
|
+
if (debug) {
|
|
455
|
+
genLogger.info("Found route imports", { count: importPaths.length, names: routeNames });
|
|
456
|
+
}
|
|
457
|
+
const routerDir = dirname(absoluteRouterPath);
|
|
458
|
+
const allRoutes = [];
|
|
459
|
+
for (const importPath of importPaths) {
|
|
460
|
+
let resolvedPath = resolve(routerDir, importPath);
|
|
461
|
+
if (!resolvedPath.endsWith(".ts")) {
|
|
462
|
+
const withTs = resolvedPath + ".ts";
|
|
463
|
+
if (existsSync(withTs)) {
|
|
464
|
+
resolvedPath = withTs;
|
|
465
|
+
} else {
|
|
466
|
+
const indexPath = join(resolvedPath, "index.ts");
|
|
467
|
+
if (existsSync(indexPath)) {
|
|
468
|
+
resolvedPath = indexPath;
|
|
469
|
+
} else {
|
|
470
|
+
resolvedPath = withTs;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (existsSync(resolvedPath)) {
|
|
475
|
+
const routes = parseRouteFile(resolvedPath);
|
|
476
|
+
allRoutes.push(...routes);
|
|
477
|
+
if (debug) {
|
|
478
|
+
genLogger.info(`Parsed ${routes.length} routes from ${relative(cwd, resolvedPath)}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
const exportedRoutes = allRoutes.filter((r) => routeNames.includes(r.name));
|
|
483
|
+
if (debug) {
|
|
484
|
+
genLogger.info(`Found ${exportedRoutes.length} exported routes`);
|
|
485
|
+
}
|
|
486
|
+
const content = generateRouteMapContent(exportedRoutes);
|
|
487
|
+
const outputDir = dirname(absoluteOutputPath);
|
|
488
|
+
if (!existsSync(outputDir)) {
|
|
489
|
+
mkdirSync(outputDir, { recursive: true });
|
|
490
|
+
}
|
|
491
|
+
writeFileSync(absoluteOutputPath, content, "utf-8");
|
|
492
|
+
genLogger.info(`Generated route map: ${relative(cwd, absoluteOutputPath)} (${exportedRoutes.length} routes)`);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
}
|
|
324
496
|
|
|
325
497
|
// src/codegen/generators/index.ts
|
|
326
|
-
var generators = {
|
|
498
|
+
var generators = {
|
|
499
|
+
"route-map": createRouteMapGenerator
|
|
500
|
+
};
|
|
327
501
|
|
|
328
502
|
export { CodegenOrchestrator, createGeneratorsFromConfig, defineConfig, defineGenerator, generators, loadCodegenConfig };
|
|
329
503
|
//# sourceMappingURL=index.js.map
|