redweb 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +573 -307
  3. package/client.d.ts +42 -0
  4. package/client.js +55 -0
  5. package/docs/LIVE_HTML.md +313 -0
  6. package/docs/MULTIPLAYER_OPERATIONS.md +50 -0
  7. package/docs/PRODUCTION_READINESS.md +68 -0
  8. package/docs/VERIFICATION_EVIDENCE.md +20 -0
  9. package/examples/live-html/cards.css +36 -0
  10. package/examples/live-html/cards.html +11 -0
  11. package/examples/live-html/cards.js +91 -0
  12. package/examples/live-html/cards.ts +35 -0
  13. package/examples/live-html/chatroom.css +156 -0
  14. package/examples/live-html/chatroom.js +268 -0
  15. package/examples/live-html/chatroom.ts +217 -0
  16. package/examples/live-html/components.css +7 -0
  17. package/examples/live-html/components.js +113 -0
  18. package/examples/live-html/components.ts +41 -0
  19. package/examples/live-html/counter.css +24 -0
  20. package/examples/live-html/counter.html +10 -0
  21. package/examples/live-html/counter.js +73 -0
  22. package/examples/live-html/counter.ts +21 -0
  23. package/examples/live-html/tsconfig.json +16 -0
  24. package/index.d.ts +538 -114
  25. package/index.js +44 -12
  26. package/package.json +39 -15
  27. package/src/htmx/Html.js +133 -0
  28. package/src/htmx/HtmlRenderer.js +88 -0
  29. package/src/htmx/HtmlSyntax.js +168 -0
  30. package/src/htmx/LiveHtmlServer.js +91 -0
  31. package/src/htmx/LivePage.js +232 -0
  32. package/src/htmx/PageAssetLoader.js +34 -0
  33. package/src/htmx/PageManager.js +435 -0
  34. package/src/htmx/StaticExporter.js +78 -0
  35. package/src/htmx/StaticSite.js +182 -0
  36. package/src/htmx/TemplateRenderer.js +231 -0
  37. package/src/htmx/browserRuntime.js +97 -0
  38. package/src/htmx/index.js +10 -0
  39. package/src/htmx/metadata.js +349 -0
  40. package/src/htmx/sourceRoot.js +28 -0
  41. package/src/htmx/start.js +17 -0
  42. package/src/htmx/synchronous.js +9 -0
  43. package/src/http/BaseHttpServer.js +82 -117
  44. package/src/http/HttpServer.js +18 -18
  45. package/src/http/HttpsServer.js +20 -20
  46. package/src/serverLifecycle.js +46 -46
  47. package/src/ws/AdmissionPolicy.js +145 -0
  48. package/src/ws/BaseHandler.js +40 -40
  49. package/src/ws/BaseSocketServer.js +199 -100
  50. package/src/ws/DefaultHandler.js +5 -5
  51. package/src/ws/DefaultRoute.js +8 -8
  52. package/src/ws/DistributionBridge.js +271 -0
  53. package/src/ws/FixedStepService.js +74 -0
  54. package/src/ws/HeartbeatMonitor.js +75 -0
  55. package/src/ws/Metrics.js +34 -0
  56. package/src/ws/ProtocolPolicy.js +130 -0
  57. package/src/ws/RoomRegistry.js +117 -0
  58. package/src/ws/RouteRuntime.js +146 -0
  59. package/src/ws/SecureSocketServer.js +9 -9
  60. package/src/ws/SessionRegistry.js +135 -0
  61. package/src/ws/SocketRoute.js +523 -254
  62. package/src/ws/SocketServer.js +8 -8
  63. package/src/ws/TaskQueue.js +64 -0
  64. package/src/ws/TokenBucket.js +31 -0
  65. package/src/ws/TransportPolicy.js +68 -0
  66. package/src/ws/index.js +7 -2
  67. package/src/ws/protocol-schema.json +13 -0
  68. package/src/ws/protocol-validation.js +21 -0
  69. package/src/ws/shutdown.js +33 -33
  70. package/src/ws/util.js +38 -30
  71. package/src/htmx/HtmxRenderer.js +0 -73
  72. package/src/htmx/RedWebHtmxComponent.js +0 -11
@@ -0,0 +1,135 @@
1
+ const { performance } = require('perf_hooks');
2
+
3
+ class SessionRegistry {
4
+ constructor(options = {}, logger = console, clock = () => performance.now()) {
5
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
6
+ throw new TypeError('`sessions` must be an object or true.');
7
+ }
8
+ const {
9
+ ttlMs = 30_000,
10
+ maxSessions = 10_000,
11
+ maxSessionIdLength = 256,
12
+ sweepIntervalMs = Math.min(ttlMs, 1000),
13
+ } = options;
14
+ for (const [name, value] of Object.entries({ ttlMs, maxSessions, maxSessionIdLength, sweepIntervalMs })) {
15
+ if (!Number.isInteger(value) || value < 1) throw new TypeError(`\`sessions.${name}\` must be a positive integer.`);
16
+ }
17
+ if (typeof clock !== 'function') throw new TypeError('`clock` must be a function.');
18
+ this.ttlMs = ttlMs;
19
+ this.maxSessions = maxSessions;
20
+ this.maxSessionIdLength = maxSessionIdLength;
21
+ this.logger = logger;
22
+ this.clock = clock;
23
+ this.sessions = new Map();
24
+ this.closed = false;
25
+ this.timer = setInterval(() => this.sweep(), sweepIntervalMs);
26
+ this.timer.unref();
27
+ }
28
+
29
+ validateId(sessionId) {
30
+ if (typeof sessionId !== 'string' || !sessionId || sessionId.length > this.maxSessionIdLength) {
31
+ throw new TypeError(`Session IDs must be non-empty strings of at most ${this.maxSessionIdLength} characters.`);
32
+ }
33
+ }
34
+
35
+ create(sessionId, data, socket) {
36
+ this.validateId(sessionId);
37
+ if (this.closed || this.sessions.has(sessionId)) return false;
38
+ if (this.sessions.size >= this.maxSessions) this.sweep();
39
+ if (this.sessions.size >= this.maxSessions) return false;
40
+ const record = { data, socket: null, expiresAt: this.clock() + this.ttlMs };
41
+ this.sessions.set(sessionId, record);
42
+ if (socket) this.assign(sessionId, record, socket);
43
+ return true;
44
+ }
45
+
46
+ resume(sessionId, socket) {
47
+ this.validateId(sessionId);
48
+ if (this.closed) return null;
49
+ const record = this.sessions.get(sessionId);
50
+ if (!record) return null;
51
+ if (!record.socket && record.expiresAt <= this.clock()) {
52
+ this.sessions.delete(sessionId);
53
+ return null;
54
+ }
55
+ this.assign(sessionId, record, socket);
56
+ return record.data;
57
+ }
58
+
59
+ assign(sessionId, record, socket) {
60
+ if (!socket) throw new TypeError('A socket is required to own a session.');
61
+ const previousSessionId = socket.__redwebSessionId;
62
+ if (previousSessionId && previousSessionId !== sessionId) this.release(socket);
63
+ const previousSocket = record.socket;
64
+ record.socket = socket;
65
+ record.expiresAt = Infinity;
66
+ socket.__redwebSessionId = sessionId;
67
+ if (socket.context) socket.context.session = { id: sessionId, data: record.data };
68
+ if (previousSocket && previousSocket !== socket) {
69
+ previousSocket.__redwebSessionId = undefined;
70
+ if (previousSocket.context) previousSocket.context.session = null;
71
+ try {
72
+ previousSocket.close?.(4000, 'Session resumed elsewhere');
73
+ } catch (error) {
74
+ this.logger?.error?.('Error closing replaced session socket:', error);
75
+ }
76
+ }
77
+ }
78
+
79
+ release(socket) {
80
+ const sessionId = socket?.__redwebSessionId;
81
+ if (!sessionId) return false;
82
+ const record = this.sessions.get(sessionId);
83
+ socket.__redwebSessionId = undefined;
84
+ if (socket.context) socket.context.session = null;
85
+ if (!record || record.socket !== socket) return false;
86
+ record.socket = null;
87
+ record.expiresAt = this.clock() + this.ttlMs;
88
+ return true;
89
+ }
90
+
91
+ remove(sessionId) {
92
+ this.validateId(sessionId);
93
+ if (this.closed) return false;
94
+ const record = this.sessions.get(sessionId);
95
+ if (!record) return false;
96
+ if (record.socket) {
97
+ record.socket.__redwebSessionId = undefined;
98
+ if (record.socket.context) record.socket.context.session = null;
99
+ }
100
+ return this.sessions.delete(sessionId);
101
+ }
102
+
103
+ get(sessionId) {
104
+ this.validateId(sessionId);
105
+ if (this.closed) return undefined;
106
+ return this.sessions.get(sessionId)?.data;
107
+ }
108
+
109
+ sweep() {
110
+ const now = this.clock();
111
+ this.sessions.forEach((record, sessionId) => {
112
+ if (!record.socket && record.expiresAt <= now) this.sessions.delete(sessionId);
113
+ });
114
+ }
115
+
116
+ stop() {
117
+ if (this.closed) return;
118
+ this.closed = true;
119
+ if (this.timer) clearInterval(this.timer);
120
+ this.timer = null;
121
+ this.sessions.forEach(record => {
122
+ if (record.socket) {
123
+ record.socket.__redwebSessionId = undefined;
124
+ if (record.socket.context) record.socket.context.session = null;
125
+ }
126
+ });
127
+ this.sessions.clear();
128
+ }
129
+
130
+ get size() {
131
+ return this.sessions.size;
132
+ }
133
+ }
134
+
135
+ module.exports = SessionRegistry;