Skip to content

Commit ec1e107

Browse files
committed
fix streaming ref materialization
1 parent 20790b1 commit ec1e107

4 files changed

Lines changed: 796 additions & 189 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
import { recordMaterializedAccessKeys } from '@/lib/execution/payloads/access-keys'
2+
import {
3+
isLargeArrayManifest,
4+
materializeLargeArrayManifest,
5+
} from '@/lib/execution/payloads/large-array-manifest'
6+
import {
7+
getLargeValueMaterializationError,
8+
isLargeValueRef,
9+
} from '@/lib/execution/payloads/large-value-ref'
10+
import {
11+
assertInlineMaterializationSize,
12+
type ExecutionMaterializationContext,
13+
MAX_INLINE_MATERIALIZATION_BYTES,
14+
} from '@/lib/execution/payloads/materialization.server'
15+
import { materializeLargeValueRef } from '@/lib/execution/payloads/store'
16+
17+
interface InlineMaterializationOptions {
18+
maxBytes?: number
19+
}
20+
21+
type InlineMaterializationMemo = WeakMap<object, Promise<unknown>>
22+
23+
export function getInlineJsonByteLength(value: unknown): number | undefined {
24+
const json = JSON.stringify(value)
25+
return json === undefined ? undefined : Buffer.byteLength(json, 'utf8')
26+
}
27+
28+
function getArrayItemByteLength(value: unknown): number {
29+
return getInlineJsonByteLength(value) ?? Buffer.byteLength('null', 'utf8')
30+
}
31+
32+
function getObjectEntryByteLength(key: string, value: unknown): number | undefined {
33+
const valueBytes = getInlineJsonByteLength(value)
34+
if (valueBytes === undefined) {
35+
return undefined
36+
}
37+
return Buffer.byteLength(JSON.stringify(key), 'utf8') + 1 + valueBytes
38+
}
39+
40+
function withLocalLargeValueExecutionIds(
41+
context: ExecutionMaterializationContext | undefined,
42+
materializedValue: unknown
43+
): ExecutionMaterializationContext | undefined {
44+
if (!context) {
45+
return context
46+
}
47+
recordMaterializedAccessKeys(context, materializedValue)
48+
return {
49+
...context,
50+
largeValueKeys: context.largeValueKeys,
51+
fileKeys: context.fileKeys,
52+
}
53+
}
54+
55+
export async function materializeInlineExecutionValue(
56+
value: unknown,
57+
context: ExecutionMaterializationContext | undefined,
58+
options: InlineMaterializationOptions = {}
59+
): Promise<unknown> {
60+
return materializeInlineExecutionValueWithinBudget(
61+
value,
62+
context,
63+
options.maxBytes ?? MAX_INLINE_MATERIALIZATION_BYTES,
64+
new WeakMap<object, Promise<unknown>>()
65+
)
66+
}
67+
68+
async function materializeInlineExecutionValueWithinBudget(
69+
value: unknown,
70+
context: ExecutionMaterializationContext | undefined,
71+
maxBytes: number,
72+
memo: InlineMaterializationMemo
73+
): Promise<unknown> {
74+
if (isLargeArrayManifest(value)) {
75+
assertInlineMaterializationSize(value.byteSize, maxBytes)
76+
const materialized = await materializeLargeArrayManifest(value, {
77+
...context,
78+
maxBytes,
79+
})
80+
return materializeInlineExecutionValueWithinBudget(
81+
materialized,
82+
withLocalLargeValueExecutionIds(context, materialized),
83+
maxBytes,
84+
memo
85+
)
86+
}
87+
88+
if (isLargeValueRef(value)) {
89+
assertInlineMaterializationSize(value.size, maxBytes)
90+
const materialized = await materializeLargeValueRef(value, {
91+
...context,
92+
maxBytes,
93+
})
94+
if (materialized === undefined) {
95+
throw getLargeValueMaterializationError(value)
96+
}
97+
return materializeInlineExecutionValueWithinBudget(
98+
materialized,
99+
withLocalLargeValueExecutionIds(context, materialized),
100+
maxBytes,
101+
memo
102+
)
103+
}
104+
105+
if (!value || typeof value !== 'object') {
106+
const valueBytes = getInlineJsonByteLength(value)
107+
if (valueBytes !== undefined) {
108+
assertInlineMaterializationSize(valueBytes, maxBytes)
109+
}
110+
return value
111+
}
112+
113+
const cached = memo.get(value)
114+
if (cached) {
115+
return cached
116+
}
117+
118+
if (Array.isArray(value)) {
119+
const result: unknown[] = []
120+
memo.set(value, Promise.resolve(result))
121+
let usedBytes = Buffer.byteLength('[]', 'utf8')
122+
for (const item of value) {
123+
const commaBytes = result.length > 0 ? 1 : 0
124+
const remainingBytes = maxBytes - usedBytes - commaBytes
125+
assertInlineMaterializationSize(0, remainingBytes)
126+
const materializedItem = await materializeInlineExecutionValueWithinBudget(
127+
item,
128+
context,
129+
remainingBytes,
130+
memo
131+
)
132+
const itemBytes = getArrayItemByteLength(materializedItem)
133+
usedBytes += commaBytes + itemBytes
134+
assertInlineMaterializationSize(usedBytes, maxBytes)
135+
result.push(materializedItem)
136+
}
137+
return result
138+
}
139+
140+
const result: Record<string, unknown> = {}
141+
memo.set(value, Promise.resolve(result))
142+
let usedBytes = Buffer.byteLength('{}', 'utf8')
143+
for (const [key, entryValue] of Object.entries(value as Record<string, unknown>)) {
144+
const keyBytes = Buffer.byteLength(JSON.stringify(key), 'utf8') + 1
145+
const commaBytes = Object.keys(result).length > 0 ? 1 : 0
146+
const remainingBytes = maxBytes - usedBytes - commaBytes - keyBytes
147+
assertInlineMaterializationSize(0, remainingBytes)
148+
const materializedEntryValue = await materializeInlineExecutionValueWithinBudget(
149+
entryValue,
150+
context,
151+
remainingBytes,
152+
memo
153+
)
154+
const entryBytes = getObjectEntryByteLength(key, materializedEntryValue)
155+
if (entryBytes !== undefined) {
156+
usedBytes += commaBytes + entryBytes
157+
assertInlineMaterializationSize(usedBytes, maxBytes)
158+
}
159+
result[key] = materializedEntryValue
160+
}
161+
return result
162+
}

0 commit comments

Comments
 (0)