@flighthq/host-web 0.4.0-next.1836.147ae1e → 0.4.0-next.1884.6a2d46f
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/dist/contract.d.ts +1 -0
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -0
- package/dist/contract.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/webGlyphRasterizer.d.ts.map +1 -1
- package/dist/webGlyphRasterizer.js +14 -35
- package/dist/webGlyphRasterizer.js.map +1 -1
- package/dist/webLoop.d.ts +3 -0
- package/dist/webLoop.d.ts.map +1 -0
- package/dist/webLoop.js +38 -0
- package/dist/webLoop.js.map +1 -0
- package/package.json +4 -3
- package/src/evidence.test.ts +56 -38
- package/src/webGlyphRasterizer.test.ts +63 -15
- package/src/webLoop.test.ts +123 -0
package/dist/contract.d.ts
CHANGED
package/dist/contract.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,WAAW,CAAC"}
|
package/dist/contract.js
CHANGED
package/dist/contract.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,sBAAsB,CAAC;AACrC,cAAc,WAAW,CAAC"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,+BAA+B,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,+BAA+B,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AACrG,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC"}
|
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,+BAA+B,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,+BAA+B,EAAE,4BAA4B,EAAE,MAAM,sBAAsB,CAAC;AACrG,OAAO,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webGlyphRasterizer.d.ts","sourceRoot":"","sources":["../src/webGlyphRasterizer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,sBAAsB,EACvB,MAAM,0BAA0B,CAAC;AAElC,wBAAgB,+BAA+B,IAAI,sBAAsB,
|
|
1
|
+
{"version":3,"file":"webGlyphRasterizer.d.ts","sourceRoot":"","sources":["../src/webGlyphRasterizer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAIV,sBAAsB,EACvB,MAAM,0BAA0B,CAAC;AAElC,wBAAgB,+BAA+B,IAAI,sBAAsB,CA8BxE;AAED,wBAAgB,4BAA4B,IAAI,IAAI,CAInD;AAsED,wBAAgB,kCAAkC,IAAI,IAAI,CAEzD"}
|
|
@@ -1,36 +1,40 @@
|
|
|
1
|
-
import { installGlyphRasterizerHostBackend } from '@flighthq/glyphatlas/contract';
|
|
1
|
+
import { installGlyphRasterizerHostBackend, observeGlyphRasterizerHostResult } from '@flighthq/glyphatlas/contract';
|
|
2
2
|
export function createWebGlyphRasterizerBackend() {
|
|
3
3
|
return {
|
|
4
4
|
measureMetrics(options) {
|
|
5
5
|
const context = _acquireGlyphRasterContext();
|
|
6
|
-
if (context === null)
|
|
6
|
+
if (context === null) {
|
|
7
|
+
observeGlyphRasterizerHostResult('measureMetrics', false);
|
|
7
8
|
return null;
|
|
9
|
+
}
|
|
8
10
|
_applyGlyphRasterFont(context, options);
|
|
9
11
|
const metrics = context.measureText('Hg');
|
|
10
12
|
const ascent = metrics.fontBoundingBoxAscent;
|
|
11
13
|
const descent = metrics.fontBoundingBoxDescent;
|
|
12
|
-
if (!(ascent > 0) || !(descent >= 0))
|
|
14
|
+
if (!(ascent > 0) || !(descent >= 0)) {
|
|
15
|
+
observeGlyphRasterizerHostResult('measureMetrics', false);
|
|
13
16
|
return null;
|
|
17
|
+
}
|
|
18
|
+
observeGlyphRasterizerHostResult('measureMetrics', true);
|
|
14
19
|
return { ascent, descent, lineGap: 0 };
|
|
15
20
|
},
|
|
16
21
|
rasterize(codepoint, options) {
|
|
17
22
|
const context = _acquireGlyphRasterContext();
|
|
18
|
-
if (context === null)
|
|
23
|
+
if (context === null) {
|
|
24
|
+
observeGlyphRasterizerHostResult('rasterize', false);
|
|
19
25
|
return null;
|
|
26
|
+
}
|
|
27
|
+
// Observe on context acquisition, not on result: null is valid for zero-ink glyphs (space).
|
|
28
|
+
observeGlyphRasterizerHostResult('rasterize', true);
|
|
20
29
|
return _rasterizeGlyphOnContext(context, codepoint, options);
|
|
21
30
|
},
|
|
22
31
|
};
|
|
23
32
|
}
|
|
24
|
-
// Probes the runtime environment for a 2D canvas context. The enabler calls this once at
|
|
25
|
-
// enable time to decide viability — the backend itself re-acquires on every call, but the
|
|
26
|
-
// viability signal is locked at enable time so explain reports a stable answer.
|
|
27
33
|
export function enableHostWebGlyphRasterizer() {
|
|
28
34
|
if (_enabled)
|
|
29
35
|
return;
|
|
30
36
|
_enabled = true;
|
|
31
|
-
|
|
32
|
-
const backend = viable ? createWebGlyphRasterizerBackend() : _unavailableBackend;
|
|
33
|
-
installGlyphRasterizerHostBackend(backend, viable);
|
|
37
|
+
installGlyphRasterizerHostBackend(createWebGlyphRasterizerBackend());
|
|
34
38
|
}
|
|
35
39
|
function _acquireGlyphRasterContext() {
|
|
36
40
|
try {
|
|
@@ -55,24 +59,6 @@ function _applyGlyphRasterFont(context, options) {
|
|
|
55
59
|
const fontWeight = options.fontWeight ?? 'normal';
|
|
56
60
|
context.font = `${fontStyle} ${fontWeight} ${options.fontSize}px ${options.fontFamily}`;
|
|
57
61
|
}
|
|
58
|
-
function _probeGlyphRasterizerViability() {
|
|
59
|
-
try {
|
|
60
|
-
if (typeof OffscreenCanvas !== 'undefined') {
|
|
61
|
-
const context = new OffscreenCanvas(1, 1).getContext('2d');
|
|
62
|
-
if (context !== null)
|
|
63
|
-
return true;
|
|
64
|
-
}
|
|
65
|
-
if (typeof document !== 'undefined' && typeof document.createElement === 'function') {
|
|
66
|
-
const context = document.createElement('canvas').getContext('2d');
|
|
67
|
-
if (context !== null)
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
return false;
|
|
73
|
-
}
|
|
74
|
-
return false;
|
|
75
|
-
}
|
|
76
62
|
function _rasterizeGlyphOnContext(context, codepoint, options) {
|
|
77
63
|
const text = String.fromCodePoint(codepoint);
|
|
78
64
|
_applyGlyphRasterFont(context, options);
|
|
@@ -112,11 +98,4 @@ export function resetHostWebGlyphRasterizerForTest() {
|
|
|
112
98
|
_enabled = false;
|
|
113
99
|
}
|
|
114
100
|
let _enabled = false;
|
|
115
|
-
// Installed when the environment lacks OffscreenCanvas/document/2D-context. A real object (not
|
|
116
|
-
// null) so get returns it and explain reports layer:host + viability:runtime-api-unavailable.
|
|
117
|
-
const _unavailableBackend = {
|
|
118
|
-
rasterize() {
|
|
119
|
-
return null;
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
101
|
//# sourceMappingURL=webGlyphRasterizer.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"webGlyphRasterizer.js","sourceRoot":"","sources":["../src/webGlyphRasterizer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iCAAiC,EAAE,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"webGlyphRasterizer.js","sourceRoot":"","sources":["../src/webGlyphRasterizer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webLoop.d.ts","sourceRoot":"","sources":["../src/webLoop.ts"],"names":[],"mappings":"AAGA,wBAAgB,iBAAiB,IAAI,IAAI,CA4BxC;AAED,wBAAgB,uBAAuB,IAAI,IAAI,CAE9C"}
|
package/dist/webLoop.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createWebLoopBackend, installLoopHostBackend, observeLoopHostResult } from '@flighthq/application/contract';
|
|
2
|
+
export function enableHostWebLoop() {
|
|
3
|
+
if (_enabled)
|
|
4
|
+
return;
|
|
5
|
+
_enabled = true;
|
|
6
|
+
const inner = createWebLoopBackend();
|
|
7
|
+
const backend = {
|
|
8
|
+
requestFrame(callback) {
|
|
9
|
+
try {
|
|
10
|
+
const handle = inner.requestFrame(callback);
|
|
11
|
+
observeLoopHostResult('requestFrame', true);
|
|
12
|
+
return handle;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
observeLoopHostResult('requestFrame', false);
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
cancelFrame(handle) {
|
|
20
|
+
try {
|
|
21
|
+
inner.cancelFrame(handle);
|
|
22
|
+
observeLoopHostResult('cancelFrame', true);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
observeLoopHostResult('cancelFrame', false);
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
now() {
|
|
29
|
+
return inner.now();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
installLoopHostBackend(backend);
|
|
33
|
+
}
|
|
34
|
+
export function resetHostWebLoopForTest() {
|
|
35
|
+
_enabled = false;
|
|
36
|
+
}
|
|
37
|
+
let _enabled = false;
|
|
38
|
+
//# sourceMappingURL=webLoop.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webLoop.js","sourceRoot":"","sources":["../src/webLoop.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAC;AAGrH,MAAM,UAAU,iBAAiB;IAC/B,IAAI,QAAQ;QAAE,OAAO;IACrB,QAAQ,GAAG,IAAI,CAAC;IAChB,MAAM,KAAK,GAAG,oBAAoB,EAAE,CAAC;IACrC,MAAM,OAAO,GAAgB;QAC3B,YAAY,CAAC,QAAgC;YAC3C,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,KAAK,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;gBAC5C,qBAAqB,CAAC,cAAc,EAAE,IAAI,CAAC,CAAC;gBAC5C,OAAO,MAAM,CAAC;YAChB,CAAC;YAAC,MAAM,CAAC;gBACP,qBAAqB,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;gBAC7C,OAAO,IAAI,CAAC;YACd,CAAC;QACH,CAAC;QACD,WAAW,CAAC,MAAe;YACzB,IAAI,CAAC;gBACH,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;gBAC1B,qBAAqB,CAAC,aAAa,EAAE,IAAI,CAAC,CAAC;YAC7C,CAAC;YAAC,MAAM,CAAC;gBACP,qBAAqB,CAAC,aAAa,EAAE,KAAK,CAAC,CAAC;YAC9C,CAAC;QACH,CAAC;QACD,GAAG;YACD,OAAO,KAAK,CAAC,GAAG,EAAE,CAAC;QACrB,CAAC;KACF,CAAC;IACF,sBAAsB,CAAC,OAAO,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,uBAAuB;IACrC,QAAQ,GAAG,KAAK,CAAC;AACnB,CAAC;AAED,IAAI,QAAQ,GAAG,KAAK,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/host-web",
|
|
3
|
-
"version": "0.4.0-next.
|
|
3
|
+
"version": "0.4.0-next.1884.6a2d46f",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,8 +38,9 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/
|
|
42
|
-
"@flighthq/
|
|
41
|
+
"@flighthq/application": "0.4.0-next.1884.6a2d46f",
|
|
42
|
+
"@flighthq/glyphatlas": "0.4.0-next.1884.6a2d46f",
|
|
43
|
+
"@flighthq/types": "0.4.0-next.1884.6a2d46f"
|
|
43
44
|
},
|
|
44
45
|
"devDependencies": {
|
|
45
46
|
"typescript": "^5.3.0"
|
package/src/evidence.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'fs';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
3
|
import type { OutputChunk, RollupOutput } from 'rollup';
|
|
4
4
|
import { build } from 'vite';
|
|
@@ -7,9 +7,17 @@ import { gzipSync } from 'zlib';
|
|
|
7
7
|
|
|
8
8
|
const ROOT = resolve(__dirname, '../../..');
|
|
9
9
|
const EVIDENCE_DIR = resolve(__dirname, '../evidence');
|
|
10
|
-
const FIXTURES = ['control', 'capability', 'cursor', 'glyph', 'both'] as const;
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
interface CapabilityEntry {
|
|
12
|
+
readonly fixture: string;
|
|
13
|
+
readonly modulePattern: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const CAPABILITIES: readonly CapabilityEntry[] = [
|
|
17
|
+
{ fixture: 'cursor', modulePattern: 'webCursor' },
|
|
18
|
+
{ fixture: 'glyph', modulePattern: 'webGlyphRasterizer' },
|
|
19
|
+
{ fixture: 'loop', modulePattern: 'webLoop' },
|
|
20
|
+
];
|
|
13
21
|
|
|
14
22
|
interface FixtureResult {
|
|
15
23
|
modules: string[];
|
|
@@ -21,7 +29,7 @@ interface FixtureResult {
|
|
|
21
29
|
function resolveWorkspaceAliases(): Record<string, string> {
|
|
22
30
|
const packagesDir = resolve(ROOT, 'packages');
|
|
23
31
|
const alias: Record<string, string> = {};
|
|
24
|
-
const entries =
|
|
32
|
+
const entries = readdirSync(packagesDir, { withFileTypes: true });
|
|
25
33
|
for (const entry of entries) {
|
|
26
34
|
if (!entry.isDirectory()) continue;
|
|
27
35
|
const pkgJsonPath = resolve(packagesDir, entry.name, 'package.json');
|
|
@@ -34,7 +42,7 @@ function resolveWorkspaceAliases(): Record<string, string> {
|
|
|
34
42
|
return alias;
|
|
35
43
|
}
|
|
36
44
|
|
|
37
|
-
async function buildFixture(name:
|
|
45
|
+
async function buildFixture(name: string): Promise<FixtureResult> {
|
|
38
46
|
const entry = resolve(EVIDENCE_DIR, `${name}.ts`);
|
|
39
47
|
const alias = resolveWorkspaceAliases();
|
|
40
48
|
|
|
@@ -76,26 +84,22 @@ async function buildFixture(name: FixtureName): Promise<FixtureResult> {
|
|
|
76
84
|
};
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
function sourceModules(modules: string[]): string[] {
|
|
87
|
+
function sourceModules(modules: readonly string[]): string[] {
|
|
80
88
|
return modules.filter((m) => !m.includes('/evidence/'));
|
|
81
89
|
}
|
|
82
90
|
|
|
83
|
-
function hasHostWebModule(modules: string[]): boolean {
|
|
91
|
+
function hasHostWebModule(modules: readonly string[]): boolean {
|
|
84
92
|
return sourceModules(modules).some((m) => m.includes('packages/host-web/'));
|
|
85
93
|
}
|
|
86
94
|
|
|
87
|
-
function
|
|
88
|
-
return sourceModules(modules).some((m) => m.includes(
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function hasCursorModule(modules: string[]): boolean {
|
|
92
|
-
return sourceModules(modules).some((m) => m.includes('webCursor'));
|
|
95
|
+
function hasCapabilityModule(modules: readonly string[], pattern: string): boolean {
|
|
96
|
+
return sourceModules(modules).some((m) => m.includes(pattern));
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
describe('evidence: tree-shaking isolation', () => {
|
|
96
|
-
const results = new Map<
|
|
100
|
+
const results = new Map<string, FixtureResult>();
|
|
97
101
|
|
|
98
|
-
async function getFixture(name:
|
|
102
|
+
async function getFixture(name: string): Promise<FixtureResult> {
|
|
99
103
|
let result = results.get(name);
|
|
100
104
|
if (result === undefined) {
|
|
101
105
|
result = await buildFixture(name);
|
|
@@ -111,37 +115,37 @@ describe('evidence: tree-shaking isolation', () => {
|
|
|
111
115
|
|
|
112
116
|
it('capability fixture contains zero host-web modules', async () => {
|
|
113
117
|
const { modules } = await getFixture('capability');
|
|
118
|
+
expect(sourceModules(modules).length).toBeGreaterThan(0);
|
|
114
119
|
expect(hasHostWebModule(modules)).toBe(false);
|
|
115
120
|
});
|
|
116
121
|
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
122
|
+
for (const cap of CAPABILITIES) {
|
|
123
|
+
for (const other of CAPABILITIES) {
|
|
124
|
+
if (other === cap) continue;
|
|
125
|
+
it(`${cap.fixture} fixture excludes ${other.fixture} modules`, async () => {
|
|
126
|
+
const { modules } = await getFixture(cap.fixture);
|
|
127
|
+
expect(sourceModules(modules).length).toBeGreaterThan(0);
|
|
128
|
+
expect(hasHostWebModule(modules)).toBe(true);
|
|
129
|
+
expect(hasCapabilityModule(modules, other.modulePattern)).toBe(false);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
128
133
|
|
|
129
|
-
it('
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
]);
|
|
135
|
-
const union = new Set([...sourceModules(cursorResult.modules), ...sourceModules(glyphResult.modules)]);
|
|
136
|
-
const outsideUnion = sourceModules(bothResult.modules).filter((m) => !union.has(m));
|
|
134
|
+
it('combined module set is within the union of individual capabilities', async () => {
|
|
135
|
+
const individualResults = await Promise.all(CAPABILITIES.map((cap) => getFixture(cap.fixture)));
|
|
136
|
+
const combinedResult = await getFixture('combined');
|
|
137
|
+
const union = new Set(individualResults.flatMap((r) => sourceModules(r.modules)));
|
|
138
|
+
const outsideUnion = sourceModules(combinedResult.modules).filter((m) => !union.has(m));
|
|
137
139
|
expect(outsideUnion).toEqual([]);
|
|
138
140
|
});
|
|
139
141
|
|
|
140
|
-
it('
|
|
142
|
+
it('normalized subadditivity: S(combined) + (N-1)*S(control) <= sum(S(individual))', async () => {
|
|
141
143
|
const control = await getFixture('control');
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
144
|
+
const combined = await getFixture('combined');
|
|
145
|
+
const individualResults = await Promise.all(CAPABILITIES.map((cap) => getFixture(cap.fixture)));
|
|
146
|
+
const n = CAPABILITIES.length;
|
|
147
|
+
|
|
148
|
+
for (const [name, fixture] of CAPABILITIES.map((cap, i) => [cap.fixture, individualResults[i]] as const)) {
|
|
145
149
|
const rawDelta = fixture.rawBytes - control.rawBytes;
|
|
146
150
|
const gzipDelta = fixture.gzipBytes - control.gzipBytes;
|
|
147
151
|
console.log(
|
|
@@ -149,6 +153,20 @@ describe('evidence: tree-shaking isolation', () => {
|
|
|
149
153
|
`gzip=${fixture.gzipBytes} (${gzipDelta >= 0 ? '+' : ''}${gzipDelta})`,
|
|
150
154
|
);
|
|
151
155
|
}
|
|
156
|
+
console.log(`combined: raw=${combined.rawBytes}, gzip=${combined.gzipBytes}`);
|
|
152
157
|
console.log(`control: raw=${control.rawBytes}, gzip=${control.gzipBytes}`);
|
|
158
|
+
|
|
159
|
+
const sumRaw = individualResults.reduce((s, r) => s + r.rawBytes, 0);
|
|
160
|
+
const sumGzip = individualResults.reduce((s, r) => s + r.gzipBytes, 0);
|
|
161
|
+
|
|
162
|
+
for (const cap of CAPABILITIES) {
|
|
163
|
+
const individual = individualResults[CAPABILITIES.indexOf(cap)];
|
|
164
|
+
expect(individual.rawBytes).toBeGreaterThan(control.rawBytes);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
expect(combined.rawBytes).toBeGreaterThan(Math.max(...individualResults.map((r) => r.rawBytes)));
|
|
168
|
+
|
|
169
|
+
expect(combined.rawBytes + (n - 1) * control.rawBytes).toBeLessThanOrEqual(sumRaw);
|
|
170
|
+
expect(combined.gzipBytes + (n - 1) * control.gzipBytes).toBeLessThanOrEqual(sumGzip);
|
|
153
171
|
});
|
|
154
172
|
});
|
|
@@ -21,12 +21,15 @@ describe('createWebGlyphRasterizerBackend', () => {
|
|
|
21
21
|
expect(backend.measureMetrics).toBeTypeOf('function');
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
it('rasterize returns null when no canvas context is available', () => {
|
|
24
|
+
it('rasterize returns null and observes failure when no canvas context is available', () => {
|
|
25
25
|
const backend = createWebGlyphRasterizerBackend();
|
|
26
|
+
installGlyphRasterizerHostBackend(backend);
|
|
26
27
|
const saved = HTMLCanvasElement.prototype.getContext;
|
|
27
28
|
HTMLCanvasElement.prototype.getContext = (() => null) as typeof HTMLCanvasElement.prototype.getContext;
|
|
28
29
|
try {
|
|
29
30
|
expect(backend.rasterize(65, { fontFamily: 'x', fontSize: 16 })).toBeNull();
|
|
31
|
+
expect(explainGlyphRasterizerBackend().viability).toBe('runtime-api-unavailable');
|
|
32
|
+
expect(explainGlyphRasterizerBackend().operation).toBe('rasterize');
|
|
30
33
|
} finally {
|
|
31
34
|
HTMLCanvasElement.prototype.getContext = saved;
|
|
32
35
|
}
|
|
@@ -55,40 +58,85 @@ describe('enableHostWebGlyphRasterizer', () => {
|
|
|
55
58
|
expect(first).toBe(second);
|
|
56
59
|
});
|
|
57
60
|
|
|
58
|
-
it('
|
|
61
|
+
it('starts unobserved — no viability claimed until a real call', () => {
|
|
59
62
|
enableHostWebGlyphRasterizer();
|
|
60
|
-
expect(explainGlyphRasterizerBackend()).toEqual({
|
|
63
|
+
expect(explainGlyphRasterizerBackend()).toEqual({
|
|
64
|
+
conflict: false,
|
|
65
|
+
layer: 'host',
|
|
66
|
+
operation: null,
|
|
67
|
+
viability: 'unobserved',
|
|
68
|
+
});
|
|
61
69
|
});
|
|
62
70
|
|
|
63
|
-
it('
|
|
71
|
+
it('observes available after a successful rasterize call', () => {
|
|
72
|
+
enableHostWebGlyphRasterizer();
|
|
73
|
+
const backend = getGlyphRasterizerBackend();
|
|
74
|
+
const result = backend.rasterize(65, { fontFamily: 'sans-serif', fontSize: 16 });
|
|
75
|
+
if (result !== null) {
|
|
76
|
+
expect(explainGlyphRasterizerBackend()).toEqual({
|
|
77
|
+
conflict: false,
|
|
78
|
+
layer: 'host',
|
|
79
|
+
operation: 'rasterize',
|
|
80
|
+
viability: 'available',
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('observes runtime-api-unavailable when context returns null', () => {
|
|
64
86
|
const saved = HTMLCanvasElement.prototype.getContext;
|
|
65
87
|
HTMLCanvasElement.prototype.getContext = (() => null) as typeof HTMLCanvasElement.prototype.getContext;
|
|
66
88
|
try {
|
|
67
89
|
enableHostWebGlyphRasterizer();
|
|
90
|
+
const backend = getGlyphRasterizerBackend();
|
|
91
|
+
backend.rasterize(65, { fontFamily: 'x', fontSize: 16 });
|
|
68
92
|
} finally {
|
|
69
93
|
HTMLCanvasElement.prototype.getContext = saved;
|
|
70
94
|
}
|
|
71
|
-
expect(explainGlyphRasterizerBackend()).toEqual({
|
|
95
|
+
expect(explainGlyphRasterizerBackend()).toEqual({
|
|
96
|
+
conflict: false,
|
|
97
|
+
layer: 'host',
|
|
98
|
+
operation: 'rasterize',
|
|
99
|
+
viability: 'runtime-api-unavailable',
|
|
100
|
+
});
|
|
72
101
|
});
|
|
73
102
|
|
|
74
|
-
it('
|
|
103
|
+
it('reflects recovery: broken environment then restored', () => {
|
|
104
|
+
enableHostWebGlyphRasterizer();
|
|
105
|
+
const backend = getGlyphRasterizerBackend();
|
|
106
|
+
|
|
107
|
+
// Break the environment — rasterize reports failure
|
|
75
108
|
const saved = HTMLCanvasElement.prototype.getContext;
|
|
76
109
|
HTMLCanvasElement.prototype.getContext = (() => null) as typeof HTMLCanvasElement.prototype.getContext;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
110
|
+
backend.rasterize(65, { fontFamily: 'x', fontSize: 16 });
|
|
111
|
+
expect(explainGlyphRasterizerBackend().viability).toBe('runtime-api-unavailable');
|
|
112
|
+
|
|
113
|
+
// Restore — next call reports success
|
|
114
|
+
HTMLCanvasElement.prototype.getContext = saved;
|
|
115
|
+
const result = backend.rasterize(65, { fontFamily: 'sans-serif', fontSize: 16 });
|
|
116
|
+
if (result !== null) {
|
|
117
|
+
expect(explainGlyphRasterizerBackend().viability).toBe('available');
|
|
81
118
|
}
|
|
82
|
-
const backend = getGlyphRasterizerBackend();
|
|
83
|
-
expect(backend.rasterize).toBeTypeOf('function');
|
|
84
|
-
expect(backend.rasterize(65, { fontFamily: 'x', fontSize: 16 })).toBeNull();
|
|
85
119
|
});
|
|
86
120
|
|
|
87
121
|
it('does not conflict with a prior install of the same backend via direct install', () => {
|
|
88
122
|
enableHostWebGlyphRasterizer();
|
|
89
123
|
const backend = getGlyphRasterizerBackend();
|
|
90
|
-
installGlyphRasterizerHostBackend(backend
|
|
91
|
-
expect(explainGlyphRasterizerBackend().
|
|
124
|
+
installGlyphRasterizerHostBackend(backend);
|
|
125
|
+
expect(explainGlyphRasterizerBackend().conflict).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('observes available even when rasterize returns null for a zero-ink glyph', () => {
|
|
129
|
+
enableHostWebGlyphRasterizer();
|
|
130
|
+
const backend = getGlyphRasterizerBackend();
|
|
131
|
+
// Space (codepoint 32) has no visible pixels — rasterize returns null, but the API is working
|
|
132
|
+
const result = backend.rasterize(32, { fontFamily: 'sans-serif', fontSize: 16 });
|
|
133
|
+
expect(result).toBeNull();
|
|
134
|
+
expect(explainGlyphRasterizerBackend()).toEqual({
|
|
135
|
+
conflict: false,
|
|
136
|
+
layer: 'host',
|
|
137
|
+
operation: 'rasterize',
|
|
138
|
+
viability: 'available',
|
|
139
|
+
});
|
|
92
140
|
});
|
|
93
141
|
});
|
|
94
142
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { explainLoopBackend, getLoopBackend, resetLoopBackendForTest } from '@flighthq/application/contract';
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
import { enableHostWebLoop, resetHostWebLoopForTest } from './webLoop';
|
|
5
|
+
|
|
6
|
+
describe('enableHostWebLoop', () => {
|
|
7
|
+
afterEach(() => {
|
|
8
|
+
resetLoopBackendForTest();
|
|
9
|
+
resetHostWebLoopForTest();
|
|
10
|
+
vi.restoreAllMocks();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('installs a host backend so get returns non-null', () => {
|
|
14
|
+
enableHostWebLoop();
|
|
15
|
+
const backend = getLoopBackend();
|
|
16
|
+
expect(backend).not.toBeNull();
|
|
17
|
+
expect(backend!.now()).toBeTypeOf('number');
|
|
18
|
+
expect(explainLoopBackend().layer).toBe('host');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('is idempotent — second call preserves provider identity', () => {
|
|
22
|
+
enableHostWebLoop();
|
|
23
|
+
const first = getLoopBackend();
|
|
24
|
+
enableHostWebLoop();
|
|
25
|
+
const second = getLoopBackend();
|
|
26
|
+
expect(first).toBe(second);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('starts unobserved — no viability claimed until a real call', () => {
|
|
30
|
+
enableHostWebLoop();
|
|
31
|
+
expect(explainLoopBackend()).toEqual({
|
|
32
|
+
conflict: false,
|
|
33
|
+
layer: 'host',
|
|
34
|
+
operation: null,
|
|
35
|
+
viability: 'unobserved',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('observes available after requestFrame call', () => {
|
|
40
|
+
enableHostWebLoop();
|
|
41
|
+
const backend = getLoopBackend()!;
|
|
42
|
+
const handle = backend.requestFrame(() => {});
|
|
43
|
+
expect(handle).toBeTypeOf('number');
|
|
44
|
+
expect(explainLoopBackend()).toEqual({
|
|
45
|
+
conflict: false,
|
|
46
|
+
layer: 'host',
|
|
47
|
+
operation: 'requestFrame',
|
|
48
|
+
viability: 'available',
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('every requestFrame re-observes — not one-shot', () => {
|
|
53
|
+
enableHostWebLoop();
|
|
54
|
+
const backend = getLoopBackend()!;
|
|
55
|
+
backend.requestFrame(() => {});
|
|
56
|
+
backend.requestFrame(() => {});
|
|
57
|
+
expect(explainLoopBackend().operation).toBe('requestFrame');
|
|
58
|
+
expect(explainLoopBackend().viability).toBe('available');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('cancelFrame observes independently from requestFrame', () => {
|
|
62
|
+
enableHostWebLoop();
|
|
63
|
+
const backend = getLoopBackend()!;
|
|
64
|
+
const handle = backend.requestFrame(() => {});
|
|
65
|
+
expect(explainLoopBackend().operation).toBe('requestFrame');
|
|
66
|
+
backend.cancelFrame(handle);
|
|
67
|
+
expect(explainLoopBackend().operation).toBe('cancelFrame');
|
|
68
|
+
expect(explainLoopBackend().viability).toBe('available');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('loss then recovery — requestFrame failure reverts to available on success', () => {
|
|
72
|
+
enableHostWebLoop();
|
|
73
|
+
const backend = getLoopBackend()!;
|
|
74
|
+
backend.requestFrame(() => {});
|
|
75
|
+
expect(explainLoopBackend().viability).toBe('available');
|
|
76
|
+
|
|
77
|
+
const raf = vi.fn().mockImplementation(() => {
|
|
78
|
+
throw new Error('rAF unavailable');
|
|
79
|
+
});
|
|
80
|
+
vi.stubGlobal('requestAnimationFrame', raf);
|
|
81
|
+
backend.requestFrame(() => {});
|
|
82
|
+
expect(explainLoopBackend().viability).toBe('runtime-api-unavailable');
|
|
83
|
+
expect(explainLoopBackend().operation).toBe('requestFrame');
|
|
84
|
+
|
|
85
|
+
vi.stubGlobal('requestAnimationFrame', vi.fn().mockReturnValue(99));
|
|
86
|
+
backend.requestFrame(() => {});
|
|
87
|
+
expect(explainLoopBackend().viability).toBe('available');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('cancelFrame failure observes unavailable', () => {
|
|
91
|
+
enableHostWebLoop();
|
|
92
|
+
const backend = getLoopBackend()!;
|
|
93
|
+
backend.requestFrame(() => {});
|
|
94
|
+
|
|
95
|
+
vi.stubGlobal(
|
|
96
|
+
'cancelAnimationFrame',
|
|
97
|
+
vi.fn().mockImplementation(() => {
|
|
98
|
+
throw new Error('cAF unavailable');
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
backend.cancelFrame(1);
|
|
102
|
+
expect(explainLoopBackend().operation).toBe('cancelFrame');
|
|
103
|
+
expect(explainLoopBackend().viability).toBe('runtime-api-unavailable');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('now returns a positive number', () => {
|
|
107
|
+
enableHostWebLoop();
|
|
108
|
+
const backend = getLoopBackend()!;
|
|
109
|
+
expect(backend.now()).toBeGreaterThanOrEqual(0);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('resetHostWebLoopForTest', () => {
|
|
114
|
+
afterEach(resetLoopBackendForTest);
|
|
115
|
+
|
|
116
|
+
it('clears the enabler flag so a subsequent enable re-installs', () => {
|
|
117
|
+
enableHostWebLoop();
|
|
118
|
+
resetHostWebLoopForTest();
|
|
119
|
+
resetLoopBackendForTest();
|
|
120
|
+
enableHostWebLoop();
|
|
121
|
+
expect(explainLoopBackend().layer).toBe('host');
|
|
122
|
+
});
|
|
123
|
+
});
|