ink-framework 0.1.0-alpha.10 → 0.1.0-alpha.13
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.
- package/DESIGN.md +168 -0
- package/package.json +9 -4
- package/src/canvas.ts +59 -0
- package/src/compiler-runtime.ts +6 -0
- package/src/controller-react.ts +33 -0
- package/src/data-views.ts +249 -0
- package/src/file-router.tsx +1 -1
- package/src/fixed-list-row.ts +65 -0
- package/src/image.ts +30 -2
- package/src/index.ts +31 -4
- package/src/jsx-runtime.ts +21 -0
- package/src/list.ts +71 -34
- package/src/native-list.ts +273 -74
- package/src/native.ts +8 -0
- package/src/prefresh.d.ts +3 -0
- package/src/react-compat.ts +24 -0
- package/src/react.ts +26 -6
- package/src/refresh.ts +29 -0
- package/src/renderer.ts +183 -137
- package/src/reorder.ts +12 -15
- package/src/snapshot.ts +1 -1
- package/src/visibility.ts +264 -0
package/src/native-list.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { createElement,
|
|
2
|
-
import {
|
|
3
|
-
import { useAction } from "./action";
|
|
1
|
+
import { createElement, useLayoutEffect, useMemo, useRef, useState, type ComponentProps, type ReactNode } from "./react";
|
|
2
|
+
import { ReactList, useListPaginationEvents, type ListPagination, type ListProps } from "./list";
|
|
4
3
|
import type { Row } from "./image";
|
|
5
4
|
import { pressHandler } from "./press";
|
|
6
5
|
import { rememberListPatch, type NativeListItem } from "./list-patch";
|
|
7
6
|
|
|
7
|
+
export { fixedListRow } from "./fixed-list-row";
|
|
8
|
+
|
|
8
9
|
type Captured = { value: unknown; prototype?: object | null; fields?: [PropertyKey, boolean, Captured][] };
|
|
9
10
|
type Scope = { identity: string; values: readonly Captured[] };
|
|
10
11
|
type Capture = { identity: string; values: () => readonly unknown[]; usesIndex?: boolean };
|
|
@@ -15,6 +16,8 @@ const keys = /* @__PURE__ */ new WeakMap<Function, string>();
|
|
|
15
16
|
const plainRecords = /* @__PURE__ */ new WeakSet<object>();
|
|
16
17
|
declare const __inkIsProxy: (value: unknown) => boolean;
|
|
17
18
|
const isProxy = typeof __inkIsProxy === "function" ? __inkIsProxy : undefined;
|
|
19
|
+
declare const __inkIsDenseArray: (value: unknown) => boolean;
|
|
20
|
+
const isDenseArray = typeof __inkIsDenseArray === "function" ? __inkIsDenseArray : undefined;
|
|
18
21
|
|
|
19
22
|
function bareFunctionPrototype(value: unknown, owner: Function): value is object {
|
|
20
23
|
if (value === null || typeof value !== "object" || isProxy?.(value)) return false;
|
|
@@ -71,7 +74,7 @@ export function listText(parts: unknown[]): string {
|
|
|
71
74
|
export function listRowFields(props: ComponentProps<typeof Row>) {
|
|
72
75
|
return { image: props.image, title: props.title, titleMaxLines: props.titleMaxLines,
|
|
73
76
|
titleIcon: props.titleIcon, subtitle: props.subtitle, subtitleIcon: props.subtitleIcon,
|
|
74
|
-
onLongPress: props.onLongPress,
|
|
77
|
+
selected: props.selected, onLongPress: props.onLongPress,
|
|
75
78
|
onPress: pressHandler(props) ?? (props.onLongPress ? () => {} : undefined) };
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -99,21 +102,73 @@ function nativeValue(value: unknown, previous?: unknown): unknown {
|
|
|
99
102
|
}
|
|
100
103
|
|
|
101
104
|
type NativeItem = NativeListItem;
|
|
102
|
-
type
|
|
103
|
-
type Records<T> = { data: NativeItem[]; rows: Map<string, RowRecord<T>>; template: string;
|
|
105
|
+
type Records<T> = { data: NativeItem[]; positions: Map<string, number>; template: string;
|
|
104
106
|
items: readonly T[]; key: string | Function; measurementKey?: string | number; scope?: Scope };
|
|
107
|
+
const cooperativeListSize = 512;
|
|
108
|
+
const preparationBudget = 4;
|
|
109
|
+
const minimumPreparationRows = 32;
|
|
110
|
+
const maximumPreparationRows = 128;
|
|
111
|
+
type Preparation<T> = { steps: Generator<void, Records<T> | null>; result?: { value: Records<T> | null }; error?: { value: unknown } };
|
|
112
|
+
type Request<T> = { items: readonly T[]; key: string | Function; plan: Plan<T>;
|
|
113
|
+
measurementKey?: string | number; scope?: Scope; preparation: Preparation<T>; synchronous?: { value: Records<T> | null };
|
|
114
|
+
eventPositions: Map<string, number | undefined> };
|
|
115
|
+
type Prepared<T> = { request: Request<T>; generation: number } &
|
|
116
|
+
({ status: "ready"; value: Records<T> | null } | { status: "error"; error: unknown });
|
|
117
|
+
|
|
118
|
+
function listItem<T>(items: readonly T[], index: number): T {
|
|
119
|
+
const property = Object.getOwnPropertyDescriptor(items, index);
|
|
120
|
+
if (!property || !("value" in property)) throw needsReact;
|
|
121
|
+
return property.value;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Reproject only the items replaced in place, when every key stays at its position.
|
|
126
|
+
* Returns undefined when keys move, so the caller rebuilds the whole list.
|
|
127
|
+
*/
|
|
128
|
+
function* replaceInPlace<T>(previous: Records<T>, items: readonly T[], plan: Plan<T>,
|
|
129
|
+
keyExtractor: ListProps<T>["keyExtractor"], dense: boolean): Generator<void, Records<T> | undefined> {
|
|
130
|
+
let data: NativeItem[] | undefined;
|
|
131
|
+
const changes: NativeItem[] = [];
|
|
132
|
+
const length = items.length;
|
|
133
|
+
for (let index = 0; index < length; index++) {
|
|
134
|
+
const item = dense ? items[index] : listItem(items, index);
|
|
135
|
+
if (item === previous.items[index]) continue;
|
|
136
|
+
const before = previous.data[index];
|
|
137
|
+
if (!plainData(item)) throw needsReact;
|
|
138
|
+
if (keyExtractor(item, index) !== before.key) return undefined;
|
|
139
|
+
const projected = nativeValue(plan.project(item, index), before.values);
|
|
140
|
+
if (projected !== before.values) {
|
|
141
|
+
const record = { key: before.key, values: projected, measurementKey: before.measurementKey };
|
|
142
|
+
(data ??= previous.data.slice())[index] = record;
|
|
143
|
+
changes.push(record);
|
|
144
|
+
}
|
|
145
|
+
yield;
|
|
146
|
+
}
|
|
147
|
+
if (data) rememberListPatch(data, { previous: previous.data, changes });
|
|
148
|
+
return { ...previous, data: data ?? previous.data, items };
|
|
149
|
+
}
|
|
105
150
|
|
|
106
|
-
function captureValue(value: unknown, cache: WeakMap<object, Captured>): Captured {
|
|
151
|
+
function captureValue(value: unknown, cache: WeakMap<object, Captured>, visiting = new Set<object>()): Captured {
|
|
107
152
|
if (value === null || typeof value !== "object" && typeof value !== "function") return { value };
|
|
108
153
|
const previous = cache.get(value);
|
|
109
|
-
if (previous) return previous;
|
|
154
|
+
if (previous && isProxy) return previous;
|
|
155
|
+
if (isProxy?.(value) || visiting.has(value) || visiting.size >= 64) throw needsReact;
|
|
156
|
+
const prototype = Object.getPrototypeOf(value);
|
|
157
|
+
if (prototype !== null && prototype !== Object.prototype && prototype !== Array.prototype
|
|
158
|
+
&& !(typeof value === "function" && prototype === Function.prototype)) throw needsReact;
|
|
159
|
+
visiting.add(value);
|
|
110
160
|
const properties: Record<PropertyKey, PropertyDescriptor> = Object.getOwnPropertyDescriptors(value);
|
|
111
|
-
const
|
|
161
|
+
const fields: NonNullable<Captured["fields"]> = [];
|
|
162
|
+
for (const key of Reflect.ownKeys(properties)) {
|
|
112
163
|
const property = properties[key];
|
|
164
|
+
if (!("value" in property)) throw needsReact;
|
|
165
|
+
if (["toString", "valueOf", Symbol.toPrimitive].includes(key) && typeof property.value === "function") throw needsReact;
|
|
113
166
|
const field = key === "prototype" && typeof value === "function" && bareFunctionPrototype(property.value, value)
|
|
114
|
-
? { value: property.value } : captureValue(property.value, cache);
|
|
115
|
-
|
|
116
|
-
}
|
|
167
|
+
? { value: property.value } : captureValue(property.value, cache, visiting);
|
|
168
|
+
fields.push([key, !!property.enumerable, field]);
|
|
169
|
+
}
|
|
170
|
+
visiting.delete(value);
|
|
171
|
+
const captured: Captured = { value, prototype, fields };
|
|
117
172
|
cache.set(value, captured);
|
|
118
173
|
return captured;
|
|
119
174
|
}
|
|
@@ -135,64 +190,146 @@ function sameScopeValues(before: readonly Captured[], after: readonly Captured[]
|
|
|
135
190
|
return before.length === after.length && after.every((value, index) => sameCapture(before[index], value, seen));
|
|
136
191
|
}
|
|
137
192
|
|
|
193
|
+
function captureScope<T>(plan: Plan<T>, items: readonly T[]): Scope | undefined {
|
|
194
|
+
if (!plan.scope) return;
|
|
195
|
+
const captured = items.length ? plan.scope.values() : [];
|
|
196
|
+
// Captures can mutate between slices; validate each fresh snapshot without the row cache.
|
|
197
|
+
return { identity: plan.scope.identity, values: captured.map(value => captureValue(value, new WeakMap())) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function sameScope(before?: Scope, after?: Scope): boolean {
|
|
201
|
+
return !!before && !!after && before.identity === after.identity && sameScopeValues(before.values, after.values);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function capturesMatch<T>(request: Request<T>, latestPlan: Plan<T> | null): boolean {
|
|
205
|
+
return !!latestPlan && sameScope(request.scope, captureScope(request.plan, request.items))
|
|
206
|
+
&& (latestPlan === request.plan || sameScope(request.scope, captureScope(latestPlan, request.items)));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function* prepareRecords<T>(previous: Records<T> | null, items: readonly T[], plan: Plan<T>,
|
|
210
|
+
keyExtractor: ListProps<T>["keyExtractor"], keyIdentity: string | Function,
|
|
211
|
+
measurementKey: string | number | undefined, scope: Scope | undefined, dense: boolean): Generator<void, Records<T> | null> {
|
|
212
|
+
try {
|
|
213
|
+
if (isProxy?.(items) || Object.getPrototypeOf(items) !== Array.prototype) throw needsReact;
|
|
214
|
+
const length = items.length;
|
|
215
|
+
const unchangedScope = sameScope(previous?.scope, scope);
|
|
216
|
+
if (previous && previous.items === items && previous.key === keyIdentity
|
|
217
|
+
&& previous.template === plan.template && previous.measurementKey === measurementKey
|
|
218
|
+
&& unchangedScope) return previous;
|
|
219
|
+
if (previous && unchangedScope && typeof keyIdentity === "string" && previous.key === keyIdentity
|
|
220
|
+
&& previous.template === plan.template && previous.measurementKey === measurementKey
|
|
221
|
+
&& previous.items.length === length) {
|
|
222
|
+
const patched = yield* replaceInPlace(previous, items, plan, keyExtractor, dense);
|
|
223
|
+
if (patched !== undefined) return { ...patched, scope };
|
|
224
|
+
}
|
|
225
|
+
const positions = new Map<string, number>();
|
|
226
|
+
let unchanged = length === previous?.data.length;
|
|
227
|
+
let reordered = !unchanged;
|
|
228
|
+
const changes: NativeItem[] = [];
|
|
229
|
+
let data: NativeItem[] = [];
|
|
230
|
+
const itemKeys: string[] = [];
|
|
231
|
+
for (let index = 0; index < length; index++) {
|
|
232
|
+
const item = dense ? items[index] : listItem(items, index);
|
|
233
|
+
if (!plainData(item)) throw needsReact;
|
|
234
|
+
const key = typeof keyIdentity === "string" && previous?.key === keyIdentity && previous.items[index] === item
|
|
235
|
+
? previous.data[index].key : keyExtractor(item, index);
|
|
236
|
+
if (typeof key !== "string" || positions.has(key)) throw new Error("List keys must be unique strings");
|
|
237
|
+
const beforeIndex = previous?.template === plan.template ? previous.positions.get(key) : undefined;
|
|
238
|
+
const before = previous && beforeIndex !== undefined ? previous.data[beforeIndex] : undefined;
|
|
239
|
+
const projected = previous && beforeIndex !== undefined && unchangedScope && previous.items[beforeIndex] === item
|
|
240
|
+
&& (beforeIndex === index || plan.scope?.usesIndex === false)
|
|
241
|
+
? previous.data[beforeIndex].values : nativeValue(plan.project(item, index), before?.values);
|
|
242
|
+
const measurement = measurementKey ?? null;
|
|
243
|
+
const record = before && before.values === projected && before.measurementKey === measurement
|
|
244
|
+
? before : { key, values: projected, measurementKey: measurement };
|
|
245
|
+
positions.set(key, index);
|
|
246
|
+
itemKeys.push(key);
|
|
247
|
+
if (record !== previous?.data[index]) {
|
|
248
|
+
unchanged = false;
|
|
249
|
+
if (record !== before) changes.push(record);
|
|
250
|
+
if (key !== previous?.data[index]?.key) reordered = true;
|
|
251
|
+
}
|
|
252
|
+
data.push(record);
|
|
253
|
+
yield;
|
|
254
|
+
}
|
|
255
|
+
if (unchanged && previous) data = previous.data;
|
|
256
|
+
else if (previous) rememberListPatch(data, { previous: previous.data, changes,
|
|
257
|
+
...(reordered ? { keys: itemKeys } : {}) });
|
|
258
|
+
return { data, positions, template: plan.template, items, key: keyIdentity, measurementKey, scope };
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error !== needsReact) throw error;
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function advancePreparation<T>(preparation: Preparation<T>, cooperative: boolean) {
|
|
266
|
+
if (preparation.result) return preparation.result;
|
|
267
|
+
if (preparation.error) throw preparation.error.value;
|
|
268
|
+
const started = cooperative ? performance.now() : 0;
|
|
269
|
+
let rows = 0;
|
|
270
|
+
while (true) {
|
|
271
|
+
let step;
|
|
272
|
+
try {
|
|
273
|
+
step = preparation.steps.next();
|
|
274
|
+
} catch (error) {
|
|
275
|
+
preparation.error = { value: error };
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
if (step.done) return preparation.result = { value: step.value };
|
|
279
|
+
rows++;
|
|
280
|
+
// Identity-only sparse scans do not yield: a single replaced row stays synchronous.
|
|
281
|
+
if (cooperative && (rows >= maximumPreparationRows
|
|
282
|
+
|| rows >= minimumPreparationRows && performance.now() - started >= preparationBudget)) return;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
138
286
|
export function nativeListPlan<T>(render: ListProps<T>["renderItem"]): Plan<T> | undefined {
|
|
139
287
|
return plans.get(render) as Plan<T> | undefined;
|
|
140
288
|
}
|
|
141
289
|
|
|
142
|
-
export function CompiledList<T>({ plan, ...props }: ListProps<T> & { plan: Plan<T
|
|
143
|
-
const { items, keyExtractor, gap = 47, followEnd, initialPosition, measurementKey
|
|
290
|
+
export function CompiledList<T>({ plan, pagination, ...props }: ListProps<T> & { plan: Plan<T>; pagination: ListPagination }) {
|
|
291
|
+
const { items, keyExtractor, gap = 47, followEnd, initialPosition, measurementKey } = props;
|
|
144
292
|
if (!Number.isFinite(gap) || gap < 0) throw new Error("List gap must be finite and non-negative");
|
|
145
|
-
const load = useAction(() => onLoadMore?.());
|
|
146
|
-
const older = useAction(() => onLoadOlder?.());
|
|
147
293
|
const committed = useRef<Records<T> | null>(null);
|
|
148
|
-
const
|
|
149
|
-
|
|
294
|
+
const committedRequest = useRef<Request<T> | null>(null);
|
|
295
|
+
const committedPlan = useRef<Plan<T> | null>(null);
|
|
296
|
+
const generation = useRef(0);
|
|
297
|
+
const [prepared, setPrepared] = useState<Prepared<T> | null>(null);
|
|
298
|
+
const request = useMemo<Request<T>>(() => {
|
|
150
299
|
const keyIdentity = keys.get(keyExtractor) ?? keyExtractor;
|
|
151
|
-
|
|
152
|
-
if (captured && !captured.every(value => plainData(value, new WeakSet()))) return null;
|
|
153
|
-
const snapshots = new WeakMap<object, Captured>();
|
|
154
|
-
const scope = plan.scope && { identity: plan.scope.identity, values: captured?.map(value => captureValue(value, snapshots)) ?? [] };
|
|
155
|
-
const sameScope = scope && previous?.scope?.identity === scope.identity && sameScopeValues(previous.scope.values, scope.values);
|
|
156
|
-
if (previous && previous.items === items && previous.key === keyIdentity
|
|
157
|
-
&& previous.template === plan.template && previous.measurementKey === measurementKey
|
|
158
|
-
&& sameScope) return previous;
|
|
159
|
-
const rows = new Map<string, RowRecord<T>>();
|
|
160
|
-
let unchanged = items.length === previous?.data.length;
|
|
161
|
-
let reordered = !unchanged;
|
|
162
|
-
const changes: NativeItem[] = [];
|
|
163
|
-
let data;
|
|
300
|
+
let scope: Scope | undefined;
|
|
164
301
|
try {
|
|
165
|
-
|
|
166
|
-
data = items.map((item, index) => {
|
|
167
|
-
const key = typeof keyIdentity === "string" && previous?.key === keyIdentity && previous.items[index] === item
|
|
168
|
-
? previous.data[index].key : keyExtractor(item, index);
|
|
169
|
-
if (typeof key !== "string" || rows.has(key)) throw new Error("List keys must be unique strings");
|
|
170
|
-
const beforeRow = previous?.template === plan.template ? previous.rows.get(key) : undefined;
|
|
171
|
-
const before = beforeRow?.native;
|
|
172
|
-
const projected = beforeRow && sameScope && beforeRow.item === item
|
|
173
|
-
&& (beforeRow.index === index || plan.scope?.usesIndex === false)
|
|
174
|
-
? beforeRow.native.values : nativeValue(plan.project(item, index), before?.values);
|
|
175
|
-
const measurement = measurementKey ?? null;
|
|
176
|
-
const record = before && before.values === projected && before.measurementKey === measurement
|
|
177
|
-
? before : { key, values: projected, measurementKey: measurement };
|
|
178
|
-
rows.set(key, { item, index, native: record });
|
|
179
|
-
if (record !== previous?.data[index]) {
|
|
180
|
-
unchanged = false;
|
|
181
|
-
if (record !== before) changes.push(record);
|
|
182
|
-
if (key !== previous?.data[index]?.key) reordered = true;
|
|
183
|
-
}
|
|
184
|
-
return record;
|
|
185
|
-
});
|
|
302
|
+
scope = captureScope(plan, items);
|
|
186
303
|
} catch (error) {
|
|
187
304
|
if (error !== needsReact) throw error;
|
|
188
|
-
return null;
|
|
305
|
+
const preparation: Preparation<T> = { steps: (function* () { return null; })(), result: { value: null } };
|
|
306
|
+
return { items, key: keyIdentity, plan, measurementKey, preparation, synchronous: preparation.result, eventPositions: new Map() };
|
|
189
307
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
308
|
+
const pending = committedRequest.current;
|
|
309
|
+
if (pending && pending.items === items && pending.key === keyIdentity
|
|
310
|
+
&& pending.plan.template === plan.template && pending.measurementKey === measurementKey
|
|
311
|
+
&& sameScope(pending.scope, scope)) return pending;
|
|
312
|
+
const previous = committed.current;
|
|
313
|
+
const dense = isDenseArray?.(items) ?? false;
|
|
314
|
+
const preparation: Preparation<T> = { steps: prepareRecords(previous, items, plan, keyExtractor, keyIdentity, measurementKey, scope, dense) };
|
|
315
|
+
// First population must exist when List's one-shot initialPosition effect scrolls its Screen.
|
|
316
|
+
const cooperative = !!isProxy && !!scope && typeof keyIdentity === "string"
|
|
317
|
+
&& !!previous?.items.length && previous.template === plan.template && items.length >= cooperativeListSize;
|
|
318
|
+
const synchronous = advancePreparation(preparation, cooperative);
|
|
319
|
+
return { items, key: keyIdentity, plan, measurementKey, scope, preparation, synchronous, eventPositions: new Map() };
|
|
194
320
|
}, [items, keyExtractor, plan, measurementKey]);
|
|
195
|
-
|
|
321
|
+
const outcome = prepared?.request === request && prepared.generation === generation.current ? prepared : undefined;
|
|
322
|
+
let records = outcome?.status === "ready" ? outcome.value
|
|
323
|
+
: request.synchronous ? request.synchronous.value : committed.current;
|
|
324
|
+
if (outcome?.status === "ready" && !request.synchronous) {
|
|
325
|
+
try {
|
|
326
|
+
if (!capturesMatch(request, committedPlan.current)) records = null;
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (error !== needsReact) throw error;
|
|
329
|
+
records = null;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const preparing = !request.synchronous && outcome?.status !== "ready";
|
|
196
333
|
const targets = useMemo(() => {
|
|
197
334
|
type Template = { id: number; props: Record<string, unknown>; children: Template[] };
|
|
198
335
|
const targets = new Map<number, Record<string, (string | number)[]>>();
|
|
@@ -210,28 +347,90 @@ export function CompiledList<T>({ plan, ...props }: ListProps<T> & { plan: Plan<
|
|
|
210
347
|
visit(JSON.parse(plan.template) as Template);
|
|
211
348
|
return targets;
|
|
212
349
|
}, [plan.template]);
|
|
213
|
-
|
|
350
|
+
const events = useRef<{
|
|
351
|
+
items: readonly T[]; keyExtractor: ListProps<T>["keyExtractor"]; plan: Plan<T>;
|
|
352
|
+
targets: typeof targets; positions: Map<string, number | undefined>;
|
|
353
|
+
} | null>(null);
|
|
354
|
+
useLayoutEffect(() => {
|
|
355
|
+
committed.current = records;
|
|
356
|
+
committedRequest.current = request;
|
|
357
|
+
committedPlan.current = plan;
|
|
358
|
+
events.current = { items, keyExtractor, plan, targets, positions: request.eventPositions };
|
|
359
|
+
return () => {
|
|
360
|
+
events.current = null;
|
|
361
|
+
committedRequest.current = null;
|
|
362
|
+
committedPlan.current = null;
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
useLayoutEffect(() => {
|
|
366
|
+
const currentGeneration = ++generation.current;
|
|
367
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
368
|
+
const current = () => generation.current === currentGeneration && committedRequest.current === request;
|
|
369
|
+
const run = () => {
|
|
370
|
+
if (!current()) return;
|
|
371
|
+
try {
|
|
372
|
+
const result = capturesMatch(request, committedPlan.current)
|
|
373
|
+
? advancePreparation(request.preparation, true) : { value: null };
|
|
374
|
+
if (!current()) return;
|
|
375
|
+
if (result) setPrepared({ request, generation: currentGeneration, status: "ready", value: result.value });
|
|
376
|
+
else timer = setTimeout(run, 0);
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (!current()) return;
|
|
379
|
+
setPrepared(error === needsReact
|
|
380
|
+
? { request, generation: currentGeneration, status: "ready", value: null }
|
|
381
|
+
: { request, generation: currentGeneration, status: "error", error });
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
if (!request.synchronous) timer = setTimeout(run, 0);
|
|
385
|
+
return () => {
|
|
386
|
+
generation.current++;
|
|
387
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
388
|
+
};
|
|
389
|
+
}, [request]);
|
|
390
|
+
const paginationProps = useListPaginationEvents(pagination, preparing || !records,
|
|
391
|
+
records?.data.length ?? 0, records?.data[0]?.key, records?.data.at(-1)?.key);
|
|
392
|
+
if (outcome?.status === "error") throw outcome.error;
|
|
393
|
+
if (!records) return createElement(ReactList<T>, { ...props, pagination });
|
|
214
394
|
const nativeProps = {
|
|
215
|
-
items: records.data, keyField: "key", template:
|
|
216
|
-
|
|
217
|
-
hasOlder: hasOlder && older.status !== "pending" && older.status !== "error",
|
|
218
|
-
onEndReached: onLoadMore ? load.run : undefined,
|
|
219
|
-
onStartReached: onLoadOlder ? older.run : undefined,
|
|
395
|
+
items: records.data, keyField: "key", template: records.template, gap, followEnd, initialEnd: initialPosition === "end",
|
|
396
|
+
...paginationProps,
|
|
220
397
|
onRowEvent(key: string, target: number, name: string, ...args: unknown[]) {
|
|
221
|
-
const
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
398
|
+
const latest = events.current;
|
|
399
|
+
const path = latest?.targets.get(target)?.[name];
|
|
400
|
+
if (!latest || !path || isProxy?.(latest.items)) return;
|
|
401
|
+
const matches = (index: number) => {
|
|
402
|
+
const property = Object.getOwnPropertyDescriptor(latest.items, index);
|
|
403
|
+
return !!property && "value" in property && plainData(property.value)
|
|
404
|
+
&& latest.keyExtractor(property.value, index) === key;
|
|
405
|
+
};
|
|
406
|
+
let index = latest.items === records.items ? records.positions.get(key) : latest.positions.get(key);
|
|
407
|
+
if (latest.items !== records.items && !latest.positions.has(key)) {
|
|
408
|
+
const beforeIndex = records.positions.get(key);
|
|
409
|
+
if (beforeIndex !== undefined && matches(beforeIndex)) index = beforeIndex;
|
|
410
|
+
else {
|
|
411
|
+
for (let candidate = 0; candidate < latest.items.length; candidate++) {
|
|
412
|
+
if (matches(candidate)) { index = candidate; break; }
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
latest.positions.set(key, index);
|
|
416
|
+
}
|
|
417
|
+
if (index === undefined) return;
|
|
418
|
+
let callback: unknown;
|
|
419
|
+
try {
|
|
420
|
+
const item = listItem(latest.items, index);
|
|
421
|
+
if (!plainData(item)) return;
|
|
422
|
+
callback = latest.plan.project(item, index);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (error === needsReact) return;
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
for (const part of path) {
|
|
225
428
|
callback = callback != null && typeof callback === "object" ? Reflect.get(callback, part) : undefined;
|
|
226
429
|
}
|
|
227
|
-
if (
|
|
430
|
+
if (typeof callback === "function") callback(...args);
|
|
228
431
|
},
|
|
229
432
|
};
|
|
230
|
-
return createElement(
|
|
231
|
-
older.status === "error" && createElement(ListLoadError, { message: older.error.message, onRetry: older.run }),
|
|
232
|
-
createElement("NativeList", nativeProps),
|
|
233
|
-
load.status === "error" && createElement(ListLoadError, { message: load.error.message, onRetry: load.run }),
|
|
234
|
-
);
|
|
433
|
+
return createElement("NativeList", nativeProps);
|
|
235
434
|
}
|
|
236
435
|
|
|
237
436
|
/** @internal Fixed native descriptions for Ink's own list components. */
|
package/src/native.ts
CHANGED
|
@@ -34,6 +34,14 @@ Object.defineProperty(globalThis, "__inkReceive", {
|
|
|
34
34
|
},
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
Object.defineProperty(globalThis, "__inkResolveNativeResult", {
|
|
38
|
+
value: (id: number, value?: string, kind?: string, message?: string, retryable?: boolean) => {
|
|
39
|
+
listeners.get("result")?.(typeof value === "string"
|
|
40
|
+
? { id, type: "result", value }
|
|
41
|
+
: { id, kind, message, retryable, type: "result" });
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
37
45
|
onNativeMessage("result", message => {
|
|
38
46
|
if (typeof message.id !== "number") throw new Error("Invalid native result ID");
|
|
39
47
|
const request = pending.get(message.id);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import React from "preact/compat";
|
|
2
|
+
import { cloneElement, isValidElement } from "preact";
|
|
3
|
+
import { Activity, useEffect, useLayoutEffect, useSyncExternalStore, useImperativeHandle } from "./visibility";
|
|
4
|
+
import { useRef, useCallback, useLayoutEffect as commitLayoutEffect } from "preact/hooks";
|
|
5
|
+
const compatibleReact = /* @__PURE__ */ Object.assign({}, React, { Activity, cloneElement, isValidElement, useEffect, useLayoutEffect, useSyncExternalStore, useImperativeHandle, useEffectEvent });
|
|
6
|
+
export default compatibleReact;
|
|
7
|
+
export {
|
|
8
|
+
Children, Component, Fragment, PureComponent, StrictMode, Suspense, SuspenseList,
|
|
9
|
+
createContext, createElement, createFactory, createPortal, createRef,
|
|
10
|
+
findDOMNode, flushSync, forwardRef, hydrate, lazy, memo, render,
|
|
11
|
+
startTransition, unmountComponentAtNode, unstable_batchedUpdates,
|
|
12
|
+
useCallback, useContext, useDebugValue, useDeferredValue,
|
|
13
|
+
useId, useInsertionEffect, useMemo, useReducer,
|
|
14
|
+
useRef, useState, useTransition, version,
|
|
15
|
+
} from "preact/compat";
|
|
16
|
+
export { cloneElement, isValidElement } from "preact";
|
|
17
|
+
export { Activity, useEffect, useLayoutEffect, useSyncExternalStore, useImperativeHandle } from "./visibility";
|
|
18
|
+
|
|
19
|
+
/** Effect events retain their identity and read the latest committed callback. */
|
|
20
|
+
export function useEffectEvent<T extends (...args: never[]) => unknown>(callback: T): T {
|
|
21
|
+
const current = useRef(callback);
|
|
22
|
+
commitLayoutEffect(() => { current.current = callback; });
|
|
23
|
+
return useCallback(((...args: Parameters<T>) => current.current(...args)) as T, []);
|
|
24
|
+
}
|
package/src/react.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { createElement as element, createContext as context, isValidElement as valid, Fragment as fragment, Component, type ComponentChildren } from "preact";
|
|
2
|
+
import { useCallback, useContext as contextValue, useMemo, useRef, useState } from "preact/hooks";
|
|
3
|
+
import type * as React from "react";
|
|
4
|
+
|
|
5
|
+
// Keep the public React declarations while executing Preact core in Ink.
|
|
6
|
+
export const createElement = element as unknown as typeof React.createElement;
|
|
7
|
+
export const createContext = context as unknown as typeof React.createContext;
|
|
8
|
+
export const useContext = contextValue as unknown as typeof React.useContext;
|
|
9
|
+
export const isValidElement = valid as unknown as typeof React.isValidElement;
|
|
10
|
+
export const Fragment = fragment as unknown as typeof React.Fragment;
|
|
11
|
+
export { Children } from "preact/compat";
|
|
12
|
+
export { useCallback, useMemo, useRef, useState };
|
|
13
|
+
export { Activity, useEffect, useLayoutEffect, useSyncExternalStore } from "./visibility";
|
|
14
|
+
export type { ComponentProps, ComponentType, ReactElement, ReactNode } from "react";
|
|
15
|
+
|
|
16
|
+
// A single core component avoids compat memo's extra function-component layer.
|
|
17
|
+
export function memo<P extends object>(render: (props: P) => React.ReactNode) {
|
|
18
|
+
return class extends Component<P> {
|
|
19
|
+
shouldComponentUpdate(next: P) {
|
|
20
|
+
const previous = this.props;
|
|
21
|
+
const keys = Object.keys(next) as (keyof P)[];
|
|
22
|
+
return keys.length !== Object.keys(previous).length || keys.some(key => !Object.is(previous[key], next[key]));
|
|
23
|
+
}
|
|
24
|
+
render() { return render(this.props) as ComponentChildren; }
|
|
25
|
+
} as unknown as (props: P) => React.ReactNode;
|
|
26
|
+
}
|
package/src/refresh.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import "@prefresh/core";
|
|
2
|
+
import { flush } from "@prefresh/utils";
|
|
3
|
+
|
|
4
|
+
declare const self: { __PREFRESH__: {
|
|
5
|
+
register(type: unknown, id: string): void;
|
|
6
|
+
sign(type: unknown, key: string, forceReset: boolean, getCustomHooks: (() => unknown[]) | undefined, status: string): string;
|
|
7
|
+
} };
|
|
8
|
+
|
|
9
|
+
export const register = (type: unknown, id: string) => self.__PREFRESH__.register(type, id);
|
|
10
|
+
export const performReactRefresh = flush;
|
|
11
|
+
|
|
12
|
+
// Oxc emits React Refresh's signature protocol; translate it to Prefresh.
|
|
13
|
+
export function createSignatureFunctionForTransform() {
|
|
14
|
+
let savedType: unknown;
|
|
15
|
+
let savedKey = "";
|
|
16
|
+
let savedReset = false;
|
|
17
|
+
let savedHooks: (() => unknown[]) | undefined;
|
|
18
|
+
let status = "begin";
|
|
19
|
+
return (type?: unknown, key?: string, forceReset = false, getCustomHooks?: () => unknown[]) => {
|
|
20
|
+
if (type) {
|
|
21
|
+
savedType = type;
|
|
22
|
+
savedKey = key ?? "";
|
|
23
|
+
savedReset = forceReset;
|
|
24
|
+
savedHooks = getCustomHooks;
|
|
25
|
+
}
|
|
26
|
+
status = self.__PREFRESH__.sign(savedType, savedKey, savedReset, savedHooks, status);
|
|
27
|
+
return type;
|
|
28
|
+
};
|
|
29
|
+
}
|