@memberjunction/ng-realtime-channels 0.0.0 → 6.2.0-edge.4
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/LICENSE +183 -0
- package/README.md +42 -28
- package/dist/lib/angular-component-channel.d.ts +244 -0
- package/dist/lib/angular-component-channel.d.ts.map +1 -0
- package/dist/lib/angular-component-channel.js +412 -0
- package/dist/lib/angular-component-channel.js.map +1 -0
- package/dist/lib/channel-frame-capture.d.ts +33 -0
- package/dist/lib/channel-frame-capture.d.ts.map +1 -0
- package/dist/lib/channel-frame-capture.js +36 -0
- package/dist/lib/channel-frame-capture.js.map +1 -0
- package/dist/lib/dom-frame-capture.d.ts +73 -0
- package/dist/lib/dom-frame-capture.d.ts.map +1 -0
- package/dist/lib/dom-frame-capture.js +135 -0
- package/dist/lib/dom-frame-capture.js.map +1 -0
- package/dist/public-api.d.ts +18 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +18 -0
- package/dist/public-api.js.map +1 -0
- package/package.json +48 -7
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The default, opt-in DOM rasterizer for realtime channels: turns a rendered
|
|
3
|
+
* component's DOM subtree into a JPEG so a video-capable model can SEE it (used by the Interactive Component channel and by
|
|
4
|
+
* channels built with the Angular adapter).
|
|
5
|
+
*
|
|
6
|
+
* It is built on `html-to-image` (MIT; already a dependency of MJ Explorer for pin thumbnails). That library draws the
|
|
7
|
+
* subtree through an SVG `foreignObject`, so the browser's own CSS engine renders it, which matters because MJ's design
|
|
8
|
+
* tokens use `color-mix()` and similar that canvas-based rasterizers cannot parse. It runs in the main thread and clones
|
|
9
|
+
* the subtree synchronously, so this module bounds the work (node cap, timeout, one capture at a time, a size ceiling).
|
|
10
|
+
*
|
|
11
|
+
* Nothing here is on by default. A host that wants the agent to see components calls
|
|
12
|
+
* {@link EnableChannelFrameCapture} once, before a realtime session is minted (Explorer does; an embeddable
|
|
13
|
+
* element exposes it as a flag). The base channel's visual pump decides WHEN to capture (change-driven, paced to the
|
|
14
|
+
* negotiated rate, deduplicated, only while exposure is `pixels` and a video track is up) and tags each frame with the change id
|
|
15
|
+
* of the state it shows; this module only decides what a frame looks like and fails soft.
|
|
16
|
+
*
|
|
17
|
+
* **Failure is soft and loud once.** Cross-origin images, tainted canvases and fonts that cannot be read make a rasterizer
|
|
18
|
+
* throw or hang. The first such failure is logged ONCE with its reason; a failure that cannot succeed on retry (a security
|
|
19
|
+
* error) disables capture for the rest of the session, and repeated transient failures (timeouts) do the same. The channel
|
|
20
|
+
* then simply stays at state-only: the model keeps getting state notes, never an error.
|
|
21
|
+
*
|
|
22
|
+
* @module @memberjunction/ng-realtime-channels
|
|
23
|
+
*/
|
|
24
|
+
import { type ElementFrameCapturer } from './channel-frame-capture';
|
|
25
|
+
/** Renders an element to a JPEG data URL; the shape of `html-to-image`'s `toJpeg`, so a test can supply a fake. */
|
|
26
|
+
export type DomRasterizer = (element: HTMLElement, options: DomRasterizerOptions) => Promise<string>;
|
|
27
|
+
/** The options this module passes to a {@link DomRasterizer} (a subset of `html-to-image`'s own). */
|
|
28
|
+
export interface DomRasterizerOptions {
|
|
29
|
+
/** JPEG quality, 0 to 1. */
|
|
30
|
+
quality: number;
|
|
31
|
+
/** Scale applied to the element's own size, so the longest edge is at most {@link DomFrameCaptureOptions.MaxEdgePx}. */
|
|
32
|
+
pixelRatio: number;
|
|
33
|
+
/** The color behind transparent areas (JPEG has no alpha). */
|
|
34
|
+
backgroundColor: string;
|
|
35
|
+
/** Skip embedding web fonts: they add seconds and cross-origin failures, and the model does not need the typeface. */
|
|
36
|
+
skipFonts: boolean;
|
|
37
|
+
}
|
|
38
|
+
/** Options for {@link CreateDomFrameCapturer}. */
|
|
39
|
+
export interface DomFrameCaptureOptions {
|
|
40
|
+
/** JPEG quality, 0 to 1. Default 0.6 (a frame of a dashboard is roughly 40 to 120 KB). */
|
|
41
|
+
Quality?: number;
|
|
42
|
+
/** The longest edge of the frame, in pixels; larger elements are scaled down. Default 1024. */
|
|
43
|
+
MaxEdgePx?: number;
|
|
44
|
+
/** Elements with more descendants than this are not rasterized (the synchronous clone would freeze the page). Default 3000. */
|
|
45
|
+
MaxNodes?: number;
|
|
46
|
+
/** How long to wait for one capture before giving up on it. Default 4000 ms. */
|
|
47
|
+
TimeoutMs?: number;
|
|
48
|
+
/** Consecutive transient failures (timeouts, unknown errors) after which capture is disabled for the session. Default 3. */
|
|
49
|
+
MaxConsecutiveFailures?: number;
|
|
50
|
+
/** The rasterizer. Default: `html-to-image`'s `toJpeg`. */
|
|
51
|
+
Rasterize?: DomRasterizer;
|
|
52
|
+
/** Called once, with the reason, the first time capture is degraded. Default: `console.warn`. */
|
|
53
|
+
OnDegraded?: (reason: string) => void;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Builds a {@link ElementFrameCapturer} that rasterizes an element to a base64 JPEG (no data-URL prefix).
|
|
57
|
+
*
|
|
58
|
+
* The returned function never throws. It returns `null` (so the pump sends no frame) when the element has no size, is too
|
|
59
|
+
* large, a capture is already in flight, a capture times out or fails, or capture has been disabled after repeated or
|
|
60
|
+
* permanent failure; in the last two cases the reason is reported once through `OnDegraded`.
|
|
61
|
+
*
|
|
62
|
+
* @param options Tuning and test seams.
|
|
63
|
+
*/
|
|
64
|
+
export declare function CreateDomFrameCapturer(options?: DomFrameCaptureOptions): ElementFrameCapturer;
|
|
65
|
+
/**
|
|
66
|
+
* Turns on picture capture of channel surfaces for this host: registers the default rasterizer. Call it once at startup, BEFORE a realtime session is minted (whether the channel can source a video track is
|
|
67
|
+
* decided at mint). Hosts opt in: MJ Explorer calls it; an embeddable element calls it only when its flag is set.
|
|
68
|
+
*
|
|
69
|
+
* @param options Tuning and test seams.
|
|
70
|
+
* @returns A function that removes the registration.
|
|
71
|
+
*/
|
|
72
|
+
export declare function EnableChannelFrameCapture(options?: DomFrameCaptureOptions): () => void;
|
|
73
|
+
//# sourceMappingURL=dom-frame-capture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom-frame-capture.d.ts","sourceRoot":"","sources":["../../src/lib/dom-frame-capture.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAEzF,mHAAmH;AACnH,MAAM,MAAM,aAAa,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,oBAAoB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;AAErG,qGAAqG;AACrG,MAAM,WAAW,oBAAoB;IACjC,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,wHAAwH;IACxH,UAAU,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,eAAe,EAAE,MAAM,CAAC;IACxB,sHAAsH;IACtH,SAAS,EAAE,OAAO,CAAC;CACtB;AAED,kDAAkD;AAClD,MAAM,WAAW,sBAAsB;IACnC,0FAA0F;IAC1F,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+FAA+F;IAC/F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+HAA+H;IAC/H,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gFAAgF;IAChF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4HAA4H;IAC5H,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,2DAA2D;IAC3D,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,iGAAiG;IACjG,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAkDD;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,GAAE,sBAA2B,GAAG,oBAAoB,CAqDjG;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,GAAE,sBAA2B,GAAG,MAAM,IAAI,CAG1F"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The default, opt-in DOM rasterizer for realtime channels: turns a rendered
|
|
3
|
+
* component's DOM subtree into a JPEG so a video-capable model can SEE it (used by the Interactive Component channel and by
|
|
4
|
+
* channels built with the Angular adapter).
|
|
5
|
+
*
|
|
6
|
+
* It is built on `html-to-image` (MIT; already a dependency of MJ Explorer for pin thumbnails). That library draws the
|
|
7
|
+
* subtree through an SVG `foreignObject`, so the browser's own CSS engine renders it, which matters because MJ's design
|
|
8
|
+
* tokens use `color-mix()` and similar that canvas-based rasterizers cannot parse. It runs in the main thread and clones
|
|
9
|
+
* the subtree synchronously, so this module bounds the work (node cap, timeout, one capture at a time, a size ceiling).
|
|
10
|
+
*
|
|
11
|
+
* Nothing here is on by default. A host that wants the agent to see components calls
|
|
12
|
+
* {@link EnableChannelFrameCapture} once, before a realtime session is minted (Explorer does; an embeddable
|
|
13
|
+
* element exposes it as a flag). The base channel's visual pump decides WHEN to capture (change-driven, paced to the
|
|
14
|
+
* negotiated rate, deduplicated, only while exposure is `pixels` and a video track is up) and tags each frame with the change id
|
|
15
|
+
* of the state it shows; this module only decides what a frame looks like and fails soft.
|
|
16
|
+
*
|
|
17
|
+
* **Failure is soft and loud once.** Cross-origin images, tainted canvases and fonts that cannot be read make a rasterizer
|
|
18
|
+
* throw or hang. The first such failure is logged ONCE with its reason; a failure that cannot succeed on retry (a security
|
|
19
|
+
* error) disables capture for the rest of the session, and repeated transient failures (timeouts) do the same. The channel
|
|
20
|
+
* then simply stays at state-only: the model keeps getting state notes, never an error.
|
|
21
|
+
*
|
|
22
|
+
* @module @memberjunction/ng-realtime-channels
|
|
23
|
+
*/
|
|
24
|
+
import { toJpeg } from 'html-to-image';
|
|
25
|
+
import { ChannelFrameCapture } from './channel-frame-capture';
|
|
26
|
+
const DEFAULTS = { Quality: 0.6, MaxEdgePx: 1024, MaxNodes: 3000, TimeoutMs: 4000, MaxConsecutiveFailures: 3 };
|
|
27
|
+
/** The data-URL prefix a JPEG from the rasterizer starts with. */
|
|
28
|
+
const JPEG_DATA_URL_PREFIX = 'data:image/jpeg;base64,';
|
|
29
|
+
/** A background color to put behind transparent areas: the nearest ancestor that paints one, else white. */
|
|
30
|
+
function resolveBackground(element) {
|
|
31
|
+
for (let node = element; node; node = node.parentElement) {
|
|
32
|
+
const color = getComputedStyle(node).backgroundColor;
|
|
33
|
+
if (color && color !== 'transparent' && !/^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0\s*\)$/.test(color)) {
|
|
34
|
+
return color;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return '#ffffff';
|
|
38
|
+
}
|
|
39
|
+
/** Whether an error is one that cannot succeed on retry in this page (a tainted canvas or blocked cross-origin read). */
|
|
40
|
+
function isPermanentFailure(error) {
|
|
41
|
+
const name = error instanceof Error ? error.name : '';
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
return name === 'SecurityError' || /tainted|cross-origin|insecure|not allowed to load/i.test(message);
|
|
44
|
+
}
|
|
45
|
+
/** A readable reason for a failure. */
|
|
46
|
+
function describeFailure(error) {
|
|
47
|
+
if (isPermanentFailure(error)) {
|
|
48
|
+
return `the page's content cannot be read for a picture (cross-origin images or a tainted canvas): ${error instanceof Error ? error.message : String(error)}`;
|
|
49
|
+
}
|
|
50
|
+
return error instanceof Error ? error.message : String(error);
|
|
51
|
+
}
|
|
52
|
+
/** Resolves to `undefined` if the promise has not settled within `ms`; the work itself is not cancelled. */
|
|
53
|
+
function withTimeout(promise, ms) {
|
|
54
|
+
return new Promise((resolve, reject) => {
|
|
55
|
+
const timer = setTimeout(() => resolve(undefined), ms);
|
|
56
|
+
promise.then((value) => {
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
resolve(value);
|
|
59
|
+
}, (error) => {
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
reject(error);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Builds a {@link ElementFrameCapturer} that rasterizes an element to a base64 JPEG (no data-URL prefix).
|
|
67
|
+
*
|
|
68
|
+
* The returned function never throws. It returns `null` (so the pump sends no frame) when the element has no size, is too
|
|
69
|
+
* large, a capture is already in flight, a capture times out or fails, or capture has been disabled after repeated or
|
|
70
|
+
* permanent failure; in the last two cases the reason is reported once through `OnDegraded`.
|
|
71
|
+
*
|
|
72
|
+
* @param options Tuning and test seams.
|
|
73
|
+
*/
|
|
74
|
+
export function CreateDomFrameCapturer(options = {}) {
|
|
75
|
+
const quality = options.Quality ?? DEFAULTS.Quality;
|
|
76
|
+
const maxEdge = options.MaxEdgePx ?? DEFAULTS.MaxEdgePx;
|
|
77
|
+
const maxNodes = options.MaxNodes ?? DEFAULTS.MaxNodes;
|
|
78
|
+
const timeoutMs = options.TimeoutMs ?? DEFAULTS.TimeoutMs;
|
|
79
|
+
const maxFailures = options.MaxConsecutiveFailures ?? DEFAULTS.MaxConsecutiveFailures;
|
|
80
|
+
const rasterize = options.Rasterize ?? ((element, rasterOptions) => toJpeg(element, rasterOptions));
|
|
81
|
+
const report = options.OnDegraded ?? ((reason) => console.warn(`[RealtimeChannels] ${reason}`));
|
|
82
|
+
let disabled = false;
|
|
83
|
+
let inFlight = false;
|
|
84
|
+
let failures = 0;
|
|
85
|
+
const disable = (reason) => {
|
|
86
|
+
if (!disabled) {
|
|
87
|
+
disabled = true;
|
|
88
|
+
report(`Picture capture of components is off for this session; the agent keeps receiving state only. Reason: ${reason}`);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
return async (element) => {
|
|
92
|
+
if (disabled || inFlight || element.clientWidth === 0 || element.clientHeight === 0) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
if (element.getElementsByTagName('*').length > maxNodes) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
inFlight = true;
|
|
99
|
+
try {
|
|
100
|
+
const ratio = Math.min(1, maxEdge / Math.max(element.clientWidth, element.clientHeight));
|
|
101
|
+
const dataUrl = await withTimeout(rasterize(element, { quality, pixelRatio: ratio, backgroundColor: resolveBackground(element), skipFonts: true }), timeoutMs);
|
|
102
|
+
if (dataUrl === undefined) {
|
|
103
|
+
failures++;
|
|
104
|
+
if (failures >= maxFailures) {
|
|
105
|
+
disable(`capturing took longer than ${timeoutMs} ms, ${failures} times in a row`);
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
failures = 0;
|
|
110
|
+
return dataUrl.startsWith(JPEG_DATA_URL_PREFIX) ? dataUrl.slice(JPEG_DATA_URL_PREFIX.length) : null;
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
failures++;
|
|
114
|
+
if (isPermanentFailure(error) || failures >= maxFailures) {
|
|
115
|
+
disable(describeFailure(error));
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
inFlight = false;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Turns on picture capture of channel surfaces for this host: registers the default rasterizer. Call it once at startup, BEFORE a realtime session is minted (whether the channel can source a video track is
|
|
126
|
+
* decided at mint). Hosts opt in: MJ Explorer calls it; an embeddable element calls it only when its flag is set.
|
|
127
|
+
*
|
|
128
|
+
* @param options Tuning and test seams.
|
|
129
|
+
* @returns A function that removes the registration.
|
|
130
|
+
*/
|
|
131
|
+
export function EnableChannelFrameCapture(options = {}) {
|
|
132
|
+
ChannelFrameCapture.Instance.Register(CreateDomFrameCapturer(options));
|
|
133
|
+
return () => ChannelFrameCapture.Instance.Register(null);
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=dom-frame-capture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dom-frame-capture.js","sourceRoot":"","sources":["../../src/lib/dom-frame-capture.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview The default, opt-in DOM rasterizer for realtime channels: turns a rendered\n * component's DOM subtree into a JPEG so a video-capable model can SEE it (used by the Interactive Component channel and by\n * channels built with the Angular adapter).\n *\n * It is built on `html-to-image` (MIT; already a dependency of MJ Explorer for pin thumbnails). That library draws the\n * subtree through an SVG `foreignObject`, so the browser's own CSS engine renders it, which matters because MJ's design\n * tokens use `color-mix()` and similar that canvas-based rasterizers cannot parse. It runs in the main thread and clones\n * the subtree synchronously, so this module bounds the work (node cap, timeout, one capture at a time, a size ceiling).\n *\n * Nothing here is on by default. A host that wants the agent to see components calls\n * {@link EnableChannelFrameCapture} once, before a realtime session is minted (Explorer does; an embeddable\n * element exposes it as a flag). The base channel's visual pump decides WHEN to capture (change-driven, paced to the\n * negotiated rate, deduplicated, only while exposure is `pixels` and a video track is up) and tags each frame with the change id\n * of the state it shows; this module only decides what a frame looks like and fails soft.\n *\n * **Failure is soft and loud once.** Cross-origin images, tainted canvases and fonts that cannot be read make a rasterizer\n * throw or hang. The first such failure is logged ONCE with its reason; a failure that cannot succeed on retry (a security\n * error) disables capture for the rest of the session, and repeated transient failures (timeouts) do the same. The channel\n * then simply stays at state-only: the model keeps getting state notes, never an error.\n *\n * @module @memberjunction/ng-realtime-channels\n */\n\nimport { toJpeg } from 'html-to-image';\nimport { ChannelFrameCapture, type ElementFrameCapturer } from './channel-frame-capture';\n\n/** Renders an element to a JPEG data URL; the shape of `html-to-image`'s `toJpeg`, so a test can supply a fake. */\nexport type DomRasterizer = (element: HTMLElement, options: DomRasterizerOptions) => Promise<string>;\n\n/** The options this module passes to a {@link DomRasterizer} (a subset of `html-to-image`'s own). */\nexport interface DomRasterizerOptions {\n /** JPEG quality, 0 to 1. */\n quality: number;\n /** Scale applied to the element's own size, so the longest edge is at most {@link DomFrameCaptureOptions.MaxEdgePx}. */\n pixelRatio: number;\n /** The color behind transparent areas (JPEG has no alpha). */\n backgroundColor: string;\n /** Skip embedding web fonts: they add seconds and cross-origin failures, and the model does not need the typeface. */\n skipFonts: boolean;\n}\n\n/** Options for {@link CreateDomFrameCapturer}. */\nexport interface DomFrameCaptureOptions {\n /** JPEG quality, 0 to 1. Default 0.6 (a frame of a dashboard is roughly 40 to 120 KB). */\n Quality?: number;\n /** The longest edge of the frame, in pixels; larger elements are scaled down. Default 1024. */\n MaxEdgePx?: number;\n /** Elements with more descendants than this are not rasterized (the synchronous clone would freeze the page). Default 3000. */\n MaxNodes?: number;\n /** How long to wait for one capture before giving up on it. Default 4000 ms. */\n TimeoutMs?: number;\n /** Consecutive transient failures (timeouts, unknown errors) after which capture is disabled for the session. Default 3. */\n MaxConsecutiveFailures?: number;\n /** The rasterizer. Default: `html-to-image`'s `toJpeg`. */\n Rasterize?: DomRasterizer;\n /** Called once, with the reason, the first time capture is degraded. Default: `console.warn`. */\n OnDegraded?: (reason: string) => void;\n}\n\nconst DEFAULTS = { Quality: 0.6, MaxEdgePx: 1024, MaxNodes: 3000, TimeoutMs: 4000, MaxConsecutiveFailures: 3 } as const;\n\n/** The data-URL prefix a JPEG from the rasterizer starts with. */\nconst JPEG_DATA_URL_PREFIX = 'data:image/jpeg;base64,';\n\n/** A background color to put behind transparent areas: the nearest ancestor that paints one, else white. */\nfunction resolveBackground(element: HTMLElement): string {\n for (let node: HTMLElement | null = element; node; node = node.parentElement) {\n const color = getComputedStyle(node).backgroundColor;\n if (color && color !== 'transparent' && !/^rgba\\(\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*\\d+\\s*,\\s*0\\s*\\)$/.test(color)) {\n return color;\n }\n }\n return '#ffffff';\n}\n\n/** Whether an error is one that cannot succeed on retry in this page (a tainted canvas or blocked cross-origin read). */\nfunction isPermanentFailure(error: unknown): boolean {\n const name = error instanceof Error ? error.name : '';\n const message = error instanceof Error ? error.message : String(error);\n return name === 'SecurityError' || /tainted|cross-origin|insecure|not allowed to load/i.test(message);\n}\n\n/** A readable reason for a failure. */\nfunction describeFailure(error: unknown): string {\n if (isPermanentFailure(error)) {\n return `the page's content cannot be read for a picture (cross-origin images or a tainted canvas): ${error instanceof Error ? error.message : String(error)}`;\n }\n return error instanceof Error ? error.message : String(error);\n}\n\n/** Resolves to `undefined` if the promise has not settled within `ms`; the work itself is not cancelled. */\nfunction withTimeout<T>(promise: Promise<T>, ms: number): Promise<T | undefined> {\n return new Promise<T | undefined>((resolve, reject) => {\n const timer = setTimeout(() => resolve(undefined), ms);\n promise.then(\n (value) => {\n clearTimeout(timer);\n resolve(value);\n },\n (error: unknown) => {\n clearTimeout(timer);\n reject(error);\n }\n );\n });\n}\n\n/**\n * Builds a {@link ElementFrameCapturer} that rasterizes an element to a base64 JPEG (no data-URL prefix).\n *\n * The returned function never throws. It returns `null` (so the pump sends no frame) when the element has no size, is too\n * large, a capture is already in flight, a capture times out or fails, or capture has been disabled after repeated or\n * permanent failure; in the last two cases the reason is reported once through `OnDegraded`.\n *\n * @param options Tuning and test seams.\n */\nexport function CreateDomFrameCapturer(options: DomFrameCaptureOptions = {}): ElementFrameCapturer {\n const quality = options.Quality ?? DEFAULTS.Quality;\n const maxEdge = options.MaxEdgePx ?? DEFAULTS.MaxEdgePx;\n const maxNodes = options.MaxNodes ?? DEFAULTS.MaxNodes;\n const timeoutMs = options.TimeoutMs ?? DEFAULTS.TimeoutMs;\n const maxFailures = options.MaxConsecutiveFailures ?? DEFAULTS.MaxConsecutiveFailures;\n const rasterize: DomRasterizer = options.Rasterize ?? ((element, rasterOptions) => toJpeg(element, rasterOptions));\n const report = options.OnDegraded ?? ((reason: string) => console.warn(`[RealtimeChannels] ${reason}`));\n\n let disabled = false;\n let inFlight = false;\n let failures = 0;\n\n const disable = (reason: string): void => {\n if (!disabled) {\n disabled = true;\n report(`Picture capture of components is off for this session; the agent keeps receiving state only. Reason: ${reason}`);\n }\n };\n\n return async (element: HTMLElement): Promise<string | null> => {\n if (disabled || inFlight || element.clientWidth === 0 || element.clientHeight === 0) {\n return null;\n }\n if (element.getElementsByTagName('*').length > maxNodes) {\n return null;\n }\n inFlight = true;\n try {\n const ratio = Math.min(1, maxEdge / Math.max(element.clientWidth, element.clientHeight));\n const dataUrl = await withTimeout(\n rasterize(element, { quality, pixelRatio: ratio, backgroundColor: resolveBackground(element), skipFonts: true }),\n timeoutMs\n );\n if (dataUrl === undefined) {\n failures++;\n if (failures >= maxFailures) {\n disable(`capturing took longer than ${timeoutMs} ms, ${failures} times in a row`);\n }\n return null;\n }\n failures = 0;\n return dataUrl.startsWith(JPEG_DATA_URL_PREFIX) ? dataUrl.slice(JPEG_DATA_URL_PREFIX.length) : null;\n } catch (error) {\n failures++;\n if (isPermanentFailure(error) || failures >= maxFailures) {\n disable(describeFailure(error));\n }\n return null;\n } finally {\n inFlight = false;\n }\n };\n}\n\n/**\n * Turns on picture capture of channel surfaces for this host: registers the default rasterizer. Call it once at startup, BEFORE a realtime session is minted (whether the channel can source a video track is\n * decided at mint). Hosts opt in: MJ Explorer calls it; an embeddable element calls it only when its flag is set.\n *\n * @param options Tuning and test seams.\n * @returns A function that removes the registration.\n */\nexport function EnableChannelFrameCapture(options: DomFrameCaptureOptions = {}): () => void {\n ChannelFrameCapture.Instance.Register(CreateDomFrameCapturer(options));\n return () => ChannelFrameCapture.Instance.Register(null);\n}\n"]}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* # @memberjunction/ng-realtime-channels
|
|
5
|
+
*
|
|
6
|
+
* Build realtime-agent channels in Angular without the call overlay.
|
|
7
|
+
*
|
|
8
|
+
* - {@link AngularComponentChannel}: wrap an EXISTING Angular component as a channel surface: a descriptor plus a few
|
|
9
|
+
* binding functions.
|
|
10
|
+
* - {@link EnableChannelFrameCapture} / {@link ChannelFrameCapture}: the opt-in DOM rasterizer that lets a channel show
|
|
11
|
+
* the model what a surface looks like.
|
|
12
|
+
*
|
|
13
|
+
* See `guides/REALTIME_CHANNELS_GUIDE.md`.
|
|
14
|
+
*/
|
|
15
|
+
export * from './lib/angular-component-channel';
|
|
16
|
+
export * from './lib/channel-frame-capture';
|
|
17
|
+
export * from './lib/dom-frame-capture';
|
|
18
|
+
//# sourceMappingURL=public-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-api.d.ts","sourceRoot":"","sources":["../src/public-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @packageDocumentation
|
|
3
|
+
*
|
|
4
|
+
* # @memberjunction/ng-realtime-channels
|
|
5
|
+
*
|
|
6
|
+
* Build realtime-agent channels in Angular without the call overlay.
|
|
7
|
+
*
|
|
8
|
+
* - {@link AngularComponentChannel}: wrap an EXISTING Angular component as a channel surface: a descriptor plus a few
|
|
9
|
+
* binding functions.
|
|
10
|
+
* - {@link EnableChannelFrameCapture} / {@link ChannelFrameCapture}: the opt-in DOM rasterizer that lets a channel show
|
|
11
|
+
* the model what a surface looks like.
|
|
12
|
+
*
|
|
13
|
+
* See `guides/REALTIME_CHANNELS_GUIDE.md`.
|
|
14
|
+
*/
|
|
15
|
+
export * from './lib/angular-component-channel';
|
|
16
|
+
export * from './lib/channel-frame-capture';
|
|
17
|
+
export * from './lib/dom-frame-capture';
|
|
18
|
+
//# sourceMappingURL=public-api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"public-api.js","sourceRoot":"","sources":["../src/public-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,cAAc,iCAAiC,CAAC;AAChD,cAAc,6BAA6B,CAAC;AAC5C,cAAc,yBAAyB,CAAC","sourcesContent":["/**\n * @packageDocumentation\n *\n * # @memberjunction/ng-realtime-channels\n *\n * Build realtime-agent channels in Angular without the call overlay.\n *\n * - {@link AngularComponentChannel}: wrap an EXISTING Angular component as a channel surface: a descriptor plus a few\n * binding functions.\n * - {@link EnableChannelFrameCapture} / {@link ChannelFrameCapture}: the opt-in DOM rasterizer that lets a channel show\n * the model what a surface looks like.\n *\n * See `guides/REALTIME_CHANNELS_GUIDE.md`.\n */\n\nexport * from './lib/angular-component-channel';\nexport * from './lib/channel-frame-capture';\nexport * from './lib/dom-frame-capture';\n"]}
|
package/package.json
CHANGED
|
@@ -1,10 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ng-realtime-channels",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "6.2.0-edge.4",
|
|
4
|
+
"description": "MemberJunction: build realtime-agent channels in Angular. Wrap an EXISTING Angular component as a channel surface with AngularComponentChannel (a descriptor plus a few binding functions), and let channels show the model pictures of DOM surfaces through an opt-in rasterizer. No overlay, no Explorer, no router.",
|
|
5
|
+
"main": "./dist/public-api.js",
|
|
6
|
+
"typings": "./dist/public-api.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"/dist"
|
|
9
|
+
],
|
|
5
10
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
"angular",
|
|
12
|
+
"realtime",
|
|
13
|
+
"channel",
|
|
14
|
+
"ai-agent",
|
|
15
|
+
"memberjunction"
|
|
16
|
+
],
|
|
17
|
+
"author": "MemberJunction",
|
|
18
|
+
"license": "BUSL-1.1",
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@angular/compiler": "21.2.22",
|
|
21
|
+
"@angular/compiler-cli": "21.2.22",
|
|
22
|
+
"@memberjunction/ng-test-utils": "6.2.0-edge.4",
|
|
23
|
+
"vitest": "^4.0.18"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@angular/common": "^21.2.22",
|
|
27
|
+
"@angular/core": "^21.2.22"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@memberjunction/ai": "6.2.0-edge.4",
|
|
31
|
+
"@memberjunction/ai-core-plus": "6.2.0-edge.4",
|
|
32
|
+
"@memberjunction/core": "6.2.0-edge.4",
|
|
33
|
+
"@memberjunction/global": "6.2.0-edge.4",
|
|
34
|
+
"@memberjunction/realtime-runtime": "6.2.0-edge.4",
|
|
35
|
+
"html-to-image": "^1.11.11",
|
|
36
|
+
"rxjs": "^7.8.2",
|
|
37
|
+
"tslib": "^2.8.1"
|
|
38
|
+
},
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "https://github.com/MemberJunction/MJ"
|
|
43
|
+
},
|
|
44
|
+
"mjUILayer": "widgets",
|
|
45
|
+
"scripts": {
|
|
46
|
+
"test": "vitest run",
|
|
47
|
+
"test:types": "tsc --noEmit -p tsconfig.spec.json",
|
|
48
|
+
"build": "ngc",
|
|
49
|
+
"test:watch": "vitest"
|
|
50
|
+
}
|
|
51
|
+
}
|