jsgui3-server 0.0.151 → 0.0.152

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 (77) hide show
  1. package/README.md +21 -0
  2. package/admin-ui/v1/controls/admin_shell.js +33 -0
  3. package/admin-ui/v1/server.js +14 -1
  4. package/docs/api-reference.md +120 -2
  5. package/docs/books/website-design/01-introduction.md +73 -0
  6. package/docs/books/website-design/02-current-state.md +195 -0
  7. package/docs/books/website-design/03-base-class.md +181 -0
  8. package/docs/books/website-design/04-webpage.md +307 -0
  9. package/docs/books/website-design/05-website.md +456 -0
  10. package/docs/books/website-design/06-pages-storage.md +170 -0
  11. package/docs/books/website-design/07-api-layer.md +285 -0
  12. package/docs/books/website-design/08-server-integration.md +271 -0
  13. package/docs/books/website-design/09-cross-agent-review.md +190 -0
  14. package/docs/books/website-design/10-open-questions.md +196 -0
  15. package/docs/books/website-design/11-converged-recommendation.md +205 -0
  16. package/docs/books/website-design/12-content-model.md +395 -0
  17. package/docs/books/website-design/13-webpage-module-spec.md +404 -0
  18. package/docs/books/website-design/14-website-module-spec.md +541 -0
  19. package/docs/books/website-design/15-multi-repo-plan.md +275 -0
  20. package/docs/books/website-design/16-minimal-first.md +203 -0
  21. package/docs/books/website-design/17-implementation-report-codex.md +81 -0
  22. package/docs/books/website-design/README.md +43 -0
  23. package/docs/configuration-reference.md +54 -0
  24. package/docs/proposals/jsgui3-website-and-webpage-design-jsgui3-server-support.md +257 -0
  25. package/docs/proposals/jsgui3-website-and-webpage-design-review.md +73 -0
  26. package/docs/proposals/jsgui3-website-and-webpage-design.md +732 -0
  27. package/docs/swagger.md +316 -0
  28. package/examples/controls/1) window/server.js +6 -1
  29. package/examples/controls/21) mvvm and declarative api/check.js +94 -0
  30. package/examples/controls/21) mvvm and declarative api/check_output.txt +25 -0
  31. package/examples/controls/21) mvvm and declarative api/check_output_2.txt +27 -0
  32. package/examples/controls/21) mvvm and declarative api/client.js +241 -0
  33. declarative api/e2e-screenshot-1-name-change.png +0 -0
  34. declarative api/e2e-screenshot-2-toggled.png +0 -0
  35. declarative api/e2e-screenshot-3-final.png +0 -0
  36. declarative api/e2e-screenshot-final.png +0 -0
  37. package/examples/controls/21) mvvm and declarative api/e2e-test.js +175 -0
  38. package/examples/controls/21) mvvm and declarative api/out.html +1 -0
  39. package/examples/controls/21) mvvm and declarative api/page_out.html +1 -0
  40. package/examples/controls/21) mvvm and declarative api/server.js +18 -0
  41. package/examples/data-views/01) query-endpoint/server.js +61 -0
  42. package/labs/website-design/001-base-class-overhead/check.js +162 -0
  43. package/labs/website-design/002-pages-storage/check.js +244 -0
  44. package/labs/website-design/002-pages-storage/results.txt +0 -0
  45. package/labs/website-design/003-type-detection/check.js +193 -0
  46. package/labs/website-design/003-type-detection/results.txt +0 -0
  47. package/labs/website-design/004-two-stage-validation/check.js +314 -0
  48. package/labs/website-design/004-two-stage-validation/results.txt +0 -0
  49. package/labs/website-design/005-normalize-input/check.js +303 -0
  50. package/labs/website-design/006-serve-website-spike/check.js +290 -0
  51. package/labs/website-design/README.md +34 -0
  52. package/labs/website-design/manifest.json +68 -0
  53. package/labs/website-design/run-all.js +60 -0
  54. package/middleware/json-body.js +126 -0
  55. package/openapi.js +474 -0
  56. package/package.json +11 -8
  57. package/publishers/Publishers.js +6 -5
  58. package/publishers/http-function-publisher.js +135 -126
  59. package/publishers/http-webpage-publisher.js +89 -11
  60. package/publishers/query-publisher.js +116 -0
  61. package/publishers/swagger-publisher.js +203 -0
  62. package/publishers/swagger-ui.js +578 -0
  63. package/resources/adapters/array-adapter.js +143 -0
  64. package/resources/query-resource.js +131 -0
  65. package/serve-factory.js +728 -18
  66. package/server.js +421 -103
  67. package/tests/README.md +23 -1
  68. package/tests/admin-ui-jsgui-controls.test.js +16 -1
  69. package/tests/helpers/playwright-e2e-harness.js +326 -0
  70. package/tests/openapi.test.js +319 -0
  71. package/tests/playwright-smoke.test.js +134 -0
  72. package/tests/publish-enhancements.test.js +673 -0
  73. package/tests/query-publisher.test.js +430 -0
  74. package/tests/quick-json-body-test.js +169 -0
  75. package/tests/serve.test.js +425 -122
  76. package/tests/swagger-publisher.test.js +1076 -0
  77. package/tests/test-runner.js +1 -0
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Lab 001: Base Class Overhead
3
+ * Book Chapter: 03-base-class
4
+ *
5
+ * Question: Does Evented_Class add meaningful overhead vs. a plain class?
6
+ *
7
+ * Run: node labs/website-design/001-base-class-overhead/check.js
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ let pass = 0, fail = 0;
13
+ function check(label, ok) {
14
+ if (ok) { console.log(` ✅ ${label}`); pass++; }
15
+ else { console.log(` ❌ ${label}`); fail++; }
16
+ }
17
+
18
+ // ── Option A: Plain class ──────────────────────────────
19
+ class Plain_Webpage {
20
+ constructor(spec = {}) {
21
+ this.name = spec.name;
22
+ this.title = spec.title;
23
+ this.path = spec.path;
24
+ this.content = spec.content;
25
+ this.meta = spec.meta || {};
26
+ }
27
+ toJSON() {
28
+ return { name: this.name, title: this.title, path: this.path, meta: this.meta };
29
+ }
30
+ }
31
+
32
+ // ── Option B: Evented_Class ────────────────────────────
33
+ const { Evented_Class } = require('jsgui3-html');
34
+
35
+ class Evented_Webpage extends Evented_Class {
36
+ constructor(spec = {}) {
37
+ super();
38
+ this.name = spec.name;
39
+ this.title = spec.title;
40
+ this.path = spec.path;
41
+ this.content = spec.content;
42
+ this.meta = spec.meta || {};
43
+ }
44
+ toJSON() {
45
+ return { name: this.name, title: this.title, path: this.path, meta: this.meta };
46
+ }
47
+ }
48
+
49
+ const N = 10000;
50
+
51
+ // ── TEST 1: Construction time ──────────────────────────
52
+ console.log('\n═══ 001: Base Class Overhead ═══\n');
53
+ console.log('TEST 1: Construction time (' + N + ' instances)');
54
+ console.log('────────────────────────────────');
55
+
56
+ const spec = { name: 'Home', title: 'Home Page', path: '/', meta: { description: 'test' } };
57
+
58
+ // warmup
59
+ for (let i = 0; i < 100; i++) { new Plain_Webpage(spec); new Evented_Webpage(spec); }
60
+
61
+ const t1 = process.hrtime.bigint();
62
+ for (let i = 0; i < N; i++) new Plain_Webpage(spec);
63
+ const t2 = process.hrtime.bigint();
64
+ for (let i = 0; i < N; i++) new Evented_Webpage(spec);
65
+ const t3 = process.hrtime.bigint();
66
+
67
+ const plainMs = Number(t2 - t1) / 1e6;
68
+ const eventMs = Number(t3 - t2) / 1e6;
69
+ const ratio = eventMs / plainMs;
70
+
71
+ console.log(` Plain class: ${plainMs.toFixed(2)} ms`);
72
+ console.log(` Evented_Class: ${eventMs.toFixed(2)} ms`);
73
+ console.log(` Ratio: ${ratio.toFixed(1)}x`);
74
+
75
+ // Even 10x is fine if absolute time is <50ms for 10k objects
76
+ check('Evented_Class < 50ms for 10k constructions', eventMs < 50);
77
+ check('Ratio < 20x (acceptable overhead)', ratio < 20);
78
+
79
+ // ── TEST 2: Memory per instance ────────────────────────
80
+ console.log('\nTEST 2: Memory per instance');
81
+ console.log('────────────────────────────────');
82
+
83
+ global.gc && global.gc();
84
+ const mem0 = process.memoryUsage().heapUsed;
85
+ const plainArr = [];
86
+ for (let i = 0; i < N; i++) plainArr.push(new Plain_Webpage(spec));
87
+ const mem1 = process.memoryUsage().heapUsed;
88
+ const eventArr = [];
89
+ for (let i = 0; i < N; i++) eventArr.push(new Evented_Webpage(spec));
90
+ const mem2 = process.memoryUsage().heapUsed;
91
+
92
+ const plainBytes = (mem1 - mem0) / N;
93
+ const eventBytes = (mem2 - mem1) / N;
94
+
95
+ console.log(` Plain class: ~${Math.round(plainBytes)} bytes/instance`);
96
+ console.log(` Evented_Class: ~${Math.round(eventBytes)} bytes/instance`);
97
+ console.log(` Delta: ~${Math.round(eventBytes - plainBytes)} bytes extra`);
98
+
99
+ // For page objects (dozens, not millions), even 1KB extra is fine
100
+ check('Evented_Class < 1KB per instance', eventBytes < 1024);
101
+ check('Delta < 500 bytes extra per instance', (eventBytes - plainBytes) < 500);
102
+
103
+ // Keep references alive to prevent GC
104
+ plainArr.length; eventArr.length;
105
+
106
+ // ── TEST 3: Event capability ───────────────────────────
107
+ console.log('\nTEST 3: Event capability (what you gain)');
108
+ console.log('────────────────────────────────');
109
+
110
+ const ew = new Evented_Webpage({ path: '/test' });
111
+ let eventCount = 0;
112
+
113
+ ew.on('finalized', () => eventCount++);
114
+ ew.raise('finalized');
115
+
116
+ check('Events fire correctly', eventCount === 1);
117
+
118
+ // Can we add lifecycle events?
119
+ let lifecycleLog = [];
120
+ ew.on('property-changed', (e) => lifecycleLog.push(e));
121
+ ew.raise('property-changed', { name: 'title', old: 'Old', value: 'New' });
122
+
123
+ check('Custom events carry data', lifecycleLog.length === 1 && lifecycleLog[0].name === 'title');
124
+
125
+ // Plain class can't do this without adding an EventEmitter
126
+ const pw = new Plain_Webpage({ path: '/test' });
127
+ check('Plain class has no .on()', typeof pw.on === 'undefined');
128
+ check('Plain class has no .raise()', typeof pw.raise === 'undefined');
129
+
130
+ // ── TEST 4: toJSON parity ──────────────────────────────
131
+ console.log('\nTEST 4: toJSON parity');
132
+ console.log('────────────────────────────────');
133
+
134
+ const pj = new Plain_Webpage(spec).toJSON();
135
+ const ej = new Evented_Webpage(spec).toJSON();
136
+
137
+ check('toJSON output is identical', JSON.stringify(pj) === JSON.stringify(ej));
138
+
139
+ // Does Evented_Class add hidden enumerable properties that leak into serialization?
140
+ const eKeys = Object.keys(new Evented_Webpage(spec));
141
+ const pKeys = Object.keys(new Plain_Webpage(spec));
142
+ const extraKeys = eKeys.filter(k => !pKeys.includes(k));
143
+ console.log(` Plain keys: [${pKeys.join(', ')}]`);
144
+ console.log(` Evented keys: [${eKeys.join(', ')}]`);
145
+ console.log(` Extra keys: [${extraKeys.join(', ')}]`);
146
+
147
+ check('No unexpected enumerable properties leaked', extraKeys.length === 0 || extraKeys.every(k => k.startsWith('_')));
148
+
149
+ // ── VERDICT ────────────────────────────────────────────
150
+ console.log('\n═══════════════════════════════════════════');
151
+ console.log(`RESULTS: ${pass} passed, ${fail} failed`);
152
+
153
+ if (fail === 0) {
154
+ console.log('\n✅ VERDICT: Evented_Class adds acceptable overhead');
155
+ console.log(' and provides event capability for free.');
156
+ console.log(' → Recommend Evented_Class as base (Ch.3 confirmed)');
157
+ } else {
158
+ console.log('\n⚠️ VERDICT: Some checks failed — review results above');
159
+ }
160
+ console.log('═══════════════════════════════════════════\n');
161
+
162
+ process.exit(fail > 0 ? 1 : 0);
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Lab 002: Pages Storage Comparison
3
+ * Book Chapter: 06-pages-storage
4
+ *
5
+ * Question: Array vs Map vs Array+Index — which is best for page storage?
6
+ *
7
+ * Run: node labs/website-design/002-pages-storage/check.js
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ let pass = 0, fail = 0;
13
+ function check(label, ok) {
14
+ if (ok) { console.log(` ✅ ${label}`); pass++; }
15
+ else { console.log(` ❌ ${label}`); fail++; }
16
+ }
17
+
18
+ // ── Simulated page objects ─────────────────────────────
19
+ function makePage(path, name) {
20
+ return { path, name, title: name, content: function () { }, meta: {} };
21
+ }
22
+
23
+ const samplePages = [
24
+ makePage('/', 'Home'),
25
+ makePage('/about', 'About'),
26
+ makePage('/contact', 'Contact'),
27
+ makePage('/blog', 'Blog'),
28
+ makePage('/blog/post-1', 'Post 1'),
29
+ makePage('/docs', 'Documentation'),
30
+ makePage('/docs/api', 'API Reference'),
31
+ makePage('/pricing', 'Pricing'),
32
+ makePage('/login', 'Login'),
33
+ makePage('/signup', 'Sign Up'),
34
+ ];
35
+
36
+ console.log('\n═══ 002: Pages Storage Comparison ═══\n');
37
+
38
+ // ── APPROACH A: Plain Array ────────────────────────────
39
+ class ArrayStore {
40
+ constructor() { this._pages = []; }
41
+ add(page) { this._pages.push(page); }
42
+ get(path) { return this._pages.find(p => p.path === path); }
43
+ has(path) { return this._pages.some(p => p.path === path); }
44
+ remove(path) {
45
+ const idx = this._pages.findIndex(p => p.path === path);
46
+ if (idx >= 0) this._pages.splice(idx, 1);
47
+ }
48
+ get routes() { return this._pages.map(p => p.path); }
49
+ get size() { return this._pages.length; }
50
+ [Symbol.iterator]() { return this._pages[Symbol.iterator](); }
51
+ toJSON() { return this._pages.map(p => ({ path: p.path, name: p.name })); }
52
+ }
53
+
54
+ // ── APPROACH B: Map ────────────────────────────────────
55
+ class MapStore {
56
+ constructor() { this._pages = new Map(); }
57
+ add(page) {
58
+ if (this._pages.has(page.path)) {
59
+ throw new Error(`Duplicate page path: ${page.path}`);
60
+ }
61
+ this._pages.set(page.path, page);
62
+ }
63
+ get(path) { return this._pages.get(path); }
64
+ has(path) { return this._pages.has(path); }
65
+ remove(path) { this._pages.delete(path); }
66
+ get routes() { return [...this._pages.keys()]; }
67
+ get size() { return this._pages.size; }
68
+ [Symbol.iterator]() { return this._pages.values(); }
69
+ toJSON() { return [...this._pages.values()].map(p => ({ path: p.path, name: p.name })); }
70
+ }
71
+
72
+ // ── APPROACH C: Array + Index ──────────────────────────
73
+ class IndexedArrayStore {
74
+ constructor() { this._pages = []; this._index = {}; }
75
+ add(page) {
76
+ if (this._index[page.path] !== undefined) {
77
+ throw new Error(`Duplicate page path: ${page.path}`);
78
+ }
79
+ this._index[page.path] = this._pages.length;
80
+ this._pages.push(page);
81
+ }
82
+ get(path) {
83
+ const idx = this._index[path];
84
+ return idx !== undefined ? this._pages[idx] : undefined;
85
+ }
86
+ has(path) { return this._index[path] !== undefined; }
87
+ remove(path) {
88
+ const idx = this._index[path];
89
+ if (idx !== undefined) {
90
+ this._pages.splice(idx, 1);
91
+ delete this._index[path];
92
+ // Rebuild index after splice
93
+ for (let i = idx; i < this._pages.length; i++) {
94
+ this._index[this._pages[i].path] = i;
95
+ }
96
+ }
97
+ }
98
+ get routes() { return this._pages.map(p => p.path); }
99
+ get size() { return this._pages.length; }
100
+ [Symbol.iterator]() { return this._pages[Symbol.iterator](); }
101
+ toJSON() { return this._pages.map(p => ({ path: p.path, name: p.name })); }
102
+ }
103
+
104
+ const stores = {
105
+ 'Array': () => new ArrayStore(),
106
+ 'Map': () => new MapStore(),
107
+ 'Array+Index': () => new IndexedArrayStore(),
108
+ };
109
+
110
+ // ── TEST 1: Basic operations ───────────────────────────
111
+ console.log('TEST 1: Basic operations');
112
+ console.log('────────────────────────────────');
113
+
114
+ for (const [name, factory] of Object.entries(stores)) {
115
+ const store = factory();
116
+ samplePages.forEach(p => store.add(p));
117
+
118
+ check(`${name}: add 10 pages → size is 10`, store.size === 10);
119
+ check(`${name}: get('/about') works`, store.get('/about')?.name === 'About');
120
+ check(`${name}: has('/contact') is true`, store.has('/contact') === true);
121
+ check(`${name}: has('/missing') is false`, store.has('/missing') === false);
122
+
123
+ store.remove('/blog');
124
+ check(`${name}: remove('/blog') → size is 9`, store.size === 9);
125
+ check(`${name}: get('/blog') is undefined after remove`, store.get('/blog') === undefined);
126
+ }
127
+
128
+ // ── TEST 2: Duplicate detection ────────────────────────
129
+ console.log('\nTEST 2: Duplicate detection');
130
+ console.log('────────────────────────────────');
131
+
132
+ // Array allows duplicates (silent bug)
133
+ const arrStore = new ArrayStore();
134
+ arrStore.add(makePage('/dup', 'First'));
135
+ arrStore.add(makePage('/dup', 'Second'));
136
+ check('Array: allows duplicates (silent bug)', arrStore.size === 2);
137
+
138
+ // Map catches duplicates
139
+ const mapStore = new MapStore();
140
+ mapStore.add(makePage('/dup', 'First'));
141
+ let mapThrew = false;
142
+ try { mapStore.add(makePage('/dup', 'Second')); } catch (e) { mapThrew = true; }
143
+ check('Map: throws on duplicate', mapThrew === true);
144
+ check('Map: only first page stored', mapStore.size === 1);
145
+
146
+ // Array+Index catches duplicates
147
+ const idxStore = new IndexedArrayStore();
148
+ idxStore.add(makePage('/dup', 'First'));
149
+ let idxThrew = false;
150
+ try { idxStore.add(makePage('/dup', 'Second')); } catch (e) { idxThrew = true; }
151
+ check('Array+Index: throws on duplicate', idxThrew === true);
152
+
153
+ // ── TEST 3: Insertion order ────────────────────────────
154
+ console.log('\nTEST 3: Iteration preserves insertion order');
155
+ console.log('────────────────────────────────');
156
+
157
+ for (const [name, factory] of Object.entries(stores)) {
158
+ const store = factory();
159
+ const ordered = ['/z-last', '/a-first', '/m-middle'];
160
+ ordered.forEach(p => store.add(makePage(p, p)));
161
+
162
+ const iterated = [...store].map(p => p.path);
163
+ const orderPreserved = iterated[0] === '/z-last' && iterated[1] === '/a-first' && iterated[2] === '/m-middle';
164
+ check(`${name}: insertion order preserved`, orderPreserved);
165
+ }
166
+
167
+ // ── TEST 4: Lookup benchmark ───────────────────────────
168
+ console.log('\nTEST 4: Lookup benchmark');
169
+ console.log('────────────────────────────────');
170
+
171
+ const bigN = 1000;
172
+ const bigPages = [];
173
+ for (let i = 0; i < bigN; i++) bigPages.push(makePage(`/page-${i}`, `Page ${i}`));
174
+
175
+ const lookupTarget = `/page-${bigN - 1}`; // worst case for array
176
+ const ITERS = 100000;
177
+
178
+ for (const [name, factory] of Object.entries(stores)) {
179
+ const store = factory();
180
+ bigPages.forEach(p => store.add(p));
181
+
182
+ const start = process.hrtime.bigint();
183
+ for (let i = 0; i < ITERS; i++) store.get(lookupTarget);
184
+ const elapsed = Number(process.hrtime.bigint() - start) / 1e6;
185
+
186
+ console.log(` ${name}: ${ITERS} lookups in ${bigN} pages → ${elapsed.toFixed(2)} ms`);
187
+ }
188
+
189
+ // Map should be fastest for large N
190
+ const mapBench = stores['Map']();
191
+ const arrBench = stores['Array']();
192
+ bigPages.forEach(p => { mapBench.add(p); arrBench.add(p); });
193
+
194
+ const mapStart = process.hrtime.bigint();
195
+ for (let i = 0; i < ITERS; i++) mapBench.get(lookupTarget);
196
+ const mapElapsed = Number(process.hrtime.bigint() - mapStart) / 1e6;
197
+
198
+ const arrStart = process.hrtime.bigint();
199
+ for (let i = 0; i < ITERS; i++) arrBench.get(lookupTarget);
200
+ const arrElapsed = Number(process.hrtime.bigint() - arrStart) / 1e6;
201
+
202
+ check('Map lookup faster than Array.find() at 1000 pages', mapElapsed < arrElapsed);
203
+ console.log(` Map: ${mapElapsed.toFixed(2)} ms vs Array: ${arrElapsed.toFixed(2)} ms (${(arrElapsed / mapElapsed).toFixed(1)}x faster)`);
204
+
205
+ // ── TEST 5: toJSON output ──────────────────────────────
206
+ console.log('\nTEST 5: toJSON output');
207
+ console.log('────────────────────────────────');
208
+
209
+ for (const [name, factory] of Object.entries(stores)) {
210
+ const store = factory();
211
+ samplePages.slice(0, 3).forEach(p => store.add(p));
212
+ const json = store.toJSON();
213
+
214
+ check(`${name}: toJSON returns array`, Array.isArray(json));
215
+ check(`${name}: toJSON entries have path+name`, json[0].path === '/' && json[0].name === 'Home');
216
+ }
217
+
218
+ // ── TEST 6: Replace page ───────────────────────────────
219
+ console.log('\nTEST 6: Replace page (remove + add)');
220
+ console.log('────────────────────────────────');
221
+
222
+ for (const [name, factory] of Object.entries(stores)) {
223
+ const store = factory();
224
+ store.add(makePage('/about', 'About v1'));
225
+ store.remove('/about');
226
+ store.add(makePage('/about', 'About v2'));
227
+
228
+ check(`${name}: replace works`, store.get('/about')?.name === 'About v2');
229
+ check(`${name}: size stays 1`, store.size === 1);
230
+ }
231
+
232
+ // ── VERDICT ────────────────────────────────────────────
233
+ console.log('\n═══════════════════════════════════════════');
234
+ console.log(`RESULTS: ${pass} passed, ${fail} failed`);
235
+
236
+ console.log('\n📊 SUMMARY:');
237
+ console.log(' Array: Simple but no duplicate detection, O(n) lookup');
238
+ console.log(' Map: O(1) lookup, duplicate detection, insertion order ✓');
239
+ console.log(' Array+Index: O(1) lookup, duplicate detection, but remove is O(n) to reindex');
240
+ console.log('\n✅ VERDICT: Map is the best balance of performance and API cleanliness.');
241
+ console.log(' → Ch.6 recommendation confirmed');
242
+ console.log('═══════════════════════════════════════════\n');
243
+
244
+ process.exit(fail > 0 ? 1 : 0);
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Lab 003: Type Detection Strategies
3
+ * Book Chapter: 08-server-integration
4
+ *
5
+ * Question: instanceof vs duck typing vs Symbol.for() — which is reliable?
6
+ *
7
+ * Run: node labs/website-design/003-type-detection/check.js
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ let pass = 0, fail = 0;
13
+ function check(label, ok) {
14
+ if (ok) { console.log(` ✅ ${label}`); pass++; }
15
+ else { console.log(` ❌ ${label}`); fail++; }
16
+ }
17
+
18
+ console.log('\n═══ 003: Type Detection Strategies ═══\n');
19
+
20
+ // ── Setup: real Website/Webpage from node_modules ──────
21
+ const Website = require('jsgui3-website');
22
+ const Webpage = require('jsgui3-webpage');
23
+
24
+ const site = new Website({ name: 'Test Site' });
25
+ const page = new Webpage({ path: '/', title: 'Home' });
26
+
27
+ // ── STRATEGY A: instanceof ─────────────────────────────
28
+ console.log('TEST 1: instanceof detection');
29
+ console.log('────────────────────────────────');
30
+
31
+ check('site instanceof Website', site instanceof Website);
32
+ check('page instanceof Webpage', page instanceof Webpage);
33
+ check('plain object is NOT instanceof Website', !({} instanceof Website));
34
+
35
+ // Simulate cross-install: different constructor with same shape
36
+ function FakeWebsite(spec) { Object.assign(this, spec); }
37
+ FakeWebsite.prototype.toJSON = function () { return this; };
38
+ const fake = new FakeWebsite({ name: 'Fake', pages: new Map() });
39
+
40
+ check('FakeWebsite fails instanceof (correct rejection)', !(fake instanceof Website));
41
+
42
+ // Simulate a re-evaluation scenario (like loading same module from different path)
43
+ // This tests whether two copies of the same class share instanceof
44
+ const Website2 = Website; // same reference = same constructor
45
+ const site2 = new Website2({ name: 'Test 2' });
46
+ check('Same-reference require: instanceof works', site2 instanceof Website);
47
+
48
+ console.log(' ⚠️ Note: instanceof breaks if jsgui3-website is installed');
49
+ console.log(' in two different node_modules trees (npm link, workspaces)');
50
+
51
+ // ── STRATEGY B: Duck typing ────────────────────────────
52
+ console.log('\nTEST 2: Duck typing detection');
53
+ console.log('────────────────────────────────');
54
+
55
+ function isWebsite_duck(obj) {
56
+ return obj != null
57
+ && typeof obj === 'object'
58
+ && typeof obj.name === 'string';
59
+ // Current Website is too minimal for meaningful duck typing
60
+ // With the proposed API: check for add_page, get_page, etc.
61
+ }
62
+
63
+ function isWebsite_duck_proposed(obj) {
64
+ return obj != null
65
+ && typeof obj === 'object'
66
+ && typeof obj.add_page === 'function'
67
+ && typeof obj.get_page === 'function'
68
+ && typeof obj.toJSON === 'function';
69
+ }
70
+
71
+ function isWebpage_duck(obj) {
72
+ return obj != null
73
+ && typeof obj === 'object'
74
+ && 'path' in obj
75
+ && typeof obj.path === 'string';
76
+ }
77
+
78
+ check('Duck typing: site detected (current)', isWebsite_duck(site));
79
+ check('Duck typing: page detected', isWebpage_duck(page));
80
+
81
+ // False positive: plain object with a name
82
+ const plainObj = { name: 'Just an object' };
83
+ check('Duck typing (current): plain object FALSE POSITIVE', isWebsite_duck(plainObj));
84
+ console.log(' ⚠️ Current Website is too minimal — duck typing produces false positives');
85
+
86
+ // With proposed API (add_page, get_page), false positive is much harder
87
+ check('Duck typing (proposed): plain object correctly rejected',
88
+ !isWebsite_duck_proposed(plainObj));
89
+
90
+ // With proposed API: FakeWebsite correctly rejected
91
+ check('Duck typing (proposed): FakeWebsite correctly rejected',
92
+ !isWebsite_duck_proposed(fake));
93
+
94
+ // Simulate a correct duck-typed object (not instanceof but has the right shape)
95
+ const duckSite = {
96
+ name: 'Duck Site',
97
+ add_page(p) { this._pages = this._pages || new Map(); this._pages.set(p.path, p); },
98
+ get_page(path) { return (this._pages || new Map()).get(path); },
99
+ toJSON() { return { name: this.name }; }
100
+ };
101
+ check('Duck typing (proposed): shape-correct object accepted',
102
+ isWebsite_duck_proposed(duckSite));
103
+
104
+ // ── STRATEGY C: Symbol.for() marker ───────────────────
105
+ console.log('\nTEST 3: Symbol.for() marker');
106
+ console.log('────────────────────────────────');
107
+
108
+ const WEBSITE_MARKER = Symbol.for('jsgui3.website');
109
+ const WEBPAGE_MARKER = Symbol.for('jsgui3.webpage');
110
+
111
+ // Simulate adding markers to prototypes
112
+ class MarkedWebsite {
113
+ constructor(spec = {}) { Object.assign(this, spec); }
114
+ get [WEBSITE_MARKER]() { return true; }
115
+ }
116
+
117
+ class MarkedWebpage {
118
+ constructor(spec = {}) { Object.assign(this, spec); }
119
+ get [WEBPAGE_MARKER]() { return true; }
120
+ }
121
+
122
+ function isWebsite_symbol(obj) {
123
+ return obj != null && obj[Symbol.for('jsgui3.website')] === true;
124
+ }
125
+
126
+ function isWebpage_symbol(obj) {
127
+ return obj != null && obj[Symbol.for('jsgui3.webpage')] === true;
128
+ }
129
+
130
+ const ms = new MarkedWebsite({ name: 'Marked' });
131
+ const mp = new MarkedWebpage({ path: '/' });
132
+
133
+ check('Symbol: MarkedWebsite detected', isWebsite_symbol(ms));
134
+ check('Symbol: MarkedWebpage detected', isWebpage_symbol(mp));
135
+ check('Symbol: plain object correctly rejected', !isWebsite_symbol({}));
136
+ check('Symbol: wrong type correctly rejected', !isWebsite_symbol(mp));
137
+
138
+ // Cross-realm: Symbol.for() is global, survives across different module loads
139
+ const markerFromOtherContext = Symbol.for('jsgui3.website');
140
+ check('Symbol.for() is globally unique', markerFromOtherContext === WEBSITE_MARKER);
141
+
142
+ // ── STRATEGY D: Combined (recommended) ────────────────
143
+ console.log('\nTEST 4: Combined detection (duck + optional symbol)');
144
+ console.log('────────────────────────────────');
145
+
146
+ function detectWebsite(obj) {
147
+ if (obj == null || typeof obj !== 'object') return false;
148
+ // Fast path: symbol marker
149
+ if (obj[Symbol.for('jsgui3.website')] === true) return true;
150
+ // Fallback: duck typing on proposed API
151
+ return typeof obj.add_page === 'function'
152
+ && typeof obj.get_page === 'function'
153
+ && typeof obj.toJSON === 'function';
154
+ }
155
+
156
+ check('Combined: symbol-marked object detected', detectWebsite(ms));
157
+ check('Combined: duck-typed object detected', detectWebsite(duckSite));
158
+ check('Combined: plain object rejected', !detectWebsite({}));
159
+ check('Combined: null rejected', !detectWebsite(null));
160
+
161
+ // ── Benchmark ──────────────────────────────────────────
162
+ console.log('\nTEST 5: Detection speed comparison');
163
+ console.log('────────────────────────────────');
164
+
165
+ const ITERS = 1000000;
166
+ const target = ms; // has both symbol and duck interface
167
+
168
+ const t1 = process.hrtime.bigint();
169
+ for (let i = 0; i < ITERS; i++) target instanceof MarkedWebsite;
170
+ const t2 = process.hrtime.bigint();
171
+ for (let i = 0; i < ITERS; i++) isWebsite_symbol(target);
172
+ const t3 = process.hrtime.bigint();
173
+ for (let i = 0; i < ITERS; i++) detectWebsite(target);
174
+ const t4 = process.hrtime.bigint();
175
+
176
+ console.log(` instanceof: ${(Number(t2 - t1) / 1e6).toFixed(2)} ms (${ITERS} checks)`);
177
+ console.log(` Symbol: ${(Number(t3 - t2) / 1e6).toFixed(2)} ms`);
178
+ console.log(` Combined: ${(Number(t4 - t3) / 1e6).toFixed(2)} ms`);
179
+
180
+ // ── VERDICT ────────────────────────────────────────────
181
+ console.log('\n═══════════════════════════════════════════');
182
+ console.log(`RESULTS: ${pass} passed, ${fail} failed`);
183
+
184
+ console.log('\n📊 SUMMARY:');
185
+ console.log(' instanceof: Fast, but breaks across npm link/workspaces');
186
+ console.log(' Duck typing: Works cross-install, but current Website too minimal');
187
+ console.log(' Symbol.for: Cross-realm safe, no false positives, explicit');
188
+ console.log(' Combined: Best reliability — symbol fast path + duck fallback');
189
+ console.log('\n✅ VERDICT: Use combined detection (symbol + duck typing).');
190
+ console.log(' → Ch.8 recommendation: capability checks with optional marker');
191
+ console.log('═══════════════════════════════════════════\n');
192
+
193
+ process.exit(fail > 0 ? 1 : 0);