@ketvietlab/ketjs 0.1.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 (283) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +36 -0
  3. package/dist/agent/capabilities.d.ts +62 -0
  4. package/dist/agent/capabilities.d.ts.map +1 -0
  5. package/dist/agent/capabilities.js +89 -0
  6. package/dist/agent/capabilities.js.map +1 -0
  7. package/dist/agent/permissions.d.ts +56 -0
  8. package/dist/agent/permissions.d.ts.map +1 -0
  9. package/dist/agent/permissions.js +209 -0
  10. package/dist/agent/permissions.js.map +1 -0
  11. package/dist/cli.d.ts +3 -0
  12. package/dist/cli.d.ts.map +1 -0
  13. package/dist/cli.js +698 -0
  14. package/dist/cli.js.map +1 -0
  15. package/dist/codegen/dts.d.ts +3 -0
  16. package/dist/codegen/dts.d.ts.map +1 -0
  17. package/dist/codegen/dts.js +94 -0
  18. package/dist/codegen/dts.js.map +1 -0
  19. package/dist/data/adapter.d.ts +4 -0
  20. package/dist/data/adapter.d.ts.map +1 -0
  21. package/dist/data/adapter.js +18 -0
  22. package/dist/data/adapter.js.map +1 -0
  23. package/dist/data/changeset.d.ts +49 -0
  24. package/dist/data/changeset.d.ts.map +1 -0
  25. package/dist/data/changeset.js +206 -0
  26. package/dist/data/changeset.js.map +1 -0
  27. package/dist/data/expr.d.ts +59 -0
  28. package/dist/data/expr.d.ts.map +1 -0
  29. package/dist/data/expr.js +61 -0
  30. package/dist/data/expr.js.map +1 -0
  31. package/dist/data/fleet.d.ts +28 -0
  32. package/dist/data/fleet.d.ts.map +1 -0
  33. package/dist/data/fleet.js +82 -0
  34. package/dist/data/fleet.js.map +1 -0
  35. package/dist/data/list-search.d.ts +93 -0
  36. package/dist/data/list-search.d.ts.map +1 -0
  37. package/dist/data/list-search.js +268 -0
  38. package/dist/data/list-search.js.map +1 -0
  39. package/dist/data/migrate.d.ts +75 -0
  40. package/dist/data/migrate.d.ts.map +1 -0
  41. package/dist/data/migrate.js +136 -0
  42. package/dist/data/migrate.js.map +1 -0
  43. package/dist/data/pool.d.ts +20 -0
  44. package/dist/data/pool.d.ts.map +1 -0
  45. package/dist/data/pool.js +92 -0
  46. package/dist/data/pool.js.map +1 -0
  47. package/dist/data/query.d.ts +111 -0
  48. package/dist/data/query.d.ts.map +1 -0
  49. package/dist/data/query.js +268 -0
  50. package/dist/data/query.js.map +1 -0
  51. package/dist/data/sqlite.d.ts +3 -0
  52. package/dist/data/sqlite.d.ts.map +1 -0
  53. package/dist/data/sqlite.js +107 -0
  54. package/dist/data/sqlite.js.map +1 -0
  55. package/dist/data/time.d.ts +9 -0
  56. package/dist/data/time.d.ts.map +1 -0
  57. package/dist/data/time.js +107 -0
  58. package/dist/data/time.js.map +1 -0
  59. package/dist/index.d.ts +79 -0
  60. package/dist/index.d.ts.map +1 -0
  61. package/dist/index.js +51 -0
  62. package/dist/index.js.map +1 -0
  63. package/dist/kernel/apps.d.ts +43 -0
  64. package/dist/kernel/apps.d.ts.map +1 -0
  65. package/dist/kernel/apps.js +234 -0
  66. package/dist/kernel/apps.js.map +1 -0
  67. package/dist/kernel/compose.d.ts +6 -0
  68. package/dist/kernel/compose.d.ts.map +1 -0
  69. package/dist/kernel/compose.js +976 -0
  70. package/dist/kernel/compose.js.map +1 -0
  71. package/dist/kernel/define.d.ts +9 -0
  72. package/dist/kernel/define.d.ts.map +1 -0
  73. package/dist/kernel/define.js +140 -0
  74. package/dist/kernel/define.js.map +1 -0
  75. package/dist/kernel/diff.d.ts +11 -0
  76. package/dist/kernel/diff.d.ts.map +1 -0
  77. package/dist/kernel/diff.js +106 -0
  78. package/dist/kernel/diff.js.map +1 -0
  79. package/dist/kernel/errors.d.ts +17 -0
  80. package/dist/kernel/errors.d.ts.map +1 -0
  81. package/dist/kernel/errors.js +43 -0
  82. package/dist/kernel/errors.js.map +1 -0
  83. package/dist/kernel/graph.d.ts +3 -0
  84. package/dist/kernel/graph.d.ts.map +1 -0
  85. package/dist/kernel/graph.js +56 -0
  86. package/dist/kernel/graph.js.map +1 -0
  87. package/dist/kernel/i18n.d.ts +43 -0
  88. package/dist/kernel/i18n.d.ts.map +1 -0
  89. package/dist/kernel/i18n.js +79 -0
  90. package/dist/kernel/i18n.js.map +1 -0
  91. package/dist/kernel/index.d.ts +7 -0
  92. package/dist/kernel/index.d.ts.map +1 -0
  93. package/dist/kernel/index.js +7 -0
  94. package/dist/kernel/index.js.map +1 -0
  95. package/dist/kernel/layout.d.ts +17 -0
  96. package/dist/kernel/layout.d.ts.map +1 -0
  97. package/dist/kernel/layout.js +77 -0
  98. package/dist/kernel/layout.js.map +1 -0
  99. package/dist/kernel/menu.d.ts +27 -0
  100. package/dist/kernel/menu.d.ts.map +1 -0
  101. package/dist/kernel/menu.js +65 -0
  102. package/dist/kernel/menu.js.map +1 -0
  103. package/dist/kernel/modules.d.ts +31 -0
  104. package/dist/kernel/modules.d.ts.map +1 -0
  105. package/dist/kernel/modules.js +322 -0
  106. package/dist/kernel/modules.js.map +1 -0
  107. package/dist/kernel/routes.d.ts +24 -0
  108. package/dist/kernel/routes.d.ts.map +1 -0
  109. package/dist/kernel/routes.js +98 -0
  110. package/dist/kernel/routes.js.map +1 -0
  111. package/dist/kernel/types.d.ts +8 -0
  112. package/dist/kernel/types.d.ts.map +1 -0
  113. package/dist/kernel/types.js +67 -0
  114. package/dist/kernel/types.js.map +1 -0
  115. package/dist/kernel/workspace.d.ts +62 -0
  116. package/dist/kernel/workspace.d.ts.map +1 -0
  117. package/dist/kernel/workspace.js +156 -0
  118. package/dist/kernel/workspace.js.map +1 -0
  119. package/dist/scaffold/index.d.ts +2 -0
  120. package/dist/scaffold/index.d.ts.map +1 -0
  121. package/dist/scaffold/index.js +49 -0
  122. package/dist/scaffold/index.js.map +1 -0
  123. package/dist/scaffold/templates/app.test.ts.tmpl +17 -0
  124. package/dist/scaffold/templates/biome.json.tmpl +30 -0
  125. package/dist/scaffold/templates/dev.mjs.tmpl +16 -0
  126. package/dist/scaffold/templates/gitignore.tmpl +24 -0
  127. package/dist/scaffold/templates/ket.workspace.ts.tmpl +18 -0
  128. package/dist/scaffold/templates/module.ts.tmpl +27 -0
  129. package/dist/scaffold/templates/package.json.tmpl +25 -0
  130. package/dist/scaffold/templates/tsconfig.json.tmpl +15 -0
  131. package/dist/server/boot.d.ts +173 -0
  132. package/dist/server/boot.d.ts.map +1 -0
  133. package/dist/server/boot.js +659 -0
  134. package/dist/server/boot.js.map +1 -0
  135. package/dist/server/config.d.ts +48 -0
  136. package/dist/server/config.d.ts.map +1 -0
  137. package/dist/server/config.js +82 -0
  138. package/dist/server/config.js.map +1 -0
  139. package/dist/server/ctx.d.ts +13 -0
  140. package/dist/server/ctx.d.ts.map +1 -0
  141. package/dist/server/ctx.js +496 -0
  142. package/dist/server/ctx.js.map +1 -0
  143. package/dist/server/fn.d.ts +34 -0
  144. package/dist/server/fn.d.ts.map +1 -0
  145. package/dist/server/fn.js +170 -0
  146. package/dist/server/fn.js.map +1 -0
  147. package/dist/server/http.d.ts +79 -0
  148. package/dist/server/http.d.ts.map +1 -0
  149. package/dist/server/http.js +459 -0
  150. package/dist/server/http.js.map +1 -0
  151. package/dist/server/idem.d.ts +31 -0
  152. package/dist/server/idem.d.ts.map +1 -0
  153. package/dist/server/idem.js +68 -0
  154. package/dist/server/idem.js.map +1 -0
  155. package/dist/server/jobs.d.ts +5 -0
  156. package/dist/server/jobs.d.ts.map +1 -0
  157. package/dist/server/jobs.js +24 -0
  158. package/dist/server/jobs.js.map +1 -0
  159. package/dist/server/multipart.d.ts +18 -0
  160. package/dist/server/multipart.d.ts.map +1 -0
  161. package/dist/server/multipart.js +183 -0
  162. package/dist/server/multipart.js.map +1 -0
  163. package/dist/server/project.d.ts +9 -0
  164. package/dist/server/project.d.ts.map +1 -0
  165. package/dist/server/project.js +67 -0
  166. package/dist/server/project.js.map +1 -0
  167. package/dist/server/queue.d.ts +80 -0
  168. package/dist/server/queue.d.ts.map +1 -0
  169. package/dist/server/queue.js +494 -0
  170. package/dist/server/queue.js.map +1 -0
  171. package/dist/server/respond.d.ts +91 -0
  172. package/dist/server/respond.d.ts.map +1 -0
  173. package/dist/server/respond.js +99 -0
  174. package/dist/server/respond.js.map +1 -0
  175. package/dist/server/runtime.d.ts +14 -0
  176. package/dist/server/runtime.d.ts.map +1 -0
  177. package/dist/server/runtime.js +25 -0
  178. package/dist/server/runtime.js.map +1 -0
  179. package/dist/server/session.d.ts +55 -0
  180. package/dist/server/session.d.ts.map +1 -0
  181. package/dist/server/session.js +172 -0
  182. package/dist/server/session.js.map +1 -0
  183. package/dist/server/sessionstore.d.ts +54 -0
  184. package/dist/server/sessionstore.d.ts.map +1 -0
  185. package/dist/server/sessionstore.js +232 -0
  186. package/dist/server/sessionstore.js.map +1 -0
  187. package/dist/server/storage/index.d.ts +11 -0
  188. package/dist/server/storage/index.d.ts.map +1 -0
  189. package/dist/server/storage/index.js +31 -0
  190. package/dist/server/storage/index.js.map +1 -0
  191. package/dist/server/storage/local.d.ts +5 -0
  192. package/dist/server/storage/local.d.ts.map +1 -0
  193. package/dist/server/storage/local.js +165 -0
  194. package/dist/server/storage/local.js.map +1 -0
  195. package/dist/server/storage/s3.d.ts +11 -0
  196. package/dist/server/storage/s3.d.ts.map +1 -0
  197. package/dist/server/storage/s3.js +133 -0
  198. package/dist/server/storage/s3.js.map +1 -0
  199. package/dist/server/storage/sigv4.d.ts +23 -0
  200. package/dist/server/storage/sigv4.d.ts.map +1 -0
  201. package/dist/server/storage/sigv4.js +79 -0
  202. package/dist/server/storage/sigv4.js.map +1 -0
  203. package/dist/server/storage/types.d.ts +37 -0
  204. package/dist/server/storage/types.d.ts.map +1 -0
  205. package/dist/server/storage/types.js +82 -0
  206. package/dist/server/storage/types.js.map +1 -0
  207. package/dist/server/stream.d.ts +43 -0
  208. package/dist/server/stream.d.ts.map +1 -0
  209. package/dist/server/stream.js +117 -0
  210. package/dist/server/stream.js.map +1 -0
  211. package/dist/server/streamstore.d.ts +29 -0
  212. package/dist/server/streamstore.d.ts.map +1 -0
  213. package/dist/server/streamstore.js +140 -0
  214. package/dist/server/streamstore.js.map +1 -0
  215. package/dist/server/tenants.d.ts +108 -0
  216. package/dist/server/tenants.d.ts.map +1 -0
  217. package/dist/server/tenants.js +172 -0
  218. package/dist/server/tenants.js.map +1 -0
  219. package/dist/server/transport/index.d.ts +5 -0
  220. package/dist/server/transport/index.d.ts.map +1 -0
  221. package/dist/server/transport/index.js +3 -0
  222. package/dist/server/transport/index.js.map +1 -0
  223. package/dist/server/transport/memory.d.ts +14 -0
  224. package/dist/server/transport/memory.d.ts.map +1 -0
  225. package/dist/server/transport/memory.js +36 -0
  226. package/dist/server/transport/memory.js.map +1 -0
  227. package/dist/server/transport/types.d.ts +44 -0
  228. package/dist/server/transport/types.d.ts.map +1 -0
  229. package/dist/server/transport/types.js +79 -0
  230. package/dist/server/transport/types.js.map +1 -0
  231. package/dist/server/worker.d.ts +33 -0
  232. package/dist/server/worker.d.ts.map +1 -0
  233. package/dist/server/worker.js +428 -0
  234. package/dist/server/worker.js.map +1 -0
  235. package/dist/testing.d.ts +146 -0
  236. package/dist/testing.d.ts.map +1 -0
  237. package/dist/testing.js +406 -0
  238. package/dist/testing.js.map +1 -0
  239. package/dist/theme/contracts.d.ts +4 -0
  240. package/dist/theme/contracts.d.ts.map +1 -0
  241. package/dist/theme/contracts.js +87 -0
  242. package/dist/theme/contracts.js.map +1 -0
  243. package/dist/theme/index.d.ts +8 -0
  244. package/dist/theme/index.d.ts.map +1 -0
  245. package/dist/theme/index.js +7 -0
  246. package/dist/theme/index.js.map +1 -0
  247. package/dist/theme/joints.d.ts +19 -0
  248. package/dist/theme/joints.d.ts.map +1 -0
  249. package/dist/theme/joints.js +126 -0
  250. package/dist/theme/joints.js.map +1 -0
  251. package/dist/theme/ktl/compile.d.ts +26 -0
  252. package/dist/theme/ktl/compile.d.ts.map +1 -0
  253. package/dist/theme/ktl/compile.js +226 -0
  254. package/dist/theme/ktl/compile.js.map +1 -0
  255. package/dist/theme/ktl/lexer.d.ts +20 -0
  256. package/dist/theme/ktl/lexer.d.ts.map +1 -0
  257. package/dist/theme/ktl/lexer.js +56 -0
  258. package/dist/theme/ktl/lexer.js.map +1 -0
  259. package/dist/theme/ktl/parser.d.ts +64 -0
  260. package/dist/theme/ktl/parser.d.ts.map +1 -0
  261. package/dist/theme/ktl/parser.js +220 -0
  262. package/dist/theme/ktl/parser.js.map +1 -0
  263. package/dist/theme/render.d.ts +19 -0
  264. package/dist/theme/render.d.ts.map +1 -0
  265. package/dist/theme/render.js +246 -0
  266. package/dist/theme/render.js.map +1 -0
  267. package/dist/theme/templates.d.ts +2 -0
  268. package/dist/theme/templates.d.ts.map +1 -0
  269. package/dist/theme/templates.js +40 -0
  270. package/dist/theme/templates.js.map +1 -0
  271. package/dist/theme/tokens.d.ts +4 -0
  272. package/dist/theme/tokens.d.ts.map +1 -0
  273. package/dist/theme/tokens.js +15 -0
  274. package/dist/theme/tokens.js.map +1 -0
  275. package/dist/theme/viewmodel.d.ts +6 -0
  276. package/dist/theme/viewmodel.d.ts.map +1 -0
  277. package/dist/theme/viewmodel.js +40 -0
  278. package/dist/theme/viewmodel.js.map +1 -0
  279. package/dist/types.d.ts +614 -0
  280. package/dist/types.d.ts.map +1 -0
  281. package/dist/types.js +3 -0
  282. package/dist/types.js.map +1 -0
  283. package/package.json +60 -0
@@ -0,0 +1,459 @@
1
+ // One server function, three surfaces: an HTTP endpoint, the typed client that
2
+ // calls it, and an agent tool descriptor — all read off the same manifest entry.
3
+ import { createServer } from 'node:http';
4
+ import { pipeline } from 'node:stream/promises';
5
+ import { isNavigationRequest, navigablePage } from './respond.js';
6
+ import { readFile } from 'node:fs/promises';
7
+ import { join, normalize, extname } from 'node:path';
8
+ import { fileURLToPath } from 'node:url';
9
+ import { callFn } from './fn.js';
10
+ import { createStreams, dbStreamStore, memoryStreamStore } from './stream.js';
11
+ import { agentDescriptor } from '../agent/capabilities.js';
12
+ import { KetError } from '../kernel/errors.js';
13
+ import { KetError as KetErr } from '../kernel/errors.js';
14
+ import { compileRoutes } from '../kernel/routes.js';
15
+ import { html, trustedMarkup } from '@ketvietlab/ketjs-view';
16
+ const ASSET_MIME = {
17
+ '.css': 'text/css',
18
+ '.js': 'text/javascript',
19
+ '.mjs': 'text/javascript',
20
+ '.svg': 'image/svg+xml',
21
+ '.png': 'image/png',
22
+ '.jpg': 'image/jpeg',
23
+ '.woff2': 'font/woff2',
24
+ '.ico': 'image/x-icon',
25
+ };
26
+ const json = (res, status, body) => {
27
+ const s = JSON.stringify(body, null, 2);
28
+ res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' });
29
+ res.end(s);
30
+ };
31
+ const contentType = (type) => {
32
+ const lower = type.toLowerCase();
33
+ const textual = lower.startsWith('text/') ||
34
+ lower === 'application/json' ||
35
+ lower.endsWith('+json') ||
36
+ lower === 'application/xml' ||
37
+ lower.endsWith('+xml') ||
38
+ lower === 'image/svg+xml';
39
+ return textual && !lower.includes('charset=') ? `${type}; charset=utf-8` : type;
40
+ };
41
+ const islandScript = '<script type="module" src="/_ket/islands.js"></script>';
42
+ const viewRuntimeUrl = '/_ket/view/index.js';
43
+ const bootstrapDocument = (body) => {
44
+ if ((!body.includes('<ket-island') && !body.includes('data-ket-slot=')) ||
45
+ body.includes('src="/_ket/islands.js"'))
46
+ return body;
47
+ const closingBody = body.lastIndexOf('</body>');
48
+ return closingBody < 0
49
+ ? body + islandScript
50
+ : body.slice(0, closingBody) + islandScript + body.slice(closingBody);
51
+ };
52
+ const browserBootstrap = (clients) => `import {
53
+ createIslandManager,
54
+ domHost,
55
+ } from ${JSON.stringify(viewRuntimeUrl)}
56
+
57
+ const definitions = ${JSON.stringify(clients)}
58
+ const registry = Object.create(null)
59
+ const loading = new Map()
60
+ const loadFactory = async (name) => {
61
+ if (registry[name]) return registry[name]
62
+ const definition = definitions[name]
63
+ if (!definition) return null
64
+ if (loading.has(name)) return loading.get(name)
65
+ const pending = (async () => {
66
+ const module = await import(definition.src)
67
+ const factory = module[definition.export]
68
+ if (typeof factory !== 'function') {
69
+ throw new TypeError('island "' + name + '" does not export a factory named "' + definition.export + '"')
70
+ }
71
+ registry[name] = factory
72
+ return factory
73
+ })()
74
+ loading.set(name, pending)
75
+ try { return await pending } finally { loading.delete(name) }
76
+ }
77
+ const loadPlaced = async (root, requireKnown = false) => {
78
+ const names = new Set(Array.from(root.querySelectorAll('ket-island'), (element) => element.getAttribute('data-island')).filter(Boolean))
79
+ if (requireKnown) {
80
+ const unknown = Array.from(names).find((name) => !definitions[name])
81
+ if (unknown) throw new Error('navigation fragment contains unknown island "' + unknown + '"')
82
+ }
83
+ await Promise.all(Array.from(names, loadFactory))
84
+ }
85
+
86
+ await loadPlaced(document)
87
+ const islands = createIslandManager(domHost(), registry, { strict: false })
88
+ islands.hydrate(document)
89
+
90
+ const event = (name, detail) => document.dispatchEvent(new CustomEvent(name, { detail }))
91
+ const hardNavigate = (target) => window.location.assign(String(target))
92
+ const fragmentsType = 'text/vnd.ket.fragments+html'
93
+ const slotSelector = (name) => '[data-ket-slot="' + name + '"]'
94
+
95
+ const applyFragments = async (markup) => {
96
+ const parsed = new DOMParser().parseFromString(markup, 'text/html')
97
+ const envelope = parsed.querySelector('ket-fragments')
98
+ if (!envelope) throw new Error('navigation response has no ket-fragments envelope')
99
+ const templates = Array.from(envelope.querySelectorAll('template[data-ket-slot]'))
100
+ if (!templates.length) throw new Error('navigation response has no slots')
101
+ const names = new Set()
102
+ for (const template of templates) {
103
+ const name = template.getAttribute('data-ket-slot')
104
+ if (!name || names.has(name)) throw new Error('navigation response has a missing or duplicate slot')
105
+ names.add(name)
106
+ if (document.querySelectorAll(slotSelector(name)).length !== 1)
107
+ throw new Error('current document does not have exactly one slot named "' + name + '"')
108
+ }
109
+ await Promise.all(templates.map((template) => loadPlaced(template.content, true)))
110
+ const changed = []
111
+ for (const template of templates) {
112
+ const name = template.getAttribute('data-ket-slot')
113
+ const slot = document.querySelector(slotSelector(name))
114
+ islands.reconcile(slot, template.content)
115
+ changed.push(slot)
116
+ }
117
+ const title = envelope.getAttribute('data-title')
118
+ if (title !== null) document.title = title
119
+ return changed
120
+ }
121
+
122
+ let active = null
123
+ const saveScroll = () => {
124
+ const state = { ...(history.state ?? {}), __ketScroll: [window.scrollX, window.scrollY] }
125
+ history.replaceState(state, '', location.href)
126
+ }
127
+ const focusAfterNavigation = (url, changed, scroll) => {
128
+ if (url.hash) {
129
+ const target = document.getElementById(decodeURIComponent(url.hash.slice(1)))
130
+ if (target) {
131
+ target.focus?.({ preventScroll: true })
132
+ target.scrollIntoView()
133
+ return
134
+ }
135
+ }
136
+ const [x, y] = scroll ?? [0, 0]
137
+ window.scrollTo(x, y)
138
+ const target = changed.find((slot) => slot.matches?.('[data-ket-slot$="content"], main')) ?? changed[0]
139
+ if (!target) return
140
+ const hadTabIndex = target.hasAttribute('tabindex')
141
+ if (!hadTabIndex) target.setAttribute('tabindex', '-1')
142
+ target.focus?.({ preventScroll: true })
143
+ if (!hadTabIndex) target.addEventListener('blur', () => target.removeAttribute('tabindex'), { once: true })
144
+ }
145
+
146
+ const navigate = async (asked, mode = 'push', scroll = null) => {
147
+ const target = new URL(asked, location.href)
148
+ if (mode === 'push') saveScroll()
149
+ active?.abort()
150
+ const controller = new AbortController()
151
+ active = controller
152
+ document.documentElement.setAttribute('data-ket-navigating', '')
153
+ document.documentElement.setAttribute('aria-busy', 'true')
154
+ event('ket:navigation-start', { url: target.href, mode })
155
+ let fallback = target.href
156
+ try {
157
+ const response = await fetch(target, {
158
+ credentials: 'same-origin',
159
+ headers: {
160
+ accept: fragmentsType + ', text/html;q=0.9',
161
+ 'x-ket-navigation': 'fragment-v1',
162
+ },
163
+ signal: controller.signal,
164
+ })
165
+ fallback = response.url || fallback
166
+ if (!response.ok || !response.headers.get('content-type')?.toLowerCase().startsWith(fragmentsType))
167
+ throw new Error('server did not return a navigation fragment')
168
+ const changed = await applyFragments(await response.text())
169
+ const finalUrl = new URL(response.url || target.href)
170
+ if (mode === 'push') history.pushState({ __ketScroll: [0, 0] }, '', finalUrl.href)
171
+ focusAfterNavigation(finalUrl, changed, scroll)
172
+ event('ket:navigation-complete', { url: finalUrl.href, mode })
173
+ } catch (caught) {
174
+ if (controller.signal.aborted) return
175
+ event('ket:navigation-error', { url: fallback, mode, error: caught })
176
+ hardNavigate(fallback)
177
+ } finally {
178
+ if (active === controller) {
179
+ active = null
180
+ document.documentElement.removeAttribute('data-ket-navigating')
181
+ document.documentElement.removeAttribute('aria-busy')
182
+ }
183
+ }
184
+ }
185
+
186
+ const optedOut = (element) => Boolean(element.closest?.('[data-ket-reload]'))
187
+ const navigationEnabled = Boolean(document.querySelector('[data-ket-slot]'))
188
+ document.addEventListener('click', (click) => {
189
+ if (!navigationEnabled) return
190
+ if (click.defaultPrevented || click.button !== 0 || click.metaKey || click.ctrlKey || click.shiftKey || click.altKey) return
191
+ const anchor = click.target.closest?.('a[href]')
192
+ if (!anchor || optedOut(anchor) || anchor.hasAttribute('download')) return
193
+ if (anchor.target && anchor.target !== '_self') return
194
+ const target = new URL(anchor.href, location.href)
195
+ if (target.origin !== location.origin) return
196
+ if (target.pathname === location.pathname && target.search === location.search && target.hash !== location.hash) return
197
+ click.preventDefault()
198
+ void navigate(target)
199
+ })
200
+
201
+ document.addEventListener('submit', (submit) => {
202
+ if (!navigationEnabled) return
203
+ if (submit.defaultPrevented) return
204
+ const form = submit.target
205
+ if (!(form instanceof HTMLFormElement) || optedOut(form) || String(form.method || 'get').toLowerCase() !== 'get') return
206
+ const target = new URL(form.action || location.href, location.href)
207
+ if (target.origin !== location.origin || (form.target && form.target !== '_self')) return
208
+ target.search = ''
209
+ for (const [name, value] of new FormData(form, submit.submitter))
210
+ target.searchParams.append(name, typeof value === 'string' ? value : value.name)
211
+ submit.preventDefault()
212
+ void navigate(target)
213
+ })
214
+
215
+ if (navigationEnabled) {
216
+ history.scrollRestoration = 'manual'
217
+ if (!history.state?.__ketScroll) history.replaceState({ ...(history.state ?? {}), __ketScroll: [window.scrollX, window.scrollY] }, '', location.href)
218
+ window.addEventListener('popstate', (pop) => void navigate(location.href, 'pop', pop.state?.__ketScroll ?? [0, 0]))
219
+ }
220
+ globalThis.__ketNavigation = { applyFragments, navigate, islands }
221
+ `;
222
+ const send = async (res, result) => {
223
+ if (typeof result.body === 'string' || result.body instanceof Uint8Array) {
224
+ const body = typeof result.body === 'string' &&
225
+ (result.type ?? 'text/html').toLowerCase().startsWith('text/html') &&
226
+ (result.body.includes('<ket-island') || result.body.includes('data-ket-slot='))
227
+ ? bootstrapDocument(result.body)
228
+ : result.body;
229
+ res.writeHead(result.status ?? 200, {
230
+ 'content-type': contentType(result.type ?? 'text/html'),
231
+ ...result.headers,
232
+ });
233
+ res.end(body);
234
+ return;
235
+ }
236
+ res.writeHead(result.status ?? 200, {
237
+ 'content-type': contentType(result.type ?? 'text/html'),
238
+ ...result.headers,
239
+ });
240
+ // pipeline owns backpressure and destroys the source when the client disappears.
241
+ // Awaiting 'drain' by hand never settles on an abort — the response emits only
242
+ // 'close' — which hung the handler and leaked one fd per cancelled download.
243
+ await pipeline(result.body, res);
244
+ };
245
+ const readBody = async (req) => {
246
+ const chunks = [];
247
+ for await (const c of req)
248
+ chunks.push(c);
249
+ if (!chunks.length)
250
+ return {};
251
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
252
+ };
253
+ export async function createKetServer(o) {
254
+ if (!o.adapter && !o.pool)
255
+ throw new KetErr({ code: 'E_NO_DATABASE', message: 'createKetServer needs either an adapter or a pool' });
256
+ if (o.pool && !o.resolveDatastore)
257
+ throw new KetErr({
258
+ code: 'E_NO_RESOLVER',
259
+ message: 'a pool needs resolveDatastore to know which database a request belongs to',
260
+ });
261
+ // Compile once: matching stays cheap per request, and an ambiguous surface is
262
+ // refused before the server starts rather than depending on object insertion order.
263
+ const matchRoute = compileRoutes(o.routes ?? {});
264
+ // Per-request database resolution. The single-adapter case is the same code path
265
+ // with a resolver that always answers the same thing.
266
+ const withDb = async (url, req, fn) => {
267
+ if (!o.pool)
268
+ return fn(o.adapter);
269
+ const key = o.resolveDatastore(url, req);
270
+ if (!key) {
271
+ throw new KetErr({
272
+ code: 'E_UNKNOWN_TENANT',
273
+ message: `no datastore for ${url.host}${url.pathname}`,
274
+ hint: 'resolveDatastore returned null — the request names a tenant this app does not serve',
275
+ });
276
+ }
277
+ return o.pool.with(key, fn);
278
+ };
279
+ // With one database the stream store lives in it. With a database per tenant it
280
+ // does not: whose database a stream belongs to is a separate question, so the
281
+ // default stays in memory and the caller passes a store when they have answered it.
282
+ const configuredMounts = o.assets ? (Array.isArray(o.assets) ? o.assets : [o.assets]) : [];
283
+ // Browser-safe ketjs-view output is framework infrastructure, like /_ket/fn:
284
+ // callers should not need to find and mount a transitive package themselves.
285
+ const mounts = [
286
+ {
287
+ prefix: '/_ket/view/',
288
+ // `tsx` resolves the workspace package to src/index.ts while production
289
+ // resolves its export to dist/index.js. Both entries share this dist root.
290
+ dir: fileURLToPath(new URL('../dist/', import.meta.resolve('@ketvietlab/ketjs-view'))),
291
+ },
292
+ ...configuredMounts,
293
+ ];
294
+ const resolveTheme = async (url, req) => !o.theme ? null : typeof o.theme === 'function' ? o.theme(url, req) : o.theme;
295
+ const resolveIslandClients = async (url, req, theme) => {
296
+ if (typeof o.islandClients === 'function')
297
+ return o.islandClients(url, req);
298
+ return o.islandClients ?? theme?.clients ?? {};
299
+ };
300
+ const streams = await createStreams(o.streamStore ?? (o.adapter ? dbStreamStore(o.adapter) : memoryStreamStore()));
301
+ const server = createServer(async (req, res) => {
302
+ const url = new URL(req.url ?? '/', `http://${req.headers.host ?? 'localhost'}`);
303
+ try {
304
+ // Static files. Several mounts, because the app has its own and every
305
+ // installed module may ship some — and a module's must stop being served the
306
+ // moment it is switched off, which is what `resolve` is for: it answers per
307
+ // request instead of being fixed when the server was built.
308
+ for (const mount of mounts) {
309
+ if (!url.pathname.startsWith(mount.prefix))
310
+ continue;
311
+ // Path traversal is the one thing a static handler must not get wrong.
312
+ const rel = normalize(url.pathname.slice(mount.prefix.length)).replace(/^(\.\.[/\\])+/, '');
313
+ const file = mount.dir !== undefined
314
+ ? rel && !rel.startsWith('..')
315
+ ? join(mount.dir, rel)
316
+ : null
317
+ : await mount.resolve(rel, url, req);
318
+ if (file === null)
319
+ break;
320
+ try {
321
+ const body = await readFile(file);
322
+ const type = ASSET_MIME[extname(rel)] ?? 'application/octet-stream';
323
+ res.writeHead(200, { 'content-type': contentType(type), 'cache-control': 'no-cache' });
324
+ return res.end(body);
325
+ }
326
+ catch {
327
+ break;
328
+ }
329
+ }
330
+ if (mounts.some((m) => url.pathname.startsWith(m.prefix))) {
331
+ res.writeHead(404, { 'content-type': 'text/plain' });
332
+ return res.end('not found');
333
+ }
334
+ const route = matchRoute(url.pathname);
335
+ if (route) {
336
+ const r = await route.value(url, req, route.params);
337
+ return await send(res, r);
338
+ }
339
+ if (url.pathname === '/_ket/manifest')
340
+ return json(res, 200, o.manifest);
341
+ if (url.pathname === '/_ket/agent')
342
+ return json(res, 200, agentDescriptor(o.manifest));
343
+ if (url.pathname === '/_ket/islands.js') {
344
+ const theme = await resolveTheme(url, req);
345
+ const clients = await resolveIslandClients(url, req, theme);
346
+ res.writeHead(200, {
347
+ 'content-type': 'text/javascript; charset=utf-8',
348
+ 'cache-control': 'no-cache',
349
+ });
350
+ return res.end(browserBootstrap(clients));
351
+ }
352
+ // Resumable stream: the client reconnects with ?from=<cursor> and gets
353
+ // exactly what it missed, never a duplicate and never a gap.
354
+ if (url.pathname.startsWith('/_ket/stream/')) {
355
+ const id = url.pathname.slice('/_ket/stream/'.length);
356
+ const from = Number(url.searchParams.get('from') ?? 0);
357
+ res.writeHead(200, {
358
+ 'content-type': 'text/event-stream',
359
+ 'cache-control': 'no-cache',
360
+ connection: 'keep-alive',
361
+ });
362
+ // A client that reloads mid-stream simply disappears. Stop tailing the
363
+ // moment that happens: the durable log keeps the chunks, and the next
364
+ // connection resumes from its cursor.
365
+ let open = true;
366
+ const stop = () => {
367
+ open = false;
368
+ };
369
+ req.on('close', stop);
370
+ res.on('close', stop);
371
+ for await (const chunk of streams.tail(id, from, { timeoutMs: 30_000 })) {
372
+ if (!open || res.writableEnded)
373
+ return;
374
+ res.write(`id: ${chunk.seq}\ndata: ${JSON.stringify(chunk.data)}\n\n`);
375
+ }
376
+ if (!open || res.writableEnded)
377
+ return;
378
+ res.write('event: done\ndata: {}\n\n');
379
+ return res.end();
380
+ }
381
+ if (url.pathname.startsWith('/_ket/fn/') && req.method === 'POST') {
382
+ const fnKey = decodeURIComponent(url.pathname.slice('/_ket/fn/'.length));
383
+ const meta = o.manifest.functions[fnKey];
384
+ if (meta?.exposure === 'internal') {
385
+ throw new KetError({
386
+ code: 'E_FUNCTION_INTERNAL',
387
+ message: `server function "${fnKey}" is internal and has no generic HTTP endpoint`,
388
+ hint: 'call it from the trusted route that owns its security policy',
389
+ });
390
+ }
391
+ const args = await readBody(req);
392
+ // Resolved before the pool lease, so a session lookup never holds one.
393
+ const scope = await o.resolveScope?.(url, req);
394
+ const allow = await o.resolveAllow?.(url, req);
395
+ const actor = await o.resolveActor?.(url, req);
396
+ const result = await withDb(url, req, (adapter) => callFn(fnKey, args, {
397
+ adapter,
398
+ manifest: o.manifest,
399
+ scope,
400
+ allow,
401
+ actor,
402
+ queueNotify: o.queueNotify,
403
+ dryRun: url.searchParams.get('dryRun') === '1',
404
+ idempotencyKey: req.headers['idempotency-key'] ?? null,
405
+ }));
406
+ return json(res, 200, result);
407
+ }
408
+ if (o.theme) {
409
+ const theme = await resolveTheme(url, req);
410
+ if (!theme) {
411
+ res.writeHead(404, { 'content-type': 'text/plain' });
412
+ return res.end('not found');
413
+ }
414
+ const scope = o.pageScope ? await o.pageScope(url, req) : {};
415
+ const pageRegion = o.pageRegion;
416
+ if (pageRegion && isNavigationRequest(req)) {
417
+ const page = scope['page'];
418
+ return send(res, navigablePage(req, {
419
+ title: String(page?.title ?? ''),
420
+ document: () => html ``,
421
+ slots: {
422
+ [pageRegion]: () => html `${trustedMarkup(theme.renderRegion(pageRegion, scope))}`,
423
+ },
424
+ }));
425
+ }
426
+ const fullHtml = bootstrapDocument(theme.renderRegion('layout', scope));
427
+ res.writeHead(200, {
428
+ 'content-type': 'text/html; charset=utf-8',
429
+ ...(o.pageRegion ? { vary: 'X-Ket-Navigation' } : {}),
430
+ });
431
+ return res.end(fullHtml);
432
+ }
433
+ return json(res, 404, { code: 'E_NOT_FOUND', message: `no route for ${url.pathname}` });
434
+ }
435
+ catch (e) {
436
+ // A streaming response has already sent its headers; there is no status code
437
+ // left to send, so the only honest thing is to close the socket.
438
+ if (res.headersSent) {
439
+ if (!res.writableEnded)
440
+ res.destroy(e);
441
+ return;
442
+ }
443
+ if (e instanceof KetError)
444
+ return json(res, e.code === 'E_PAYLOAD_TOO_LARGE' ? 413 : 400, e.toJSON());
445
+ return json(res, 500, { code: 'E_INTERNAL', message: e.message });
446
+ }
447
+ });
448
+ return {
449
+ server,
450
+ streams,
451
+ listen(port = o.port ?? 3000) {
452
+ return new Promise((resolve) => server.listen(port, () => resolve(server.address().port)));
453
+ },
454
+ close() {
455
+ return new Promise((r) => server.close(() => r()));
456
+ },
457
+ };
458
+ }
459
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../../../../../../packages/ketjs/src/server/http.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ import type { Adapter } from '../types.ts';
2
+ export declare const IDEM_DDL = "\nCREATE TABLE IF NOT EXISTS ket_idem (\n key TEXT PRIMARY KEY,\n fn TEXT NOT NULL,\n state TEXT NOT NULL,\n result TEXT,\n created_at TEXT NOT NULL\n);\n";
3
+ export declare const IDEM_DDL_PG: string;
4
+ export type IdemRecord = {
5
+ state: 'pending' | 'done';
6
+ result: unknown;
7
+ };
8
+ export declare function createIdempotency(adapter: Adapter, o?: {
9
+ now?: () => string;
10
+ }): Promise<{
11
+ /**
12
+ * Claim a key. False means somebody else holds it; the primary key decides.
13
+ *
14
+ * A caller that dies between claiming and finishing would otherwise block the
15
+ * key forever, so a claim older than `staleMs` is treated as abandoned and
16
+ * taken over. This is a liveness/safety trade made explicit: too short and a
17
+ * slow call gets run twice, too long and a stuck key blocks retries.
18
+ */
19
+ claim(key: string, fn: string, staleMs?: number): Promise<boolean>;
20
+ read(key: string): Promise<IdemRecord | null>;
21
+ complete(key: string, result: unknown): Promise<void>;
22
+ /** Drop a claim whose call then failed, so a retry is not locked out. */
23
+ release(key: string): Promise<void>;
24
+ /**
25
+ * Records are only useful for as long as a client might retry. Without this the
26
+ * table grows forever, which is the quiet way a correctness feature becomes an
27
+ * operational problem.
28
+ */
29
+ sweep(olderThanMs?: number): Promise<number>;
30
+ }>;
31
+ //# sourceMappingURL=idem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idem.d.ts","sourceRoot":"","sources":["../../../../../packages/ketjs/src/server/idem.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAE1C,eAAO,MAAM,QAAQ,4LAQpB,CAAA;AAED,eAAO,MAAM,WAAW,QAAkF,CAAA;AAE1G,MAAM,MAAM,UAAU,GAAG;IAAE,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAA;AAEvE,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,OAAO,EAAE,CAAC,GAAE;IAAE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAA;CAAO;IAOpF;;;;;;;OAOG;eACc,MAAM,MAAM,MAAM,qBAAyB,OAAO,CAAC,OAAO,CAAC;cAc5D,MAAM,GAAG,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC;kBAS/B,MAAM,UAAU,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IAM3D,yEAAyE;iBACtD,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzC;;;;OAIG;iCAC0C,OAAO,CAAC,MAAM,CAAC;GAM/D"}
@@ -0,0 +1,68 @@
1
+ // Idempotency records: durable, shared between instances, and cold. This is the one
2
+ // use that genuinely belongs in the database and always did — it is a keyed lookup,
3
+ // not a log, so it gets a keyed table.
4
+ export const IDEM_DDL = `
5
+ CREATE TABLE IF NOT EXISTS ket_idem (
6
+ key TEXT PRIMARY KEY,
7
+ fn TEXT NOT NULL,
8
+ state TEXT NOT NULL,
9
+ result TEXT,
10
+ created_at TEXT NOT NULL
11
+ );
12
+ `;
13
+ export const IDEM_DDL_PG = IDEM_DDL.replace('created_at TEXT NOT NULL', 'created_at TIMESTAMPTZ NOT NULL');
14
+ export async function createIdempotency(adapter, o = {}) {
15
+ const now = o.now ?? (() => new Date().toISOString());
16
+ const pg = adapter.name === 'postgres';
17
+ const p = (n) => (pg ? `$${n}` : '?');
18
+ await adapter.exec(pg ? IDEM_DDL_PG : IDEM_DDL);
19
+ return {
20
+ /**
21
+ * Claim a key. False means somebody else holds it; the primary key decides.
22
+ *
23
+ * A caller that dies between claiming and finishing would otherwise block the
24
+ * key forever, so a claim older than `staleMs` is treated as abandoned and
25
+ * taken over. This is a liveness/safety trade made explicit: too short and a
26
+ * slow call gets run twice, too long and a stuck key blocks retries.
27
+ */
28
+ async claim(key, fn, staleMs = 5 * 60_000) {
29
+ const r = await adapter.run(`INSERT INTO ket_idem (key, fn, state, created_at) VALUES (${p(1)}, ${p(2)}, 'pending', ${p(3)}) ON CONFLICT DO NOTHING`, [key, fn, now()]);
30
+ if (r.changes === 1)
31
+ return true;
32
+ const cutoff = new Date(Date.parse(now()) - staleMs).toISOString();
33
+ const taken = await adapter.run(`UPDATE ket_idem SET created_at = ${p(1)} WHERE key = ${p(2)} AND state = 'pending' AND created_at < ${p(3)}`, [now(), key, cutoff]);
34
+ return taken.changes === 1;
35
+ },
36
+ async read(key) {
37
+ const rows = await adapter.all(`SELECT state, result FROM ket_idem WHERE key = ${p(1)}`, [key]);
38
+ const r = rows[0];
39
+ if (!r)
40
+ return null;
41
+ return {
42
+ state: String(r.state),
43
+ result: r.result == null ? null : JSON.parse(String(r.result)),
44
+ };
45
+ },
46
+ async complete(key, result) {
47
+ await adapter.run(`UPDATE ket_idem SET state = 'done', result = ${p(1)} WHERE key = ${p(2)}`, [
48
+ JSON.stringify(result),
49
+ key,
50
+ ]);
51
+ },
52
+ /** Drop a claim whose call then failed, so a retry is not locked out. */
53
+ async release(key) {
54
+ await adapter.run(`DELETE FROM ket_idem WHERE key = ${p(1)} AND state = 'pending'`, [key]);
55
+ },
56
+ /**
57
+ * Records are only useful for as long as a client might retry. Without this the
58
+ * table grows forever, which is the quiet way a correctness feature becomes an
59
+ * operational problem.
60
+ */
61
+ async sweep(olderThanMs = 24 * 60 * 60_000) {
62
+ const cutoff = new Date(Date.parse(now()) - olderThanMs).toISOString();
63
+ const r = await adapter.run(`DELETE FROM ket_idem WHERE created_at < ${p(1)}`, [cutoff]);
64
+ return r.changes;
65
+ },
66
+ };
67
+ }
68
+ //# sourceMappingURL=idem.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idem.js","sourceRoot":"","sources":["../../../../../../packages/ketjs/src/server/idem.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { JobSpec, KetModule } from '../types.ts';
2
+ export declare function defineJob(spec: JobSpec): JobSpec;
3
+ export declare function registerJobs(modules: KetModule[]): Map<string, JobSpec>;
4
+ export declare const jobDefinition: (key: string) => JobSpec | null;
5
+ //# sourceMappingURL=jobs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../../../../packages/ketjs/src/server/jobs.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAIrD,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAShD;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,SAAS,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAKvE;AAED,eAAO,MAAM,aAAa,QAAS,MAAM,KAAG,OAAO,GAAG,IAAiC,CAAA"}
@@ -0,0 +1,24 @@
1
+ // Job declarations are data in the manifest and executable handlers in this
2
+ // process. Keeping the two joined by one qualified key mirrors server functions
3
+ // while allowing `ket worker` to load the same AppSpec without booting HTTP.
4
+ import { KetError } from '../kernel/errors.js';
5
+ const registry = new Map();
6
+ export function defineJob(spec) {
7
+ if (spec.idempotent !== true)
8
+ throw new KetError({
9
+ code: 'E_JOB_NOT_IDEMPOTENT',
10
+ message: 'defineJob() requires idempotent: true because delivery is at-least-once',
11
+ });
12
+ if (typeof spec.handler !== 'function')
13
+ throw new KetError({ code: 'E_JOB_NO_HANDLER', message: 'defineJob() requires a handler' });
14
+ return spec;
15
+ }
16
+ export function registerJobs(modules) {
17
+ registry.clear();
18
+ for (const module of modules)
19
+ for (const [name, spec] of Object.entries(module.jobs))
20
+ registry.set(`${module.name}.${name}`, spec);
21
+ return registry;
22
+ }
23
+ export const jobDefinition = (key) => registry.get(key) ?? null;
24
+ //# sourceMappingURL=jobs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobs.js","sourceRoot":"","sources":["../../../../../../packages/ketjs/src/server/jobs.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,gFAAgF;AAChF,6EAA6E;AAE7E,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAA;AAG9C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAmB,CAAA;AAE3C,MAAM,UAAU,SAAS,CAAC,IAAa;IACrC,IAAI,IAAI,CAAC,UAAU,KAAK,IAAI;QAC1B,MAAM,IAAI,QAAQ,CAAC;YACjB,IAAI,EAAE,sBAAsB;YAC5B,OAAO,EAAE,yEAAyE;SACnF,CAAC,CAAA;IACJ,IAAI,OAAO,IAAI,CAAC,OAAO,KAAK,UAAU;QACpC,MAAM,IAAI,QAAQ,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,gCAAgC,EAAE,CAAC,CAAA;IAC7F,OAAO,IAAI,CAAA;AACb,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,OAAoB;IAC/C,QAAQ,CAAC,KAAK,EAAE,CAAA;IAChB,KAAK,MAAM,MAAM,IAAI,OAAO;QAC1B,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,QAAQ,CAAC,GAAG,CAAC,GAAG,MAAM,CAAC,IAAI,IAAI,IAAI,EAAE,EAAE,IAAI,CAAC,CAAA;IACtG,OAAO,QAAQ,CAAA;AACjB,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,GAAW,EAAkB,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,CAAA"}
@@ -0,0 +1,18 @@
1
+ export type MultipartPart = {
2
+ name: string;
3
+ filename?: string;
4
+ type?: string;
5
+ body: AsyncIterable<Uint8Array>;
6
+ };
7
+ export type MultipartOptions = {
8
+ maxBytes?: number;
9
+ maxParts?: number;
10
+ maxHeaderBytes?: number;
11
+ };
12
+ /**
13
+ * Parse multipart sequentially without buffering file bodies. A part body must be
14
+ * consumed before requesting the next part; if a caller stops early, the parser
15
+ * drains only to the next boundary so the remaining parts stay readable.
16
+ */
17
+ export declare function multipart(source: AsyncIterable<Uint8Array>, contentType: string, options?: MultipartOptions): AsyncGenerator<MultipartPart>;
18
+ //# sourceMappingURL=multipart.d.ts.map