tina4-nodejs 3.0.0-rc.2

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 (119) hide show
  1. package/BENCHMARK_REPORT.md +96 -0
  2. package/CARBONAH.md +140 -0
  3. package/CLAUDE.md +599 -0
  4. package/COMPARISON.md +194 -0
  5. package/README.md +595 -0
  6. package/package.json +59 -0
  7. package/packages/cli/src/bin.ts +110 -0
  8. package/packages/cli/src/commands/init.ts +194 -0
  9. package/packages/cli/src/commands/migrate.ts +96 -0
  10. package/packages/cli/src/commands/migrateCreate.ts +59 -0
  11. package/packages/cli/src/commands/routes.ts +61 -0
  12. package/packages/cli/src/commands/serve.ts +58 -0
  13. package/packages/cli/src/commands/test.ts +83 -0
  14. package/packages/core/gallery/auth/meta.json +1 -0
  15. package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +22 -0
  16. package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +16 -0
  17. package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +97 -0
  18. package/packages/core/gallery/database/meta.json +1 -0
  19. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +13 -0
  20. package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +17 -0
  21. package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +23 -0
  22. package/packages/core/gallery/error-overlay/meta.json +1 -0
  23. package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +17 -0
  24. package/packages/core/gallery/orm/meta.json +1 -0
  25. package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +12 -0
  26. package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +7 -0
  27. package/packages/core/gallery/queue/meta.json +1 -0
  28. package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +16 -0
  29. package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +10 -0
  30. package/packages/core/gallery/rest-api/meta.json +1 -0
  31. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +6 -0
  32. package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +7 -0
  33. package/packages/core/gallery/templates/meta.json +1 -0
  34. package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +15 -0
  35. package/packages/core/gallery/templates/src/templates/gallery_page.twig +257 -0
  36. package/packages/core/public/css/tina4.css +2463 -0
  37. package/packages/core/public/css/tina4.min.css +1 -0
  38. package/packages/core/public/favicon.ico +0 -0
  39. package/packages/core/public/images/logo.svg +5 -0
  40. package/packages/core/public/images/tina4-logo-icon.webp +0 -0
  41. package/packages/core/public/js/frond.min.js +420 -0
  42. package/packages/core/public/js/tina4-dev-admin.min.js +327 -0
  43. package/packages/core/public/js/tina4.min.js +93 -0
  44. package/packages/core/public/swagger/index.html +90 -0
  45. package/packages/core/public/swagger/oauth2-redirect.html +63 -0
  46. package/packages/core/src/ai.ts +359 -0
  47. package/packages/core/src/api.ts +248 -0
  48. package/packages/core/src/auth.ts +287 -0
  49. package/packages/core/src/cache.ts +121 -0
  50. package/packages/core/src/constants.ts +48 -0
  51. package/packages/core/src/container.ts +90 -0
  52. package/packages/core/src/devAdmin.ts +2024 -0
  53. package/packages/core/src/devMailbox.ts +316 -0
  54. package/packages/core/src/dotenv.ts +172 -0
  55. package/packages/core/src/errorOverlay.test.ts +122 -0
  56. package/packages/core/src/errorOverlay.ts +278 -0
  57. package/packages/core/src/events.ts +112 -0
  58. package/packages/core/src/fakeData.ts +309 -0
  59. package/packages/core/src/graphql.ts +812 -0
  60. package/packages/core/src/health.ts +31 -0
  61. package/packages/core/src/htmlElement.ts +172 -0
  62. package/packages/core/src/i18n.ts +136 -0
  63. package/packages/core/src/index.ts +88 -0
  64. package/packages/core/src/logger.ts +226 -0
  65. package/packages/core/src/messenger.ts +822 -0
  66. package/packages/core/src/middleware.ts +138 -0
  67. package/packages/core/src/queue.ts +481 -0
  68. package/packages/core/src/queueBackends/kafkaBackend.ts +348 -0
  69. package/packages/core/src/queueBackends/rabbitmqBackend.ts +479 -0
  70. package/packages/core/src/rateLimiter.ts +107 -0
  71. package/packages/core/src/request.ts +189 -0
  72. package/packages/core/src/response.ts +146 -0
  73. package/packages/core/src/routeDiscovery.ts +87 -0
  74. package/packages/core/src/router.ts +398 -0
  75. package/packages/core/src/scss.ts +366 -0
  76. package/packages/core/src/server.ts +610 -0
  77. package/packages/core/src/service.ts +380 -0
  78. package/packages/core/src/session.ts +480 -0
  79. package/packages/core/src/sessionHandlers/mongoHandler.ts +286 -0
  80. package/packages/core/src/sessionHandlers/valkeyHandler.ts +184 -0
  81. package/packages/core/src/static.ts +58 -0
  82. package/packages/core/src/testing.ts +233 -0
  83. package/packages/core/src/types.ts +98 -0
  84. package/packages/core/src/watcher.ts +37 -0
  85. package/packages/core/src/websocket.ts +408 -0
  86. package/packages/core/src/wsdl.ts +546 -0
  87. package/packages/core/templates/errors/302.twig +14 -0
  88. package/packages/core/templates/errors/401.twig +9 -0
  89. package/packages/core/templates/errors/403.twig +29 -0
  90. package/packages/core/templates/errors/404.twig +29 -0
  91. package/packages/core/templates/errors/500.twig +38 -0
  92. package/packages/core/templates/errors/502.twig +9 -0
  93. package/packages/core/templates/errors/503.twig +12 -0
  94. package/packages/core/templates/errors/base.twig +37 -0
  95. package/packages/frond/src/engine.ts +1475 -0
  96. package/packages/frond/src/index.ts +2 -0
  97. package/packages/orm/src/adapters/firebird.ts +455 -0
  98. package/packages/orm/src/adapters/mssql.ts +440 -0
  99. package/packages/orm/src/adapters/mysql.ts +355 -0
  100. package/packages/orm/src/adapters/postgres.ts +362 -0
  101. package/packages/orm/src/adapters/sqlite.ts +270 -0
  102. package/packages/orm/src/autoCrud.ts +231 -0
  103. package/packages/orm/src/baseModel.ts +536 -0
  104. package/packages/orm/src/database.ts +321 -0
  105. package/packages/orm/src/fakeData.ts +118 -0
  106. package/packages/orm/src/index.ts +49 -0
  107. package/packages/orm/src/migration.ts +392 -0
  108. package/packages/orm/src/model.ts +56 -0
  109. package/packages/orm/src/query.ts +113 -0
  110. package/packages/orm/src/seeder.ts +120 -0
  111. package/packages/orm/src/sqlTranslation.ts +272 -0
  112. package/packages/orm/src/types.ts +110 -0
  113. package/packages/orm/src/validation.ts +93 -0
  114. package/packages/swagger/src/generator.ts +189 -0
  115. package/packages/swagger/src/index.ts +2 -0
  116. package/packages/swagger/src/ui.ts +48 -0
  117. package/skills/tina4-developer.skill +0 -0
  118. package/skills/tina4-js.skill +0 -0
  119. package/skills/tina4-maintainer.skill +0 -0
@@ -0,0 +1,480 @@
1
+ /**
2
+ * Tina4 Session — Pluggable session backends, zero core dependencies.
3
+ *
4
+ * File-based sessions by default. Redis backend available via raw TCP (no ioredis needed).
5
+ *
6
+ * import { Session, RedisSessionHandler } from "@tina4/core";
7
+ *
8
+ * // File backend (default)
9
+ * const session = new Session();
10
+ *
11
+ * // Redis backend
12
+ * const session = new Session("redis", {
13
+ * redisHost: "127.0.0.1",
14
+ * redisPort: 6379,
15
+ * });
16
+ *
17
+ * const id = session.start();
18
+ * session.set("user", { name: "Alice" });
19
+ * session.get("user"); // { name: "Alice" }
20
+ * session.destroy();
21
+ */
22
+ import { randomBytes } from "node:crypto";
23
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
24
+ import { join } from "node:path";
25
+ import { execFileSync } from "node:child_process";
26
+
27
+ // ── Types ─────────────────────────────────────────────────────────
28
+
29
+ export interface SessionConfig {
30
+ /** Session backend type: "file" or "redis" */
31
+ backend?: string;
32
+ /** File storage path (default: "data/sessions") */
33
+ path?: string;
34
+ /** Time-to-live in seconds (default: 3600) */
35
+ ttl?: number;
36
+ /** Redis host (default: "127.0.0.1") */
37
+ redisHost?: string;
38
+ /** Redis port (default: 6379) */
39
+ redisPort?: number;
40
+ /** Redis password (optional) */
41
+ redisPassword?: string;
42
+ /** Redis key prefix (default: "tina4:session:") */
43
+ redisPrefix?: string;
44
+ /** Redis database index (default: 0) */
45
+ redisDb?: number;
46
+ }
47
+
48
+ interface SessionData {
49
+ _created: number;
50
+ _accessed: number;
51
+ [key: string]: unknown;
52
+ }
53
+
54
+ // ── Session Handler Interface ─────────────────────────────────────
55
+
56
+ /**
57
+ * Base interface for session storage backends.
58
+ * Implementations must provide read, write, and destroy.
59
+ */
60
+ export interface SessionHandler {
61
+ read(sessionId: string): SessionData | null;
62
+ write(sessionId: string, data: SessionData, ttl: number): void;
63
+ destroy(sessionId: string): void;
64
+ }
65
+
66
+ // ── File Session Handler ──────────────────────────────────────────
67
+
68
+ export class FileSessionHandler implements SessionHandler {
69
+ private storagePath: string;
70
+
71
+ constructor(storagePath?: string) {
72
+ this.storagePath = storagePath
73
+ ?? process.env.TINA4_SESSION_PATH
74
+ ?? "data/sessions";
75
+ }
76
+
77
+ private ensureDir(): void {
78
+ if (!existsSync(this.storagePath)) {
79
+ mkdirSync(this.storagePath, { recursive: true });
80
+ }
81
+ }
82
+
83
+ private filePath(id: string): string {
84
+ return join(this.storagePath, `${id}.json`);
85
+ }
86
+
87
+ read(sessionId: string): SessionData | null {
88
+ const filePath = this.filePath(sessionId);
89
+ try {
90
+ if (!existsSync(filePath)) return null;
91
+ const raw = readFileSync(filePath, "utf-8");
92
+ return JSON.parse(raw) as SessionData;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+
98
+ write(sessionId: string, data: SessionData, _ttl: number): void {
99
+ this.ensureDir();
100
+ writeFileSync(this.filePath(sessionId), JSON.stringify(data), "utf-8");
101
+ }
102
+
103
+ destroy(sessionId: string): void {
104
+ const filePath = this.filePath(sessionId);
105
+ try {
106
+ if (existsSync(filePath)) unlinkSync(filePath);
107
+ } catch { /* ignore */ }
108
+ }
109
+ }
110
+
111
+ // ── Redis Session Handler (raw TCP, zero dependencies) ────────────
112
+
113
+ /**
114
+ * Redis session handler using raw TCP (RESP protocol).
115
+ *
116
+ * Uses synchronous socket communication — no external Redis client required.
117
+ * Stores session data as JSON strings with Redis TTL for automatic expiry.
118
+ *
119
+ * Configure via environment variables:
120
+ * TINA4_SESSION_REDIS_HOST (default: "127.0.0.1")
121
+ * TINA4_SESSION_REDIS_PORT (default: 6379)
122
+ * TINA4_SESSION_REDIS_PASSWORD (optional)
123
+ * TINA4_SESSION_REDIS_PREFIX (default: "tina4:session:")
124
+ * TINA4_SESSION_REDIS_DB (default: 0)
125
+ *
126
+ * Or pass via SessionConfig.
127
+ */
128
+ export class RedisSessionHandler implements SessionHandler {
129
+ private host: string;
130
+ private port: number;
131
+ private password: string;
132
+ private prefix: string;
133
+ private db: number;
134
+
135
+ constructor(config?: SessionConfig) {
136
+ this.host = config?.redisHost
137
+ ?? process.env.TINA4_SESSION_REDIS_HOST
138
+ ?? "127.0.0.1";
139
+ this.port = config?.redisPort
140
+ ?? (process.env.TINA4_SESSION_REDIS_PORT
141
+ ? parseInt(process.env.TINA4_SESSION_REDIS_PORT, 10)
142
+ : 6379);
143
+ this.password = config?.redisPassword
144
+ ?? process.env.TINA4_SESSION_REDIS_PASSWORD
145
+ ?? "";
146
+ this.prefix = config?.redisPrefix
147
+ ?? process.env.TINA4_SESSION_REDIS_PREFIX
148
+ ?? "tina4:session:";
149
+ this.db = config?.redisDb
150
+ ?? (process.env.TINA4_SESSION_REDIS_DB
151
+ ? parseInt(process.env.TINA4_SESSION_REDIS_DB, 10)
152
+ : 0);
153
+ }
154
+
155
+ /**
156
+ * Execute a Redis command synchronously via a short-lived TCP connection.
157
+ *
158
+ * Returns the raw RESP response string.
159
+ */
160
+ private execSync(args: string[]): string {
161
+ const script = `
162
+ const net = require("node:net");
163
+ const host = ${JSON.stringify(this.host)};
164
+ const port = ${this.port};
165
+ const password = ${JSON.stringify(this.password)};
166
+ const db = ${this.db};
167
+ const args = ${JSON.stringify(args)};
168
+
169
+ function buildCommand(a) {
170
+ let cmd = "*" + a.length + "\\r\\n";
171
+ for (const s of a) cmd += "$" + Buffer.byteLength(s) + "\\r\\n" + s + "\\r\\n";
172
+ return cmd;
173
+ }
174
+
175
+ function parseResp(buf) {
176
+ const str = buf.toString("utf-8");
177
+ if (str.startsWith("+")) return str.slice(1).split("\\r\\n")[0];
178
+ if (str.startsWith("-")) return "ERR:" + str.slice(1).split("\\r\\n")[0];
179
+ if (str.startsWith(":")) return str.slice(1).split("\\r\\n")[0];
180
+ if (str.startsWith("$-1")) return null;
181
+ if (str.startsWith("$")) {
182
+ const nl = str.indexOf("\\r\\n");
183
+ const len = parseInt(str.slice(1, nl), 10);
184
+ const start = nl + 2;
185
+ return str.slice(start, start + len);
186
+ }
187
+ return str;
188
+ }
189
+
190
+ const sock = net.createConnection({ host, port }, () => {
191
+ let commands = "";
192
+ if (password) commands += buildCommand(["AUTH", password]);
193
+ if (db !== 0) commands += buildCommand(["SELECT", String(db)]);
194
+ commands += buildCommand(args);
195
+ sock.write(commands);
196
+ });
197
+
198
+ let buffer = Buffer.alloc(0);
199
+ sock.on("data", (chunk) => {
200
+ buffer = Buffer.concat([buffer, chunk]);
201
+ });
202
+ sock.on("end", () => {
203
+ // Parse last response (skip AUTH/SELECT responses)
204
+ const lines = buffer.toString("utf-8").split("\\r\\n");
205
+ let responses = [];
206
+ let i = 0;
207
+ while (i < lines.length) {
208
+ const line = lines[i];
209
+ if (!line) { i++; continue; }
210
+ if (line.startsWith("+") || line.startsWith("-") || line.startsWith(":")) {
211
+ responses.push(line);
212
+ i++;
213
+ } else if (line.startsWith("$")) {
214
+ const len = parseInt(line.slice(1), 10);
215
+ if (len === -1) { responses.push(null); i++; }
216
+ else { responses.push(lines[i+1] || ""); i += 2; }
217
+ } else { i++; }
218
+ }
219
+ // The last response is our actual command result
220
+ const result = responses[responses.length - 1];
221
+ if (result === null) process.stdout.write("__NULL__");
222
+ else if (typeof result === "string" && result.startsWith("-")) process.stdout.write("__ERR__" + result);
223
+ else process.stdout.write(String(result ?? "__NULL__"));
224
+ });
225
+ sock.on("error", (err) => {
226
+ process.stderr.write(err.message);
227
+ process.exit(1);
228
+ });
229
+ setTimeout(() => { sock.destroy(); process.exit(1); }, 3000);
230
+ `;
231
+
232
+ try {
233
+ const result = execFileSync(process.execPath, ["-e", script], {
234
+ encoding: "utf-8",
235
+ timeout: 5000,
236
+ stdio: ["pipe", "pipe", "pipe"],
237
+ });
238
+ if (result === "__NULL__") return "";
239
+ if (result.startsWith("__ERR__")) return "";
240
+ return result;
241
+ } catch {
242
+ return "";
243
+ }
244
+ }
245
+
246
+ private key(sessionId: string): string {
247
+ return `${this.prefix}${sessionId}`;
248
+ }
249
+
250
+ read(sessionId: string): SessionData | null {
251
+ const raw = this.execSync(["GET", this.key(sessionId)]);
252
+ if (!raw) return null;
253
+ try {
254
+ return JSON.parse(raw) as SessionData;
255
+ } catch {
256
+ return null;
257
+ }
258
+ }
259
+
260
+ write(sessionId: string, data: SessionData, ttl: number): void {
261
+ const json = JSON.stringify(data);
262
+ if (ttl > 0) {
263
+ this.execSync(["SETEX", this.key(sessionId), String(ttl), json]);
264
+ } else {
265
+ this.execSync(["SET", this.key(sessionId), json]);
266
+ }
267
+ }
268
+
269
+ destroy(sessionId: string): void {
270
+ this.execSync(["DEL", this.key(sessionId)]);
271
+ }
272
+ }
273
+
274
+ // ── Flash data prefix ─────────────────────────────────────────────
275
+
276
+ const FLASH_PREFIX = "_flash_";
277
+
278
+ // ── Session Class ─────────────────────────────────────────────────
279
+
280
+ export class Session {
281
+ private handler: SessionHandler;
282
+ private ttl: number;
283
+ private sessionId: string | null = null;
284
+ private data: SessionData | null = null;
285
+
286
+ constructor(backend?: string, config?: SessionConfig) {
287
+ const backendType = backend
288
+ ?? config?.backend
289
+ ?? process.env.TINA4_SESSION_BACKEND
290
+ ?? "file";
291
+
292
+ this.ttl = config?.ttl
293
+ ?? (process.env.TINA4_SESSION_TTL ? parseInt(process.env.TINA4_SESSION_TTL, 10) : 3600);
294
+
295
+ // Select handler based on backend type
296
+ switch (backendType) {
297
+ case "redis":
298
+ this.handler = new RedisSessionHandler(config);
299
+ break;
300
+ case "valkey": {
301
+ const { ValkeySessionHandler } = require("./sessionHandlers/valkeyHandler.js");
302
+ this.handler = new ValkeySessionHandler(config);
303
+ break;
304
+ }
305
+ case "mongo":
306
+ case "mongodb": {
307
+ const { MongoSessionHandler } = require("./sessionHandlers/mongoHandler.js");
308
+ this.handler = new MongoSessionHandler(config);
309
+ break;
310
+ }
311
+ case "file":
312
+ default:
313
+ this.handler = new FileSessionHandler(config?.path);
314
+ break;
315
+ }
316
+ }
317
+
318
+ /**
319
+ * Use a custom session handler (for advanced use cases).
320
+ */
321
+ setHandler(handler: SessionHandler): void {
322
+ this.handler = handler;
323
+ }
324
+
325
+ /**
326
+ * Start or resume a session.
327
+ * @param sessionId - Existing session ID to resume (optional)
328
+ * @returns The session ID
329
+ */
330
+ start(sessionId?: string): string {
331
+ if (sessionId) {
332
+ const loaded = this.handler.read(sessionId);
333
+ if (loaded) {
334
+ // Check TTL for file backend (Redis handles TTL natively)
335
+ const now = Math.floor(Date.now() / 1000);
336
+ if (loaded._accessed && (now - loaded._accessed) > this.ttl) {
337
+ this.handler.destroy(sessionId);
338
+ } else {
339
+ this.sessionId = sessionId;
340
+ this.data = loaded;
341
+ this.data._accessed = now;
342
+ this.handler.write(this.sessionId, this.data, this.ttl);
343
+ return sessionId;
344
+ }
345
+ }
346
+ }
347
+
348
+ // Generate new session
349
+ this.sessionId = randomBytes(16).toString("hex");
350
+ const now = Math.floor(Date.now() / 1000);
351
+ this.data = { _created: now, _accessed: now };
352
+ this.handler.write(this.sessionId, this.data, this.ttl);
353
+ return this.sessionId;
354
+ }
355
+
356
+ /**
357
+ * Get a value from the session.
358
+ */
359
+ get(key: string, defaultValue?: unknown): unknown {
360
+ if (!this.data) return defaultValue;
361
+ if (key in this.data) {
362
+ return this.data[key] ?? defaultValue;
363
+ }
364
+ return defaultValue;
365
+ }
366
+
367
+ /**
368
+ * Set a value in the session.
369
+ */
370
+ set(key: string, value: unknown): void {
371
+ if (!this.data) return;
372
+ this.data[key] = value;
373
+ this.save();
374
+ }
375
+
376
+ /**
377
+ * Delete a key from the session.
378
+ */
379
+ delete(key: string): void {
380
+ if (!this.data) return;
381
+ delete this.data[key];
382
+ this.save();
383
+ }
384
+
385
+ /**
386
+ * Destroy the entire session.
387
+ */
388
+ destroy(): void {
389
+ if (this.sessionId) {
390
+ this.handler.destroy(this.sessionId);
391
+ }
392
+ this.sessionId = null;
393
+ this.data = null;
394
+ }
395
+
396
+ /**
397
+ * Get all session data (excluding internal keys).
398
+ */
399
+ all(): Record<string, unknown> {
400
+ if (!this.data) return {};
401
+ const result: Record<string, unknown> = {};
402
+ for (const [k, v] of Object.entries(this.data)) {
403
+ if (!k.startsWith("_")) {
404
+ result[k] = v;
405
+ }
406
+ }
407
+ return result;
408
+ }
409
+
410
+ /**
411
+ * Clear all session data (but keep the session alive).
412
+ */
413
+ clear(): void {
414
+ if (!this.data) return;
415
+ const now = Math.floor(Date.now() / 1000);
416
+ this.data = { _created: this.data._created, _accessed: now };
417
+ this.save();
418
+ }
419
+
420
+ /**
421
+ * Check if a key exists in the session.
422
+ */
423
+ has(key: string): boolean {
424
+ if (!this.data) return false;
425
+ return key in this.data;
426
+ }
427
+
428
+ /**
429
+ * Regenerate the session ID (keeps data, new ID).
430
+ */
431
+ regenerate(): string {
432
+ const oldId = this.sessionId;
433
+ const oldData = this.data;
434
+
435
+ // Remove old session
436
+ if (oldId) {
437
+ this.handler.destroy(oldId);
438
+ }
439
+
440
+ // New ID, keep data
441
+ this.sessionId = randomBytes(16).toString("hex");
442
+ this.data = oldData ?? { _created: Math.floor(Date.now() / 1000), _accessed: Math.floor(Date.now() / 1000) };
443
+ this.data._accessed = Math.floor(Date.now() / 1000);
444
+ this.save();
445
+ return this.sessionId;
446
+ }
447
+
448
+ /**
449
+ * Set flash data (auto-deleted after first read).
450
+ */
451
+ flash(key: string, value: unknown): void {
452
+ this.set(`${FLASH_PREFIX}${key}`, value);
453
+ }
454
+
455
+ /**
456
+ * Get flash data (auto-deleted after read).
457
+ */
458
+ getFlash(key: string, defaultValue?: unknown): unknown {
459
+ const flashKey = `${FLASH_PREFIX}${key}`;
460
+ if (!this.data || !(flashKey in this.data)) return defaultValue;
461
+ const value = this.data[flashKey];
462
+ delete this.data[flashKey];
463
+ this.save();
464
+ return value ?? defaultValue;
465
+ }
466
+
467
+ /**
468
+ * Get the current session ID.
469
+ */
470
+ getId(): string | null {
471
+ return this.sessionId;
472
+ }
473
+
474
+ // ── Private ───────────────────────────────────────────────────
475
+
476
+ private save(): void {
477
+ if (!this.sessionId || !this.data) return;
478
+ this.handler.write(this.sessionId, this.data, this.ttl);
479
+ }
480
+ }