-
-
Notifications
You must be signed in to change notification settings - Fork 610
/
streaming.ts
117 lines (109 loc) · 3.42 KB
/
streaming.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import { raw } from '../helper/html'
import type { HtmlEscapedString } from '../utils/html'
import { childrenToString } from './components'
import type { FC, Child } from './index'
let suspenseCounter = 0
/**
* @experimental
* `Suspense` is an experimental feature.
* The API might be changed.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const Suspense: FC<{ fallback: any }> = async ({ children, fallback }) => {
if (!children) {
return fallback.toString()
}
if (!Array.isArray(children)) {
children = [children]
}
let resArray: HtmlEscapedString[] | Promise<HtmlEscapedString[]>[] = []
try {
resArray = children.map((c) => c.toString()) as HtmlEscapedString[]
} catch (e) {
if (e instanceof Promise) {
resArray = [e.then(() => childrenToString(children as Child[]))] as Promise<
HtmlEscapedString[]
>[]
} else {
throw e
}
}
if (resArray.some((res) => (res as {}) instanceof Promise)) {
const index = suspenseCounter++
return raw(`<template id="H:${index}"></template>${fallback.toString()}<!--/$-->`, [
({ buffer }) => {
return Promise.all(resArray).then((htmlArray) => {
htmlArray = htmlArray.flat()
const content = htmlArray.join('')
if (buffer) {
buffer[0] = buffer[0].replace(
new RegExp(`<template id="H:${index}"></template>.*?<!--/\\$-->`),
content
)
}
const html = buffer
? ''
: `<template>${content}</template><script>
((d,c,n) => {
c=d.currentScript.previousSibling
d=d.getElementById('H:${index}')
if(!d)return
do{n=d.nextSibling;n.remove()}while(n.nodeType!=8||n.nodeValue!='/$')
d.replaceWith(c.content)
})(document)
</script>`
if (htmlArray.every((html) => !(html as HtmlEscapedString).callbacks?.length)) {
return html
}
return raw(
html,
htmlArray.map((html) => (html as HtmlEscapedString).callbacks || []).flat()
)
})
},
])
} else {
return raw(resArray.join(''))
}
}
const textEncoder = new TextEncoder()
/**
* @experimental
* `renderToReadableStream()` is an experimental feature.
* The API might be changed.
*/
export const renderToReadableStream = (
str: HtmlEscapedString | Promise<HtmlEscapedString>
): ReadableStream<Uint8Array> => {
const reader = new ReadableStream<Uint8Array>({
async start(controller) {
const resolved = str instanceof Promise ? await str : await str.toString()
controller.enqueue(textEncoder.encode(resolved))
let resolvedCount = 0
const callbacks: Promise<void>[] = []
const then = (promise: Promise<string>) => {
callbacks.push(
promise
.catch((err) => {
console.trace(err)
return ''
})
.then((res) => {
if ((res as HtmlEscapedString).callbacks) {
const callbacks = (res as HtmlEscapedString).callbacks || []
callbacks.map((c) => c({})).forEach(then)
}
resolvedCount++
controller.enqueue(textEncoder.encode(res))
})
)
}
;(resolved as HtmlEscapedString).callbacks?.map((c) => c({})).forEach(then)
while (resolvedCount !== callbacks.length) {
await Promise.all(callbacks)
}
controller.close()
},
})
return reader
}