jsgui3-server 0.0.148 → 0.0.149

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 (134) hide show
  1. package/.github/workflows/control-scan-manifest-check.yml +31 -0
  2. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-071799b982906680f5fd699d.js +40 -0
  3. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-07352945ad5c92654fcb8b65.js +39 -0
  4. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-138a601fadb6191ea314c6fd.js +39 -0
  5. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-171f6c381c2cadf2e9fa7087.js +39 -0
  6. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-1d973388156b84a04373fac9.js +39 -0
  7. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-20e117bc8a10d2cd16234bbe.js +40 -0
  8. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-2b028a82b0e5efddba42425f.js +39 -0
  9. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-4518556cd5c7e059e82b22b8.js +40 -0
  10. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5bac1aa0f213902f718ed74f.js +40 -0
  11. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-5f9996ac7822caf777d92f56.js +39 -0
  12. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-60a92c702e65fd9cf748e3ec.js +39 -0
  13. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6164c1f8f738995c541895d2.js +44 -0
  14. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-6718a85eb9e5aa782dd47a05.js +45 -0
  15. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-69e280f14e37aee76a1d4675.js +39 -0
  16. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7570d1b030d44b111ed59c4c.js +39 -0
  17. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7798c9bbd55e510d5039f936.js +42 -0
  18. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-78cd511ea1ef18ecb03d1be5.js +40 -0
  19. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-7d482e0b95bcb5e3c543118b.js +43 -0
  20. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-80e9476d1127c55b40fdb36f.js +40 -0
  21. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-810ced55d5320a3088a05b13.js +40 -0
  22. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-8423565f1a40e329afc8c6cf.js +40 -0
  23. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-900bef783b8cee36506ec282.js +39 -0
  24. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-a1a37aff6416fdad74040ddf.js +39 -0
  25. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-ad48d5e8eda40f175b4df090.js +39 -0
  26. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-aec5a2d963015528c9099462.js +39 -0
  27. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-af9d34e0f1722fab9e28c269.js +39 -0
  28. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-b818e4015e2f1fe86280b5ab.js +41 -0
  29. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bcb2541adc70b7aba61768c5.js +44 -0
  30. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-bfe89d2c78ed44f95ed7dd73.js +40 -0
  31. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c06f04806a1e688e1187110c.js +40 -0
  32. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-c3f3adf904f585afc544b96a.js +39 -0
  33. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-d45acb873e1d8e32d5e60f2e.js +39 -0
  34. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-db06f132533706f4a0163b8c.js +39 -0
  35. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-f660f40d78b135fc8560a862.js +39 -0
  36. package/.jsgui3-server-cache/jsgui3-html-shims/jsgui3-html-controls-shim-f9dee4ec18a96e09bee06bae.js +39 -0
  37. package/README.md +85 -3
  38. package/admin-ui/client.js +8 -8
  39. package/dev-status.svg +139 -0
  40. package/docs/api-reference.md +301 -43
  41. package/docs/books/jsgui3-bundling-research-book/00-table-of-contents.md +35 -0
  42. package/docs/books/jsgui3-bundling-research-book/01-pipeline-and-runtime-semantics.md +34 -0
  43. package/docs/books/jsgui3-bundling-research-book/02-javascript-bundling-core.md +36 -0
  44. package/docs/books/jsgui3-bundling-research-book/03-style-extraction-and-css-compilation.md +35 -0
  45. package/docs/books/jsgui3-bundling-research-book/04-static-publishing-and-delivery.md +39 -0
  46. package/docs/books/jsgui3-bundling-research-book/05-current-limits-and-size-bloat-vectors.md +25 -0
  47. package/docs/books/jsgui3-bundling-research-book/06-unused-module-elimination-strategy.md +77 -0
  48. package/docs/books/jsgui3-bundling-research-book/07-jsgui3-html-control-and-mixin-pruning.md +63 -0
  49. package/docs/books/jsgui3-bundling-research-book/08-test-and-verification-methodology.md +43 -0
  50. package/docs/books/jsgui3-bundling-research-book/09-roadmap-and-rollout.md +42 -0
  51. package/docs/books/jsgui3-bundling-research-book/10-further-research-strategies-and-upgrades.md +211 -0
  52. package/docs/books/jsgui3-bundling-research-book/README.md +35 -0
  53. package/docs/bundling-system-deep-dive.md +9 -4
  54. package/docs/comprehensive-documentation.md +49 -18
  55. package/docs/configuration-reference.md +152 -27
  56. package/docs/core/README.md +19 -0
  57. package/docs/core/jsgui3-server-core-book/00-table-of-contents.md +21 -0
  58. package/docs/core/jsgui3-server-core-book/01-startup-readiness-state-machine.md +41 -0
  59. package/docs/core/jsgui3-server-core-book/02-resource-abstraction-and-lifecycle.md +92 -0
  60. package/docs/core/jsgui3-server-core-book/03-resource-pool-and-event-topology.md +47 -0
  61. package/docs/core/jsgui3-server-core-book/04-sse-publisher-semantics.md +41 -0
  62. package/docs/core/jsgui3-server-core-book/05-serve-factory-resource-wiring.md +46 -0
  63. package/docs/core/jsgui3-server-core-book/06-e2e-testing-methodology.md +48 -0
  64. package/docs/core/jsgui3-server-core-book/07-defect-detection-and-hardening-loop.md +47 -0
  65. package/docs/publishers-guide.md +59 -4
  66. package/docs/resources-guide.md +184 -35
  67. package/docs/simple-server-api-design.md +72 -17
  68. package/docs/system-architecture.md +18 -14
  69. package/examples/controls/15) window, observable SSE/server.js +6 -1
  70. package/examples/controls/19) window, auto observable ui/server.js +9 -0
  71. package/examples/controls/20) window, task manager app/README.md +133 -0
  72. package/examples/controls/20) window, task manager app/client.js +797 -0
  73. package/examples/controls/20) window, task manager app/server.js +178 -0
  74. package/examples/controls/6) window, color_palette/client.js +165 -68
  75. package/examples/controls/9) window, date picker/client.js +362 -76
  76. package/examples/controls/9b) window, shared data.model mirrored date pickers/client.js +104 -83
  77. package/examples/jsgui3-html/06) theming/client.js +22 -1
  78. package/examples/jsgui3-html/10) binding-debugger/client.js +137 -1
  79. package/http/responders/static/Static_Route_HTTP_Responder.js +52 -34
  80. package/lab/experiments/capture-color-controls.js +196 -0
  81. package/lab/results/screenshots/color-controls/full_page.png +0 -0
  82. package/lab/results/screenshots/color-controls/section_1_color_grid_12x12.png +0 -0
  83. package/lab/results/screenshots/color-controls/section_2_color_grid_4x2.png +0 -0
  84. package/lab/results/screenshots/color-controls/section_3_color_palette.png +0 -0
  85. package/lab/results/screenshots/color-controls/section_4_palette_comparison.png +0 -0
  86. package/lab/results/screenshots/color-controls/section_5_raw_swatches.png +0 -0
  87. package/lab/results/screenshots/color-controls/section_6_optimized_crayola.png +0 -0
  88. package/lab/results/screenshots/color-controls/section_7_pastel_palette.png +0 -0
  89. package/lab/results/screenshots/color-controls/section_8_extended_144.png +0 -0
  90. package/lab/screenshot-utils.js +248 -0
  91. package/module.js +11 -4
  92. package/package.json +12 -2
  93. package/publishers/Publishers.js +4 -3
  94. package/publishers/helpers/assigners/static-compressed-response-buffers/Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner.js +5 -5
  95. package/publishers/http-sse-publisher.js +341 -0
  96. package/resources/process-resource.js +950 -0
  97. package/resources/processors/bundlers/js/esbuild/Advanced_JS_Bundler_Using_ESBuild.js +129 -33
  98. package/resources/processors/bundlers/js/esbuild/Core_JS_Non_Minifying_Bundler_Using_ESBuild.js +18 -7
  99. package/resources/processors/bundlers/js/esbuild/JSGUI3_HTML_Control_Optimizer.js +829 -0
  100. package/resources/remote-process-resource.js +355 -0
  101. package/resources/server-resource-pool.js +354 -41
  102. package/serve-factory.js +441 -259
  103. package/server.js +89 -13
  104. package/tests/README.md +66 -4
  105. package/tests/admin-ui-render.test.js +24 -0
  106. package/tests/assigners.test.js +56 -40
  107. package/tests/bundling-default-control-elimination.puppeteer.test.js +260 -0
  108. package/tests/configuration-validation.test.js +21 -18
  109. package/tests/content-analysis.test.js +7 -6
  110. package/tests/control-optimizer-cache-behavior.test.js +52 -0
  111. package/tests/control-scan-manifest-regression.test.js +144 -0
  112. package/tests/end-to-end.test.js +15 -14
  113. package/tests/error-handling.test.js +222 -179
  114. package/tests/fixtures/bundling-default-button-client.js +37 -0
  115. package/tests/fixtures/bundling-default-window-client.js +34 -0
  116. package/tests/fixtures/control_scan_manifest_expectations.json +48 -0
  117. package/tests/fixtures/resource-monitor-client.js +319 -0
  118. package/tests/helpers/puppeteer-e2e-harness.js +317 -0
  119. package/tests/http-sse-publisher.test.js +136 -0
  120. package/tests/performance.test.js +69 -65
  121. package/tests/process-resource.test.js +138 -0
  122. package/tests/publishers.test.js +7 -7
  123. package/tests/remote-process-resource.test.js +160 -0
  124. package/tests/sass-controls.e2e.test.js +7 -1
  125. package/tests/serve-resources.test.js +270 -0
  126. package/tests/serve.test.js +120 -50
  127. package/tests/server-resource-pool.test.js +106 -0
  128. package/tests/small-controls-bundle-size.test.js +252 -0
  129. package/tests/test-runner.js +13 -1
  130. package/tests/window-examples.puppeteer.test.js +204 -1
  131. package/tests/window-resource-integration.puppeteer.test.js +585 -0
  132. package/tests/temp_invalid.js +0 -7
  133. package/tests/temp_invalid_utf8.js +0 -1
  134. package/tests/temp_malformed.js +0 -10
@@ -0,0 +1,248 @@
1
+ /**
2
+ * Screenshot Utilities for jsgui3-server
3
+ *
4
+ * Reusable Puppeteer-based screenshot capture, extracted from
5
+ * tests/window-examples.puppeteer.test.js patterns.
6
+ *
7
+ * Usage:
8
+ * const { launch_browser, capture_page, close_browser } = require('./screenshot-utils');
9
+ * await launch_browser();
10
+ * const result = await capture_page('http://localhost:3600', {
11
+ * output_path: './screenshots/full_page.png',
12
+ * full_page: true
13
+ * });
14
+ * await close_browser();
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ let puppeteer;
21
+ let browser;
22
+
23
+ /**
24
+ * Launch a headless Puppeteer browser.
25
+ * @param {Object} [options]
26
+ * @param {boolean} [options.headless=true]
27
+ * @param {string} [options.executable_path] - Custom Chrome/Chromium path
28
+ * @returns {Promise<import('puppeteer').Browser>}
29
+ */
30
+ const launch_browser = async (options = {}) => {
31
+ if (browser) return browser;
32
+
33
+ if (!puppeteer) {
34
+ puppeteer = require('puppeteer');
35
+ }
36
+
37
+ const launch_options = {
38
+ headless: options.headless !== false ? 'new' : false,
39
+ args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']
40
+ };
41
+
42
+ if (options.executable_path || process.env.PUPPETEER_EXECUTABLE_PATH) {
43
+ launch_options.executablePath = options.executable_path || process.env.PUPPETEER_EXECUTABLE_PATH;
44
+ }
45
+
46
+ browser = await puppeteer.launch(launch_options);
47
+ return browser;
48
+ };
49
+
50
+ /**
51
+ * Capture a screenshot of a URL.
52
+ * @param {string} url - URL to navigate to
53
+ * @param {Object} [options]
54
+ * @param {string} [options.output_path] - File path for the PNG
55
+ * @param {boolean} [options.full_page=true] - Capture full scrollable page
56
+ * @param {number} [options.width=1280] - Viewport width
57
+ * @param {number} [options.height=720] - Viewport height
58
+ * @param {number} [options.wait_ms=1000] - Wait time after load before capture
59
+ * @param {string} [options.wait_for_selector] - CSS selector to wait for
60
+ * @returns {Promise<{path: string, width: number, height: number, size_bytes: number}>}
61
+ */
62
+ const capture_page = async (url, options = {}) => {
63
+ if (!browser) {
64
+ throw new Error('Browser not launched. Call launch_browser() first.');
65
+ }
66
+
67
+ const {
68
+ output_path,
69
+ full_page = true,
70
+ width = 1280,
71
+ height = 720,
72
+ wait_ms = 1000,
73
+ wait_for_selector
74
+ } = options;
75
+
76
+ const page = await browser.newPage();
77
+
78
+ try {
79
+ await page.setViewport({ width, height });
80
+ await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });
81
+
82
+ if (wait_for_selector) {
83
+ await page.waitForSelector(wait_for_selector, { timeout: 10000 });
84
+ }
85
+
86
+ if (wait_ms > 0) {
87
+ await new Promise(r => setTimeout(r, wait_ms));
88
+ }
89
+
90
+ // Ensure output directory exists
91
+ if (output_path) {
92
+ fs.mkdirSync(path.dirname(output_path), { recursive: true });
93
+ }
94
+
95
+ const screenshot_path = output_path || path.join(process.cwd(), 'screenshot.png');
96
+ await page.screenshot({ path: screenshot_path, fullPage: full_page });
97
+
98
+ const stats = fs.statSync(screenshot_path);
99
+ const dimensions = await page.evaluate(() => ({
100
+ width: document.documentElement.scrollWidth,
101
+ height: document.documentElement.scrollHeight
102
+ }));
103
+
104
+ return {
105
+ path: screenshot_path,
106
+ width: dimensions.width,
107
+ height: dimensions.height,
108
+ size_bytes: stats.size
109
+ };
110
+ } finally {
111
+ await page.close();
112
+ }
113
+ };
114
+
115
+ /**
116
+ * Capture a screenshot of a specific element on a page.
117
+ * @param {string} url - URL to navigate to
118
+ * @param {string} selector - CSS selector for the element
119
+ * @param {Object} [options]
120
+ * @param {string} [options.output_path] - File path for the PNG
121
+ * @param {number} [options.width=1280] - Viewport width
122
+ * @param {number} [options.height=720] - Viewport height
123
+ * @param {number} [options.wait_ms=1000] - Wait time after load
124
+ * @returns {Promise<{path: string, width: number, height: number, size_bytes: number}>}
125
+ */
126
+ const capture_element = async (url, selector, options = {}) => {
127
+ if (!browser) {
128
+ throw new Error('Browser not launched. Call launch_browser() first.');
129
+ }
130
+
131
+ const {
132
+ output_path,
133
+ width = 1280,
134
+ height = 720,
135
+ wait_ms = 1000
136
+ } = options;
137
+
138
+ const page = await browser.newPage();
139
+
140
+ try {
141
+ await page.setViewport({ width, height });
142
+ await page.goto(url, { waitUntil: 'networkidle0', timeout: 30000 });
143
+ await page.waitForSelector(selector, { timeout: 10000 });
144
+
145
+ if (wait_ms > 0) {
146
+ await new Promise(r => setTimeout(r, wait_ms));
147
+ }
148
+
149
+ if (output_path) {
150
+ fs.mkdirSync(path.dirname(output_path), { recursive: true });
151
+ }
152
+
153
+ const element = await page.$(selector);
154
+ if (!element) {
155
+ throw new Error(`Element not found: ${selector}`);
156
+ }
157
+
158
+ const screenshot_path = output_path || path.join(process.cwd(), 'element_screenshot.png');
159
+ await element.screenshot({ path: screenshot_path });
160
+
161
+ const stats = fs.statSync(screenshot_path);
162
+ const box = await element.boundingBox();
163
+
164
+ return {
165
+ path: screenshot_path,
166
+ width: Math.round(box.width),
167
+ height: Math.round(box.height),
168
+ size_bytes: stats.size
169
+ };
170
+ } finally {
171
+ await page.close();
172
+ }
173
+ };
174
+
175
+ /**
176
+ * Capture multiple screenshots from a single URL — full page + individual sections.
177
+ * @param {string} url - URL to screenshot
178
+ * @param {string} output_dir - Directory to save screenshots
179
+ * @param {Object} [options]
180
+ * @param {string[]} [options.section_selectors] - CSS selectors for individual section captures
181
+ * @param {string[]} [options.section_names] - Names for screenshot files (parallel to selectors)
182
+ * @param {number} [options.width=1280]
183
+ * @param {number} [options.height=720]
184
+ * @param {number} [options.wait_ms=1000]
185
+ * @returns {Promise<Array<{name: string, path: string, width: number, height: number, size_bytes: number}>>}
186
+ */
187
+ const capture_url_screenshots = async (url, output_dir, options = {}) => {
188
+ const {
189
+ section_selectors = [],
190
+ section_names = [],
191
+ width = 1280,
192
+ height = 720,
193
+ wait_ms = 1000
194
+ } = options;
195
+
196
+ fs.mkdirSync(output_dir, { recursive: true });
197
+
198
+ const results = [];
199
+
200
+ // Full page capture
201
+ const full_result = await capture_page(url, {
202
+ output_path: path.join(output_dir, 'full_page.png'),
203
+ full_page: true,
204
+ width,
205
+ height,
206
+ wait_ms
207
+ });
208
+ results.push({ name: 'full_page', ...full_result });
209
+
210
+ // Section captures
211
+ for (let i = 0; i < section_selectors.length; i++) {
212
+ const selector = section_selectors[i];
213
+ const name = section_names[i] || `section_${i + 1}`;
214
+
215
+ try {
216
+ const section_result = await capture_element(url, selector, {
217
+ output_path: path.join(output_dir, `${name}.png`),
218
+ width,
219
+ height,
220
+ wait_ms: 500
221
+ });
222
+ results.push({ name, ...section_result });
223
+ } catch (err) {
224
+ console.warn(` ⚠ Failed to capture "${name}" (${selector}): ${err.message}`);
225
+ results.push({ name, error: err.message });
226
+ }
227
+ }
228
+
229
+ return results;
230
+ };
231
+
232
+ /**
233
+ * Close the Puppeteer browser.
234
+ */
235
+ const close_browser = async () => {
236
+ if (browser) {
237
+ await browser.close();
238
+ browser = null;
239
+ }
240
+ };
241
+
242
+ module.exports = {
243
+ launch_browser,
244
+ capture_page,
245
+ capture_element,
246
+ capture_url_screenshots,
247
+ close_browser
248
+ };
package/module.js CHANGED
@@ -16,10 +16,17 @@ jsgui.controls.Active_HTML_Document = require('./controls/Active_HTML_Document')
16
16
 
17
17
  //const Resource_Publisher = require('./publishing/http-resource-publisher');
18
18
  //jsgui.Resource_Publisher = Resource_Publisher;
19
- const Server = require('./server');
20
- jsgui.Server = Server;
21
- jsgui.serve = Server.serve;
22
- jsgui.fs2 = require('./fs2');
19
+ const Server = require('./server');
20
+ jsgui.Server = Server;
21
+ jsgui.serve = Server.serve;
22
+ jsgui.Process_Resource = Server.Process_Resource;
23
+ jsgui.Remote_Process_Resource = Server.Remote_Process_Resource;
24
+ jsgui.HTTP_SSE_Publisher = Server.HTTP_SSE_Publisher;
25
+ if (jsgui.Resource) {
26
+ jsgui.Resource.Process = Server.Process_Resource;
27
+ jsgui.Resource.Remote_Process = Server.Remote_Process_Resource;
28
+ }
29
+ jsgui.fs2 = require('./fs2');
23
30
 
24
31
  // Port utilities for auto-port selection
25
32
  const port_utils = require('./port-utils');
package/package.json CHANGED
@@ -43,7 +43,7 @@
43
43
  "type": "git",
44
44
  "url": "https://github.com/metabench/jsgui3-server.git"
45
45
  },
46
- "version": "0.0.148",
46
+ "version": "0.0.149",
47
47
  "scripts": {
48
48
  "cli": "node cli.js",
49
49
  "serve": "node cli.js serve",
@@ -53,13 +53,23 @@
53
53
  "test:assigners": "node tests/test-runner.js --test=assigners.test.js",
54
54
  "test:publishers": "node tests/test-runner.js --test=publishers.test.js",
55
55
  "test:config": "node tests/test-runner.js --test=configuration-validation.test.js",
56
+ "test:admin-ui": "node tests/test-runner.js --test=admin-ui-render.test.js",
57
+ "test:serve": "node tests/test-runner.js --test=serve.test.js",
58
+ "test:serve:resources": "node tests/test-runner.js --test=serve-resources.test.js",
59
+ "test:resources:process": "node tests/test-runner.js --test=process-resource.test.js",
60
+ "test:resources:remote": "node tests/test-runner.js --test=remote-process-resource.test.js",
61
+ "test:resources:pool": "node tests/test-runner.js --test=server-resource-pool.test.js",
62
+ "test:sse": "node tests/test-runner.js --test=http-sse-publisher.test.js",
56
63
  "test:e2e": "node tests/test-runner.js --test=end-to-end.test.js",
57
64
  "test:content": "node tests/test-runner.js --test=content-analysis.test.js",
65
+ "test:bundler:cache": "node tests/test-runner.js --test=control-optimizer-cache-behavior.test.js",
58
66
  "test:performance": "node tests/test-runner.js --test=performance.test.js",
59
67
  "test:errors": "node tests/test-runner.js --test=error-handling.test.js",
60
68
  "test:examples:controls": "node tests/test-runner.js --test=examples-controls.e2e.test.js",
69
+ "test:puppeteer:bundling": "node tests/test-runner.js --test=bundling-default-control-elimination.puppeteer.test.js",
61
70
  "test:puppeteer:windows": "node tests/test-runner.js --test=window-examples.puppeteer.test.js",
71
+ "test:puppeteer:resources": "node tests/test-runner.js --test=window-resource-integration.puppeteer.test.js",
62
72
  "test:debug": "node tests/test-runner.js --debug",
63
73
  "test:verbose": "node tests/test-runner.js --verbose"
64
74
  }
65
- }
75
+ }
@@ -7,10 +7,11 @@ const Publishers = {
7
7
  'html': require('./http-html-publisher'),
8
8
  'jpeg': require('./http-jpeg-publisher'),
9
9
  'js': require('./http-js-publisher'),
10
- 'observable': require('./http-observable-publisher'),
11
- 'png': require('./http-png-publisher'),
10
+ 'observable': require('./http-observable-publisher'),
11
+ 'sse': require('./http-sse-publisher'),
12
+ 'png': require('./http-png-publisher'),
12
13
  'resource': require('./http-resource-publisher'),
13
14
  'svg': require('./http-svg-publisher')
14
15
  }
15
16
 
16
- module.exports = Publishers;
17
+ module.exports = Publishers;
@@ -65,11 +65,11 @@ class Single_Control_Webpage_Server_Static_Compressed_Response_Buffers_Assigner
65
65
 
66
66
  async assign(arr_bundled_items) {
67
67
  // Get compression configuration with defaults
68
- const enabled = this.compression_config.enabled !== false; // Default: true
69
- const algorithms = this.compression_config.algorithms || ['gzip', 'br'];
70
- const gzipLevel = this.compression_config.gzip?.level || 6;
71
- const brotliQuality = this.compression_config.brotli?.quality || 6;
72
- const threshold = this.compression_config.threshold || 1024;
68
+ const enabled = this.compression_config.enabled !== false; // Default: true
69
+ const algorithms = this.compression_config.algorithms || ['gzip', 'br'];
70
+ const gzipLevel = this.compression_config.gzip?.level ?? 6;
71
+ const brotliQuality = this.compression_config.brotli?.quality ?? 6;
72
+ const threshold = this.compression_config.threshold ?? 1024;
73
73
 
74
74
  if (!enabled) {
75
75
  console.log('Compression disabled, skipping compression assignment');
@@ -0,0 +1,341 @@
1
+ const HTTP_Publisher = require('./http-publisher');
2
+
3
+ const default_keepalive_interval_ms = 15000;
4
+ const default_event_history_size = 200;
5
+
6
+ const to_data_string = (data_value) => {
7
+ if (data_value === undefined) {
8
+ return '';
9
+ }
10
+ if (typeof data_value === 'string') {
11
+ return data_value;
12
+ }
13
+ try {
14
+ return JSON.stringify(data_value);
15
+ } catch (error) {
16
+ return JSON.stringify({
17
+ error: 'non_serializable',
18
+ message: error instanceof Error ? error.message : String(error)
19
+ });
20
+ }
21
+ };
22
+
23
+ class HTTP_SSE_Publisher extends HTTP_Publisher {
24
+ constructor(spec = {}) {
25
+ super(spec);
26
+
27
+ this.name = spec.name || this.name || 'events';
28
+ this.keepalive_interval_ms = Number.isFinite(spec.keepaliveIntervalMs)
29
+ ? Number(spec.keepaliveIntervalMs)
30
+ : default_keepalive_interval_ms;
31
+ this.max_clients = Number.isFinite(spec.maxClients)
32
+ ? Number(spec.maxClients)
33
+ : Number.POSITIVE_INFINITY;
34
+ this.event_history_size = Number.isInteger(spec.eventHistorySize)
35
+ ? spec.eventHistorySize
36
+ : default_event_history_size;
37
+
38
+ this.clients = new Map();
39
+ this.event_history = [];
40
+ this.next_event_id = 0;
41
+
42
+ this._keepalive_timer = null;
43
+ }
44
+
45
+ get client_count() {
46
+ return this.clients.size;
47
+ }
48
+
49
+ _generate_client_id() {
50
+ return `sse_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
51
+ }
52
+
53
+ _parse_connection_url(req) {
54
+ const host_header = req.headers.host || 'localhost';
55
+ return new URL(req.url || '/', `http://${host_header}`);
56
+ }
57
+
58
+ _extract_last_event_id(req, parsed_url) {
59
+ const query_value = parsed_url.searchParams.get('lastEventId')
60
+ || parsed_url.searchParams.get('last_event_id');
61
+ const header_value = req.headers['last-event-id'];
62
+ const raw_value = query_value || header_value;
63
+
64
+ if (raw_value === undefined || raw_value === null || raw_value === '') {
65
+ return null;
66
+ }
67
+
68
+ const parsed_value = Number.parseInt(String(raw_value), 10);
69
+ if (!Number.isFinite(parsed_value)) {
70
+ return null;
71
+ }
72
+ return parsed_value;
73
+ }
74
+
75
+ _ensure_keepalive_timer() {
76
+ if (this._keepalive_timer || !Number.isFinite(this.keepalive_interval_ms) || this.keepalive_interval_ms <= 0) {
77
+ return;
78
+ }
79
+
80
+ this._keepalive_timer = setInterval(() => {
81
+ this._broadcast_comment('keepalive');
82
+ }, this.keepalive_interval_ms);
83
+ this._keepalive_timer.unref?.();
84
+ }
85
+
86
+ _maybe_stop_keepalive_timer() {
87
+ if (this._keepalive_timer && this.clients.size === 0) {
88
+ clearInterval(this._keepalive_timer);
89
+ this._keepalive_timer = null;
90
+ }
91
+ }
92
+
93
+ _write_to_client(client_id, payload_text) {
94
+ const client_entry = this.clients.get(client_id);
95
+ if (!client_entry) {
96
+ return false;
97
+ }
98
+
99
+ const { res } = client_entry;
100
+ if (!res || res.destroyed || res.writableEnded) {
101
+ this._disconnect_client(client_id, 'write_failed');
102
+ return false;
103
+ }
104
+
105
+ try {
106
+ res.write(payload_text);
107
+ return true;
108
+ } catch {
109
+ this._disconnect_client(client_id, 'write_failed');
110
+ return false;
111
+ }
112
+ }
113
+
114
+ _format_event_payload(event_name, data_value, event_id) {
115
+ const payload_lines = [];
116
+ if (event_id !== undefined && event_id !== null) {
117
+ payload_lines.push(`id: ${event_id}`);
118
+ }
119
+
120
+ if (event_name) {
121
+ payload_lines.push(`event: ${event_name}`);
122
+ }
123
+
124
+ const data_text = to_data_string(data_value);
125
+ const data_lines = data_text.split(/\r?\n/);
126
+ if (data_lines.length === 0) {
127
+ payload_lines.push('data:');
128
+ } else {
129
+ for (const line of data_lines) {
130
+ payload_lines.push(`data: ${line}`);
131
+ }
132
+ }
133
+
134
+ payload_lines.push('');
135
+ payload_lines.push('');
136
+ return payload_lines.join('\n');
137
+ }
138
+
139
+ _record_event(event_record) {
140
+ this.event_history.push(event_record);
141
+ while (this.event_history.length > this.event_history_size) {
142
+ this.event_history.shift();
143
+ }
144
+ }
145
+
146
+ _broadcast_comment(comment_text) {
147
+ const payload = `:${comment_text}\n\n`;
148
+ const client_ids = Array.from(this.clients.keys());
149
+ for (const client_id of client_ids) {
150
+ this._write_to_client(client_id, payload);
151
+ }
152
+ }
153
+
154
+ _send_event_to_clients(client_ids, event_name, data_value) {
155
+ const event_id = ++this.next_event_id;
156
+ const payload_text = this._format_event_payload(event_name, data_value, event_id);
157
+
158
+ const delivered_client_ids = [];
159
+ for (const client_id of client_ids) {
160
+ const did_write = this._write_to_client(client_id, payload_text);
161
+ if (did_write) {
162
+ const client_entry = this.clients.get(client_id);
163
+ if (client_entry) {
164
+ client_entry.last_event_id = event_id;
165
+ }
166
+ delivered_client_ids.push(client_id);
167
+ }
168
+ }
169
+
170
+ this._record_event({
171
+ id: event_id,
172
+ event: event_name,
173
+ data: data_value,
174
+ timestamp: Date.now()
175
+ });
176
+
177
+ return {
178
+ eventId: event_id,
179
+ deliveredClientIds: delivered_client_ids
180
+ };
181
+ }
182
+
183
+ _replay_events_since(client_id, last_event_id) {
184
+ if (last_event_id === null) {
185
+ return;
186
+ }
187
+
188
+ const replay_candidates = this.event_history.filter((entry) => entry.id > last_event_id);
189
+ for (const replay_event of replay_candidates) {
190
+ const payload_text = this._format_event_payload(replay_event.event, replay_event.data, replay_event.id);
191
+ const did_write = this._write_to_client(client_id, payload_text);
192
+ if (!did_write) {
193
+ break;
194
+ }
195
+ }
196
+ }
197
+
198
+ _disconnect_client(client_id, reason = 'disconnect') {
199
+ const client_entry = this.clients.get(client_id);
200
+ if (!client_entry) {
201
+ return;
202
+ }
203
+
204
+ this.clients.delete(client_id);
205
+ const { req, res } = client_entry;
206
+
207
+ if (req) {
208
+ req.removeAllListeners('close');
209
+ }
210
+
211
+ try {
212
+ if (res && !res.writableEnded) {
213
+ res.end();
214
+ }
215
+ } catch {
216
+ // Ignore disconnect write errors.
217
+ }
218
+
219
+ this.raise('client_disconnected', {
220
+ clientId: client_id,
221
+ reason,
222
+ timestamp: Date.now(),
223
+ connectedClients: this.client_count
224
+ });
225
+
226
+ this._maybe_stop_keepalive_timer();
227
+ }
228
+
229
+ handle_http(req, res) {
230
+ const request_method = String(req.method || 'GET').toUpperCase();
231
+ if (request_method !== 'GET') {
232
+ res.writeHead(405, {
233
+ 'Content-Type': 'application/json'
234
+ });
235
+ res.end(JSON.stringify({
236
+ ok: false,
237
+ error: 'Method Not Allowed'
238
+ }));
239
+ return;
240
+ }
241
+
242
+ if (this.client_count >= this.max_clients) {
243
+ res.writeHead(503, {
244
+ 'Content-Type': 'application/json'
245
+ });
246
+ res.end(JSON.stringify({
247
+ ok: false,
248
+ error: 'SSE client limit reached'
249
+ }));
250
+ return;
251
+ }
252
+
253
+ const parsed_url = this._parse_connection_url(req);
254
+ const requested_client_id = parsed_url.searchParams.get('clientId') || parsed_url.searchParams.get('client_id');
255
+ const client_id = requested_client_id || this._generate_client_id();
256
+ const last_event_id = this._extract_last_event_id(req, parsed_url);
257
+
258
+ if (this.clients.has(client_id)) {
259
+ this._disconnect_client(client_id, 'replaced');
260
+ }
261
+
262
+ res.writeHead(200, {
263
+ 'Content-Type': 'text/event-stream',
264
+ 'Cache-Control': 'no-cache',
265
+ 'Connection': 'keep-alive',
266
+ 'Transfer-Encoding': 'chunked',
267
+ 'X-Accel-Buffering': 'no'
268
+ });
269
+
270
+ res.write(`retry: 3000\n`);
271
+ res.write(':connected\n\n');
272
+
273
+ this.clients.set(client_id, {
274
+ client_id,
275
+ req,
276
+ res,
277
+ connected_at: Date.now(),
278
+ last_event_id
279
+ });
280
+
281
+ this._ensure_keepalive_timer();
282
+
283
+ this.raise('client_connected', {
284
+ clientId: client_id,
285
+ lastEventId: last_event_id,
286
+ timestamp: Date.now(),
287
+ connectedClients: this.client_count
288
+ });
289
+
290
+ this._replay_events_since(client_id, last_event_id);
291
+
292
+ const disconnect_handler = () => {
293
+ this._disconnect_client(client_id, 'closed');
294
+ };
295
+
296
+ req.on('close', disconnect_handler);
297
+ res.on('close', disconnect_handler);
298
+ res.on('finish', disconnect_handler);
299
+ }
300
+
301
+ broadcast(event_name, data_value) {
302
+ const target_client_ids = Array.from(this.clients.keys());
303
+ return this._send_event_to_clients(target_client_ids, event_name, data_value);
304
+ }
305
+
306
+ send(client_id, event_name, data_value) {
307
+ if (!this.clients.has(client_id)) {
308
+ return {
309
+ eventId: null,
310
+ deliveredClientIds: []
311
+ };
312
+ }
313
+
314
+ return this._send_event_to_clients([client_id], event_name, data_value);
315
+ }
316
+
317
+ stop(callback) {
318
+ const stop_promise = new Promise((resolve) => {
319
+ if (this._keepalive_timer) {
320
+ clearInterval(this._keepalive_timer);
321
+ this._keepalive_timer = null;
322
+ }
323
+
324
+ const connected_client_ids = Array.from(this.clients.keys());
325
+ for (const client_id of connected_client_ids) {
326
+ this._disconnect_client(client_id, 'stopped');
327
+ }
328
+
329
+ resolve(true);
330
+ });
331
+
332
+ if (typeof callback === 'function') {
333
+ stop_promise.then(() => callback(null, true), callback);
334
+ return;
335
+ }
336
+
337
+ return stop_promise;
338
+ }
339
+ }
340
+
341
+ module.exports = HTTP_SSE_Publisher;