peachsim 0.1.626 → 0.1.628
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/LICENSE +1 -1
- package/README.md +3 -2
- package/cli/bin.ts +6 -2
- package/cli/bridge-flow-runner.ts +79 -72
- package/cli/commands/box/bundle-plane.ts +7 -2
- package/cli/commands/box.ts +9 -2
- package/cli/commands/cleanup.ts +6 -6
- package/cli/commands/control.ts +36 -6
- package/cli/commands/detox.ts +25 -11
- package/cli/commands/flow.ts +20 -8
- package/cli/commands/inspect.ts +22 -37
- package/cli/commands/maestro-generate.ts +5 -12
- package/cli/commands/maestro.ts +16 -6
- package/cli/commands/mode.ts +11 -7
- package/cli/commands/platform.ts +1 -0
- package/cli/commands/report-issue.ts +1 -1
- package/cli/commands/setup.ts +16 -11
- package/cli/commands/show-touches-mode.ts +33 -0
- package/cli/commands/skills.ts +16 -0
- package/cli/commands/test.ts +22 -1
- package/cli/commands/upload.ts +18 -8
- package/cli/commands/welcome.ts +3 -3
- package/cli/drivers/playwright-sim-host.ts +16 -8
- package/cli/drivers/playwright.ts +8 -5
- package/cli/help-core.ts +9 -4
- package/cli/internal-child.ts +14 -2
- package/cli/levenshtein.ts +27 -0
- package/cli/main.ts +28 -18
- package/cli/run-rnx.ts +8 -4
- package/cli/ws-bridge.ts +3 -2
- package/detox/index.ts +234 -7
- package/dist-lib/agent-daemon-client.cjs +2 -1
- package/dist-lib/agent-events.cjs +1 -1
- package/dist-lib/agent-identity.cjs +1 -1
- package/dist-lib/agent-sessions.cjs +1 -1
- package/dist-lib/attached-projects.cjs +1 -1
- package/dist-lib/auth/shared-session.cjs +1 -1
- package/dist-lib/backend-origin.cjs +1 -1
- package/dist-lib/beta.cjs +1 -1
- package/dist-lib/beta.mjs +1 -1
- package/dist-lib/bridge-constants.cjs +4 -1
- package/dist-lib/bridge-contract-input.cjs +1 -1
- package/dist-lib/bridge-contract-input.mjs +1 -1
- package/dist-lib/bridge-contract.cjs +1 -1
- package/dist-lib/bridge-contract.mjs +1 -1
- package/dist-lib/capture-contract.cjs +18 -8
- package/dist-lib/capture-contract.mjs +18 -8
- package/dist-lib/cli-constants.cjs +1 -1
- package/dist-lib/cloud-contract.cjs +1 -1
- package/dist-lib/cloud-contract.mjs +1 -1
- package/dist-lib/cloud-test-args.cjs +1 -1
- package/dist-lib/cloud-test-args.mjs +1 -1
- package/dist-lib/cloud.cjs +1 -1
- package/dist-lib/cloud.mjs +1 -1
- package/dist-lib/config.cjs +1 -1
- package/dist-lib/detox/index.cjs +170 -13
- package/dist-lib/dev-bundle-resolution.cjs +2 -2
- package/dist-lib/home-paths.cjs +1 -1
- package/dist-lib/host/bridge-host.cjs +280 -138
- package/dist-lib/host/fetch-proxy-handler.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.cjs +1 -1
- package/dist-lib/host/fetch-proxy-overrides.mjs +1 -1
- package/dist-lib/host/replacement-module-handler.cjs +1 -1
- package/dist-lib/host/websocket-proxy.cjs +1 -1
- package/dist-lib/index.cjs +62 -12
- package/dist-lib/jump-to-source-babel.cjs +1 -1
- package/dist-lib/jump-to-source-native.cjs +1 -1
- package/dist-lib/menu.cjs +7 -1
- package/dist-lib/menu.mjs +7 -1
- package/dist-lib/metro-fingerprint-registry.cjs +1 -1
- package/dist-lib/metro-fingerprint-registry.mjs +1 -1
- package/dist-lib/metro-production-bundle.cjs +1 -1
- package/dist-lib/metro-production-bundle.mjs +1 -1
- package/dist-lib/metro.cjs +43 -4
- package/dist-lib/profiles.cjs +1 -1
- package/dist-lib/public-brand.cjs +1 -1
- package/dist-lib/react-native-host-modules.cjs +1 -1
- package/dist-lib/react-native-host-modules.mjs +1 -1
- package/dist-lib/render-mode.cjs +1 -1
- package/dist-lib/scripts/dev-server-scanner.cjs +215 -28
- package/dist-lib/sdk.cjs +1 -1
- package/dist-lib/sdk.mjs +1 -1
- package/dist-lib/skills.cjs +431 -276
- package/dist-lib/swift-compile.cjs +1 -1
- package/dist-lib/swift-compile.mjs +1 -1
- package/dist-lib/swift-package.cjs +1 -1
- package/dist-lib/swift-package.mjs +1 -1
- package/dist-lib/swift-submit.cjs +1 -1
- package/dist-lib/swift-submit.mjs +1 -1
- package/dist-lib/swift.cjs +14512 -14208
- package/dist-lib/vite-kotlin.cjs +1 -1
- package/dist-lib/vite-swift.cjs +1 -1
- package/dist-lib/vite.cjs +44 -4
- package/install.mjs +16 -0
- package/install.sh +199 -0
- package/package.json +6 -1
- package/scripts/dev-server-scanner.ts +77 -46
- package/skills/contrast/SKILL.md +1 -1
- package/src/bridge-constants.ts +11 -0
- package/src/capture-contract.ts +40 -7
- package/src/dev-bundle-resolution.ts +1 -1
- package/src/host/bridge-host.ts +26 -137
- package/src/menu.ts +7 -0
- package/src/native-seam-manifest.ts +4 -16
- package/src/runtime-assets.ts +59 -2
- package/src/vite-plugin.ts +723 -25
package/src/host/bridge-host.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from '../../scripts/dev-server-scanner.ts'
|
|
10
10
|
import { buildBrowserAssetProxyUrl } from '../backend-origin.ts'
|
|
11
11
|
import {
|
|
12
|
+
AUTOMATION_SIM_IDLE_REAP_TTL_MS,
|
|
12
13
|
DEFAULT_SOOTSIM_BRIDGE_PORT,
|
|
13
14
|
SOOTSIM_BRIDGE_SIM_CLOSE_CODE,
|
|
14
15
|
SOOTSIM_BRIDGE_SIM_CLOSE_REASON,
|
|
@@ -40,6 +41,7 @@ import {
|
|
|
40
41
|
resolveDevCheckoutRuntimeRoot,
|
|
41
42
|
resolveRuntimeRootForHostname,
|
|
42
43
|
runtimeVersionFromHostname,
|
|
44
|
+
sendFileWithRanges,
|
|
43
45
|
} from '../runtime-assets.ts'
|
|
44
46
|
import { rnxRuntime } from '../runtime-delivery.ts'
|
|
45
47
|
import { AgentHost, type AgentHostOptions } from './agent-host.ts'
|
|
@@ -260,23 +262,6 @@ export interface BridgeHostOptions {
|
|
|
260
262
|
* `peach serve` command resolves this with the same probe the
|
|
261
263
|
* upload itself uses (`resolveDefaultUploadOrigin`). */
|
|
262
264
|
contrastOrigin?: string
|
|
263
|
-
/** override the abandoned-sim GC TTL (ms). defaults to
|
|
264
|
-
* SIM_IDLE_REAP_TTL_MS (30 min). exists so the reaper integration test
|
|
265
|
-
* can drive the real timer path deterministically instead of mocking
|
|
266
|
-
* time. production callers should never set this. */
|
|
267
|
-
simIdleReapTtlMs?: number
|
|
268
|
-
/** override the idle GC TTL (ms) for automation sims (playwright-driver,
|
|
269
|
-
* capture/factory). defaults to AUTOMATION_SIM_IDLE_REAP_TTL_MS (10 min).
|
|
270
|
-
* test-only, like simIdleReapTtlMs. */
|
|
271
|
-
automationSimIdleReapTtlMs?: number
|
|
272
|
-
/** cap on concurrent non-primary automation sims before the reaper drops
|
|
273
|
-
* the most-idle overflow. defaults to MAX_CONCURRENT_AUTOMATION_SIMS (6).
|
|
274
|
-
* test-only. */
|
|
275
|
-
maxConcurrentAutomationSims?: number
|
|
276
|
-
/** grace (ms) below which an automation sim is never reaped by the cap,
|
|
277
|
-
* even when over it. defaults to AUTOMATION_SIM_ACTIVE_GRACE_MS (60 s).
|
|
278
|
-
* test-only. */
|
|
279
|
-
automationSimActiveGraceMs?: number
|
|
280
265
|
}
|
|
281
266
|
|
|
282
267
|
const DAEMON_HEARTBEAT_INTERVAL_MS = 5_000
|
|
@@ -417,25 +402,9 @@ export class SootSimBridgeHost {
|
|
|
417
402
|
// terminal 4001 code, so the client closes the abandoned window instead
|
|
418
403
|
// of reconnecting the zombie straight back in.
|
|
419
404
|
private static SIM_IDLE_REAP_TTL_MS = 30 * 60_000
|
|
420
|
-
// automation sims (spawned by the playwright driver: `peach open --driver
|
|
421
|
-
// playwright`, the factory/screenshot capture path — `meta.sootsimHostDriver
|
|
422
|
-
// === 'playwright'`) are unattended one-shot browsers, never a human's tab. a
|
|
423
|
-
// capture run opens one per slide/app/retry and the 30-min human/dev TTL lets
|
|
424
|
-
// hundreds pile up inside the window — orphaned ms-playwright chromiums that
|
|
425
|
-
// drove the box to swap-thrash (load hit 157 in one incident). give them a far
|
|
426
|
-
// shorter idle TTL, and cap how many can sit concurrently: reap the most-idle
|
|
427
|
-
// overflow, but never a sim active within ACTIVE_GRACE (an agent composing
|
|
428
|
-
// between captures), nor primary / cli-attached / leased sims.
|
|
429
|
-
private static AUTOMATION_SIM_IDLE_REAP_TTL_MS = 10 * 60_000
|
|
430
|
-
private static MAX_CONCURRENT_AUTOMATION_SIMS = 6
|
|
431
|
-
private static AUTOMATION_SIM_ACTIVE_GRACE_MS = 60_000
|
|
432
405
|
|
|
433
406
|
private preferredPort: number
|
|
434
407
|
private portFallbackCount: number
|
|
435
|
-
private simIdleReapTtlMs: number
|
|
436
|
-
private automationSimIdleReapTtlMs: number
|
|
437
|
-
private maxConcurrentAutomationSims: number
|
|
438
|
-
private automationSimActiveGraceMs: number
|
|
439
408
|
private shouldWriteLockfile: boolean
|
|
440
409
|
private shouldWriteDevLockfile: boolean
|
|
441
410
|
private getShellPort: (() => number | null) | null
|
|
@@ -486,14 +455,6 @@ export class SootSimBridgeHost {
|
|
|
486
455
|
getExcludePorts: opts.agentScanExcludes,
|
|
487
456
|
})
|
|
488
457
|
this.contrastOrigin = opts.contrastOrigin?.replace(/\/$/, '') || null
|
|
489
|
-
this.simIdleReapTtlMs =
|
|
490
|
-
opts.simIdleReapTtlMs ?? SootSimBridgeHost.SIM_IDLE_REAP_TTL_MS
|
|
491
|
-
this.automationSimIdleReapTtlMs =
|
|
492
|
-
opts.automationSimIdleReapTtlMs ?? SootSimBridgeHost.AUTOMATION_SIM_IDLE_REAP_TTL_MS
|
|
493
|
-
this.maxConcurrentAutomationSims =
|
|
494
|
-
opts.maxConcurrentAutomationSims ?? SootSimBridgeHost.MAX_CONCURRENT_AUTOMATION_SIMS
|
|
495
|
-
this.automationSimActiveGraceMs =
|
|
496
|
-
opts.automationSimActiveGraceMs ?? SootSimBridgeHost.AUTOMATION_SIM_ACTIVE_GRACE_MS
|
|
497
458
|
}
|
|
498
459
|
|
|
499
460
|
/** expose the agent host so tests and embedders can inspect state or
|
|
@@ -502,14 +463,6 @@ export class SootSimBridgeHost {
|
|
|
502
463
|
return this.agentHost
|
|
503
464
|
}
|
|
504
465
|
|
|
505
|
-
/** run the abandoned-sim GC pass on demand. the reaper normally fires off
|
|
506
|
-
* the 30s idle-sweep timer; this lets the F21-3 integration test drive
|
|
507
|
-
* the real path without waiting for the timer. not part of the public
|
|
508
|
-
* WS protocol. */
|
|
509
|
-
reapIdleSimsForTest(now = Date.now()): void {
|
|
510
|
-
this.reapIdleSims(now)
|
|
511
|
-
}
|
|
512
|
-
|
|
513
466
|
/** synchronous wrapper around startAsync for callers that don't care
|
|
514
467
|
* about port fallback outcomes. returns immediately; actual binding
|
|
515
468
|
* happens on the event loop. callers that need to know the bound port
|
|
@@ -816,6 +769,13 @@ export class SootSimBridgeHost {
|
|
|
816
769
|
return
|
|
817
770
|
}
|
|
818
771
|
|
|
772
|
+
// a capture the page is still running after its cli exited (`peach
|
|
773
|
+
// record start`) is use the idle reaper cannot see in cli traffic.
|
|
774
|
+
if (msg.type === 'bridge:capture-active' && sim) {
|
|
775
|
+
sim.lastActiveAt = Date.now()
|
|
776
|
+
return
|
|
777
|
+
}
|
|
778
|
+
|
|
819
779
|
// write a partial patch into ~/.peach/config.json. fired by the
|
|
820
780
|
// engine's settingsStore when a persisted key flips in a browser
|
|
821
781
|
// tab (which has no fs access of its own). after the merge, we
|
|
@@ -1345,43 +1305,10 @@ export class SootSimBridgeHost {
|
|
|
1345
1305
|
|
|
1346
1306
|
const contentType =
|
|
1347
1307
|
HTTP_MIME_TYPES[path.extname(filePath).toLowerCase()] || 'application/octet-stream'
|
|
1348
|
-
|
|
1349
|
-
const range = /^bytes=(\d*)-(\d*)$/.exec(req.headers.range || '')
|
|
1350
|
-
if (range) {
|
|
1351
|
-
const [, startRaw, endRaw] = range
|
|
1352
|
-
const start = startRaw ? Number(startRaw) : 0
|
|
1353
|
-
const end = endRaw ? Math.min(Number(endRaw), total - 1) : total - 1
|
|
1354
|
-
if (!Number.isFinite(start) || start > end || start >= total) {
|
|
1355
|
-
res.writeHead(416, { ...cors, 'Content-Range': `bytes */${total}` })
|
|
1356
|
-
res.end()
|
|
1357
|
-
return
|
|
1358
|
-
}
|
|
1359
|
-
res.writeHead(206, {
|
|
1360
|
-
...cors,
|
|
1361
|
-
'Content-Type': contentType,
|
|
1362
|
-
'Content-Range': `bytes ${start}-${end}/${total}`,
|
|
1363
|
-
'Accept-Ranges': 'bytes',
|
|
1364
|
-
'Content-Length': String(end - start + 1),
|
|
1365
|
-
})
|
|
1366
|
-
if (method === 'HEAD') {
|
|
1367
|
-
res.end()
|
|
1368
|
-
return
|
|
1369
|
-
}
|
|
1370
|
-
fs.createReadStream(filePath, { start, end }).pipe(res)
|
|
1371
|
-
return
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
|
-
res.writeHead(200, {
|
|
1308
|
+
sendFileWithRanges(req, res, filePath, stats.size, {
|
|
1375
1309
|
...cors,
|
|
1376
1310
|
'Content-Type': contentType,
|
|
1377
|
-
'Accept-Ranges': 'bytes',
|
|
1378
|
-
'Content-Length': String(total),
|
|
1379
1311
|
})
|
|
1380
|
-
if (method === 'HEAD') {
|
|
1381
|
-
res.end()
|
|
1382
|
-
return
|
|
1383
|
-
}
|
|
1384
|
-
fs.createReadStream(filePath).pipe(res)
|
|
1385
1312
|
}
|
|
1386
1313
|
|
|
1387
1314
|
private handleServerScan(res: ServerResponse) {
|
|
@@ -2025,71 +1952,33 @@ export class SootSimBridgeHost {
|
|
|
2025
1952
|
// automation sims are spawned by the playwright driver (`peach open
|
|
2026
1953
|
// --driver playwright`, the factory/screenshot capture path), which tags them
|
|
2027
1954
|
// with `meta.sootsimHostDriver === 'playwright'` via the host's addInitScript.
|
|
2028
|
-
// they are unattended one-shot browsers, never a human's tab, so they get a
|
|
2029
|
-
// shorter idle TTL and a concurrent cap.
|
|
2030
1955
|
private isAutomationSim(sim: BridgeSimConnection): boolean {
|
|
2031
1956
|
return sim.meta?.sootsimHostDriver === 'playwright'
|
|
2032
1957
|
}
|
|
2033
1958
|
|
|
2034
|
-
// an automation sim whose spawning playwright host process is still alive is
|
|
2035
|
-
// an ACTIVE run, not an abandoned tab. mirrors the cli's playwrightHostPidForSim
|
|
2036
|
-
// (control.ts): extract the host pid from meta, then check liveness so the
|
|
2037
|
-
// host-side idle GC never kills a live capture/factory sim mid-run.
|
|
2038
|
-
private automationSimOwnerAlive(sim: BridgeSimConnection): boolean {
|
|
2039
|
-
const pid = Number(sim.meta?.sootsimHostPid)
|
|
2040
|
-
if (!Number.isInteger(pid) || pid <= 1 || pid === process.pid) return false
|
|
2041
|
-
try {
|
|
2042
|
-
process.kill(pid, 0) // ESRCH → dead; EPERM → alive but unsignalable
|
|
2043
|
-
return true
|
|
2044
|
-
} catch (err) {
|
|
2045
|
-
return (err as NodeJS.ErrnoException).code === 'EPERM'
|
|
2046
|
-
}
|
|
2047
|
-
}
|
|
2048
|
-
|
|
2049
1959
|
private reapIdleSims(now = Date.now()) {
|
|
2050
1960
|
const attachedSimIds = new Set(this.cliSimBySocket.values())
|
|
2051
|
-
// automation sims that pass every "someone cares" gate but are still under
|
|
2052
|
-
// their TTL — candidates for the concurrent-cap pass below.
|
|
2053
|
-
const automationLive: { sim: BridgeSimConnection; idleMs: number }[] = []
|
|
2054
1961
|
for (const sim of this.sims.values()) {
|
|
2055
|
-
if (sim.id === this.primarySimId) continue
|
|
2056
1962
|
if (attachedSimIds.has(sim.id)) continue
|
|
2057
|
-
if (this.getActiveLease(sim)) continue
|
|
2058
|
-
// never-CLI-driven sims have lastActiveAt 0 — fall back to connectedAt
|
|
2059
|
-
// so a freshly opened, not-yet-primary tab isn't reaped on the spot.
|
|
2060
|
-
const lastTouched = Math.max(sim.lastActiveAt, sim.connectedAt)
|
|
2061
|
-
const idleMs = now - lastTouched
|
|
2062
1963
|
const automation = this.isAutomationSim(sim)
|
|
2063
|
-
|
|
2064
|
-
//
|
|
2065
|
-
//
|
|
2066
|
-
//
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
const ttl = automation ? this.automationSimIdleReapTtlMs : this.simIdleReapTtlMs
|
|
2071
|
-
if (idleMs >= ttl) {
|
|
2072
|
-
// terminal close → client closes the window, does not reconnect; the
|
|
2073
|
-
// 'close' handler runs rememberDisconnectedSim → this.sims.delete.
|
|
2074
|
-
this.closeSimSocketFromHost(sim.ws)
|
|
1964
|
+
const lease = this.getActiveLease(sim)
|
|
1965
|
+
// an automation sim is nobody's tab, so being primary or holding a cli
|
|
1966
|
+
// lease (10 min from its last grant) says nothing about use: only a human
|
|
1967
|
+
// interacting with a headed window keeps one past its idle window.
|
|
1968
|
+
if (
|
|
1969
|
+
automation ? lease?.kind === 'user-active' : sim.id === this.primarySimId || lease
|
|
1970
|
+
) {
|
|
2075
1971
|
continue
|
|
2076
1972
|
}
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
for (const { sim, idleMs } of automationLive.slice(
|
|
2087
|
-
0,
|
|
2088
|
-
automationLive.length - cap,
|
|
2089
|
-
)) {
|
|
2090
|
-
if (idleMs < this.automationSimActiveGraceMs) continue
|
|
2091
|
-
this.closeSimSocketFromHost(sim.ws)
|
|
2092
|
-
}
|
|
1973
|
+
// never-CLI-driven sims have lastActiveAt 0 — fall back to connectedAt
|
|
1974
|
+
// so a freshly opened, not-yet-primary tab isn't reaped on the spot.
|
|
1975
|
+
const idleMs = now - Math.max(sim.lastActiveAt, sim.connectedAt)
|
|
1976
|
+
const ttl = automation
|
|
1977
|
+
? AUTOMATION_SIM_IDLE_REAP_TTL_MS
|
|
1978
|
+
: SootSimBridgeHost.SIM_IDLE_REAP_TTL_MS
|
|
1979
|
+
// terminal close → client closes the window, does not reconnect; the
|
|
1980
|
+
// 'close' handler runs rememberDisconnectedSim → this.sims.delete.
|
|
1981
|
+
if (idleMs >= ttl) this.closeSimSocketFromHost(sim.ws)
|
|
2093
1982
|
}
|
|
2094
1983
|
}
|
|
2095
1984
|
|
package/src/menu.ts
CHANGED
|
@@ -80,6 +80,7 @@ export interface SootSimMenuState {
|
|
|
80
80
|
// can reach.
|
|
81
81
|
developmentMode: boolean
|
|
82
82
|
showBezels: boolean
|
|
83
|
+
showTouches: boolean
|
|
83
84
|
showMenuBar: boolean
|
|
84
85
|
showRail: boolean
|
|
85
86
|
solidWindow: boolean
|
|
@@ -493,6 +494,12 @@ function windowMenu(state: SootSimMenuState): SootSimMenuDef {
|
|
|
493
494
|
checked: state.showBezels,
|
|
494
495
|
action: 'toggle-bezels',
|
|
495
496
|
},
|
|
497
|
+
{
|
|
498
|
+
label: 'Show Touches',
|
|
499
|
+
type: 'checkbox',
|
|
500
|
+
checked: state.showTouches,
|
|
501
|
+
action: 'toggle-touches',
|
|
502
|
+
},
|
|
496
503
|
{
|
|
497
504
|
label: 'Show Menu Bar',
|
|
498
505
|
type: 'checkbox',
|
|
@@ -275,25 +275,13 @@ export const NATIVE_SEAM_MANIFEST: NativeSeamEntry[] = [
|
|
|
275
275
|
},
|
|
276
276
|
{
|
|
277
277
|
pkg: 'expo-image',
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
// the view-component seam translates expo-image's prop shape
|
|
282
|
-
// (contentFit, tintColor, source) onto sootsim's existing
|
|
283
|
-
// <sootsim-image> host. no new canvas component is needed — sootsim's
|
|
284
|
-
// image-loader + canvaskit-renderer already handle resizeMode,
|
|
285
|
-
// tintColor, and onLoad/onError lifecycle.
|
|
286
|
-
seamBasenames: ['ExpoImage'],
|
|
287
|
-
target: seamPath('expo-image'),
|
|
288
|
-
notes:
|
|
289
|
-
'expo-image native view — thin prop translator onto sootsim-image host (contentFit→resizeMode, tintColor passthrough); upstream Image.tsx + ImageBackground + useImage resolve from node_modules unchanged',
|
|
290
|
-
},
|
|
291
|
-
{
|
|
292
|
-
pkg: 'expo-image',
|
|
278
|
+
// src/ImageModule.ts → requireNativeModule('ExpoImage'). the native view
|
|
279
|
+
// is src/ExpoImage.tsx's requireNativeViewManager('ExpoImage') call inside
|
|
280
|
+
// a real JS component, so it comes from expo-image-register instead.
|
|
293
281
|
seamBasenames: ['ImageModule'],
|
|
294
282
|
target: seamPath('expo-image-module'),
|
|
295
283
|
notes:
|
|
296
|
-
'expo-image native module — prefetch/loadAsync route through
|
|
284
|
+
'expo-image native module — prefetch/loadAsync/readFromCacheAsync route through the engine image loader, blurhash and thumbhash generation run the swift encoders, cache configuration has nothing to set in a browser',
|
|
297
285
|
},
|
|
298
286
|
{
|
|
299
287
|
pkg: '@notifee/react-native',
|
package/src/runtime-assets.ts
CHANGED
|
@@ -7,6 +7,7 @@ import path from 'path'
|
|
|
7
7
|
import { fileURLToPath } from 'url'
|
|
8
8
|
import { isValidRuntimeVersion } from '@contrast/runtime-delivery'
|
|
9
9
|
import { readActiveRuntime, runtimeDir } from './home-paths.ts'
|
|
10
|
+
import type { ServerResponse } from 'http'
|
|
10
11
|
|
|
11
12
|
export const SOOTSIM_RUNTIME_MISSING_MESSAGE =
|
|
12
13
|
'[peach] no engine runtime installed. run `peach runtime install`'
|
|
@@ -97,6 +98,10 @@ const MIME_TYPES: Record<string, string> = {
|
|
|
97
98
|
'.woff2': 'font/woff2',
|
|
98
99
|
'.mp3': 'audio/mpeg',
|
|
99
100
|
'.wav': 'audio/wav',
|
|
101
|
+
'.mp4': 'video/mp4',
|
|
102
|
+
'.m4v': 'video/mp4',
|
|
103
|
+
'.mov': 'video/quicktime',
|
|
104
|
+
'.webm': 'video/webm',
|
|
100
105
|
}
|
|
101
106
|
|
|
102
107
|
const ROOT_RUNTIME_PATHS = [
|
|
@@ -111,7 +116,7 @@ const ROOT_RUNTIME_PATHS = [
|
|
|
111
116
|
'/spike/',
|
|
112
117
|
'/test-wallpaper.jpg',
|
|
113
118
|
'/preview-sw.js',
|
|
114
|
-
'/
|
|
119
|
+
'/peach.svg',
|
|
115
120
|
'/bedrock-sqlite.wasm',
|
|
116
121
|
'/bedrock-sqlite.js',
|
|
117
122
|
'/sootsim/',
|
|
@@ -238,5 +243,57 @@ export function serveRuntimeFile(req: any, res: any, fullPath: string) {
|
|
|
238
243
|
res.end()
|
|
239
244
|
return
|
|
240
245
|
}
|
|
241
|
-
|
|
246
|
+
sendFileWithRanges(req, res, fullPath, stat.size, {})
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// a file body that honours a single `bytes=` range. a media element can only
|
|
250
|
+
// seek a resource whose server answers ranges, so any file a <video> or
|
|
251
|
+
// <audio> might load goes through here.
|
|
252
|
+
export function sendFileWithRanges(
|
|
253
|
+
req: { method?: string; headers?: { range?: string } } | undefined,
|
|
254
|
+
res: ServerResponse,
|
|
255
|
+
filePath: string,
|
|
256
|
+
total: number,
|
|
257
|
+
headers: Record<string, string>,
|
|
258
|
+
) {
|
|
259
|
+
const head = (req?.method || 'GET').toUpperCase() === 'HEAD'
|
|
260
|
+
const range = /^bytes=(\d*)-(\d*)$/.exec(req?.headers?.range || '')
|
|
261
|
+
// a last byte before the first makes the header invalid, which a server
|
|
262
|
+
// ignores (RFC 9110 14.1.1)
|
|
263
|
+
const invalid = range && range[1] && range[2] && Number(range[2]) < Number(range[1])
|
|
264
|
+
if (range && (range[1] || range[2]) && !invalid) {
|
|
265
|
+
const [, startRaw, endRaw] = range
|
|
266
|
+
// a suffix range (bytes=-N) is the last N bytes
|
|
267
|
+
const start = startRaw ? Number(startRaw) : Math.max(0, total - Number(endRaw))
|
|
268
|
+
const end = startRaw && endRaw ? Math.min(Number(endRaw), total - 1) : total - 1
|
|
269
|
+
if (start > end || start >= total) {
|
|
270
|
+
res.writeHead(416, { ...headers, 'Content-Range': `bytes */${total}` })
|
|
271
|
+
res.end()
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
res.writeHead(206, {
|
|
275
|
+
...headers,
|
|
276
|
+
'Content-Range': `bytes ${start}-${end}/${total}`,
|
|
277
|
+
'Accept-Ranges': 'bytes',
|
|
278
|
+
'Content-Length': String(end - start + 1),
|
|
279
|
+
})
|
|
280
|
+
if (head) res.end()
|
|
281
|
+
else streamFile(res, fs.createReadStream(filePath, { start, end }))
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
res.writeHead(200, {
|
|
285
|
+
...headers,
|
|
286
|
+
'Accept-Ranges': 'bytes',
|
|
287
|
+
'Content-Length': String(total),
|
|
288
|
+
})
|
|
289
|
+
// the body stops at the stat'd size so a file a rebuild is still growing
|
|
290
|
+
// cannot run past its Content-Length
|
|
291
|
+
if (head || total === 0) res.end()
|
|
292
|
+
else streamFile(res, fs.createReadStream(filePath, { start: 0, end: total - 1 }))
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// the status line is already out, so a read failure can only cut the body
|
|
296
|
+
function streamFile(res: ServerResponse, stream: fs.ReadStream) {
|
|
297
|
+
stream.on('error', (error) => res.destroy(error))
|
|
298
|
+
stream.pipe(res)
|
|
242
299
|
}
|