openui-telegram 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +106 -2
- package/dist/client-features.js +9 -0
- package/dist/client-text.d.ts +1 -0
- package/dist/client-text.js +6 -0
- package/dist/client.d.ts +16 -0
- package/dist/client.js +196 -0
- package/dist/compiler.d.ts +94 -0
- package/dist/compiler.js +157 -0
- package/dist/controller.d.ts +42 -0
- package/dist/controller.js +61 -0
- package/dist/dom.js +72 -0
- package/dist/host-client-bundle.d.ts +19 -0
- package/dist/host-client-bundle.js +183 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +8 -0
- package/dist/javascript-data.js +39 -0
- package/dist/message.d.ts +10 -0
- package/dist/message.js +44 -0
- package/dist/runtime.d.ts +174 -0
- package/dist/runtime.js +514 -0
- package/dist/session.d.ts +40 -0
- package/dist/session.js +71 -0
- package/dist/transport.d.ts +37 -0
- package/dist/transport.js +34 -0
- package/dist/wire-apply.d.ts +17 -0
- package/dist/wire-apply.js +48 -0
- package/dist/wire.d.ts +3 -0
- package/dist/wire.js +68 -0
- package/package.json +58 -4
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { parseMessage } from '@openuidev/lang-core';
|
|
2
|
+
import type { Library, ParseResult } from '@openuidev/lang-core';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { WidgetRuntime } from './runtime.js';
|
|
5
|
+
import type { RuntimeOptions, WidgetComponent } from './runtime.js';
|
|
6
|
+
/** Props stay inferred; all synchronization belongs to WidgetRuntime. */
|
|
7
|
+
export declare function defineWidgetComponent<T extends z.ZodObject>(config: {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
props: T;
|
|
11
|
+
} & WidgetComponent<z.output<T>>): import("@openuidev/lang-core").DefinedComponent<T, WidgetComponent<any>>;
|
|
12
|
+
export declare class WidgetSession {
|
|
13
|
+
readonly library: Library<WidgetComponent>;
|
|
14
|
+
readonly runtime: WidgetRuntime;
|
|
15
|
+
source: string;
|
|
16
|
+
message: ReturnType<typeof parseMessage>;
|
|
17
|
+
private parser;
|
|
18
|
+
private streamingParser;
|
|
19
|
+
private stream;
|
|
20
|
+
private complete;
|
|
21
|
+
constructor(library: Library<WidgetComponent>, source: string, options: RuntimeOptions);
|
|
22
|
+
update(source: string): ParseResult;
|
|
23
|
+
patch(patch: string): ParseResult;
|
|
24
|
+
push(chunk: string, complete?: boolean): ParseResult;
|
|
25
|
+
resetStream(): void;
|
|
26
|
+
get parts(): import("./message.js").ResponsePart[];
|
|
27
|
+
get diagnostics(): ({
|
|
28
|
+
code: import("@openuidev/lang-core").ValidationErrorCode;
|
|
29
|
+
component: string;
|
|
30
|
+
path: string;
|
|
31
|
+
message: string;
|
|
32
|
+
statementId?: string;
|
|
33
|
+
severity?: "warning";
|
|
34
|
+
source: 'parser';
|
|
35
|
+
} | import("./runtime.js").Diagnostic)[];
|
|
36
|
+
/** Stored context is deliberately opt-in, following the OpenUI message protocol. */
|
|
37
|
+
restoreContext(select: (context: unknown) => Parameters<WidgetRuntime['restore']>[0]): void;
|
|
38
|
+
dispose(): void;
|
|
39
|
+
}
|
|
40
|
+
export declare function createInterface(library: Library<WidgetComponent>, source: string, options: RuntimeOptions): WidgetSession;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { createParser, createStreamingParser, defineComponent, mergeStatements, parseMessage, markReactive, isReactiveSchema } from '@openuidev/lang-core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { WidgetRuntime, runtimeLibrary } from './runtime.js';
|
|
4
|
+
import { responseParts } from './message.js';
|
|
5
|
+
/** Props stay inferred; all synchronization belongs to WidgetRuntime. */
|
|
6
|
+
export function defineWidgetComponent(config) {
|
|
7
|
+
const { name, description, props, ...component } = config;
|
|
8
|
+
let registered = props;
|
|
9
|
+
const shape = { ...props.shape };
|
|
10
|
+
for (const key of config.reactive ?? []) {
|
|
11
|
+
if (!Object.hasOwn(props.shape, key))
|
|
12
|
+
throw new TypeError(`Unknown reactive prop ${name}.${key}`);
|
|
13
|
+
shape[key] = props.shape[key].clone();
|
|
14
|
+
markReactive(shape[key]);
|
|
15
|
+
}
|
|
16
|
+
if (config.reactive?.length)
|
|
17
|
+
registered = props.clone({ ...props.def, shape }, { parent: true });
|
|
18
|
+
const reactive = Object.keys(registered.shape).filter(key => isReactiveSchema(registered.shape[key]));
|
|
19
|
+
if (reactive.length)
|
|
20
|
+
component.reactive = reactive;
|
|
21
|
+
return defineComponent({ name, description, props: registered, component });
|
|
22
|
+
}
|
|
23
|
+
export class WidgetSession {
|
|
24
|
+
library;
|
|
25
|
+
runtime;
|
|
26
|
+
source;
|
|
27
|
+
message;
|
|
28
|
+
parser;
|
|
29
|
+
streamingParser;
|
|
30
|
+
stream = '';
|
|
31
|
+
complete = true;
|
|
32
|
+
constructor(library, source, options) {
|
|
33
|
+
this.library = library;
|
|
34
|
+
this.parser = createParser(library.toJSONSchema(), library.root);
|
|
35
|
+
this.streamingParser = createStreamingParser(library.toJSONSchema(), library.root);
|
|
36
|
+
this.source = source;
|
|
37
|
+
this.message = parseMessage(source);
|
|
38
|
+
this.runtime = new WidgetRuntime(runtimeLibrary(library), this.parser.parse(this.message.content), options);
|
|
39
|
+
}
|
|
40
|
+
update(source) {
|
|
41
|
+
this.complete = true;
|
|
42
|
+
this.message = parseMessage(source);
|
|
43
|
+
this.source = source;
|
|
44
|
+
this.runtime.replace(this.parser.parse(this.message.content));
|
|
45
|
+
return this.runtime.parsed;
|
|
46
|
+
}
|
|
47
|
+
patch(patch) { return this.update(mergeStatements(this.message.content, parseMessage(patch).content)); }
|
|
48
|
+
push(chunk, complete = false) {
|
|
49
|
+
this.complete = complete;
|
|
50
|
+
this.stream += chunk;
|
|
51
|
+
this.message = parseMessage(this.stream, { streaming: !complete });
|
|
52
|
+
this.source = this.stream;
|
|
53
|
+
const result = this.streamingParser.set(this.message.content, complete);
|
|
54
|
+
this.runtime.replace(result);
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
resetStream() { this.stream = ''; }
|
|
58
|
+
get parts() { return responseParts(this.message.content); }
|
|
59
|
+
get diagnostics() {
|
|
60
|
+
return [...(this.complete ? this.runtime.parsed.meta.errors.map(e => ({ ...e, source: 'parser' })) : []),
|
|
61
|
+
...this.runtime.diagnostics];
|
|
62
|
+
}
|
|
63
|
+
/** Stored context is deliberately opt-in, following the OpenUI message protocol. */
|
|
64
|
+
restoreContext(select) {
|
|
65
|
+
this.runtime.restore(select(this.message.context));
|
|
66
|
+
}
|
|
67
|
+
dispose() { this.runtime.dispose(); }
|
|
68
|
+
}
|
|
69
|
+
export function createInterface(library, source, options) {
|
|
70
|
+
return new WidgetSession(library, source, options);
|
|
71
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { SavedState } from './runtime.js';
|
|
2
|
+
export declare const TELEGRAM_PROFILE: {
|
|
3
|
+
readonly profile: 1;
|
|
4
|
+
readonly documentationChecked: '2026-11-11';
|
|
5
|
+
readonly openui: '1.1.0';
|
|
6
|
+
readonly language: '1.0 (inherits 0.5)';
|
|
7
|
+
readonly html: 32768;
|
|
8
|
+
readonly compressedHtml: 4096;
|
|
9
|
+
readonly request: 64;
|
|
10
|
+
readonly response: 1024;
|
|
11
|
+
readonly state: 1024;
|
|
12
|
+
};
|
|
13
|
+
export declare function utf8Bytes(value: string): number;
|
|
14
|
+
export declare class TransportSizeError extends RangeError {
|
|
15
|
+
readonly field: string;
|
|
16
|
+
readonly bytes: number;
|
|
17
|
+
readonly limit: number;
|
|
18
|
+
constructor(field: string, bytes: number, limit: number);
|
|
19
|
+
}
|
|
20
|
+
export declare function encodeTransport(value: unknown, field: 'request' | 'response' | 'state'): string;
|
|
21
|
+
export interface StateUpdate {
|
|
22
|
+
instanceId: string;
|
|
23
|
+
version: number;
|
|
24
|
+
revision: number;
|
|
25
|
+
state: SavedState;
|
|
26
|
+
}
|
|
27
|
+
export interface TelegramEnvironment {
|
|
28
|
+
request(data: string): Promise<{
|
|
29
|
+
result: string | null;
|
|
30
|
+
}>;
|
|
31
|
+
chatData: string | null;
|
|
32
|
+
onEvent(event: string, handler: () => void): void;
|
|
33
|
+
offEvent(event: string, handler: () => void): void;
|
|
34
|
+
openLink(url: string): void;
|
|
35
|
+
openTelegramLink(url: string): void;
|
|
36
|
+
ready?(): void;
|
|
37
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const REQUEST_LIMIT = 64, RESPONSE_LIMIT = 1024;
|
|
2
|
+
export const TELEGRAM_PROFILE = {
|
|
3
|
+
profile: 1, documentationChecked: '2026-11-11', openui: '1.1.0', language: '1.0 (inherits 0.5)',
|
|
4
|
+
html: 32768, compressedHtml: 4096, request: REQUEST_LIMIT, response: RESPONSE_LIMIT, state: RESPONSE_LIMIT,
|
|
5
|
+
};
|
|
6
|
+
// QuickJS does not need TextEncoder for byte accounting.
|
|
7
|
+
export function utf8Bytes(value) {
|
|
8
|
+
let bytes = 0;
|
|
9
|
+
for (const c of value) {
|
|
10
|
+
const cp = c.codePointAt(0);
|
|
11
|
+
bytes += cp < 0x80 ? 1 : cp < 0x800 ? 2 : cp < 0x10000 ? 3 : 4;
|
|
12
|
+
}
|
|
13
|
+
return bytes;
|
|
14
|
+
}
|
|
15
|
+
export class TransportSizeError extends RangeError {
|
|
16
|
+
field;
|
|
17
|
+
bytes;
|
|
18
|
+
limit;
|
|
19
|
+
constructor(field, bytes, limit) {
|
|
20
|
+
super(`${field} is ${bytes} UTF-8 bytes; Telegram allows ${limit}`);
|
|
21
|
+
this.field = field;
|
|
22
|
+
this.bytes = bytes;
|
|
23
|
+
this.limit = limit;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function encodeTransport(value, field) {
|
|
27
|
+
const result = JSON.stringify(value);
|
|
28
|
+
if (result === undefined)
|
|
29
|
+
throw new TypeError('Transport value must be JSON serializable');
|
|
30
|
+
const size = utf8Bytes(result), limit = field === 'request' ? REQUEST_LIMIT : RESPONSE_LIMIT;
|
|
31
|
+
if (size > limit)
|
|
32
|
+
throw new TransportSizeError(field, size, limit);
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { WidgetFrame } from './controller.js';
|
|
2
|
+
export type FrameChange = [0, string[], unknown] | [1, string[]] | [2, string[], [number, number, string][]] | [
|
|
3
|
+
3,
|
|
4
|
+
string[],
|
|
5
|
+
number,
|
|
6
|
+
number,
|
|
7
|
+
unknown[]
|
|
8
|
+
];
|
|
9
|
+
export interface WidgetUpdate {
|
|
10
|
+
i: string;
|
|
11
|
+
v: number;
|
|
12
|
+
r: number;
|
|
13
|
+
b: [version: number, revision: number];
|
|
14
|
+
d: FrameChange[];
|
|
15
|
+
}
|
|
16
|
+
/** Apply atomically against the exact base, including when a patch changes the program version. */
|
|
17
|
+
export declare function applyWidgetUpdate(base: WidgetFrame, update: WidgetUpdate): WidgetFrame;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Apply atomically against the exact base, including when a patch changes the program version. */
|
|
2
|
+
export function applyWidgetUpdate(base, update) {
|
|
3
|
+
if (update.i !== base.i || update.b[0] !== base.v || update.b[1] !== base.r)
|
|
4
|
+
throw new Error('Update requires a different base frame');
|
|
5
|
+
if (update.v < base.v || update.v === base.v && update.r <= base.r)
|
|
6
|
+
throw new Error('Obsolete frame update');
|
|
7
|
+
const result = JSON.parse(JSON.stringify(base));
|
|
8
|
+
for (const change of update.d) {
|
|
9
|
+
const [kind, path] = change;
|
|
10
|
+
if (!path.length || !['h', 'c', 'f', 'e', 'l', 's'].includes(path[0]))
|
|
11
|
+
throw new Error('Invalid update path');
|
|
12
|
+
let parent = result;
|
|
13
|
+
for (const key of path.slice(0, -1)) {
|
|
14
|
+
if (!parent || typeof parent !== 'object' || !Object.prototype.hasOwnProperty.call(parent, key))
|
|
15
|
+
throw new Error('Missing update parent');
|
|
16
|
+
parent = parent[key];
|
|
17
|
+
}
|
|
18
|
+
const key = path[path.length - 1];
|
|
19
|
+
if (kind === 3) {
|
|
20
|
+
if (!Array.isArray(parent[key]))
|
|
21
|
+
throw new Error('Array update requires an array');
|
|
22
|
+
const [, , start, remove, items] = change;
|
|
23
|
+
if (!Number.isInteger(start) || !Number.isInteger(remove) || start < 0 || remove < 0 || start + remove > parent[key].length)
|
|
24
|
+
throw new Error('Invalid array update range');
|
|
25
|
+
parent[key].splice(start, remove, ...items);
|
|
26
|
+
}
|
|
27
|
+
else if (kind === 1)
|
|
28
|
+
delete parent[key];
|
|
29
|
+
else {
|
|
30
|
+
let value = change[2];
|
|
31
|
+
if (kind === 2) {
|
|
32
|
+
if (typeof parent[key] !== 'string')
|
|
33
|
+
throw new Error('Text update requires a string');
|
|
34
|
+
let text = parent[key];
|
|
35
|
+
for (const [offset, remove, insert] of [...change[2]].reverse()) {
|
|
36
|
+
if (!Number.isInteger(offset) || !Number.isInteger(remove) || offset < 0 || remove < 0 || offset + remove > text.length)
|
|
37
|
+
throw new Error('Invalid text update range');
|
|
38
|
+
text = text.slice(0, offset) + insert + text.slice(offset + remove);
|
|
39
|
+
}
|
|
40
|
+
value = text;
|
|
41
|
+
}
|
|
42
|
+
Object.defineProperty(parent, key, { value, enumerable: true, configurable: true, writable: true });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
result.v = update.v;
|
|
46
|
+
result.r = update.r;
|
|
47
|
+
return result;
|
|
48
|
+
}
|
package/dist/wire.d.ts
ADDED
package/dist/wire.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { diffChars } from 'diff';
|
|
2
|
+
import { utf8Bytes } from './transport.js';
|
|
3
|
+
export function createWidgetUpdate(base, next) {
|
|
4
|
+
if (base.i !== next.i)
|
|
5
|
+
throw new Error('Cannot diff different interface instances');
|
|
6
|
+
const changes = [];
|
|
7
|
+
const diff = (a, b, path) => {
|
|
8
|
+
if (JSON.stringify(a) === JSON.stringify(b))
|
|
9
|
+
return;
|
|
10
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
11
|
+
let offset = 0, edit;
|
|
12
|
+
const edits = [];
|
|
13
|
+
for (const part of diffChars(a, b)) {
|
|
14
|
+
if (!part.added && !part.removed) {
|
|
15
|
+
offset += part.value.length;
|
|
16
|
+
edit = undefined;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (!edit) {
|
|
20
|
+
edit = [offset, 0, ''];
|
|
21
|
+
edits.push(edit);
|
|
22
|
+
}
|
|
23
|
+
if (part.removed) {
|
|
24
|
+
edit[1] += part.value.length;
|
|
25
|
+
offset += part.value.length;
|
|
26
|
+
}
|
|
27
|
+
else
|
|
28
|
+
edit[2] += part.value;
|
|
29
|
+
}
|
|
30
|
+
const delta = [2, path, edits], replacement = [0, path, b];
|
|
31
|
+
changes.push(utf8Bytes(JSON.stringify(delta)) < utf8Bytes(JSON.stringify(replacement)) ? delta : replacement);
|
|
32
|
+
}
|
|
33
|
+
else if (Array.isArray(a) && Array.isArray(b)) {
|
|
34
|
+
if (a.length === b.length) {
|
|
35
|
+
for (let i = 0; i < a.length; i++)
|
|
36
|
+
diff(a[i], b[i], [...path, String(i)]);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
let start = 0, end = 0;
|
|
40
|
+
while (start < Math.min(a.length, b.length) && JSON.stringify(a[start]) === JSON.stringify(b[start]))
|
|
41
|
+
start++;
|
|
42
|
+
while (end < Math.min(a.length, b.length) - start && JSON.stringify(a[a.length - 1 - end]) === JSON.stringify(b[b.length - 1 - end]))
|
|
43
|
+
end++;
|
|
44
|
+
changes.push([3, path, start, a.length - start - end, b.slice(start, b.length - end)]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (a && b && typeof a === 'object' && typeof b === 'object' && !Array.isArray(a) && !Array.isArray(b)) {
|
|
48
|
+
for (const key of Object.keys(a))
|
|
49
|
+
if (!Object.hasOwn(b, key))
|
|
50
|
+
changes.push([1, [...path, key]]);
|
|
51
|
+
for (const key of Object.keys(b)) {
|
|
52
|
+
if (!Object.hasOwn(a, key))
|
|
53
|
+
changes.push([0, [...path, key], b[key]]);
|
|
54
|
+
else
|
|
55
|
+
diff(a[key], b[key], [...path, key]);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else
|
|
59
|
+
changes.push([0, path, b]);
|
|
60
|
+
};
|
|
61
|
+
for (const key of ['h', 'c', 'f', 'e', 'l', 's']) {
|
|
62
|
+
if (base[key] !== undefined && next[key] === undefined)
|
|
63
|
+
changes.push([1, [key]]);
|
|
64
|
+
else
|
|
65
|
+
diff(base[key], next[key], [key]);
|
|
66
|
+
}
|
|
67
|
+
return { i: next.i, v: next.v, r: next.r, b: [base.v, base.r], d: changes };
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openui-telegram",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Compile OpenUI Lang into self-contained Telegram widgets with an application-owned runtime and transport",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public",
|
|
9
|
+
"registry": "https://registry.npmjs.org/"
|
|
10
|
+
},
|
|
11
|
+
"engines": {
|
|
12
|
+
"node": "^22.15.0 || ^24.0.0 || ^26.0.0"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/pavel-voronin/openui-telegram.git"
|
|
17
|
+
},
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./client": {
|
|
25
|
+
"types": "./dist/client.d.ts",
|
|
26
|
+
"import": "./dist/client.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "node scripts/build.mjs",
|
|
34
|
+
"typecheck": "npm run build && node scripts/build.mjs --support",
|
|
35
|
+
"test": "npm run typecheck && node --experimental-test-module-mocks --test test/*.test.js",
|
|
36
|
+
"test:coverage": "npm run typecheck && node --experimental-test-coverage --experimental-test-module-mocks --test-coverage-include=dist/** --test-coverage-lines=100 --test-coverage-branches=100 --test-coverage-functions=100 --test test/*.test.js",
|
|
37
|
+
"check:package": "node scripts/check-package.mjs",
|
|
38
|
+
"check": "npm run test:coverage && npm run check:package",
|
|
39
|
+
"demo:simple": "npm run typecheck && node examples/demo-server.mjs simple",
|
|
40
|
+
"demo:full": "npm run typecheck && node examples/demo-server.mjs full",
|
|
41
|
+
"demo:telegram": "npm run typecheck && node examples/telegram.mjs",
|
|
42
|
+
"prepare": "npm run build",
|
|
43
|
+
"prepack": "npm run build"
|
|
44
|
+
},
|
|
45
|
+
"devDependencies": {
|
|
46
|
+
"@types/node": "^22.20.5",
|
|
47
|
+
"linkedom": "^0.18.15",
|
|
48
|
+
"typescript": "^7.0.2",
|
|
49
|
+
"zod": "^4.6.7"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@openuidev/lang-core": "1.1.0",
|
|
53
|
+
"acorn": "^8.19.0",
|
|
54
|
+
"diff": "^9.0.0",
|
|
55
|
+
"esbuild": "^0.25.12"
|
|
56
|
+
},
|
|
57
|
+
"peerDependencies": {
|
|
58
|
+
"zod": "^4.0.0"
|
|
59
|
+
}
|
|
60
|
+
}
|