baguette v0.1.93
Added
-
Network conditioning —
baguette network set|clear|status. Makes a
simulator's apps see a worse network than your Mac has: added latency, a
capped downlink, a proportion of requests failing, or hard offline. Named
presets borrow Network Link Conditioner's vocabulary and its figures
(wifi,dsl,lte,3g,edge,very-bad-network,100-loss), so
3gmeans what everyone already means by 3G. NLC itself, and the
dnctl/pfctlrules under it, are system-wide — simulator apps use
the host's stack as the host user, so there is no interface to scope a
rule to and conditioning one simulator that way degrades the whole Mac.
Injecting into the app under test is the only way to scope it, so this
works the way the virtual camera and
motion do. Only apps launched after
network setare conditioned; changing the condition afterwards reaches
a running app without a relaunch.
Which interception mechanism matters was measured before any of it was
designed:+[NSURLProtocol registerClass:]reaches onlyNSURLConnection
andNSURLSession.shared, and against a real React Native app it caught
zero of the app's own requests over 100 seconds. Swizzling
+defaultSessionConfigurationis what reachesfetch, image loading and
REST clients; both ship, and the load banner says which took. Two more
measured facts shape the code: request bodies arrive only as
HTTPBodyStream, which reads once, so a conditioned request is never
retried; and the response is paced as bytes arrive rather than buffered
and replayed, so a 23 MB bundle doesn't sit in memory.
The hazard this is designed against is forgetting it is on — unlike a
wrong camera picture, a throttle reads as "the app is slow" days later. So
plainbaguette networkreports the current condition,network clear
un-conditions apps that are already running, and the browser keeps an
amber dot lit whether or not its card was ever opened. Honest about its
reach: URLSession-shaped traffic only.URLSessionWebSocketTaskgets its
own hooks and takes latency, loss and offline (not bandwidth — an app
cannot observe a partial message). Not conditioned:WKWebViewpage
loads,NWConnection/Network.framework, raw sockets, and realtime SDKs
that open their own socket — Ably'sably-cocoavendors SocketRocket, so
--offlinewill not feel offline to it. See
docs/features/network.md. -
Install plugins from the browser — from bakeries you already trust.
The rail's + now opens a shelf of every trusted bakery, its pinned
commit, and what it offers: an Install button on anything you don't
have, Installed on anything you do, and the rail picks the new plugin
up without a reload. Previously the browser could only preview and hand
you a command to paste.
The trust boundary moved, but only halfway, and the half that matters
stayed put.POST /bakeries/installnames a bakery by its recorded
id, never a URL or a git ref — so a request can only reach a source
already inbakeries.json, at the commit pinned there, and a plugin
that source's own menu lists. Installing writes files baguette later
executes from and the only thing in front of a browser route is a set
of origin heuristics; naming sources by recorded id is what keeps the
blast radius of a wrong one at "installs from a repo you already
vetted" instead of "clones anything onto your disk". A refusal never
echoes the id it was handed back into the page.
Trusting a new source is still not something a page can do —
baguette bakery addstays a terminal act, because a modal button
isn't consent (the page sets the flag it then checks) and trust is the
decision that actually matters. Preview still ends by handing you the
command. The decision isInstallDecisioninDomain/Bakery/with
every refusal path unit-tested, and installing still only copies
files — nothing runs until you open the plugin's panel.
GET /bakeries.jsonnow reports each offer's install state, decided
host-side from what the plugin scan can see rather than from
installed.json(a bundled plugin has no provenance record and must
still read as satisfied). See
docs/features/plugins.md.
Fixed
-
Adding a bakery no longer collides with itself and kills the clone.
GitCheckout.cloneemptied the cache directory and then cloned into it,
leaving the live tree half-written for the tens of seconds a clone takes.
A second clone of the same bakery arriving in that window — the browser's
preview against a terminalbakery add, or one impatient second press of
Preview — deleted the tree the first was still writing into, and git died
on its own vanished temp pack:
fatal: could not open '…/pack/tmp_pack_XXXXXX' for reading: No such file or directory/fatal: fetch-pack: invalid index-pack output.
Each clone now assembles the checkout in a staging directory of its own
beside the destination and moves it in only once it is complete, so
nothing ever deletes a checkout in progress and whoever finishes last
wins the swap. Two consequences worth having on their own: a failed clone
now leaves the checkout you already had instead of a network blip taking
the working copy with it, and a reader never sees the cache directory
mid-delete. The modal also runs one preview at a time — the button
disables while a clone is in flight, and says that a cold one takes a
minute rather than showing a bare "Fetching…". -
The add-a-bakery modal drew behind the device. It mounted into
.right-railsalongside the plugin rail, and.right-railsis
position: fixed— which creates a stacking context whatever its
z-indexsays. So the modal'sz-index: 60was weighed against its
siblings inside the rail rather than against the page, and the device's
z-index: 2screen area a level up painted over it: the phone cut the
dialog in half and the scrim dimmed everything except the thing it was
covering. Page-covering UI now hangs off the view root instead. The
comment on.right-railsclaimed the opposite ("withz-index: auto
this container doesn't create a stacking context") and has been corrected
—position: fixedalone is enough.
What's Changed
- feat(network): per-simulator network conditioning by @crockalet in #66
Full Changelog: v0.1.92...v0.1.93