@drawmotive/textgraph 0.1.0-alpha.1

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 (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +92 -0
  3. package/docs/api-v1.md +87 -0
  4. package/docs/releases.md +100 -0
  5. package/examples/browser.html +5 -0
  6. package/examples/browser.js +4 -0
  7. package/examples/node.mjs +4 -0
  8. package/examples/render-png.mjs +11 -0
  9. package/examples/worker.js +6 -0
  10. package/generated/wasm/DrawMotive.TextGraph.Bridge.runtimeconfig.json +52 -0
  11. package/generated/wasm/DrawMotive.TextGraph.Bridge.wasm +0 -0
  12. package/generated/wasm/FuzzyBubbles-LICENSE.txt +93 -0
  13. package/generated/wasm/FuzzyBubbles-Regular.ttf +0 -0
  14. package/generated/wasm/Graphics.Core.wasm +0 -0
  15. package/generated/wasm/Grpc.Core.Api.wasm +0 -0
  16. package/generated/wasm/HarfBuzzSharp.wasm +0 -0
  17. package/generated/wasm/MagicOnion.Abstractions.wasm +0 -0
  18. package/generated/wasm/MagicOnion.Serialization.MessagePack.wasm +0 -0
  19. package/generated/wasm/MagicOnion.Shared.wasm +0 -0
  20. package/generated/wasm/Markdig.wasm +0 -0
  21. package/generated/wasm/Math.Core.wasm +0 -0
  22. package/generated/wasm/MemoryPack.Core.wasm +0 -0
  23. package/generated/wasm/MessagePack.Annotations.wasm +0 -0
  24. package/generated/wasm/MessagePack.wasm +0 -0
  25. package/generated/wasm/Microsoft.AspNetCore.Components.Web.wasm +0 -0
  26. package/generated/wasm/Microsoft.Extensions.DependencyInjection.Abstractions.wasm +0 -0
  27. package/generated/wasm/Microsoft.Extensions.DependencyInjection.wasm +0 -0
  28. package/generated/wasm/Microsoft.Extensions.Hosting.Abstractions.wasm +0 -0
  29. package/generated/wasm/Microsoft.Extensions.Logging.Abstractions.wasm +0 -0
  30. package/generated/wasm/Microsoft.Extensions.Logging.wasm +0 -0
  31. package/generated/wasm/Microsoft.Extensions.Options.wasm +0 -0
  32. package/generated/wasm/Microsoft.Extensions.Primitives.wasm +0 -0
  33. package/generated/wasm/Microsoft.NET.StringTools.wasm +0 -0
  34. package/generated/wasm/Nanoid.wasm +0 -0
  35. package/generated/wasm/NotoSans-LICENSE.txt +93 -0
  36. package/generated/wasm/NotoSans-Regular.ttf +0 -0
  37. package/generated/wasm/Polly.Core.wasm +0 -0
  38. package/generated/wasm/Polly.wasm +0 -0
  39. package/generated/wasm/R3.BlazorWebAssembly.wasm +0 -0
  40. package/generated/wasm/R3.wasm +0 -0
  41. package/generated/wasm/RBush.wasm +0 -0
  42. package/generated/wasm/SkiaSharp.HarfBuzz.wasm +0 -0
  43. package/generated/wasm/SkiaSharp.wasm +0 -0
  44. package/generated/wasm/Stateless.wasm +0 -0
  45. package/generated/wasm/System.Collections.Concurrent.wasm +0 -0
  46. package/generated/wasm/System.Collections.Immutable.wasm +0 -0
  47. package/generated/wasm/System.Collections.NonGeneric.wasm +0 -0
  48. package/generated/wasm/System.Collections.Specialized.wasm +0 -0
  49. package/generated/wasm/System.Collections.wasm +0 -0
  50. package/generated/wasm/System.ComponentModel.Annotations.wasm +0 -0
  51. package/generated/wasm/System.ComponentModel.Primitives.wasm +0 -0
  52. package/generated/wasm/System.ComponentModel.TypeConverter.wasm +0 -0
  53. package/generated/wasm/System.ComponentModel.wasm +0 -0
  54. package/generated/wasm/System.Console.wasm +0 -0
  55. package/generated/wasm/System.Diagnostics.StackTrace.wasm +0 -0
  56. package/generated/wasm/System.Diagnostics.Tracing.wasm +0 -0
  57. package/generated/wasm/System.IO.Compression.Brotli.wasm +0 -0
  58. package/generated/wasm/System.IO.Compression.wasm +0 -0
  59. package/generated/wasm/System.IO.Pipelines.wasm +0 -0
  60. package/generated/wasm/System.Linq.Expressions.wasm +0 -0
  61. package/generated/wasm/System.Linq.wasm +0 -0
  62. package/generated/wasm/System.Memory.wasm +0 -0
  63. package/generated/wasm/System.Net.Http.wasm +0 -0
  64. package/generated/wasm/System.Net.Primitives.wasm +0 -0
  65. package/generated/wasm/System.Numerics.Vectors.wasm +0 -0
  66. package/generated/wasm/System.ObjectModel.wasm +0 -0
  67. package/generated/wasm/System.Private.CoreLib.wasm +0 -0
  68. package/generated/wasm/System.Private.Uri.wasm +0 -0
  69. package/generated/wasm/System.Reflection.Emit.ILGeneration.wasm +0 -0
  70. package/generated/wasm/System.Reflection.Emit.wasm +0 -0
  71. package/generated/wasm/System.Reflection.Primitives.wasm +0 -0
  72. package/generated/wasm/System.Runtime.InteropServices.JavaScript.wasm +0 -0
  73. package/generated/wasm/System.Runtime.InteropServices.wasm +0 -0
  74. package/generated/wasm/System.Runtime.Intrinsics.wasm +0 -0
  75. package/generated/wasm/System.Runtime.Loader.wasm +0 -0
  76. package/generated/wasm/System.Runtime.Numerics.wasm +0 -0
  77. package/generated/wasm/System.Runtime.Serialization.Primitives.wasm +0 -0
  78. package/generated/wasm/System.Runtime.wasm +0 -0
  79. package/generated/wasm/System.Security.Cryptography.wasm +0 -0
  80. package/generated/wasm/System.Text.Encoding.Extensions.wasm +0 -0
  81. package/generated/wasm/System.Text.Encodings.Web.wasm +0 -0
  82. package/generated/wasm/System.Text.Json.wasm +0 -0
  83. package/generated/wasm/System.Text.RegularExpressions.wasm +0 -0
  84. package/generated/wasm/System.Threading.Channels.wasm +0 -0
  85. package/generated/wasm/System.Threading.Thread.wasm +0 -0
  86. package/generated/wasm/System.Threading.ThreadPool.wasm +0 -0
  87. package/generated/wasm/System.Threading.wasm +0 -0
  88. package/generated/wasm/System.wasm +0 -0
  89. package/generated/wasm/ZstdSharp.wasm +0 -0
  90. package/generated/wasm/dotnet.boot.js +443 -0
  91. package/generated/wasm/dotnet.js +4 -0
  92. package/generated/wasm/dotnet.native.js +5513 -0
  93. package/generated/wasm/dotnet.native.wasm +0 -0
  94. package/generated/wasm/dotnet.runtime.js +4 -0
  95. package/generated/wasm/main.mjs +5 -0
  96. package/generated/wasm/netstandard.wasm +0 -0
  97. package/generated/wasm/themes.css +85 -0
  98. package/generated/wasm-manifest.js +572 -0
  99. package/generated/wasm-manifest.json +572 -0
  100. package/package.json +66 -0
  101. package/schemas/render.schema.json +167 -0
  102. package/schemas/validation.schema.json +70 -0
  103. package/schemas/wasm-manifest.schema.json +208 -0
  104. package/src/adapters.js +27 -0
  105. package/src/index.d.ts +171 -0
  106. package/src/index.js +78 -0
  107. package/src/platform/browser.d.ts +13 -0
  108. package/src/platform/browser.js +13 -0
  109. package/src/platform/node.d.ts +3 -0
  110. package/src/platform/node.js +12 -0
  111. package/src/platform/worker.d.ts +3 -0
  112. package/src/platform/worker.js +4 -0
  113. package/src/runtime/assets.js +27 -0
  114. package/src/runtime/browser.js +41 -0
  115. package/src/runtime/dotnet.js +100 -0
  116. package/src/runtime/errors.js +14 -0
  117. package/src/runtime/integrity.js +12 -0
  118. package/src/runtime/language-packs.js +32 -0
  119. package/src/runtime/lifecycle.js +84 -0
  120. package/src/runtime/manifest.js +60 -0
  121. package/src/runtime/node.js +21 -0
  122. package/src/runtime/render-resources.js +51 -0
  123. package/src/runtime/rendering.js +66 -0
  124. package/src/runtime/shared.js +17 -0
  125. package/src/runtime/validation.js +38 -0
package/src/index.js ADDED
@@ -0,0 +1,78 @@
1
+ import { validateTextGraphAdapters } from './adapters.js';
2
+ import { createManagedInstance, throwIfAborted, validateAbi, waitForInitialResource } from './runtime/lifecycle.js';
3
+ import { decodeValidation, validateSignal } from './runtime/validation.js';
4
+ import { DrawMotiveError } from './runtime/errors.js';
5
+ import { normalizeLanguagePacks } from './runtime/language-packs.js';
6
+ import { decodeRender, normalizeRenderOptions } from './runtime/rendering.js';
7
+ import manifest from '../generated/wasm-manifest.js';
8
+
9
+ export { validateTextGraphAdapters } from './adapters.js';
10
+ export { DrawMotiveError } from './runtime/errors.js';
11
+ export { resolveRuntimeAssets } from './runtime/assets.js';
12
+
13
+ const freeze = value => {
14
+ if (value && typeof value === 'object') { Object.values(value).forEach(freeze); Object.freeze(value); }
15
+ return value;
16
+ };
17
+ export const abiManifest = freeze(structuredClone(manifest));
18
+
19
+ /** Initializes one headless TextGraph instance through the selected platform loader. */
20
+ export async function initializeTextGraph(options = {}) {
21
+ if (!options || typeof options !== 'object') throw new DrawMotiveError('INVALID_ARGUMENT', 'Initialization options must be an object');
22
+ for (const key of ['resolveAsset', 'fetch', 'loadRuntime']) {
23
+ if (options[key] !== undefined && typeof options[key] !== 'function') throw new DrawMotiveError('INVALID_ARGUMENT', `${key} must be a function`);
24
+ }
25
+ validateSignal(options.signal);
26
+ if (typeof options.loadRuntime !== 'function') {
27
+ throw new TypeError('initializeTextGraph requires a loadRuntime function');
28
+ }
29
+ const normalized = Object.freeze({ ...options, adapters: validateTextGraphAdapters(options.adapters), languagePacks: normalizeLanguagePacks(options.languagePacks) });
30
+ const runtime = await waitForInitialResource(() => options.loadRuntime(normalized), options.signal);
31
+ await validateAbi(runtime, abiManifest.abiVersion);
32
+ const instance = createManagedInstance([runtime]);
33
+ const capabilities = runtime.capabilities ?? [
34
+ ...(typeof runtime.validate === 'function' ? ['textgraph-validate-v1'] : []),
35
+ ...(typeof runtime.execute === 'function' ? ['textgraph-render-v1'] : []),
36
+ ];
37
+ const info = Object.freeze({ packageName: abiManifest.packageName, packageVersion: abiManifest.packageVersion, abiVersion: runtime.abiVersion, protocolVersion: 1, capabilities: Object.freeze([...capabilities]) });
38
+ return Object.assign(instance, {
39
+ info,
40
+ async renderPng(source, callOptions = {}) {
41
+ if (instance.state !== 'ready') throw new DrawMotiveError('INSTANCE_DISPOSED', 'The runtime instance is disposing or disposed');
42
+ const { request, encoding, signal } = normalizeRenderOptions(source, callOptions);
43
+ throwIfAborted(signal);
44
+ if (typeof runtime.execute !== 'function' || !capabilities.includes('textgraph-render-v1')) throw new DrawMotiveError('UNSUPPORTED_CAPABILITY', 'Runtime does not support PNG rendering');
45
+ return instance.mutate(async () => {
46
+ throwIfAborted(signal);
47
+ let response;
48
+ try { response = await runtime.execute(JSON.stringify(request), { signal }); }
49
+ catch (cause) {
50
+ throwIfAborted(signal);
51
+ if (cause instanceof DrawMotiveError || cause?.name === 'AbortError') throw cause;
52
+ throw new DrawMotiveError('RUNTIME_FAILED', 'PNG rendering could not execute', { cause });
53
+ }
54
+ throwIfAborted(signal);
55
+ return decodeRender(response, encoding);
56
+ });
57
+ },
58
+ async validate(source, callOptions = {}) {
59
+ if (instance.state !== 'ready') throw new DrawMotiveError('INSTANCE_DISPOSED', 'The runtime instance is disposing or disposed');
60
+ if (typeof source !== 'string' || !callOptions || typeof callOptions !== 'object') throw new DrawMotiveError('INVALID_ARGUMENT', 'Validation requires a source string and options object');
61
+ validateSignal(callOptions.signal);
62
+ throwIfAborted(callOptions.signal);
63
+ if (typeof runtime.validate !== 'function') throw new DrawMotiveError('UNSUPPORTED_CAPABILITY', 'Runtime does not support validation');
64
+ return instance.mutate(async () => {
65
+ throwIfAborted(callOptions.signal);
66
+ let response;
67
+ try { response = await runtime.validate(source); }
68
+ catch (cause) {
69
+ throwIfAborted(callOptions.signal);
70
+ if (cause?.name === 'AbortError') throw cause;
71
+ throw new DrawMotiveError('RUNTIME_FAILED', 'Validation could not execute', { cause });
72
+ }
73
+ throwIfAborted(callOptions.signal);
74
+ return decodeValidation(response);
75
+ });
76
+ },
77
+ });
78
+ }
@@ -0,0 +1,13 @@
1
+ export * from '../index.js';
2
+
3
+ import type { RuntimeAsset, RuntimeAssetLoader, RuntimeAssetManifest } from '../index.js';
4
+
5
+ export declare const platform: 'browser';
6
+
7
+ export declare function createTextGraphRuntimeLoader(options: {
8
+ manifest: RuntimeAssetManifest;
9
+ moduleUrl?: string | URL;
10
+ baseUrl?: string | URL;
11
+ resolveAsset?(asset: RuntimeAsset, defaultUrl: URL): string | URL;
12
+ fetch?: typeof fetch;
13
+ }): RuntimeAssetLoader;
@@ -0,0 +1,13 @@
1
+ export * from '../index.js';
2
+ import { initializeTextGraph as initialize } from '../index.js';
3
+ import { loadBrowserRuntime } from '../runtime/browser.js';
4
+ import manifest from '../../generated/wasm-manifest.js';
5
+ import { DrawMotiveError } from '../runtime/errors.js';
6
+
7
+ export async function initializeTextGraph(options = {}) {
8
+ if (!options || typeof options !== 'object') throw new DrawMotiveError('INVALID_ARGUMENT', 'Initialization options must be an object');
9
+ return initialize({ ...options, loadRuntime: options.loadRuntime ?? (context => loadBrowserRuntime(context, manifest)) });
10
+ }
11
+ export { createTextGraphRuntimeLoader } from '../runtime/browser.js';
12
+
13
+ export const platform = 'browser';
@@ -0,0 +1,3 @@
1
+ export * from '../index.js';
2
+
3
+ export declare const platform: 'node';
@@ -0,0 +1,12 @@
1
+ export * from '../index.js';
2
+ import { initializeTextGraph as initialize } from '../index.js';
3
+ import { loadNodeRuntime } from '../runtime/node.js';
4
+ import manifest from '../../generated/wasm-manifest.js';
5
+ import { DrawMotiveError } from '../runtime/errors.js';
6
+
7
+ export async function initializeTextGraph(options = {}) {
8
+ if (!options || typeof options !== 'object') throw new DrawMotiveError('INVALID_ARGUMENT', 'Initialization options must be an object');
9
+ return initialize({ ...options, loadRuntime: options.loadRuntime ?? (context => loadNodeRuntime(context, manifest)) });
10
+ }
11
+
12
+ export const platform = 'node';
@@ -0,0 +1,3 @@
1
+ export * from './browser.js';
2
+
3
+ export declare const platform: 'worker';
@@ -0,0 +1,4 @@
1
+ export * from './browser.js';
2
+ export { createTextGraphRuntimeLoader } from '../runtime/browser.js';
3
+
4
+ export const platform = 'worker';
@@ -0,0 +1,27 @@
1
+ import { DrawMotiveError } from './errors.js';
2
+
3
+ /** Resolves manifest assets relative to this package without ambient base URLs. */
4
+ export function resolveRuntimeAssets({ manifest, moduleUrl = import.meta.url, resolveAsset }) {
5
+ if (!Array.isArray(manifest?.assets)) {
6
+ throw new DrawMotiveError('INVALID_MANIFEST', 'The runtime manifest must contain an assets array');
7
+ }
8
+ const generatedBase = new URL('../../generated/', new URL(moduleUrl));
9
+ return manifest.assets.map((asset) => {
10
+ const defaultUrl = new URL(asset.path, generatedBase);
11
+ const override = resolveAsset ? resolveAsset(asset, defaultUrl) : defaultUrl;
12
+ return Object.freeze({ asset, url: absoluteUrl(override, asset.path) });
13
+ });
14
+ }
15
+
16
+ function absoluteUrl(value, assetPath) {
17
+ try {
18
+ const url = value instanceof URL ? new URL(value.href) : new URL(value);
19
+ if (!url.protocol) throw new TypeError('Missing protocol');
20
+ return url;
21
+ } catch (cause) {
22
+ throw new DrawMotiveError('INVALID_ASSET_URL', `Asset ${assetPath} must resolve to an absolute URL`, {
23
+ cause,
24
+ details: { asset: assetPath, value: String(value) },
25
+ });
26
+ }
27
+ }
@@ -0,0 +1,41 @@
1
+ import { createRuntimeAssetPlan } from './shared.js';
2
+ import { DrawMotiveError } from './errors.js';
3
+ import { startTextGraphRuntime } from './dotnet.js';
4
+
5
+ /** Browser and module Workers share URL-based startup without DOM dependencies. */
6
+ export function loadBrowserRuntime(options, manifest) {
7
+ const fetchResource = options.fetch ?? options.adapters?.network?.fetch ?? globalThis.fetch;
8
+ return startTextGraphRuntime(options, manifest, async (item, context) => {
9
+ try {
10
+ const response = await fetchResource(item.url, { signal: context.signal });
11
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
12
+ return response;
13
+ } catch (cause) {
14
+ if (cause?.name === 'AbortError') throw cause;
15
+ throw new DrawMotiveError('RESOURCE_NOT_FOUND', `Could not load ${item.asset.path}`, { cause, details: { asset: item.asset.path } });
16
+ }
17
+ });
18
+ }
19
+
20
+ /** Creates the browser-compatible loader for TextGraph's package-owned runtime assets. */
21
+ export function createTextGraphRuntimeLoader(options) {
22
+ const fetchResource = options.fetch ?? globalThis.fetch;
23
+ if (typeof fetchResource !== 'function') throw new TypeError('A fetch implementation is required');
24
+ const plan = createRuntimeAssetPlan(options);
25
+ return {
26
+ plan,
27
+ async loadAssets() {
28
+ const loaded = new Map();
29
+ for (const item of plan) {
30
+ const response = await fetchResource(item.url);
31
+ if (!response.ok) {
32
+ throw new DrawMotiveError('RESOURCE_NOT_FOUND', `Failed to load ${item.asset.path}: HTTP ${response.status}`, {
33
+ details: { asset: item.asset.path, status: response.status, url: item.url.href },
34
+ });
35
+ }
36
+ loaded.set(item.asset.path, { ...item.asset, url: item.url, bytes: new Uint8Array(await response.arrayBuffer()) });
37
+ }
38
+ return loaded;
39
+ },
40
+ };
41
+ }
@@ -0,0 +1,100 @@
1
+ import { createRuntimeAssetPlan } from './shared.js';
2
+ import { DrawMotiveError } from './errors.js';
3
+ import { validateManifest, validateBootConfig, validationCapability, renderingCapability, isRuntimeAsset } from './manifest.js';
4
+ import { throwIfAborted } from './lifecycle.js';
5
+ import { readVerifiedAsset } from './integrity.js';
6
+ import { createRenderingExecutor } from './render-resources.js';
7
+ import { decodeConfiguration } from './rendering.js';
8
+
9
+ /** Starts one isolated .NET module graph; platform loaders own all data I/O. */
10
+ export async function startTextGraphRuntime(options, manifest, readAsset) {
11
+ validateManifest(manifest);
12
+ throwIfAborted(options.signal);
13
+ if (!globalThis.crypto?.subtle || typeof globalThis.crypto.randomUUID !== 'function') {
14
+ throw new DrawMotiveError('UNSUPPORTED_ENVIRONMENT', 'TextGraph requires Web Crypto in HTTPS or localhost');
15
+ }
16
+ const plan = createRuntimeAssetPlan({ manifest: { assets: manifest.assets.filter(isRuntimeAsset) }, moduleUrl: import.meta.url, resolveAsset: options.resolveAsset });
17
+ const byName = new Map(plan.map(item => [item.asset.path.slice(5), item]));
18
+ const instanceKey = crypto.randomUUID();
19
+ const moduleUrl = item => {
20
+ const url = new URL(item.url);
21
+ // .NET ESM modules retain mutable initialization state, so each instance needs its own graph.
22
+ url.searchParams.set('textgraph-instance', instanceKey);
23
+ return url.href;
24
+ };
25
+ let bridge;
26
+ try {
27
+ // .NET treats download failures as fatal and may terminate a Node host. Own
28
+ // loading/cancellation here, before entering its non-cancellable startup.
29
+ const data = new Map();
30
+ for (const item of plan) {
31
+ throwIfAborted(options.signal);
32
+ if (/[.](mjs|js)$/.test(item.asset.path)) continue;
33
+ data.set(item.asset.path, await readVerifiedAsset(item, options, readAsset));
34
+ }
35
+ throwIfAborted(options.signal);
36
+ // Detect unsupported WASM features before the runtime takes ownership of failure handling.
37
+ await WebAssembly.compile(data.get(manifest.runtimeWasm));
38
+ // Import dependency modules before create(): its asynchronous imports otherwise
39
+ // turn missing modules into fatal unhandled runtime startup failures.
40
+ for (const item of plan) {
41
+ if (!/[.]js$/.test(item.asset.path) || item.asset.path === manifest.runtimeModule) continue;
42
+ let imported;
43
+ try { imported = await import(/* webpackIgnore: true */ /* @vite-ignore */ moduleUrl(item)); }
44
+ catch (cause) { throw new DrawMotiveError('RESOURCE_NOT_FOUND', `Could not import ${item.asset.path}`, { cause, details: { asset: item.asset.path } }); }
45
+ if (item.asset.path === 'wasm/dotnet.boot.js') validateBootConfig(imported.config, manifest);
46
+ }
47
+ throwIfAborted(options.signal);
48
+ const runtimeEntry = byName.get(manifest.runtimeModule.slice(5));
49
+ let dotnet;
50
+ try { ({ dotnet } = await import(/* webpackIgnore: true */ /* @vite-ignore */ moduleUrl(runtimeEntry))); }
51
+ catch (cause) { throw new DrawMotiveError('RESOURCE_NOT_FOUND', 'Could not import the runtime module', { cause, details: { asset: runtimeEntry.asset.path } }); }
52
+ const runtime = await dotnet.withResourceLoader((type, name) => {
53
+ const item = byName.get(name);
54
+ if (!item) throw new DrawMotiveError('RESOURCE_NOT_FOUND', 'Runtime requested an unlisted asset', { details: { asset: name } });
55
+ if (/[.](mjs|js)$/.test(name)) return moduleUrl(item);
56
+ return Promise.resolve(new Response(data.get(item.asset.path), { headers: { 'Content-Type': item.asset.mediaType } }));
57
+ }).create();
58
+ throwIfAborted(options.signal);
59
+ const exports = await runtime.getAssemblyExports(manifest.bridge.assembly);
60
+ bridge = manifest.bridge.type.split('.').reduce((value, key) => value?.[key], exports);
61
+ if (typeof bridge?.[manifest.bridge.info] !== 'function' || typeof bridge?.[manifest.bridge.validate] !== 'function') {
62
+ throw new DrawMotiveError('ABI_MISMATCH', 'Required bridge exports are missing');
63
+ }
64
+ let info;
65
+ try { info = JSON.parse(bridge[manifest.bridge.info]()); }
66
+ catch (cause) { throw new DrawMotiveError('ABI_MISMATCH', 'Invalid bridge handshake', { cause }); }
67
+ if (!info || info.abiVersion !== manifest.abiVersion || info.packageKind !== 'textgraph' || info.protocolVersion !== manifest.protocolVersion) {
68
+ throw new DrawMotiveError('ABI_MISMATCH', 'Bridge and manifest disagree');
69
+ }
70
+ if (!Array.isArray(info.capabilities) || !info.capabilities.includes(validationCapability)) throw new DrawMotiveError('UNSUPPORTED_CAPABILITY', 'Bridge does not implement validation');
71
+ const renders = manifest.capabilities.includes(renderingCapability);
72
+ if (renders && !info.capabilities.includes(renderingCapability)) throw new DrawMotiveError('UNSUPPORTED_CAPABILITY', 'Bridge does not implement rendering');
73
+ if (renders && typeof bridge[manifest.bridge.execute] !== 'function') throw new DrawMotiveError('ABI_MISMATCH', 'Rendering bridge export is missing');
74
+ return createBridgeRuntime({ manifest, options, readAsset, bridge, info });
75
+ } catch (cause) {
76
+ bridge = undefined;
77
+ if (cause instanceof DrawMotiveError || cause?.name === 'AbortError') throw cause;
78
+ throw new DrawMotiveError('INITIALIZATION_FAILED', 'Could not initialize TextGraph WASM', { cause });
79
+ }
80
+ }
81
+
82
+ /** Owns managed bridge resources; the instance queue drains calls before invoking disposal. */
83
+ export function createBridgeRuntime({ manifest, options, readAsset, bridge, info }) {
84
+ const renders = manifest.capabilities.includes(renderingCapability);
85
+ return {
86
+ abiVersion: info.abiVersion, capabilities: info.capabilities,
87
+ validate: source => bridge[manifest.bridge.validate](source),
88
+ ...(renders ? { execute: createRenderingExecutor({ manifest, options, readAsset, execute: request => bridge[manifest.bridge.execute](request) }) } : {}),
89
+ async dispose() {
90
+ if (!bridge) return;
91
+ try {
92
+ // Release native font caches and their timers without .NET exit(), which terminates Node.
93
+ if (renders) decodeConfiguration(await bridge[manifest.bridge.execute](JSON.stringify({ protocolVersion: 1, operation: 'dispose' })), 'disposal');
94
+ } catch (cause) {
95
+ if (cause instanceof DrawMotiveError) throw cause;
96
+ throw new DrawMotiveError('RUNTIME_FAILED', 'Native resource disposal failed', { cause });
97
+ } finally { bridge = undefined; }
98
+ },
99
+ };
100
+ }
@@ -0,0 +1,14 @@
1
+ /** Represents a stable public DrawMotive runtime failure. */
2
+ export class DrawMotiveError extends Error {
3
+ constructor(code, message, options = {}) {
4
+ super(message, { cause: options.cause });
5
+ this.name = 'DrawMotiveError';
6
+ this.code = code;
7
+ this.details = Object.freeze({ ...(options.details ?? {}) });
8
+ }
9
+ }
10
+
11
+ /** Creates the standard cancellation error used by all package entrypoints. */
12
+ export function createAbortError() {
13
+ return new DOMException('The operation was aborted', 'AbortError');
14
+ }
@@ -0,0 +1,12 @@
1
+ import { DrawMotiveError } from './errors.js';
2
+
3
+ /** Check package-owned data before passing it to native code, including lazy render resources. */
4
+ export async function readVerifiedAsset(item, options, readAsset) {
5
+ const response = await readAsset(item, options);
6
+ const bytes = new Uint8Array(await response.arrayBuffer());
7
+ const hash = Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', bytes)), byte => byte.toString(16).padStart(2, '0')).join('');
8
+ if (bytes.byteLength !== item.asset.bytes || hash !== item.asset.sha256) {
9
+ throw new DrawMotiveError('ASSET_INTEGRITY_MISMATCH', `Asset integrity failed: ${item.asset.path}`, { details: { asset: item.asset.path } });
10
+ }
11
+ return bytes;
12
+ }
@@ -0,0 +1,32 @@
1
+ import { DrawMotiveError } from './errors.js';
2
+
3
+ const defaultFamilies = ['NotoSans-Regular', 'FuzzyBubbles-Regular'];
4
+
5
+ /** Copy caller-owned font descriptors before asynchronous startup without performing font I/O. */
6
+ export function normalizeLanguagePacks(packs = []) {
7
+ const invalid = message => { throw new DrawMotiveError('INVALID_ARGUMENT', message); };
8
+ if (!Array.isArray(packs)) invalid('languagePacks must be an array');
9
+ const families = new Set(defaultFamilies);
10
+ const normalized = packs.map(pack => {
11
+ if (!pack || !Array.isArray(pack.fonts) || pack.fonts.length === 0) invalid('Each language pack must contain a non-empty fonts array');
12
+ const fonts = pack.fonts.map(font => {
13
+ if (!font || typeof font.family !== 'string' || !font.family.trim() || font.family !== font.family.trim()) invalid('Font family must be a non-empty trimmed string');
14
+ if (families.has(font.family)) invalid(`Duplicate font family: ${font.family}`);
15
+ families.add(font.family);
16
+ let source;
17
+ if (font.source instanceof URL) {
18
+ if (!['https:', 'http:', 'file:', 'data:', 'blob:'].includes(font.source.protocol)) invalid(`Unsupported font URL protocol: ${font.source.protocol}`);
19
+ source = new URL(font.source.href);
20
+ } else if (font.source instanceof Uint8Array && font.source.byteLength > 0) source = new Uint8Array(font.source);
21
+ else invalid(`Font ${font.family} source must be a URL or non-empty Uint8Array`);
22
+ return Object.freeze({ family: font.family, source });
23
+ });
24
+ const fallbackFamilies = pack.fallbackFamilies ?? [];
25
+ if (!Array.isArray(fallbackFamilies) || !fallbackFamilies.every(family => typeof family === 'string' && family.trim() && family === family.trim())) invalid('fallbackFamilies must be an array of font family names');
26
+ return Object.freeze({ fonts: Object.freeze(fonts), fallbackFamilies: Object.freeze([...fallbackFamilies]) });
27
+ });
28
+ for (const pack of normalized) for (const family of pack.fallbackFamilies) {
29
+ if (!families.has(family)) invalid(`Fallback font family is not configured: ${family}`);
30
+ }
31
+ return Object.freeze(normalized);
32
+ }
@@ -0,0 +1,84 @@
1
+ import { createAbortError, DrawMotiveError } from './errors.js';
2
+
3
+ /** Throws before host work begins when initialization was already cancelled. */
4
+ export function throwIfAborted(signal) {
5
+ if (signal?.aborted) throw createAbortError();
6
+ }
7
+
8
+ /** Waits for an initialization resource while guaranteeing cleanup after cancellation. */
9
+ export async function waitForInitialResource(factory, signal) {
10
+ throwIfAborted(signal);
11
+ const resourcePromise = Promise.resolve().then(factory);
12
+ if (!signal) return resourcePromise;
13
+
14
+ let rejectAbort;
15
+ const abortPromise = new Promise((_, reject) => { rejectAbort = reject; });
16
+ const abort = () => rejectAbort(createAbortError());
17
+ signal.addEventListener('abort', abort, { once: true });
18
+ try {
19
+ return await Promise.race([resourcePromise, abortPromise]);
20
+ } catch (error) {
21
+ if (error?.name === 'AbortError') {
22
+ void resourcePromise.then((resource) => resource?.dispose?.()).catch(() => undefined);
23
+ }
24
+ throw error;
25
+ } finally {
26
+ signal.removeEventListener('abort', abort);
27
+ }
28
+ }
29
+
30
+ /** Verifies the runtime boundary before exposing an instance to the host. */
31
+ export async function validateAbi(resource, expectedAbi) {
32
+ if (resource?.abiVersion === expectedAbi) return resource;
33
+ await resource?.dispose?.();
34
+ throw new DrawMotiveError('ABI_MISMATCH', `Expected ABI ${expectedAbi} but received ${resource?.abiVersion ?? 'none'}`, {
35
+ details: { expected: expectedAbi, actual: resource?.abiVersion ?? null },
36
+ });
37
+ }
38
+
39
+ /** Wraps a runtime in an isolated read/write scheduler with deterministic disposal. */
40
+ export function createManagedInstance(resources) {
41
+ let state = 'ready';
42
+ let mutationTail = Promise.resolve();
43
+ let disposePromise;
44
+ const active = new Set();
45
+
46
+ const ensureReady = () => {
47
+ if (state !== 'ready') {
48
+ throw new DrawMotiveError('INSTANCE_DISPOSED', 'The runtime instance is disposing or disposed', { details: { state } });
49
+ }
50
+ };
51
+ const track = (promise) => {
52
+ const tracked = Promise.resolve(promise);
53
+ active.add(tracked);
54
+ void tracked.finally(() => active.delete(tracked)).catch(() => undefined);
55
+ return tracked;
56
+ };
57
+
58
+ return {
59
+ get state() { return state; },
60
+ async readonly(operation) {
61
+ ensureReady();
62
+ return track(Promise.resolve().then(operation));
63
+ },
64
+ async mutate(operation) {
65
+ ensureReady();
66
+ const result = mutationTail.then(operation);
67
+ mutationTail = result.catch(() => undefined);
68
+ return track(result);
69
+ },
70
+ dispose() {
71
+ if (disposePromise) return disposePromise;
72
+ state = 'disposing';
73
+ disposePromise = (async () => {
74
+ await Promise.allSettled([...active]);
75
+ try {
76
+ const results = await Promise.allSettled(resources.map(resource => Promise.resolve().then(() => resource?.dispose?.())));
77
+ const failure = results.find(result => result.status === 'rejected');
78
+ if (failure) throw failure.reason;
79
+ } finally { state = 'disposed'; }
80
+ })();
81
+ return disposePromise;
82
+ },
83
+ };
84
+ }
@@ -0,0 +1,60 @@
1
+ import { DrawMotiveError } from './errors.js';
2
+
3
+ export const validationCapability = 'textgraph-validate-v1';
4
+ export const renderingCapability = 'textgraph-render-v1';
5
+
6
+ /** Only native runtime formats belong to startup; fonts, themes and licenses are separate assets. */
7
+ export function isRuntimeAsset(asset) {
8
+ return /[.](wasm|js|mjs|json|dat)$/.test(asset.path);
9
+ }
10
+
11
+ /** Boot metadata cannot expand the preflighted asset authority after native startup begins. */
12
+ export function validateBootConfig(config, manifest) {
13
+ if (config?.mainAssemblyName !== manifest.bridge.assembly) throw new DrawMotiveError('ABI_MISMATCH', 'Unexpected boot entry assembly');
14
+ const paths = new Set(manifest.assets.filter(isRuntimeAsset).map(asset => asset.path.slice(5)));
15
+ if (!config.resources || typeof config.resources !== 'object') throw new DrawMotiveError('INVALID_MANIFEST', 'Missing boot resources');
16
+ const visit = value => {
17
+ if (!value || typeof value !== 'object') return;
18
+ if (Object.hasOwn(value, 'name') && !paths.has(value.name)) throw new DrawMotiveError('INVALID_MANIFEST', 'Boot resource is absent from the asset manifest', { details: { asset: value.name } });
19
+ Object.values(value).forEach(visit);
20
+ };
21
+ visit(config.resources);
22
+ }
23
+
24
+ /** Reject incompatible bundles and ambiguous paths before any executable asset is imported. */
25
+ export function validateManifest(manifest) {
26
+ const invalid = () => { throw new DrawMotiveError('INVALID_MANIFEST', 'Invalid TextGraph runtime manifest'); };
27
+ if (!manifest || manifest.schemaVersion !== 1 || manifest.packageName !== '@drawmotive/textgraph'
28
+ || typeof manifest.packageVersion !== 'string' || !manifest.packageVersion || manifest.protocolVersion !== 1
29
+ || manifest.targetFramework !== 'net10.0' || !/^[a-f0-9]{40}$/.test(manifest.privateSource?.commit ?? '')
30
+ || manifest.privateSource?.project !== 'DrawMotive.TextGraph.Bridge' || !Array.isArray(manifest.assets)) invalid();
31
+ if (manifest.abiVersion !== '1.0.0') throw new DrawMotiveError('ABI_MISMATCH', 'Unsupported TextGraph ABI');
32
+ if (!Array.isArray(manifest.capabilities) || !manifest.capabilities.every(item => typeof item === 'string')) invalid();
33
+ if (!manifest.capabilities.includes(validationCapability)) throw new DrawMotiveError('UNSUPPORTED_CAPABILITY', 'Validation capability is missing');
34
+ const paths = new Set();
35
+ for (const asset of manifest.assets) {
36
+ if (!asset || !/^wasm[/][A-Za-z0-9_-][A-Za-z0-9._-]*$/.test(asset.path) || paths.has(asset.path)
37
+ || /[.](pdb|map|br|gz|cs)$/.test(asset.path) || !/^[a-f0-9]{64}$/.test(asset.sha256)
38
+ || !Number.isSafeInteger(asset.bytes) || asset.bytes < 0 || typeof asset.mediaType !== 'string') invalid();
39
+ paths.add(asset.path);
40
+ }
41
+ for (const key of ['runtimeModule', 'entryAssembly', 'runtimeWasm', 'runtimeConfig']) if (!paths.has(manifest[key])) invalid();
42
+ if (!paths.has('wasm/dotnet.boot.js') || manifest.bridge?.assembly !== 'DrawMotive.TextGraph.Bridge.dll'
43
+ || manifest.bridge?.type !== 'DrawMotive.TextGraph.Bridge.Program' || manifest.bridge?.info !== 'GetRuntimeInfo'
44
+ || manifest.bridge?.validate !== 'Validate') invalid();
45
+ if (manifest.bridge.execute !== undefined && manifest.bridge.execute !== 'Execute') invalid();
46
+ if (manifest.capabilities.includes(renderingCapability) && (!manifest.rendering || manifest.bridge.execute !== 'Execute')) invalid();
47
+ if (manifest.rendering !== undefined) {
48
+ const rendering = manifest.rendering;
49
+ if (!rendering || !paths.has(rendering.theme) || !rendering.theme.endsWith('.css')
50
+ || !Array.isArray(rendering.fonts) || rendering.fonts.length === 0) invalid();
51
+ const families = new Set();
52
+ const fontPaths = new Set();
53
+ for (const font of rendering.fonts) {
54
+ if (!font || typeof font.family !== 'string' || !font.family.trim() || font.family !== font.family.trim()
55
+ || families.has(font.family) || fontPaths.has(font.asset) || !paths.has(font.asset) || !/[.](ttf|otf)$/.test(font.asset)) invalid();
56
+ families.add(font.family); fontPaths.add(font.asset);
57
+ }
58
+ }
59
+ return manifest;
60
+ }
@@ -0,0 +1,21 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { startTextGraphRuntime } from './dotnet.js';
3
+ import { DrawMotiveError } from './errors.js';
4
+
5
+ /** Node file loading is isolated from the browser module graph. */
6
+ export function loadNodeRuntime(options, manifest) {
7
+ return startTextGraphRuntime(options, manifest, async (item, context) => {
8
+ try {
9
+ if (item.url.protocol === 'file:') {
10
+ return new Response(await readFile(item.url, { signal: context.signal }), { headers: { 'Content-Type': item.asset.mediaType } });
11
+ }
12
+ const fetchResource = context.fetch ?? context.adapters?.network?.fetch ?? globalThis.fetch;
13
+ const response = await fetchResource(item.url, { signal: context.signal });
14
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
15
+ return response;
16
+ } catch (cause) {
17
+ if (cause?.name === 'AbortError') throw cause;
18
+ throw new DrawMotiveError('RESOURCE_NOT_FOUND', `Could not load ${item.asset.path}`, { cause, details: { asset: item.asset.path } });
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,51 @@
1
+ import { createRuntimeAssetPlan } from './shared.js';
2
+ import { readVerifiedAsset } from './integrity.js';
3
+ import { decodeConfiguration } from './rendering.js';
4
+ import { throwIfAborted } from './lifecycle.js';
5
+
6
+ function base64(bytes) {
7
+ const chunks = [];
8
+ for (let offset = 0; offset < bytes.length; offset += 32768) chunks.push(String.fromCharCode(...bytes.subarray(offset, offset + 32768)));
9
+ return btoa(chunks.join(''));
10
+ }
11
+
12
+ /** Preparation lives inside the instance operation queue; validation never triggers font I/O. */
13
+ export function createRenderingExecutor({ manifest, options, readAsset, execute }) {
14
+ let configured = false;
15
+ return async (request, context = {}) => {
16
+ const loadingOptions = { ...options, signal: context.signal };
17
+ throwIfAborted(context.signal);
18
+ if (!configured) {
19
+ const selected = new Set([manifest.rendering.theme, ...manifest.rendering.fonts.map(font => font.asset)]);
20
+ const plan = createRuntimeAssetPlan({ manifest: { assets: manifest.assets.filter(asset => selected.has(asset.path)) }, moduleUrl: import.meta.url, resolveAsset: options.resolveAsset });
21
+ const byPath = new Map(plan.map(item => [item.asset.path, item]));
22
+ const load = async path => {
23
+ throwIfAborted(context.signal);
24
+ const bytes = await readVerifiedAsset(byPath.get(path), loadingOptions, readAsset);
25
+ throwIfAborted(context.signal);
26
+ return bytes;
27
+ };
28
+ const theme = new TextDecoder('utf-8', { fatal: true }).decode(await load(manifest.rendering.theme));
29
+ const fonts = [];
30
+ for (const font of manifest.rendering.fonts) fonts.push({ family: font.family, data: base64(await load(font.asset)) });
31
+ const fallbackFamilies = [];
32
+ for (const pack of options.languagePacks ?? []) {
33
+ for (const font of pack.fonts) {
34
+ throwIfAborted(context.signal);
35
+ const bytes = font.source instanceof Uint8Array ? font.source : new Uint8Array(await (await readAsset({
36
+ asset: { path: `font:${font.family}`, mediaType: 'font/ttf' }, url: font.source,
37
+ }, loadingOptions)).arrayBuffer());
38
+ throwIfAborted(context.signal);
39
+ fonts.push({ family: font.family, data: base64(bytes) });
40
+ }
41
+ for (const family of pack.fallbackFamilies) if (!fallbackFamilies.includes(family)) fallbackFamilies.push(family);
42
+ }
43
+ throwIfAborted(context.signal);
44
+ decodeConfiguration(await execute(JSON.stringify({ protocolVersion: 1, operation: 'configure', theme, fonts, fallbackFamilies })));
45
+ // Native configuration is committed even when the caller cancels during that synchronous operation.
46
+ configured = true;
47
+ throwIfAborted(context.signal);
48
+ }
49
+ return execute(request);
50
+ };
51
+ }