@nyte-ai/host 0.0.18 → 0.0.19

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 (102) hide show
  1. package/THIRD-PARTY-NOTICES.md +66 -0
  2. package/dist/chunks/{canvas-preview-5hxat7hh.js → canvas-preview-11nadadc.js} +8 -6
  3. package/dist/chunks/{canvas-preview-rp6fz0w5.js → canvas-preview-2jwbe14k.js} +5 -5
  4. package/dist/chunks/{canvas-preview-m02v4yy6.js → canvas-preview-55s0y68g.js} +3 -2
  5. package/dist/chunks/{canvas-preview-xn10d4f8.js → canvas-preview-5ekjx6sw.js} +9 -4
  6. package/dist/chunks/{canvas-preview-75db60xy.js → canvas-preview-8xc0vn7v.js} +5 -4
  7. package/dist/chunks/{canvas-preview-zn2d0k9d.js → canvas-preview-af86jz8t.js} +14 -6
  8. package/dist/chunks/{canvas-preview-d9wwhbtz.js → canvas-preview-aq5j4p25.js} +1 -1
  9. package/dist/chunks/{canvas-preview-j1tnf51w.js → canvas-preview-b2v8wfnz.js} +1 -1
  10. package/dist/chunks/canvas-preview-befkjea2.js +60 -0
  11. package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
  12. package/dist/chunks/{canvas-preview-vax9ne4e.js → canvas-preview-cawy6vbz.js} +22 -11
  13. package/dist/chunks/{canvas-preview-senpjwep.js → canvas-preview-crmzxxt8.js} +3 -3
  14. package/dist/chunks/{canvas-preview-ymgz0937.js → canvas-preview-drctegz4.js} +8 -4
  15. package/dist/chunks/{canvas-preview-yrfz2ak6.js → canvas-preview-e3vnkkc5.js} +6 -6
  16. package/dist/chunks/canvas-preview-ec2mt7es.js +108 -0
  17. package/dist/chunks/{canvas-preview-1ta3avah.js → canvas-preview-fxa0rfb0.js} +10 -5
  18. package/dist/chunks/{canvas-preview-8xgt67ka.js → canvas-preview-g5fkjs34.js} +1 -1
  19. package/dist/chunks/{canvas-preview-g3qj3y3k.js → canvas-preview-jeddjex8.js} +13 -3
  20. package/dist/chunks/{canvas-preview-0nrnbt76.js → canvas-preview-k1yg4fgp.js} +199 -125
  21. package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
  22. package/dist/chunks/{canvas-preview-ewgspmsd.js → canvas-preview-n2a35zv5.js} +1 -1
  23. package/dist/chunks/{canvas-preview-h8hshnff.js → canvas-preview-pkzqg96v.js} +1 -1
  24. package/dist/chunks/{canvas-preview-evh92ywd.js → canvas-preview-tnqe7k0b.js} +15 -10
  25. package/dist/chunks/{canvas-preview-ehf3vp31.js → canvas-preview-vm9zdp2t.js} +45 -58
  26. package/dist/chunks/{canvas-preview-76fwvd6e.js → canvas-preview-w54ymtb3.js} +1 -1
  27. package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
  28. package/dist/chunks/{canvas-preview-98aj5p34.js → canvas-preview-ycepb05s.js} +1 -1
  29. package/dist/src/canvas-preview.js +7 -3
  30. package/dist/src/deploy/cloudflare/worker.js +1 -1
  31. package/dist/src/deploy/vercel/server/routes/[...].js +2 -2
  32. package/dist/src/deploy/vercel/server/utils/host.js +1 -1
  33. package/dist/src/deploy/vercel/server/workflows/drive.js +3 -3
  34. package/dist/src/images/exif.d.ts +1 -0
  35. package/dist/src/images/exif.js +6 -0
  36. package/dist/src/images/node.d.ts +37 -0
  37. package/dist/src/images/node.js +12 -0
  38. package/dist/src/images/photon.d.ts +3 -0
  39. package/dist/src/images/photon.js +7 -0
  40. package/dist/src/images/worker.d.ts +1 -0
  41. package/dist/src/images/worker.js +23 -0
  42. package/dist/src/index.d.ts +0 -1
  43. package/dist/src/index.js +23 -22
  44. package/dist/src/listen-fetch.js +3 -3
  45. package/dist/src/listen.js +4 -4
  46. package/dist/src/local/command.d.ts +5 -0
  47. package/dist/src/local/command.js +6 -2
  48. package/dist/src/local/files.js +2 -2
  49. package/dist/src/local/git-watch.d.ts +1 -1
  50. package/dist/src/local/git-watch.js +1 -1
  51. package/dist/src/local/git.js +3 -3
  52. package/dist/src/local/github.js +2 -2
  53. package/dist/src/local/jobs.d.ts +1 -1
  54. package/dist/src/local/jobs.js +2 -1
  55. package/dist/src/local/provider.js +9 -9
  56. package/dist/src/local/snapshots.js +2 -2
  57. package/dist/src/local/worktrees.js +2 -2
  58. package/dist/src/mcp/connect.d.ts +0 -13
  59. package/dist/src/mcp/connect.js +1 -1
  60. package/dist/src/mcp/hub.js +2 -2
  61. package/dist/src/open.d.ts +2 -30
  62. package/dist/src/open.js +20 -18
  63. package/dist/src/serve/binding.js +1 -1
  64. package/dist/src/serve/handler.js +2 -2
  65. package/dist/src/sqlite.d.ts +2 -1
  66. package/dist/src/sqlite.js +4 -3
  67. package/dist/src/store-usage.js +4 -1
  68. package/dist/src/usage-history.d.ts +54 -0
  69. package/dist/src/usage-history.js +14 -0
  70. package/dist/src/usage-service.d.ts +2 -1
  71. package/dist/src/usage-service.js +7 -3
  72. package/dist/src/usage-worker.js +8 -4
  73. package/dist/src/usage.d.ts +150 -27
  74. package/dist/src/usage.js +12 -2
  75. package/package.json +16 -8
  76. package/src/canvas-preview.ts +4 -3
  77. package/src/images/exif.ts +69 -0
  78. package/src/images/node.ts +163 -0
  79. package/src/images/photon.ts +141 -0
  80. package/src/images/worker.ts +29 -0
  81. package/src/index.ts +19 -1
  82. package/src/local/command.ts +64 -12
  83. package/src/local/files.ts +8 -3
  84. package/src/local/git-watch.ts +16 -6
  85. package/src/local/git.ts +7 -2
  86. package/src/local/github.ts +3 -4
  87. package/src/local/jobs.ts +55 -69
  88. package/src/local/snapshots.ts +4 -2
  89. package/src/local/worktrees.ts +4 -4
  90. package/src/mcp/connect.ts +1 -14
  91. package/src/open.ts +16 -36
  92. package/src/serve/binding.ts +4 -3
  93. package/src/sqlite.ts +33 -8
  94. package/src/store-usage.ts +5 -2
  95. package/src/usage-history.ts +219 -0
  96. package/src/usage-service.ts +12 -3
  97. package/src/usage.ts +251 -176
  98. package/dist/chunks/canvas-preview-a3yw0j70.js +0 -72
  99. package/dist/chunks/canvas-preview-azg56359.js +0 -68
  100. package/dist/src/store-owner.d.ts +0 -15
  101. package/dist/src/store-owner.js +0 -9
  102. package/src/store-owner.ts +0 -98
@@ -1,10 +1,14 @@
1
- import"../chunks/canvas-preview-srgfpdmf.js";
2
1
  import"../chunks/canvas-preview-69fkk9yn.js";
3
- import"../chunks/canvas-preview-5hxat7hh.js";
4
- import"../chunks/canvas-preview-0nrnbt76.js";
2
+ import"../chunks/canvas-preview-zaaj99wd.js";
3
+ import"../chunks/canvas-preview-0cmbkp2j.js";
4
+ import"../chunks/canvas-preview-tnqe7k0b.js";
5
+ import"../chunks/canvas-preview-srgfpdmf.js";
6
+ import"../chunks/canvas-preview-11nadadc.js";
7
+ import"../chunks/canvas-preview-kp75gb63.js";
8
+ import"../chunks/canvas-preview-k1yg4fgp.js";
5
9
  import {
6
10
  UsageScanner2
7
- } from "../chunks/canvas-preview-1ta3avah.js";
11
+ } from "../chunks/canvas-preview-fxa0rfb0.js";
8
12
 
9
13
  // src/usage-worker.ts
10
14
  import { parentPort } from "node:worker_threads";
@@ -1,29 +1,83 @@
1
+ import { Type, type Static } from "typebox";
1
2
  import { type Models } from "@nyte-ai/ai";
2
- import type { LocalHistoryUsage } from "./usage-types.ts";
3
- import type { Usage } from "@nyte-ai/schema/model";
3
+ import { UsageScanCache } from "./usage-history.ts";
4
+ import type { LocalHistoryUsage, UsageWindow } from "./usage-types.ts";
4
5
  export interface ClaudeCodeUsageOptions {
5
6
  readonly models: Pick<Models, "getModels">;
7
+ readonly window: UsageWindow;
6
8
  readonly configDir?: string;
7
9
  readonly signal?: AbortSignal;
8
10
  readonly cache?: UsageScanCache<ClaudeCodeFileScan>;
9
11
  }
10
- export type UsageScanCache<T> = Map<string, FileScan<T>>;
11
- export interface FileScan<T> {
12
- readonly size: number;
13
- readonly mtimeMs: number;
14
- readonly scan: T;
15
- }
16
- export interface ClaudeCodeFileScan {
17
- readonly keyed: readonly (readonly [string, Snapshot])[];
18
- readonly anonymous: readonly Snapshot[];
19
- readonly malformedRecords: number;
20
- }
21
12
  export type ClaudeCodeUsage = LocalHistoryUsage;
22
- export interface Snapshot {
23
- readonly model: string;
24
- readonly usage: Usage;
25
- readonly costUSD: number | undefined;
26
- }
13
+ declare const snapshot: Type.TObject<{
14
+ model: Type.TString;
15
+ usage: Type.TObject<{
16
+ input: Type.TNumber;
17
+ output: Type.TNumber;
18
+ cacheRead: Type.TNumber;
19
+ cacheWrite: Type.TNumber;
20
+ cacheWrite1h: Type.TOptional<Type.TNumber>;
21
+ reasoning: Type.TOptional<Type.TNumber>;
22
+ totalTokens: Type.TNumber;
23
+ cost: Type.TObject<{
24
+ input: Type.TNumber;
25
+ output: Type.TNumber;
26
+ cacheRead: Type.TNumber;
27
+ cacheWrite: Type.TNumber;
28
+ total: Type.TNumber;
29
+ }>;
30
+ }>;
31
+ costUSD: Type.TOptional<Type.TNumber>;
32
+ at: Type.TUnion<[Type.TNumber, Type.TNull]>;
33
+ }>;
34
+ export type Snapshot = Static<typeof snapshot>;
35
+ declare const claudeCodeFileScan: Type.TObject<{
36
+ keyed: Type.TArray<Type.TTuple<[Type.TString, Type.TObject<{
37
+ model: Type.TString;
38
+ usage: Type.TObject<{
39
+ input: Type.TNumber;
40
+ output: Type.TNumber;
41
+ cacheRead: Type.TNumber;
42
+ cacheWrite: Type.TNumber;
43
+ cacheWrite1h: Type.TOptional<Type.TNumber>;
44
+ reasoning: Type.TOptional<Type.TNumber>;
45
+ totalTokens: Type.TNumber;
46
+ cost: Type.TObject<{
47
+ input: Type.TNumber;
48
+ output: Type.TNumber;
49
+ cacheRead: Type.TNumber;
50
+ cacheWrite: Type.TNumber;
51
+ total: Type.TNumber;
52
+ }>;
53
+ }>;
54
+ costUSD: Type.TOptional<Type.TNumber>;
55
+ at: Type.TUnion<[Type.TNumber, Type.TNull]>;
56
+ }>]>>;
57
+ anonymous: Type.TArray<Type.TObject<{
58
+ model: Type.TString;
59
+ usage: Type.TObject<{
60
+ input: Type.TNumber;
61
+ output: Type.TNumber;
62
+ cacheRead: Type.TNumber;
63
+ cacheWrite: Type.TNumber;
64
+ cacheWrite1h: Type.TOptional<Type.TNumber>;
65
+ reasoning: Type.TOptional<Type.TNumber>;
66
+ totalTokens: Type.TNumber;
67
+ cost: Type.TObject<{
68
+ input: Type.TNumber;
69
+ output: Type.TNumber;
70
+ cacheRead: Type.TNumber;
71
+ cacheWrite: Type.TNumber;
72
+ total: Type.TNumber;
73
+ }>;
74
+ }>;
75
+ costUSD: Type.TOptional<Type.TNumber>;
76
+ at: Type.TUnion<[Type.TNumber, Type.TNull]>;
77
+ }>>;
78
+ malformedRecords: Type.TInteger;
79
+ }>;
80
+ export type ClaudeCodeFileScan = Static<typeof claudeCodeFileScan>;
27
81
  /**
28
82
  * Reads Claude Code's local usage under `<configDir>/projects`. The configured root may be a
29
83
  * symlink (as may a system ancestor such as macOS `/tmp`), but links below it are skipped, to
@@ -35,19 +89,82 @@ export declare function readClaudeCodeUsage(options: ClaudeCodeUsageOptions): Pr
35
89
  export type CodexUsage = LocalHistoryUsage;
36
90
  export interface CodexUsageOptions {
37
91
  readonly models: Pick<Models, "getModels">;
92
+ readonly window: UsageWindow;
38
93
  readonly homeDir?: string;
39
94
  readonly signal?: AbortSignal;
40
95
  readonly cache?: UsageScanCache<CodexFileScan>;
41
96
  }
42
- export interface CodexFileScan {
43
- readonly modern: readonly (readonly [string, CodexRow])[];
44
- readonly legacy: readonly CodexRow[];
45
- readonly malformedRecords: number;
46
- }
47
- export interface CodexRow {
48
- readonly model: string;
49
- readonly usage: Usage;
50
- }
97
+ declare const codexRow: Type.TObject<{
98
+ model: Type.TString;
99
+ usage: Type.TObject<{
100
+ input: Type.TNumber;
101
+ output: Type.TNumber;
102
+ cacheRead: Type.TNumber;
103
+ cacheWrite: Type.TNumber;
104
+ cacheWrite1h: Type.TOptional<Type.TNumber>;
105
+ reasoning: Type.TOptional<Type.TNumber>;
106
+ totalTokens: Type.TNumber;
107
+ cost: Type.TObject<{
108
+ input: Type.TNumber;
109
+ output: Type.TNumber;
110
+ cacheRead: Type.TNumber;
111
+ cacheWrite: Type.TNumber;
112
+ total: Type.TNumber;
113
+ }>;
114
+ }>;
115
+ at: Type.TUnion<[Type.TNumber, Type.TNull]>;
116
+ }>;
117
+ export type CodexRow = Static<typeof codexRow>;
118
+ declare const codexFileScan: Type.TObject<{
119
+ modern: Type.TArray<Type.TTuple<[Type.TString, Type.TObject<{
120
+ model: Type.TString;
121
+ usage: Type.TObject<{
122
+ input: Type.TNumber;
123
+ output: Type.TNumber;
124
+ cacheRead: Type.TNumber;
125
+ cacheWrite: Type.TNumber;
126
+ cacheWrite1h: Type.TOptional<Type.TNumber>;
127
+ reasoning: Type.TOptional<Type.TNumber>;
128
+ totalTokens: Type.TNumber;
129
+ cost: Type.TObject<{
130
+ input: Type.TNumber;
131
+ output: Type.TNumber;
132
+ cacheRead: Type.TNumber;
133
+ cacheWrite: Type.TNumber;
134
+ total: Type.TNumber;
135
+ }>;
136
+ }>;
137
+ at: Type.TUnion<[Type.TNumber, Type.TNull]>;
138
+ }>]>>;
139
+ legacy: Type.TArray<Type.TObject<{
140
+ model: Type.TString;
141
+ usage: Type.TObject<{
142
+ input: Type.TNumber;
143
+ output: Type.TNumber;
144
+ cacheRead: Type.TNumber;
145
+ cacheWrite: Type.TNumber;
146
+ cacheWrite1h: Type.TOptional<Type.TNumber>;
147
+ reasoning: Type.TOptional<Type.TNumber>;
148
+ totalTokens: Type.TNumber;
149
+ cost: Type.TObject<{
150
+ input: Type.TNumber;
151
+ output: Type.TNumber;
152
+ cacheRead: Type.TNumber;
153
+ cacheWrite: Type.TNumber;
154
+ total: Type.TNumber;
155
+ }>;
156
+ }>;
157
+ at: Type.TUnion<[Type.TNumber, Type.TNull]>;
158
+ }>>;
159
+ malformedRecords: Type.TInteger;
160
+ cursor: Type.TObject<{
161
+ model: Type.TString;
162
+ sawMeta: Type.TBoolean;
163
+ signature: Type.TOptional<Type.TString>;
164
+ forkAnchorMs: Type.TOptional<Type.TNumber>;
165
+ }>;
166
+ }>;
167
+ export type CodexFileScan = Static<typeof codexFileScan>;
51
168
  /**
52
169
  * Reads Codex's local usage from `sessions` and `archived_sessions` (where the legacy format
53
170
  * lives). A `response_id` is unique across files, so one map dedups a forked rollout's repeated
@@ -61,12 +178,18 @@ export interface UsageScanCaches {
61
178
  readonly codex: UsageScanCache<CodexFileScan>;
62
179
  }
63
180
  export declare function createUsageScanCaches(): UsageScanCaches;
181
+ /** Restores the scans {@link saveUsageScanCaches} wrote to `path`; each still answers only while its file is unchanged. */
182
+ export declare function loadUsageScanCaches(path: string): Promise<UsageScanCaches>;
183
+ /** Writes `caches` to `path` when a read changed them since they were last saved; a failed write is tried again on the next save. */
184
+ export declare function saveUsageScanCaches(path: string, caches: UsageScanCaches): Promise<void>;
64
185
  export interface LocalUsage {
65
186
  readonly claudeCode: LocalHistoryUsage;
66
187
  readonly codex: LocalHistoryUsage;
67
188
  }
68
189
  export declare function readLocalUsage(options: {
69
190
  readonly models: Pick<Models, "getModels">;
191
+ readonly window: UsageWindow;
70
192
  readonly signal?: AbortSignal;
71
193
  readonly caches?: UsageScanCaches;
72
194
  }): Promise<LocalUsage>;
195
+ export {};
package/dist/src/usage.js CHANGED
@@ -2,12 +2,22 @@ import {
2
2
  readClaudeCodeUsage2,
3
3
  readCodexUsage2,
4
4
  createUsageScanCaches2,
5
+ loadUsageScanCaches2,
6
+ saveUsageScanCaches2,
5
7
  readLocalUsage2
6
- } from "../chunks/canvas-preview-0nrnbt76.js";
8
+ } from "../chunks/canvas-preview-k1yg4fgp.js";
9
+ import"../chunks/canvas-preview-11nadadc.js";
10
+ import"../chunks/canvas-preview-kp75gb63.js";
7
11
  import"../chunks/canvas-preview-srgfpdmf.js";
12
+ import"../chunks/canvas-preview-tnqe7k0b.js";
13
+ import"../chunks/canvas-preview-zaaj99wd.js";
14
+ import"../chunks/canvas-preview-0cmbkp2j.js";
15
+ import"../chunks/canvas-preview-69fkk9yn.js";
8
16
  export {
9
17
  createUsageScanCaches2 as createUsageScanCaches,
18
+ loadUsageScanCaches2 as loadUsageScanCaches,
10
19
  readClaudeCodeUsage2 as readClaudeCodeUsage,
11
20
  readCodexUsage2 as readCodexUsage,
12
- readLocalUsage2 as readLocalUsage
21
+ readLocalUsage2 as readLocalUsage,
22
+ saveUsageScanCaches2 as saveUsageScanCaches
13
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nyte-ai/host",
3
- "version": "0.0.18",
3
+ "version": "0.0.19",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -36,6 +36,14 @@
36
36
  "types": "./dist/src/identity.d.ts",
37
37
  "import": "./dist/src/identity.js"
38
38
  },
39
+ "./images": {
40
+ "types": "./dist/src/images/node.d.ts",
41
+ "import": "./dist/src/images/node.js"
42
+ },
43
+ "./image-worker": {
44
+ "types": "./dist/src/images/worker.d.ts",
45
+ "import": "./dist/src/images/worker.js"
46
+ },
39
47
  "./listen-fetch": {
40
48
  "types": "./dist/src/listen-fetch.d.ts",
41
49
  "import": "./dist/src/listen-fetch.js"
@@ -88,11 +96,12 @@
88
96
  "dependencies": {
89
97
  "@earendil-works/pi-mcp": "1.3.0",
90
98
  "@lydell/node-pty": "1.2.0-beta.15",
91
- "@nyte-ai/ai": "0.0.18",
92
- "@nyte-ai/core": "0.0.18",
93
- "@nyte-ai/plugin": "0.0.18",
94
- "@nyte-ai/protocol": "0.0.18",
95
- "@nyte-ai/schema": "0.0.18",
99
+ "@nyte-ai/ai": "0.0.19",
100
+ "@nyte-ai/core": "0.0.19",
101
+ "@nyte-ai/plugin": "0.0.19",
102
+ "@nyte-ai/protocol": "0.0.19",
103
+ "@nyte-ai/schema": "0.0.19",
104
+ "@silvia-odwyer/photon": "0.3.3",
96
105
  "fflate": "0.8.3",
97
106
  "ignore": "7.0.9",
98
107
  "pg": "8.23.0",
@@ -126,10 +135,9 @@
126
135
  },
127
136
  "scripts": {
128
137
  "clean": "rm -rf node_modules .turbo",
129
- "typecheck": "tsgo --noEmit && tsgo --noEmit -p src/deploy/vercel",
138
+ "typecheck": "tsrs --noEmit && tsrs --noEmit -p src/deploy/vercel",
130
139
  "test": "vitest --run",
131
140
  "check:store": "node scripts/check-store.ts",
132
- "compact:store": "node scripts/compact-store.ts",
133
141
  "build:package": "bun ../../scripts/sdk-packages.mjs build host"
134
142
  }
135
143
  }
@@ -27,6 +27,7 @@ import type {
27
27
  import { Type } from "typebox";
28
28
  import type { TSchema } from "typebox";
29
29
  import { Value } from "typebox/value";
30
+ import { killTree } from "./local/command.ts";
30
31
 
31
32
  const ORIGIN = "https://canvas.preview/";
32
33
 
@@ -312,12 +313,12 @@ function stop(child: ChildProcess): Promise<void> {
312
313
  if (child.exitCode !== null || child.signalCode !== null) return Promise.resolve();
313
314
 
314
315
  return new Promise((resolve) => {
315
- const kill = setTimeout(() => child.kill("SIGKILL"), 2000);
316
+ const kill = setTimeout(() => killTree(child, "SIGKILL"), 2000);
316
317
  child.once("exit", () => {
317
318
  clearTimeout(kill);
318
319
  resolve();
319
320
  });
320
- child.kill();
321
+ killTree(child, "SIGTERM");
321
322
  });
322
323
  }
323
324
 
@@ -396,7 +397,7 @@ export function chromiumCanvasPreview(
396
397
  }
397
398
  } finally {
398
399
  await stop(child);
399
- await rm(profile, { recursive: true, force: true });
400
+ await rm(profile, { recursive: true, force: true, maxRetries: 10 });
400
401
  }
401
402
  };
402
403
  }
@@ -0,0 +1,69 @@
1
+ const ORIENTATION_TAG = 0x0112;
2
+
3
+ export function exifOrientation(bytes: Uint8Array): number {
4
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
5
+ const tiff = tiffStart(bytes, view);
6
+
7
+ if (tiff < 0 || tiff + 8 > bytes.length) return 1;
8
+ const little = bytes[tiff] === 0x49 && bytes[tiff + 1] === 0x49;
9
+ const directory = tiff + view.getUint32(tiff + 4, little);
10
+
11
+ if (directory + 2 > bytes.length) return 1;
12
+ const entries = view.getUint16(directory, little);
13
+
14
+ for (let index = 0; index < entries; index++) {
15
+ const entry = directory + 2 + index * 12;
16
+
17
+ if (entry + 12 > bytes.length) return 1;
18
+
19
+ if (view.getUint16(entry, little) === ORIENTATION_TAG) {
20
+ const value = view.getUint16(entry + 8, little);
21
+
22
+ return value >= 1 && value <= 8 ? value : 1;
23
+ }
24
+ }
25
+
26
+ return 1;
27
+ }
28
+
29
+ function tiffStart(bytes: Uint8Array, view: DataView): number {
30
+ if (bytes[0] === 0xff && bytes[1] === 0xd8) {
31
+ for (let offset = 2; offset + 4 <= bytes.length;) {
32
+ if (bytes[offset] !== 0xff) return -1;
33
+ const marker = view.getUint8(offset + 1);
34
+
35
+ if (marker === 0xff) {
36
+ offset++;
37
+ continue;
38
+ }
39
+
40
+ if (marker === 0xe1 && startsExif(bytes, offset + 4)) return offset + 10;
41
+ offset += 2 + view.getUint16(offset + 2);
42
+ }
43
+
44
+ return -1;
45
+ }
46
+
47
+ if (ascii(bytes, 0, "RIFF") && ascii(bytes, 8, "WEBP")) {
48
+ for (let offset = 12; offset + 8 <= bytes.length;) {
49
+ const size = view.getUint32(offset + 4, true);
50
+
51
+ if (ascii(bytes, offset, "EXIF"))
52
+ return startsExif(bytes, offset + 8) ? offset + 14 : offset + 8;
53
+ offset += 8 + size + (size % 2);
54
+ }
55
+ }
56
+
57
+ return -1;
58
+ }
59
+
60
+ function startsExif(bytes: Uint8Array, offset: number): boolean {
61
+ return ascii(bytes, offset, "Exif") && bytes[offset + 4] === 0 && bytes[offset + 5] === 0;
62
+ }
63
+
64
+ function ascii(bytes: Uint8Array, offset: number, text: string): boolean {
65
+ for (let index = 0; index < text.length; index++)
66
+ if (bytes[offset + index] !== text.charCodeAt(index)) return false;
67
+
68
+ return true;
69
+ }
@@ -0,0 +1,163 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { fileURLToPath } from "node:url";
3
+ import { Worker } from "node:worker_threads";
4
+ import type { ImageLimits, ImageProcessor, PreparedImage } from "@nyte-ai/core";
5
+ import { Type } from "typebox";
6
+ import { Value } from "typebox/value";
7
+ import { photonImages } from "./photon.ts";
8
+
9
+ export interface NodeImagesOptions {
10
+ readonly worker?: string | URL;
11
+ readonly wasm?: string;
12
+ }
13
+
14
+ export interface PrepareRequest {
15
+ readonly id: number;
16
+ readonly bytes: Uint8Array;
17
+ readonly mimeType: string;
18
+ readonly limits: ImageLimits;
19
+ }
20
+
21
+ const size = Type.Object({ width: Type.Number(), height: Type.Number() });
22
+
23
+ export const prepareReply = Type.Union([
24
+ Type.Object({ id: Type.Integer(), result: Type.Null() }),
25
+ Type.Object({
26
+ id: Type.Integer(),
27
+ result: Type.Object({
28
+ data: Type.String(),
29
+ mimeType: Type.String(),
30
+ resized: Type.Optional(Type.Object({ from: size, to: size })),
31
+ }),
32
+ }),
33
+ Type.Object({ id: Type.Integer(), error: Type.String() }),
34
+ ]);
35
+
36
+ const IDLE_MS = 30_000;
37
+
38
+ export function nodeImages(options: NodeImagesOptions = {}): ImageProcessor {
39
+ const worker =
40
+ options.worker ??
41
+ new URL(import.meta.url.endsWith(".ts") ? "./worker.ts" : "./worker.js", import.meta.url);
42
+
43
+ let wasm: Promise<Uint8Array> | undefined;
44
+
45
+ const loadWasm = (): Promise<Uint8Array> =>
46
+ (wasm ??= readFile(options.wasm ?? resolvePhotonWasmPath()));
47
+
48
+ let thread: PhotonThread | undefined;
49
+ let inProcess: ImageProcessor | undefined;
50
+
51
+ return {
52
+ async prepare(bytes, mimeType, limits) {
53
+ if (inProcess !== undefined) return inProcess.prepare(bytes, mimeType, limits);
54
+ const module = await loadWasm();
55
+
56
+ try {
57
+ thread ??= new PhotonThread(worker, module, (ended) => {
58
+ if (thread === ended) thread = undefined;
59
+ });
60
+
61
+ return await thread.prepare(bytes, mimeType, limits);
62
+ } catch (cause) {
63
+ if (!(cause instanceof WorkerUnavailable)) throw cause;
64
+ inProcess ??= photonImages(module);
65
+
66
+ return inProcess.prepare(bytes, mimeType, limits);
67
+ }
68
+ },
69
+ };
70
+ }
71
+
72
+ export function resolvePhotonWasmPath(): string {
73
+ return fileURLToPath(import.meta.resolve("@silvia-odwyer/photon/photon_rs_bg.wasm"));
74
+ }
75
+
76
+ class WorkerUnavailable extends Error {}
77
+
78
+ class PhotonThread {
79
+ readonly #worker: Worker;
80
+ readonly #pending = new Map<number, PromiseWithResolvers<PreparedImage | undefined>>();
81
+ readonly #onExit: (ended: PhotonThread) => void;
82
+ #nextId = 0;
83
+ #idle: ReturnType<typeof setTimeout> | undefined;
84
+ #started = false;
85
+
86
+ constructor(entry: string | URL, wasm: Uint8Array, onExit: (ended: PhotonThread) => void) {
87
+ this.#onExit = onExit;
88
+
89
+ try {
90
+ this.#worker = new Worker(entry, { workerData: wasm });
91
+ } catch (cause) {
92
+ throw new WorkerUnavailable("The image worker cannot start", { cause });
93
+ }
94
+
95
+ this.#worker.on("message", (reply: unknown) => {
96
+ if (!Value.Check(prepareReply, reply)) {
97
+ this.#fail(new Error("Malformed image worker reply"));
98
+ void this.#worker.terminate();
99
+
100
+ return;
101
+ }
102
+
103
+ this.#started = true;
104
+ const request = this.#pending.get(reply.id);
105
+
106
+ if (request === undefined) return;
107
+ this.#pending.delete(reply.id);
108
+
109
+ if ("error" in reply) request.reject(new Error(reply.error));
110
+ else request.resolve(reply.result ?? undefined);
111
+ this.#settled();
112
+ });
113
+
114
+ this.#worker.on("error", (cause) => this.#fail(cause));
115
+
116
+ this.#worker.on("exit", (code) =>
117
+ this.#fail(new Error(`The image worker exited with code ${String(code)}`)),
118
+ );
119
+ }
120
+
121
+ prepare(
122
+ bytes: Uint8Array,
123
+ mimeType: string,
124
+ limits: ImageLimits,
125
+ ): Promise<PreparedImage | undefined> {
126
+ const id = this.#nextId++;
127
+ const request = Promise.withResolvers<PreparedImage | undefined>();
128
+ this.#pending.set(id, request);
129
+ clearTimeout(this.#idle);
130
+ this.#idle = undefined;
131
+ this.#worker.ref();
132
+ const copy = bytes.slice();
133
+
134
+ this.#worker.postMessage({ id, bytes: copy, mimeType, limits } satisfies PrepareRequest, [
135
+ copy.buffer,
136
+ ]);
137
+
138
+ return request.promise;
139
+ }
140
+
141
+ #settled(): void {
142
+ if (this.#pending.size > 0) return;
143
+ this.#worker.unref();
144
+
145
+ this.#idle = setTimeout(() => {
146
+ this.#onExit(this);
147
+ void this.#worker.terminate();
148
+ }, IDLE_MS);
149
+
150
+ this.#idle.unref();
151
+ }
152
+
153
+ #fail(cause: unknown): void {
154
+ const failure = this.#started
155
+ ? cause
156
+ : new WorkerUnavailable("The image worker cannot start", { cause });
157
+
158
+ for (const request of this.#pending.values()) request.reject(failure);
159
+ this.#pending.clear();
160
+ clearTimeout(this.#idle);
161
+ this.#onExit(this);
162
+ }
163
+ }