@randajan/koa-io-session 2.2.0 → 3.0.1
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 +166 -104
- package/dist/cjs/index.cjs +433 -209
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/cjs/stores/FileStore.cjs +117 -0
- package/dist/cjs/stores/FileStore.cjs.map +7 -0
- package/dist/esm/chunk-NKL2ZYZW.js +110 -0
- package/dist/esm/chunk-NKL2ZYZW.js.map +7 -0
- package/dist/esm/index.mjs +393 -251
- package/dist/esm/index.mjs.map +4 -4
- package/dist/esm/stores/FileStore.mjs +66 -0
- package/dist/esm/stores/FileStore.mjs.map +7 -0
- package/package.json +8 -1
package/dist/cjs/index.cjs
CHANGED
|
@@ -29,50 +29,84 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
29
29
|
// src/index.js
|
|
30
30
|
var index_exports = {};
|
|
31
31
|
__export(index_exports, {
|
|
32
|
+
LiveStore: () => LiveStore,
|
|
32
33
|
SessionBridge: () => SessionBridge,
|
|
33
|
-
SessionStore: () => SessionStore,
|
|
34
34
|
bridgeSession: () => bridgeSession,
|
|
35
35
|
default: () => index_default,
|
|
36
|
-
generateUid: () => generateUid
|
|
36
|
+
generateUid: () => generateUid,
|
|
37
|
+
ms: () => ms
|
|
37
38
|
});
|
|
38
39
|
module.exports = __toCommonJS(index_exports);
|
|
39
40
|
|
|
40
41
|
// src/class/SessionBridge.js
|
|
41
|
-
var
|
|
42
|
+
var import_events = require("events");
|
|
42
43
|
var import_http = require("http");
|
|
43
44
|
var import_props4 = require("@randajan/props");
|
|
44
45
|
|
|
45
46
|
// src/tools.js
|
|
46
47
|
var import_crypto = __toESM(require("crypto"), 1);
|
|
48
|
+
|
|
49
|
+
// src/const.js
|
|
50
|
+
var _errPrefix = "[koa-io-session]";
|
|
51
|
+
var ms = {
|
|
52
|
+
s: (v = 1) => v * 1e3,
|
|
53
|
+
m: (v = 1) => ms.s(v * 60),
|
|
54
|
+
h: (v = 1) => ms.m(v * 60),
|
|
55
|
+
d: (v = 1) => ms.h(v * 24),
|
|
56
|
+
w: (v = 1) => ms.d(v * 7),
|
|
57
|
+
M: (v = 1) => ms.d(v * 30),
|
|
58
|
+
y: (v = 1) => ms.d(v * 365)
|
|
59
|
+
};
|
|
60
|
+
var _customOptKeys = /* @__PURE__ */ new Set([
|
|
61
|
+
"appKeys",
|
|
62
|
+
"store",
|
|
63
|
+
"autoCleanup",
|
|
64
|
+
"autoCleanupMs",
|
|
65
|
+
"clientKey",
|
|
66
|
+
"clientMaxAge",
|
|
67
|
+
"clientAlwaysRoll"
|
|
68
|
+
]);
|
|
69
|
+
var _privs = /* @__PURE__ */ new WeakMap();
|
|
70
|
+
|
|
71
|
+
// src/tools.js
|
|
47
72
|
var generateUid = (len = 16) => import_crypto.default.randomBytes(len).toString("base64url").slice(0, len);
|
|
48
73
|
var is = (type, any) => typeof any === type;
|
|
74
|
+
var _typeOf = (any) => any === null ? "null" : Array.isArray(any) ? "array" : typeof any;
|
|
75
|
+
var _err = (msg) => `${_errPrefix} ${msg}`;
|
|
49
76
|
var valid = (type, any, req = false, msg = "argument") => {
|
|
50
77
|
if (any == null) {
|
|
51
78
|
if (!req) {
|
|
52
79
|
return;
|
|
53
80
|
}
|
|
54
|
-
throw new
|
|
81
|
+
throw new TypeError(_err(`Missing required '${msg}'. Expected type '${type}'.`));
|
|
55
82
|
}
|
|
56
83
|
if (is(type, any)) {
|
|
57
84
|
return any;
|
|
58
85
|
}
|
|
59
|
-
throw new
|
|
86
|
+
throw new TypeError(_err(`Invalid '${msg}'. Expected type '${type}', received '${_typeOf(any)}'.`));
|
|
60
87
|
};
|
|
61
|
-
var validRange = (min, max,
|
|
88
|
+
var validRange = (any, min, max, req = false, msg = "argument") => {
|
|
62
89
|
const num = valid("number", any, req, msg);
|
|
63
90
|
if (num == null) {
|
|
64
91
|
return;
|
|
65
92
|
}
|
|
66
|
-
if (num < min) {
|
|
67
|
-
throw new
|
|
68
|
-
}
|
|
69
|
-
if (num > max) {
|
|
70
|
-
throw new Error(`${msg} must be less than ${max}`);
|
|
93
|
+
if (num < min || num > max) {
|
|
94
|
+
throw new RangeError(_err(`Invalid '${msg}'. Expected value in range <${min}, ${max}>, received '${num}'.`));
|
|
71
95
|
}
|
|
72
96
|
return num;
|
|
73
97
|
};
|
|
74
98
|
var validInterval = (any, req = false, msg = "argument") => {
|
|
75
|
-
return validRange(10, 2147483647,
|
|
99
|
+
return validRange(any, 10, 2147483647, req, msg);
|
|
100
|
+
};
|
|
101
|
+
var validArray = (any, req = false, msg = "argument") => {
|
|
102
|
+
const obj = valid("object", any, req, msg);
|
|
103
|
+
if (obj == null) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (Array.isArray(obj)) {
|
|
107
|
+
return obj;
|
|
108
|
+
}
|
|
109
|
+
throw new TypeError(_err(`Invalid '${msg}'. Expected plain array, received plain object.`));
|
|
76
110
|
};
|
|
77
111
|
var validObject = (any, req = false, msg = "argument") => {
|
|
78
112
|
const obj = valid("object", any, req, msg);
|
|
@@ -82,25 +116,27 @@ var validObject = (any, req = false, msg = "argument") => {
|
|
|
82
116
|
if (!Array.isArray(obj)) {
|
|
83
117
|
return obj;
|
|
84
118
|
}
|
|
85
|
-
throw new
|
|
119
|
+
throw new TypeError(_err(`Invalid '${msg}'. Expected plain object, received 'array'.`));
|
|
86
120
|
};
|
|
87
|
-
var validStore = (store) => {
|
|
121
|
+
var validStore = (store, req = false) => {
|
|
122
|
+
store = validObject(store, req, "store");
|
|
123
|
+
if (!store) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
88
126
|
const missing = [];
|
|
89
|
-
if (!is("function", store
|
|
127
|
+
if (!is("function", store.get)) {
|
|
90
128
|
missing.push("get()");
|
|
91
129
|
}
|
|
92
|
-
if (!is("function", store
|
|
130
|
+
if (!is("function", store.set)) {
|
|
93
131
|
missing.push("set()");
|
|
94
132
|
}
|
|
95
|
-
if (!is("function", store
|
|
133
|
+
if (!is("function", store.destroy)) {
|
|
96
134
|
missing.push("destroy()");
|
|
97
135
|
}
|
|
98
|
-
if (!is("function", store?.on)) {
|
|
99
|
-
missing.push("on()");
|
|
100
|
-
}
|
|
101
136
|
if (missing.length) {
|
|
102
|
-
throw new TypeError(`store
|
|
137
|
+
throw new TypeError(_err(`Invalid 'store'. Missing required API: ${missing.join(", ")}.`));
|
|
103
138
|
}
|
|
139
|
+
valid("function", store.list, false, "store.list()");
|
|
104
140
|
return store;
|
|
105
141
|
};
|
|
106
142
|
|
|
@@ -127,10 +163,32 @@ var wrapExternalKey = (opt, onSet) => {
|
|
|
127
163
|
};
|
|
128
164
|
|
|
129
165
|
// src/httpSession.js
|
|
130
|
-
var
|
|
131
|
-
|
|
166
|
+
var ensureAppKeys = (app, keys, allowRnd = false) => {
|
|
167
|
+
if (app.keys) {
|
|
168
|
+
if (!keys) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
throw new Error(`${_errPrefix} Cannot set 'appKeys' because app.keys is already defined.`);
|
|
172
|
+
}
|
|
173
|
+
if (keys) {
|
|
174
|
+
app.keys = keys;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
app.keys = keys = Array(2).fill().map(() => generateUid(32));
|
|
178
|
+
if (!allowRnd) {
|
|
179
|
+
console.warn([
|
|
180
|
+
`${_errPrefix} app.keys were generated at runtime.`,
|
|
181
|
+
`${_errPrefix} Resolve this by adding one of these options to bridge constructor:`,
|
|
182
|
+
`${_errPrefix} 1) "appKeys": ${JSON.stringify(app.keys)}`,
|
|
183
|
+
`${_errPrefix} 2) "allowRndAppKeys": true`
|
|
184
|
+
].join("\n"));
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
var createKoaSession = (app, gw, opt, onSet) => {
|
|
188
|
+
const maxAge = gw.maxAge;
|
|
189
|
+
const store = wrapStore(gw);
|
|
132
190
|
const externalKey = wrapExternalKey(opt, onSet);
|
|
133
|
-
const koaSession = (0, import_koa_session.default)({ ...opt, store, externalKey }, app);
|
|
191
|
+
const koaSession = (0, import_koa_session.default)({ ...opt, maxAge, store, externalKey }, app);
|
|
134
192
|
return [koaSession, externalKey];
|
|
135
193
|
};
|
|
136
194
|
var createClientCookie = (opt) => {
|
|
@@ -150,41 +208,6 @@ var createClientCookie = (opt) => {
|
|
|
150
208
|
// src/socketSession.js
|
|
151
209
|
var import_props = require("@randajan/props");
|
|
152
210
|
var sidLocks = /* @__PURE__ */ new Map();
|
|
153
|
-
var createSessionCtx = (sessionId, session2, socket) => (0, import_props.solids)({ session: session2 }, { sessionId, socket });
|
|
154
|
-
var createSessionHash = (session2) => {
|
|
155
|
-
try {
|
|
156
|
-
return JSON.stringify(session2 ?? null);
|
|
157
|
-
} catch {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
};
|
|
161
|
-
var isSessionHashChanged = (originalHash, session2) => {
|
|
162
|
-
const nextHash = createSessionHash(session2);
|
|
163
|
-
if (originalHash == null || nextHash == null) {
|
|
164
|
-
return true;
|
|
165
|
-
}
|
|
166
|
-
return originalHash !== nextHash;
|
|
167
|
-
};
|
|
168
|
-
var withLock = async (task, socket, ...args) => {
|
|
169
|
-
const sid = socket.sessionId;
|
|
170
|
-
const previous = sidLocks.get(sid);
|
|
171
|
-
let releaseCurrent;
|
|
172
|
-
const current = new Promise((resolve) => {
|
|
173
|
-
releaseCurrent = resolve;
|
|
174
|
-
});
|
|
175
|
-
sidLocks.set(sid, current);
|
|
176
|
-
if (previous) {
|
|
177
|
-
await previous;
|
|
178
|
-
}
|
|
179
|
-
try {
|
|
180
|
-
return await task(socket, ...args);
|
|
181
|
-
} finally {
|
|
182
|
-
releaseCurrent();
|
|
183
|
-
if (sidLocks.get(sid) === current) {
|
|
184
|
-
sidLocks.delete(sid);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
};
|
|
188
211
|
var applyOnMissing = (onMissing) => {
|
|
189
212
|
if (onMissing instanceof Error) {
|
|
190
213
|
throw onMissing;
|
|
@@ -194,126 +217,55 @@ var applyOnMissing = (onMissing) => {
|
|
|
194
217
|
}
|
|
195
218
|
return onMissing;
|
|
196
219
|
};
|
|
197
|
-
var runSessionHandler = async (socket, handler,
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
if (!current) {
|
|
220
|
+
var runSessionHandler = async (sid, socket, handler, gw, onMissing) => {
|
|
221
|
+
const session2 = await gw.get(sid);
|
|
222
|
+
if (!session2) {
|
|
201
223
|
return applyOnMissing(onMissing);
|
|
202
224
|
}
|
|
203
|
-
const
|
|
204
|
-
const sessionCtx = createSessionCtx(sid, session2, socket);
|
|
205
|
-
const originalHash = createSessionHash(sessionCtx.session);
|
|
225
|
+
const sessionCtx = (0, import_props.solids)({ session: session2 }, { sessionId: sid, socket });
|
|
206
226
|
const result = await handler(sessionCtx, socket);
|
|
207
|
-
if (
|
|
208
|
-
await
|
|
209
|
-
return result;
|
|
210
|
-
}
|
|
211
|
-
sessionCtx.session = validObject(sessionCtx.session, false, "session");
|
|
212
|
-
if (isSessionHashChanged(originalHash, sessionCtx.session)) {
|
|
213
|
-
await store.set(sid, sessionCtx.session);
|
|
227
|
+
if (sid === socket.sessionId) {
|
|
228
|
+
await gw.set(sid, sessionCtx.session);
|
|
214
229
|
}
|
|
215
230
|
return result;
|
|
216
231
|
};
|
|
217
|
-
var
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
var import_props2 = require("@randajan/props");
|
|
229
|
-
var import_events = require("events");
|
|
230
|
-
|
|
231
|
-
// src/const.js
|
|
232
|
-
var ms = {
|
|
233
|
-
s: (v = 1) => v * 1e3,
|
|
234
|
-
m: (v = 1) => ms.s(v * 60),
|
|
235
|
-
h: (v = 1) => ms.m(v * 60),
|
|
236
|
-
d: (v = 1) => ms.h(v * 24),
|
|
237
|
-
w: (v = 1) => ms.d(v * 7),
|
|
238
|
-
M: (v = 1) => ms.d(v * 30),
|
|
239
|
-
y: (v = 1) => ms.d(v * 365)
|
|
240
|
-
};
|
|
241
|
-
var _customOptKeys = /* @__PURE__ */ new Set([
|
|
242
|
-
"store",
|
|
243
|
-
"autoCleanup",
|
|
244
|
-
"autoCleanupMs",
|
|
245
|
-
"clientKey",
|
|
246
|
-
"clientMaxAge",
|
|
247
|
-
"clientAlwaysRoll"
|
|
248
|
-
]);
|
|
249
|
-
|
|
250
|
-
// src/class/SessionStore.js
|
|
251
|
-
var formatState = (session2, maxAge, prevTTL, maxAgeDefault) => {
|
|
252
|
-
const ttl = maxAge ?? prevTTL ?? maxAgeDefault;
|
|
253
|
-
const expiresAt = Date.now() + ttl;
|
|
254
|
-
return { session: session2, expiresAt, ttl };
|
|
255
|
-
};
|
|
256
|
-
var SessionStore = class extends Map {
|
|
257
|
-
constructor(opt = {}) {
|
|
258
|
-
super();
|
|
259
|
-
const maxAge = validRange(ms.s(), ms.y(), opt.maxAge, false, "maxAge") ?? ms.M();
|
|
260
|
-
const autoCleanup = valid("boolean", opt.autoCleanup, false, "autoCleanup") ?? true;
|
|
261
|
-
const autoCleanupMs = validInterval(opt.autoCleanupMs, false, "autoCleanupMs") ?? Math.max(ms.s(), Math.min(ms.h(), maxAge / 10));
|
|
262
|
-
(0, import_props2.solid)(this, "maxAge", maxAge);
|
|
263
|
-
(0, import_props2.solid)(this, "event", new import_events.EventEmitter());
|
|
264
|
-
if (!autoCleanup) {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
setInterval((_) => this.cleanup(), autoCleanupMs);
|
|
268
|
-
}
|
|
269
|
-
on(eventName, callback) {
|
|
270
|
-
return this.event.on(eventName, callback);
|
|
271
|
-
}
|
|
272
|
-
get(sid) {
|
|
273
|
-
const d = super.get(sid);
|
|
274
|
-
if (!d) {
|
|
275
|
-
return;
|
|
232
|
+
var createLock = (sid) => {
|
|
233
|
+
let _release;
|
|
234
|
+
const previous = sidLocks.get(sid);
|
|
235
|
+
const current = new Promise((resolve) => {
|
|
236
|
+
_release = resolve;
|
|
237
|
+
});
|
|
238
|
+
sidLocks.set(sid, current);
|
|
239
|
+
const release = () => {
|
|
240
|
+
_release();
|
|
241
|
+
if (sidLocks.get(sid) === current) {
|
|
242
|
+
sidLocks.delete(sid);
|
|
276
243
|
}
|
|
277
|
-
|
|
278
|
-
|
|
244
|
+
};
|
|
245
|
+
return [previous, release];
|
|
246
|
+
};
|
|
247
|
+
var applySessionHandler = async (socket, handler, gw, onMissing) => {
|
|
248
|
+
valid("function", handler, true, "handler");
|
|
249
|
+
for (let i = 0; i < 5; i++) {
|
|
250
|
+
const sid = socket.sessionId;
|
|
251
|
+
if (!sid) {
|
|
252
|
+
return applyOnMissing(onMissing);
|
|
279
253
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
const d = super.get(sid);
|
|
284
|
-
if (session2 == null) {
|
|
285
|
-
return !d || this.destroy(sid);
|
|
254
|
+
const [previous, release] = createLock(sid);
|
|
255
|
+
if (previous) {
|
|
256
|
+
await previous;
|
|
286
257
|
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
delete(sid) {
|
|
292
|
-
return this.destroy(sid);
|
|
293
|
-
}
|
|
294
|
-
destroy(sid) {
|
|
295
|
-
if (this.has(sid)) {
|
|
296
|
-
super.delete(sid);
|
|
297
|
-
this.event.emit("destroy", this, sid);
|
|
298
|
-
}
|
|
299
|
-
return true;
|
|
300
|
-
}
|
|
301
|
-
cleanup() {
|
|
302
|
-
const now = Date.now();
|
|
303
|
-
let cleared = 0;
|
|
304
|
-
for (const [sid, d] of this.entries()) {
|
|
305
|
-
if (now < d.expiresAt) {
|
|
306
|
-
continue;
|
|
307
|
-
}
|
|
308
|
-
if (this.destroy(sid)) {
|
|
309
|
-
cleared++;
|
|
310
|
-
}
|
|
258
|
+
if (previous && sid !== socket.sessionId) {
|
|
259
|
+
release();
|
|
260
|
+
continue;
|
|
311
261
|
}
|
|
312
|
-
|
|
313
|
-
|
|
262
|
+
try {
|
|
263
|
+
return await runSessionHandler(sid, socket, handler, gw, onMissing);
|
|
264
|
+
} finally {
|
|
265
|
+
release();
|
|
314
266
|
}
|
|
315
|
-
return cleared;
|
|
316
267
|
}
|
|
268
|
+
throw new Error(`${_errPrefix} socket.sessionId changed during withSession execution.`);
|
|
317
269
|
};
|
|
318
270
|
|
|
319
271
|
// src/formatOptions.js
|
|
@@ -325,20 +277,22 @@ var pickKoaOpt = (rawOpt) => {
|
|
|
325
277
|
}
|
|
326
278
|
koaOpt[key] = rawOpt[key];
|
|
327
279
|
}
|
|
328
|
-
koaOpt.key = valid("string", koaOpt.key, false, "key") ??
|
|
329
|
-
koaOpt.maxAge = validRange(ms.s(), ms.y(), koaOpt.maxAge, false, "maxAge") ?? ms.M();
|
|
280
|
+
koaOpt.key = valid("string", koaOpt.key, false, "key") ?? "sid";
|
|
330
281
|
koaOpt.signed = valid("boolean", koaOpt.signed, false, "signed") ?? true;
|
|
331
|
-
koaOpt.store = validStore(rawOpt.store || new SessionStore(rawOpt));
|
|
332
282
|
return koaOpt;
|
|
333
283
|
};
|
|
334
284
|
var formatOptions = (opt = {}) => {
|
|
335
285
|
opt = validObject(opt, true, "options");
|
|
286
|
+
const appKeys = validArray(opt.appKeys, false, "appKeys");
|
|
287
|
+
const allowRndAppKeys = valid("boolean", opt.allowRndAppKeys, false, "allowRnddAppKeys") ?? false;
|
|
336
288
|
const koaOpt = pickKoaOpt(opt);
|
|
337
|
-
const clientKey = valid("string", opt.clientKey) ??
|
|
289
|
+
const clientKey = valid("string", opt.clientKey) ?? "cid";
|
|
338
290
|
const clientMaxAge = validInterval(opt.clientMaxAge, false, "clientMaxAge") ?? ms.y();
|
|
339
291
|
const clientAlwaysRoll = valid("boolean", opt.clientAlwaysRoll, false, "clientAlwaysRoll") ?? true;
|
|
340
292
|
const clientOpt = { ...koaOpt, key: clientKey, maxAge: clientMaxAge };
|
|
341
293
|
return {
|
|
294
|
+
appKeys,
|
|
295
|
+
allowRndAppKeys,
|
|
342
296
|
koaOpt,
|
|
343
297
|
clientOpt,
|
|
344
298
|
clientAlwaysRoll
|
|
@@ -346,11 +300,11 @@ var formatOptions = (opt = {}) => {
|
|
|
346
300
|
};
|
|
347
301
|
|
|
348
302
|
// src/class/Bridge.js
|
|
349
|
-
var
|
|
303
|
+
var import_props2 = require("@randajan/props");
|
|
350
304
|
var Bridge = class {
|
|
351
305
|
constructor(opt = {}) {
|
|
352
306
|
const { onSet, onDelete } = opt;
|
|
353
|
-
(0,
|
|
307
|
+
(0, import_props2.solids)(this, {
|
|
354
308
|
onSet,
|
|
355
309
|
onDelete,
|
|
356
310
|
s2c: /* @__PURE__ */ new Map(),
|
|
@@ -368,7 +322,7 @@ var Bridge = class {
|
|
|
368
322
|
}
|
|
369
323
|
this.c2s.set(cid, sid);
|
|
370
324
|
this.s2c.set(sid, cid);
|
|
371
|
-
this.onSet(
|
|
325
|
+
this.onSet(cid, sid);
|
|
372
326
|
return true;
|
|
373
327
|
}
|
|
374
328
|
getByCid(cid) {
|
|
@@ -387,7 +341,7 @@ var Bridge = class {
|
|
|
387
341
|
}
|
|
388
342
|
this.s2c.delete(sid);
|
|
389
343
|
this.c2s.delete(cid);
|
|
390
|
-
this.onDelete(
|
|
344
|
+
this.onDelete(cid, sid);
|
|
391
345
|
return true;
|
|
392
346
|
}
|
|
393
347
|
deleteByCid(cid, skipIf) {
|
|
@@ -400,30 +354,243 @@ var Bridge = class {
|
|
|
400
354
|
}
|
|
401
355
|
this.c2s.delete(cid);
|
|
402
356
|
this.s2c.delete(sid);
|
|
403
|
-
this.onDelete(
|
|
357
|
+
this.onDelete(cid, sid);
|
|
358
|
+
return true;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
// src/class/TempMap.js
|
|
363
|
+
var TempMap = class extends Map {
|
|
364
|
+
constructor(ttl) {
|
|
365
|
+
super();
|
|
366
|
+
const _p = {
|
|
367
|
+
ttl: validInterval(ttl, true, "ttl"),
|
|
368
|
+
ts: /* @__PURE__ */ new Map()
|
|
369
|
+
};
|
|
370
|
+
_privs.set(this, _p);
|
|
371
|
+
}
|
|
372
|
+
set(key, value, overwrite = true) {
|
|
373
|
+
const { ts, ttl } = _privs.get(this);
|
|
374
|
+
if (!overwrite && this.has(key)) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
this.delete(key);
|
|
378
|
+
super.set(key, value);
|
|
379
|
+
const t = setTimeout((_) => {
|
|
380
|
+
this.delete(key);
|
|
381
|
+
}, ttl);
|
|
382
|
+
ts.set(key, t);
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
get(key, andDelete = false) {
|
|
386
|
+
const v = super.get(key);
|
|
387
|
+
if (andDelete) {
|
|
388
|
+
this.delete(key);
|
|
389
|
+
}
|
|
390
|
+
return v;
|
|
391
|
+
}
|
|
392
|
+
delete(key) {
|
|
393
|
+
if (!super.delete(key)) {
|
|
394
|
+
return false;
|
|
395
|
+
}
|
|
396
|
+
const { ts } = _privs.get(this);
|
|
397
|
+
const t = ts.get(key);
|
|
398
|
+
clearTimeout(t);
|
|
399
|
+
ts.delete(key);
|
|
400
|
+
return true;
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
// src/class/StoreGateway.js
|
|
405
|
+
var import_props3 = require("@randajan/props");
|
|
406
|
+
|
|
407
|
+
// src/stores/LiveStore.js
|
|
408
|
+
var LiveStore = class extends Map {
|
|
409
|
+
get(sid) {
|
|
410
|
+
return super.get(sid);
|
|
411
|
+
}
|
|
412
|
+
set(sid, state) {
|
|
413
|
+
super.set(sid, state);
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
destroy(sid) {
|
|
417
|
+
return super.delete(sid);
|
|
418
|
+
}
|
|
419
|
+
list() {
|
|
420
|
+
return this.keys();
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// src/class/StoreGateway.js
|
|
425
|
+
var formatState = (session2, maxAge, maxAgeDefault) => {
|
|
426
|
+
const ttl = maxAge ?? maxAgeDefault;
|
|
427
|
+
const expiresAt = Date.now() + ttl;
|
|
428
|
+
return { session: session2, expiresAt, ttl };
|
|
429
|
+
};
|
|
430
|
+
var requireList = (store) => {
|
|
431
|
+
if (!store.list) {
|
|
432
|
+
throw new TypeError(`${_errPrefix} store.list() is required`);
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
var StoreGateway = class _StoreGateway {
|
|
436
|
+
static is(any) {
|
|
437
|
+
return any instanceof _StoreGateway;
|
|
438
|
+
}
|
|
439
|
+
constructor(opt = {}, emit = {}) {
|
|
440
|
+
const maxAge = validRange(opt.maxAge, ms.m(), ms.y(), false, "maxAge") ?? ms.M();
|
|
441
|
+
const autoCleanup = valid("boolean", opt.autoCleanup, false, "autoCleanup") ?? false;
|
|
442
|
+
(0, import_props3.solid)(this, "emit", emit);
|
|
443
|
+
(0, import_props3.solid)(this, "maxAge", maxAge);
|
|
444
|
+
(0, import_props3.solid)(this, "store", validStore(opt.store, false) ?? new LiveStore());
|
|
445
|
+
if (autoCleanup) {
|
|
446
|
+
this.startAutoCleanup(validInterval(opt.autoCleanupMs, false, "autoCleanupMs"));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
async list() {
|
|
450
|
+
const { store } = this;
|
|
451
|
+
requireList(store);
|
|
452
|
+
return store.list();
|
|
453
|
+
}
|
|
454
|
+
async get(sid) {
|
|
455
|
+
const { store } = this;
|
|
456
|
+
const c = await store.get(sid);
|
|
457
|
+
if (!c) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (Date.now() >= c.expiresAt) {
|
|
461
|
+
await this.destroy(sid);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
return JSON.parse(c.session);
|
|
466
|
+
} catch {
|
|
467
|
+
await this.destroy(sid);
|
|
468
|
+
throw new Error(`${_errPrefix} Invalid session JSON for sid='${sid}'.`);
|
|
469
|
+
}
|
|
470
|
+
;
|
|
471
|
+
}
|
|
472
|
+
async set(sid, session2, maxAge) {
|
|
473
|
+
const { store } = this;
|
|
474
|
+
const ses = validObject(session2, false, "session");
|
|
475
|
+
if (ses == null) {
|
|
476
|
+
return this.destroy(sid);
|
|
477
|
+
}
|
|
478
|
+
const to = JSON.stringify(ses);
|
|
479
|
+
const from = await store.get(sid, false);
|
|
480
|
+
if (to === from?.session) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
const isOk = await store.set(sid, formatState(to, maxAge, this.maxAge));
|
|
484
|
+
if (isOk === false) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
this.emit.notifySet(sid, !from);
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
async destroy(sid) {
|
|
491
|
+
const { store } = this;
|
|
492
|
+
const isOk = await store.destroy(sid);
|
|
493
|
+
if (isOk === false) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
this.emit.notifyDestroy(sid);
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
async cleanup() {
|
|
500
|
+
const { store } = this;
|
|
501
|
+
requireList(store);
|
|
502
|
+
const list = await store.list();
|
|
503
|
+
const now = Date.now();
|
|
504
|
+
let cleared = 0;
|
|
505
|
+
await Promise.all([...list].map(async (sid) => {
|
|
506
|
+
const d = await store.get(sid);
|
|
507
|
+
if (!d) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
if (now < d.expiresAt) {
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
if (await this.destroy(sid)) {
|
|
514
|
+
cleared++;
|
|
515
|
+
}
|
|
516
|
+
}));
|
|
517
|
+
if (is("function", store.optimize)) {
|
|
518
|
+
await store.optimize(cleared);
|
|
519
|
+
}
|
|
520
|
+
this.emit.notifyCleanup(cleared);
|
|
521
|
+
return cleared;
|
|
522
|
+
}
|
|
523
|
+
startAutoCleanup(interval) {
|
|
524
|
+
const { store, maxAge, _intId } = this;
|
|
525
|
+
requireList(store);
|
|
526
|
+
const int = validRange(interval, ms.m(), ms.d(), false, "interval") ?? Math.max(ms.m(), Math.min(ms.d(), maxAge / 4));
|
|
527
|
+
clearInterval(_intId);
|
|
528
|
+
this._intId = setInterval((_) => this.cleanup().catch(() => {
|
|
529
|
+
}), int);
|
|
530
|
+
return true;
|
|
531
|
+
}
|
|
532
|
+
stopAutoCleanup() {
|
|
533
|
+
const { _intId } = this;
|
|
534
|
+
if (!_intId) {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
clearInterval(_intId);
|
|
538
|
+
delete this._intId;
|
|
404
539
|
return true;
|
|
405
540
|
}
|
|
406
541
|
};
|
|
407
542
|
|
|
408
543
|
// src/class/SessionBridge.js
|
|
409
|
-
var SessionBridge = class extends
|
|
544
|
+
var SessionBridge = class extends import_events.EventEmitter {
|
|
410
545
|
constructor(app, io, opt = {}) {
|
|
411
546
|
super();
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
547
|
+
const { appKeys, allowRndAppKeys, koaOpt, clientOpt, clientAlwaysRoll } = formatOptions(opt);
|
|
548
|
+
const _p = {};
|
|
549
|
+
ensureAppKeys(app, appKeys, allowRndAppKeys);
|
|
550
|
+
const tmp = new TempMap(5e3);
|
|
551
|
+
const brg = _p.brg = new Bridge({
|
|
552
|
+
onSet: (clientId, sessionId) => {
|
|
553
|
+
const isNew = !!tmp.get(sessionId, true);
|
|
554
|
+
this.emit("sessionSet", { clientId, sessionId, isNew, isInit: true });
|
|
555
|
+
},
|
|
556
|
+
onDelete: (clientId, sessionId) => this.emit("sessionDestroy", { clientId, sessionId })
|
|
420
557
|
});
|
|
421
|
-
|
|
422
|
-
|
|
558
|
+
_p.notifySet = (sid, isNew) => {
|
|
559
|
+
if (!sid) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const cid = brg.getBySid(sid);
|
|
563
|
+
if (!cid) {
|
|
564
|
+
tmp.set(sid, isNew, false);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
this.emit("sessionSet", { clientId: cid, sessionId: sid, isNew: !!isNew, isInit: false });
|
|
568
|
+
};
|
|
569
|
+
_p.notifyDestroy = (sid) => {
|
|
570
|
+
if (sid) {
|
|
571
|
+
brg.deleteBySid(sid);
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
_p.notifyCleanup = (cleared) => {
|
|
575
|
+
this.emit("cleanup", cleared);
|
|
576
|
+
};
|
|
577
|
+
const gw = _p.gw = new StoreGateway(opt, _p);
|
|
578
|
+
const cc = createClientCookie(clientOpt);
|
|
579
|
+
const [koaSession, sc] = createKoaSession(app, gw, koaOpt, (ctx, sid) => {
|
|
423
580
|
const cid = cc.get(ctx);
|
|
424
581
|
brg.set(cid, sid);
|
|
425
582
|
});
|
|
426
|
-
const
|
|
583
|
+
const reviveCid = (ctx) => {
|
|
584
|
+
let cid = cc.get(ctx);
|
|
585
|
+
if (!cid) {
|
|
586
|
+
cc.set(ctx, cid = generateUid(24));
|
|
587
|
+
} else if (clientAlwaysRoll) {
|
|
588
|
+
cc.set(ctx, cid);
|
|
589
|
+
}
|
|
590
|
+
return cid;
|
|
591
|
+
};
|
|
592
|
+
const reviveSid = async (ctx, cid) => {
|
|
593
|
+
const reqSid = sc.get(ctx);
|
|
427
594
|
if (cid == null || reqSid == null) {
|
|
428
595
|
return;
|
|
429
596
|
}
|
|
@@ -437,21 +604,15 @@ var SessionBridge = class extends import_events2.EventEmitter {
|
|
|
437
604
|
if (brg.getBySid(reqSid)) {
|
|
438
605
|
return;
|
|
439
606
|
}
|
|
440
|
-
if (!await
|
|
607
|
+
if (!await gw.get(reqSid)) {
|
|
441
608
|
return;
|
|
442
609
|
}
|
|
443
610
|
brg.set(cid, reqSid);
|
|
444
611
|
};
|
|
445
612
|
app.use(koaSession);
|
|
446
613
|
app.use(async (ctx, next) => {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (!cid) {
|
|
450
|
-
cc.set(ctx, cid = generateUid(24));
|
|
451
|
-
} else if (o.clientAlwaysRoll) {
|
|
452
|
-
cc.set(ctx, cid);
|
|
453
|
-
}
|
|
454
|
-
await regenerateSid(ctx, cid, sid);
|
|
614
|
+
const cid = reviveCid(ctx);
|
|
615
|
+
await reviveSid(ctx, cid);
|
|
455
616
|
(0, import_props4.solid)(ctx, "clientId", cid);
|
|
456
617
|
(0, import_props4.virtual)(ctx, "sessionId", (_) => brg.getByCid(cid));
|
|
457
618
|
await next();
|
|
@@ -462,24 +623,87 @@ var SessionBridge = class extends import_events2.EventEmitter {
|
|
|
462
623
|
const ctx = app.createContext(req, res);
|
|
463
624
|
await koaSession(ctx, async () => {
|
|
464
625
|
});
|
|
465
|
-
const cid =
|
|
466
|
-
|
|
467
|
-
await regenerateSid(ctx, cid, sid);
|
|
626
|
+
const cid = reviveCid(ctx);
|
|
627
|
+
await reviveSid(ctx, cid);
|
|
468
628
|
(0, import_props4.solid)(socket, "clientId", cid);
|
|
469
629
|
(0, import_props4.virtual)(socket, "sessionId", (_) => brg.getByCid(cid));
|
|
470
630
|
(0, import_props4.solid)(socket, "withSession", async function(handler, onMissing) {
|
|
471
|
-
const onm = arguments.length > 1 ? onMissing : new Error(
|
|
472
|
-
return applySessionHandler(socket, handler,
|
|
631
|
+
const onm = arguments.length > 1 ? onMissing : new Error(`${_errPrefix} Session is missing for this socket.`);
|
|
632
|
+
return applySessionHandler(socket, handler, gw, onm);
|
|
473
633
|
}, false);
|
|
474
634
|
await next();
|
|
475
635
|
});
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
636
|
+
_privs.set(this, _p);
|
|
637
|
+
}
|
|
638
|
+
async getById(sessionId) {
|
|
639
|
+
const { gw, brg } = _privs.get(this);
|
|
640
|
+
const cid = brg.getBySid(sessionId);
|
|
641
|
+
if (cid) {
|
|
642
|
+
return gw.get(sessionId);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
async destroyById(sessionId) {
|
|
646
|
+
const { gw, brg } = _privs.get(this);
|
|
647
|
+
const cid = brg.getBySid(sessionId);
|
|
648
|
+
if (cid) {
|
|
649
|
+
return gw.destroy(sessionId);
|
|
650
|
+
}
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
async setById(sessionId, session2, maxAge) {
|
|
654
|
+
const { gw, brg } = _privs.get(this);
|
|
655
|
+
const cid = brg.getBySid(sessionId);
|
|
656
|
+
if (cid) {
|
|
657
|
+
return gw.set(sessionId, session2, maxAge);
|
|
658
|
+
}
|
|
659
|
+
throw new Error(`${_errPrefix} Creating session via setById() is prohibited. sessionId='${sessionId}'.`);
|
|
660
|
+
}
|
|
661
|
+
async getByClientId(clientId) {
|
|
662
|
+
const { gw, brg } = _privs.get(this);
|
|
663
|
+
const sid = brg.getByCid(clientId);
|
|
664
|
+
if (sid) {
|
|
665
|
+
return gw.get(sid);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async destroyByClientId(clientId) {
|
|
669
|
+
const { gw, brg } = _privs.get(this);
|
|
670
|
+
const sid = brg.getByCid(clientId);
|
|
671
|
+
if (sid) {
|
|
672
|
+
return gw.destroy(sid);
|
|
673
|
+
}
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
async setByClientId(clientId, session2, maxAge) {
|
|
677
|
+
const { gw, brg } = _privs.get(this);
|
|
678
|
+
const sid = brg.getByCid(clientId);
|
|
679
|
+
if (sid) {
|
|
680
|
+
return gw.set(sid, session2, maxAge);
|
|
681
|
+
}
|
|
682
|
+
throw new Error(`${_errPrefix} Creating session via setByClientId() is prohibited. clientId='${clientId}'.`);
|
|
683
|
+
}
|
|
684
|
+
getSessionId(clientId) {
|
|
685
|
+
return _privs.get(this).brg.getByCid(clientId);
|
|
686
|
+
}
|
|
687
|
+
getClientId(sessionId) {
|
|
688
|
+
return _privs.get(this).brg.getBySid(sessionId);
|
|
689
|
+
}
|
|
690
|
+
async cleanup() {
|
|
691
|
+
return _privs.get(this).gw.cleanup();
|
|
692
|
+
}
|
|
693
|
+
startAutoCleanup(interval) {
|
|
694
|
+
return _privs.get(this).gw.startAutoCleanup(interval);
|
|
695
|
+
}
|
|
696
|
+
stopAutoCleanup() {
|
|
697
|
+
return _privs.get(this).gw.stopAutoCleanup();
|
|
698
|
+
}
|
|
699
|
+
notifyStoreSet(sessionId, isNew) {
|
|
700
|
+
return _privs.get(this).notifySet(sessionId, isNew);
|
|
701
|
+
}
|
|
702
|
+
notifyStoreDestroy(sessionId) {
|
|
703
|
+
return _privs.get(this).notifyDestroy(sessionId);
|
|
704
|
+
}
|
|
705
|
+
notifyStoreCleanup(clearedCount) {
|
|
706
|
+
return _privs.get(this).notifyCleanup(clearedCount);
|
|
483
707
|
}
|
|
484
708
|
};
|
|
485
709
|
|