browser-broker 0.1.0
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/.env.example +173 -0
- package/LICENSE +21 -0
- package/README.md +374 -0
- package/RELEASES.md +97 -0
- package/dist/package.json +58 -0
- package/dist/src/adapter/conformance/case.js +1 -0
- package/dist/src/adapter/conformance/cases.js +429 -0
- package/dist/src/adapter/conformance/discovery.js +156 -0
- package/dist/src/adapter/conformance/driver.js +1 -0
- package/dist/src/adapter/conformance/drivers.js +36 -0
- package/dist/src/adapter/conformance/run.js +224 -0
- package/dist/src/adapter/conformance/service-subject.js +165 -0
- package/dist/src/adapter/contract.js +24 -0
- package/dist/src/adapter/operations.js +114 -0
- package/dist/src/adapter/service-seam.js +1 -0
- package/dist/src/artifacts/names.js +229 -0
- package/dist/src/artifacts/store.js +174 -0
- package/dist/src/bin/broker-tool.js +63 -0
- package/dist/src/bin/broker.js +111 -0
- package/dist/src/browser/adoption.js +143 -0
- package/dist/src/browser/automation-probe.js +113 -0
- package/dist/src/browser/conformance/case.js +1 -0
- package/dist/src/browser/conformance/cases.js +192 -0
- package/dist/src/browser/conformance/run.js +102 -0
- package/dist/src/browser/conformance/subjects.js +19 -0
- package/dist/src/browser/discovery.js +226 -0
- package/dist/src/browser/driver.js +195 -0
- package/dist/src/browser/fake.js +585 -0
- package/dist/src/browser/launch.js +504 -0
- package/dist/src/browser/real.js +1425 -0
- package/dist/src/browser/setup.js +161 -0
- package/dist/src/capture/accounting.js +59 -0
- package/dist/src/capture/image.js +112 -0
- package/dist/src/capture/ladder.js +72 -0
- package/dist/src/capture/legibility.js +195 -0
- package/dist/src/capture/pipeline.js +153 -0
- package/dist/src/capture/tiers.js +166 -0
- package/dist/src/cli/adapter.js +233 -0
- package/dist/src/cli/commands.js +270 -0
- package/dist/src/cli/conformance-driver.js +119 -0
- package/dist/src/cli/diffs.js +122 -0
- package/dist/src/cli/image.js +274 -0
- package/dist/src/cli/index.js +895 -0
- package/dist/src/cli/login-command.js +401 -0
- package/dist/src/cli/operations-commands.js +186 -0
- package/dist/src/cli/reconcile-command.js +137 -0
- package/dist/src/cli/sign-in.js +134 -0
- package/dist/src/cli/telemetry.js +222 -0
- package/dist/src/config/environment.js +446 -0
- package/dist/src/diff/artifact-path.js +77 -0
- package/dist/src/diff/crops.js +102 -0
- package/dist/src/diff/geometry.js +122 -0
- package/dist/src/diff/image.js +132 -0
- package/dist/src/diff/mask.js +46 -0
- package/dist/src/diff/regions.js +263 -0
- package/dist/src/diff/settings.js +135 -0
- package/dist/src/doctor/checks.js +588 -0
- package/dist/src/doctor/report.js +152 -0
- package/dist/src/doctor/session.js +161 -0
- package/dist/src/errors.js +36 -0
- package/dist/src/feedback/read.js +119 -0
- package/dist/src/feedback/record.js +199 -0
- package/dist/src/operations/addresses.js +175 -0
- package/dist/src/operations/derive.js +109 -0
- package/dist/src/operations/ledger.js +194 -0
- package/dist/src/operations/status.js +197 -0
- package/dist/src/operations/telemetry.js +280 -0
- package/dist/src/report/document.js +419 -0
- package/dist/src/report/escape.js +68 -0
- package/dist/src/report/snapshot.js +97 -0
- package/dist/src/service/arbitration.js +537 -0
- package/dist/src/service/artifacts.js +85 -0
- package/dist/src/service/bridge.js +577 -0
- package/dist/src/service/broker.js +120 -0
- package/dist/src/service/browser-session.js +269 -0
- package/dist/src/service/capacity.js +62 -0
- package/dist/src/service/capture-seam.js +83 -0
- package/dist/src/service/capture-store.js +91 -0
- package/dist/src/service/comparison-store.js +101 -0
- package/dist/src/service/comparison.js +173 -0
- package/dist/src/service/events.js +93 -0
- package/dist/src/service/keys.js +68 -0
- package/dist/src/service/leases.js +147 -0
- package/dist/src/service/nudge.js +66 -0
- package/dist/src/service/operations/claim.js +692 -0
- package/dist/src/service/operations/give-back.js +131 -0
- package/dist/src/service/operations/pages.js +771 -0
- package/dist/src/service/operations/sign-in.js +915 -0
- package/dist/src/service/operations/status.js +62 -0
- package/dist/src/service/ownership.js +93 -0
- package/dist/src/service/pages.js +616 -0
- package/dist/src/service/pending-seeds.js +20 -0
- package/dist/src/service/queue.js +233 -0
- package/dist/src/service/reconcile.js +220 -0
- package/dist/src/service/refusals.js +262 -0
- package/dist/src/service/runtime.js +131 -0
- package/dist/src/service/signin-recovery.js +148 -0
- package/dist/src/service/storage-seed.js +239 -0
- package/dist/src/service/tabs.js +123 -0
- package/dist/src/store/budget.js +99 -0
- package/dist/src/store/location.js +42 -0
- package/dist/src/store/network-path.js +182 -0
- package/dist/src/store/network-volume.js +92 -0
- package/dist/src/store/open.js +226 -0
- package/dist/src/store/schema/step-001-initial.js +523 -0
- package/dist/src/store/schema/step-002-tab-budget.js +53 -0
- package/dist/src/store/schema/step-003-queue-order.js +110 -0
- package/dist/src/store/schema/step-004-tab-never-opened.js +100 -0
- package/dist/src/store/schema/step-005-storage-seed-event.js +90 -0
- package/dist/src/store/schema/step-006-signin-events.js +104 -0
- package/dist/src/store/schema/step-007-signin-without-process.js +92 -0
- package/dist/src/store/schema/step-008-signin-owner.js +76 -0
- package/dist/src/store/schema/step-009-named-browsers.js +138 -0
- package/dist/src/store/schema/step-010-signin-request.js +135 -0
- package/dist/src/store/schema/step.js +172 -0
- package/dist/src/store/schema/steps.js +58 -0
- package/dist/src/store/transaction.js +37 -0
- package/dist/src/tool/adapter.js +90 -0
- package/dist/src/tool/conformance-driver.js +184 -0
- package/dist/src/tool/protocol.js +310 -0
- package/dist/src/tool/session.js +351 -0
- package/dist/src/tool/tools.js +310 -0
- package/package.json +58 -0
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step one: the whole schema.
|
|
3
|
+
*
|
|
4
|
+
* **This step is history the moment it has run anywhere.** Every change after
|
|
5
|
+
* it is a new step with an `ALTER`, never an edit to this file — a step that
|
|
6
|
+
* has run somewhere means two installations reporting the same version with
|
|
7
|
+
* different schemas, which nothing reports until something breaks far from the
|
|
8
|
+
* cause (`SCHEMA.md` §1.2d, `MILESTONES.md` #7).
|
|
9
|
+
*
|
|
10
|
+
* ── Why this is raw SQL ─────────────────────────────────────────────────
|
|
11
|
+
*
|
|
12
|
+
* The schema is written in the language the database speaks rather than
|
|
13
|
+
* generated from a model of it, so there are not two descriptions of one
|
|
14
|
+
* schema to reconcile (`SCHEMA.md` §1.11). Two of the things below cannot be
|
|
15
|
+
* expressed by most model layers at all: a **partial** unique index, and a
|
|
16
|
+
* **composite** foreign key naming a two-column target.
|
|
17
|
+
*
|
|
18
|
+
* ── Conventions that hold on every table, so they are stated once ────────
|
|
19
|
+
*
|
|
20
|
+
* `SCHEMA.md` §1.1:
|
|
21
|
+
*
|
|
22
|
+
* - **Time is a timestamp and every process reads the same clock.** Stored in
|
|
23
|
+
* a single fixed textual form that sorts in chronological order, so a
|
|
24
|
+
* comparison is a comparison and needs no conversion inside a transaction
|
|
25
|
+
* every other caller is waiting behind. The default is read from the
|
|
26
|
+
* database's own clock, never from a value a caller computed — several
|
|
27
|
+
* processes are running by design (§1.0a) and two of them disagreeing by a
|
|
28
|
+
* second would make an expiry non-deterministic in a way nothing will ever
|
|
29
|
+
* reproduce.
|
|
30
|
+
* - **`created_at` and `updated_at` are on every table.**
|
|
31
|
+
* - **Identifiers are opaque text**, except `browsers.id`, which is one of two
|
|
32
|
+
* words because callers type it, and the two counter keys, which count
|
|
33
|
+
* upward because they double as a cursor.
|
|
34
|
+
*
|
|
35
|
+
* Enums are `CHECK` constraints rather than a lookup table: the value sets are
|
|
36
|
+
* fixed by this design, and a join to read a word the row already spells is a
|
|
37
|
+
* cost paid on every read to buy nothing.
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* The clock every default reads.
|
|
41
|
+
*
|
|
42
|
+
* Sub-second precision, because two leases created in the same second are
|
|
43
|
+
* ordinary at this rate and the queue is ordered by `created_at` (§1.5). The
|
|
44
|
+
* tie-break on `id` is what settles the remainder; a whole-second clock would
|
|
45
|
+
* push far too much onto it.
|
|
46
|
+
*/
|
|
47
|
+
const NOW = "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
48
|
+
/**
|
|
49
|
+
* `browsers` — a fixed two-row table (§1.2).
|
|
50
|
+
*
|
|
51
|
+
* The two allowed values are enforced by the check and the primary key makes
|
|
52
|
+
* each unique, so **at most** two rows can exist; the seed at the bottom of
|
|
53
|
+
* this step creates both, so **at least** two do. There is no create or delete
|
|
54
|
+
* operation for a browser on any surface (§3.13), so the pair is fixed.
|
|
55
|
+
*
|
|
56
|
+
* `UNIQUE (id, browser_id)`-style composite target: `claims` gets one below,
|
|
57
|
+
* not this table.
|
|
58
|
+
*/
|
|
59
|
+
const BROWSERS = `
|
|
60
|
+
CREATE TABLE browsers (
|
|
61
|
+
id TEXT PRIMARY KEY
|
|
62
|
+
CHECK (id IN ('regular', 'private')),
|
|
63
|
+
state TEXT NOT NULL DEFAULT 'stopped'
|
|
64
|
+
CHECK (state IN ('stopped', 'starting', 'running', 'signing-in', 'failed')),
|
|
65
|
+
-- The isolation fact: the service acts on the process recorded here and on
|
|
66
|
+
-- nothing else, so a browser somebody else is running is never touched.
|
|
67
|
+
pid INTEGER,
|
|
68
|
+
launched_at TEXT,
|
|
69
|
+
-- A claim, never a proof (1.2c). It survives the browser dying, so nothing
|
|
70
|
+
-- may attach on the strength of this column without checking first.
|
|
71
|
+
endpoint TEXT,
|
|
72
|
+
-- What makes the endpoint safe to trust once it has been checked: a port
|
|
73
|
+
-- can be handed to something else entirely after the browser that had it
|
|
74
|
+
-- exits, and matching the port alone would attach to a stranger.
|
|
75
|
+
browser_uuid TEXT,
|
|
76
|
+
restart_count INTEGER NOT NULL DEFAULT 0
|
|
77
|
+
CHECK (restart_count >= 0),
|
|
78
|
+
created_at TEXT NOT NULL DEFAULT (${NOW}),
|
|
79
|
+
updated_at TEXT NOT NULL DEFAULT (${NOW}),
|
|
80
|
+
-- A stopped browser has no process, and a running one has one. Stated as a
|
|
81
|
+
-- constraint rather than as a convention because 'pid is null' is what the
|
|
82
|
+
-- reclamation path branches on.
|
|
83
|
+
CHECK ((state = 'stopped') = (pid IS NULL))
|
|
84
|
+
) STRICT
|
|
85
|
+
`;
|
|
86
|
+
/**
|
|
87
|
+
* `claims` — the lease (§1.3). The entity the whole service is about.
|
|
88
|
+
*
|
|
89
|
+
* **The secret key is never stored.** `key_hash` is a one-way hash of it, and
|
|
90
|
+
* every call that carries a key hashes what it was handed and looks the lease
|
|
91
|
+
* up by this value.
|
|
92
|
+
*/
|
|
93
|
+
const CLAIMS = `
|
|
94
|
+
CREATE TABLE claims (
|
|
95
|
+
id TEXT PRIMARY KEY,
|
|
96
|
+
key_hash TEXT NOT NULL UNIQUE,
|
|
97
|
+
-- Not a foreign key, and there is no table of sessions: session identity is
|
|
98
|
+
-- a shared key this service does not own, so a constraint here would mean
|
|
99
|
+
-- inventing a registry for something another system mints.
|
|
100
|
+
session_id TEXT NOT NULL,
|
|
101
|
+
browser_id TEXT NOT NULL REFERENCES browsers (id),
|
|
102
|
+
state TEXT NOT NULL
|
|
103
|
+
CHECK (state IN ('queued', 'active', 'released', 'expired', 'revoked')),
|
|
104
|
+
-- Mandatory, and its justification is revoking: an operator taking capacity
|
|
105
|
+
-- off a caller decides which caller, and session_id is a key another system
|
|
106
|
+
-- minted. Three to two hundred characters (1.3).
|
|
107
|
+
purpose TEXT NOT NULL
|
|
108
|
+
CHECK (length(purpose) BETWEEN 3 AND 200),
|
|
109
|
+
-- One column for both live states: queued and active leases expire by the
|
|
110
|
+
-- same mechanism and only the duration differs.
|
|
111
|
+
expires_at TEXT NOT NULL,
|
|
112
|
+
-- The duration in force for this lease, fixed when it entered its current
|
|
113
|
+
-- state. A renewal has to extend by the duration the caller was told.
|
|
114
|
+
ttl_seconds INTEGER NOT NULL
|
|
115
|
+
CHECK (ttl_seconds > 0),
|
|
116
|
+
activated_at TEXT,
|
|
117
|
+
renew_count INTEGER NOT NULL DEFAULT 0
|
|
118
|
+
CHECK (renew_count >= 0),
|
|
119
|
+
-- When the lease actually lapsed, which is not when a sweep noticed (2.4a).
|
|
120
|
+
expired_at TEXT,
|
|
121
|
+
ended_at TEXT,
|
|
122
|
+
revoke_reason TEXT,
|
|
123
|
+
created_at TEXT NOT NULL DEFAULT (${NOW}),
|
|
124
|
+
updated_at TEXT NOT NULL DEFAULT (${NOW}),
|
|
125
|
+
-- An operator taking capacity off a caller owes a sentence, and the
|
|
126
|
+
-- caller's next call is refused with it. Required only when revoked, and
|
|
127
|
+
-- meaningless otherwise, so both halves are said.
|
|
128
|
+
CHECK ((state = 'revoked') = (revoke_reason IS NOT NULL)),
|
|
129
|
+
-- Final is final, and a final lease has an end (2.1). One column rather
|
|
130
|
+
-- than three, because state already says which.
|
|
131
|
+
CHECK ((state IN ('released', 'expired', 'revoked')) = (ended_at IS NOT NULL)),
|
|
132
|
+
-- Null forever on a lease that expired while waiting, and set at the moment
|
|
133
|
+
-- a lease stops waiting. A queued lease has never had a tab.
|
|
134
|
+
CHECK (state <> 'queued' OR activated_at IS NULL)
|
|
135
|
+
) STRICT
|
|
136
|
+
`;
|
|
137
|
+
/**
|
|
138
|
+
* `tabs` — the unit of capacity and the unit of ownership (§1.4).
|
|
139
|
+
*
|
|
140
|
+
* **There is no column recording where a tab is**, and that deliberate absence
|
|
141
|
+
* is the single largest privacy improvement in this design. A table of
|
|
142
|
+
* addresses kept over months is a browsing history; there is no such table, so
|
|
143
|
+
* there is no retention setting to get wrong and no clear-history command to
|
|
144
|
+
* build. `captures.url` is a different column and survives — it records what
|
|
145
|
+
* one picture was of, which a tab's later address is not.
|
|
146
|
+
*
|
|
147
|
+
* **The composite foreign key is the reason `foreign_keys` is set explicitly
|
|
148
|
+
* in `open.ts`.** `claim_id` alone would let a tab name a browser its own
|
|
149
|
+
* lease did not; naming the pair makes the database refuse it. That target
|
|
150
|
+
* needs `claims (id, browser_id)` to be unique, which is what the index below
|
|
151
|
+
* provides — free, given `id` is already unique.
|
|
152
|
+
*/
|
|
153
|
+
const TABS = `
|
|
154
|
+
CREATE TABLE tabs (
|
|
155
|
+
id TEXT PRIMARY KEY,
|
|
156
|
+
-- The ownership fact. Set once, never null, never changed — which is why
|
|
157
|
+
-- "one live lease per tab" is structural rather than an index (1.11).
|
|
158
|
+
claim_id TEXT NOT NULL,
|
|
159
|
+
-- A copy of the lease's browser, kept because a uniqueness rule can only be
|
|
160
|
+
-- written over columns on one row. It cannot drift: the composite key below
|
|
161
|
+
-- refuses a tab whose browser disagrees with its lease's.
|
|
162
|
+
browser_id TEXT NOT NULL,
|
|
163
|
+
-- Whatever the automation tool calls this tab. Never returned to a caller
|
|
164
|
+
-- on any surface — it is the tool's namespace, and exposing it hands
|
|
165
|
+
-- callers a second, non-opaque way to name a tab.
|
|
166
|
+
driver_tab_id TEXT,
|
|
167
|
+
-- 'closing' is not ceremony: it is the honest representation of "the tool
|
|
168
|
+
-- was asked and has not answered", and it is what stops a page that may
|
|
169
|
+
-- still exist being counted as free.
|
|
170
|
+
state TEXT NOT NULL
|
|
171
|
+
CHECK (state IN ('opening', 'open', 'closing', 'closed', 'failed')),
|
|
172
|
+
opened_at TEXT,
|
|
173
|
+
closed_at TEXT,
|
|
174
|
+
-- A leaked tab, not a leaked lease (2.4b): the budget is not affected, a
|
|
175
|
+
-- page is. This is the flag the clear-a-leaked-tab operation selects on.
|
|
176
|
+
close_failed INTEGER NOT NULL DEFAULT 0
|
|
177
|
+
CHECK (close_failed IN (0, 1)),
|
|
178
|
+
close_attempts INTEGER NOT NULL DEFAULT 0
|
|
179
|
+
CHECK (close_attempts >= 0),
|
|
180
|
+
created_at TEXT NOT NULL DEFAULT (${NOW}),
|
|
181
|
+
updated_at TEXT NOT NULL DEFAULT (${NOW}),
|
|
182
|
+
FOREIGN KEY (claim_id, browser_id) REFERENCES claims (id, browser_id),
|
|
183
|
+
-- A tab that has not opened has no driver name to be unique against, and
|
|
184
|
+
-- one that has opened does. Without this the partial unique index below
|
|
185
|
+
-- would be satisfied by any number of live rows holding null.
|
|
186
|
+
CHECK ((state = 'opening') = (driver_tab_id IS NULL))
|
|
187
|
+
) STRICT
|
|
188
|
+
`;
|
|
189
|
+
/**
|
|
190
|
+
* `events` — one row per decision, kept in order (§1.6).
|
|
191
|
+
*
|
|
192
|
+
* **Every decision, allowed and refused alike.** A record containing only
|
|
193
|
+
* refusals cannot answer "was this rule ever actually reached", which is the
|
|
194
|
+
* first question anybody asks the day something behaves oddly.
|
|
195
|
+
*
|
|
196
|
+
* The kind list is fixed rather than free text, because a typo in free text
|
|
197
|
+
* creates a phantom category every count then silently misses. It is added to
|
|
198
|
+
* only when the code that writes a new kind exists — which is why this list is
|
|
199
|
+
* exactly the one §1.6 names and not one entry more.
|
|
200
|
+
*/
|
|
201
|
+
const EVENTS = `
|
|
202
|
+
CREATE TABLE events (
|
|
203
|
+
-- Counts upward because it doubles as an "everything since here" cursor.
|
|
204
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
205
|
+
-- For an expiry this is not when the lease lapsed; that is claims.expired_at
|
|
206
|
+
-- and 2.4a says why it is recorded separately.
|
|
207
|
+
at TEXT NOT NULL DEFAULT (${NOW}),
|
|
208
|
+
kind TEXT NOT NULL
|
|
209
|
+
CHECK (kind IN (
|
|
210
|
+
'claim_requested', 'claim_granted', 'claim_queued', 'claim_promoted',
|
|
211
|
+
'claim_renewed', 'claim_released', 'claim_expired', 'claim_revoked',
|
|
212
|
+
'tab_opening', 'tab_open_failed', 'tab_closing',
|
|
213
|
+
'navigate', 'act', 'read', 'evaluate', 'capture', 'compare',
|
|
214
|
+
'browser_launched', 'browser_adopted', 'browser_exited',
|
|
215
|
+
'launch_race_lost', 'sweep'
|
|
216
|
+
)),
|
|
217
|
+
-- Separate from kind so "how often is this refused" is one question rather
|
|
218
|
+
-- than a set of parallel event names that have to be kept in step.
|
|
219
|
+
outcome TEXT NOT NULL
|
|
220
|
+
CHECK (outcome IN ('allow', 'deny')),
|
|
221
|
+
-- Which rule refused, named from section 7's list.
|
|
222
|
+
guard TEXT,
|
|
223
|
+
claim_id TEXT REFERENCES claims (id),
|
|
224
|
+
tab_id TEXT REFERENCES tabs (id),
|
|
225
|
+
-- The one denormalisation in the schema that earns its place outright: a
|
|
226
|
+
-- refused request never becomes a lease, so without this column every
|
|
227
|
+
-- refusal on the busiest rule in the service is anonymous.
|
|
228
|
+
session_id TEXT,
|
|
229
|
+
-- Which door the call came in through. The last value is work the service
|
|
230
|
+
-- did on its own behalf inside somebody else's call — not a background job,
|
|
231
|
+
-- because with no long-lived process there is nothing running in the
|
|
232
|
+
-- background, so a sweep is attributed to the call that performed it.
|
|
233
|
+
adapter TEXT NOT NULL
|
|
234
|
+
CHECK (adapter IN ('tool-stdio', 'tool-http', 'cli', 'internal')),
|
|
235
|
+
-- A record, not a restriction: nothing refuses anything on the strength of
|
|
236
|
+
-- this column.
|
|
237
|
+
browser_id TEXT REFERENCES browsers (id),
|
|
238
|
+
-- The rest, shaped per kind. One queryable stream: a column per kind would
|
|
239
|
+
-- be a wide, mostly-empty table, and a table per kind would turn every read
|
|
240
|
+
-- of the ledger into a fifteen-way union.
|
|
241
|
+
detail TEXT,
|
|
242
|
+
-- A guard names the rule that refused, so it belongs on a denial and means
|
|
243
|
+
-- nothing on an allow.
|
|
244
|
+
CHECK ((outcome = 'deny') = (guard IS NOT NULL))
|
|
245
|
+
) STRICT
|
|
246
|
+
`;
|
|
247
|
+
/**
|
|
248
|
+
* `captures` — what a picture cost (§1.7).
|
|
249
|
+
*
|
|
250
|
+
* `path` is **relative to the artifact root, never absolute** (§1.7a). The
|
|
251
|
+
* root can move, and an absolute path pins every row to one machine's layout
|
|
252
|
+
* the moment it is written.
|
|
253
|
+
*/
|
|
254
|
+
const CAPTURES = `
|
|
255
|
+
CREATE TABLE captures (
|
|
256
|
+
id TEXT PRIMARY KEY,
|
|
257
|
+
-- Who took it. Survives the lease ending, which is the point.
|
|
258
|
+
claim_id TEXT NOT NULL REFERENCES claims (id),
|
|
259
|
+
tab_id TEXT NOT NULL REFERENCES tabs (id),
|
|
260
|
+
taken_at TEXT NOT NULL DEFAULT (${NOW}),
|
|
261
|
+
kind TEXT NOT NULL
|
|
262
|
+
CHECK (kind IN ('viewport', 'element', 'full_page')),
|
|
263
|
+
-- Which resolution rung was asked for. Stored rather than inferred from the
|
|
264
|
+
-- dimensions, because the rungs are configuration and can move between
|
|
265
|
+
-- installations.
|
|
266
|
+
tier TEXT NOT NULL
|
|
267
|
+
CHECK (tier IN ('default', 'detail', 'max')),
|
|
268
|
+
-- Free text, and required only on the top tier. The entire mechanism by
|
|
269
|
+
-- which anyone learns why callers escalate; an enum could only report which
|
|
270
|
+
-- of the author's guesses somebody picked.
|
|
271
|
+
reason TEXT,
|
|
272
|
+
source_width INTEGER NOT NULL CHECK (source_width > 0),
|
|
273
|
+
source_height INTEGER NOT NULL CHECK (source_height > 0),
|
|
274
|
+
-- Equal to the pair above when nothing was shrunk, which is how "was this
|
|
275
|
+
-- downscaled" is answered without a flag that could disagree with the
|
|
276
|
+
-- numbers beside it.
|
|
277
|
+
width INTEGER NOT NULL CHECK (width > 0),
|
|
278
|
+
height INTEGER NOT NULL CHECK (height > 0),
|
|
279
|
+
bytes INTEGER NOT NULL CHECK (bytes >= 0),
|
|
280
|
+
path TEXT NOT NULL,
|
|
281
|
+
selector TEXT,
|
|
282
|
+
-- The breakpoint, stored as a number rather than a name because a named set
|
|
283
|
+
-- of breakpoints is a vocabulary the service would have to own.
|
|
284
|
+
viewport_width INTEGER NOT NULL CHECK (viewport_width > 0),
|
|
285
|
+
-- What page this was a picture of. Nothing else records it, and it is a
|
|
286
|
+
-- different fact from where a tab is — for which there is no column at all.
|
|
287
|
+
url TEXT,
|
|
288
|
+
warned INTEGER NOT NULL DEFAULT 0
|
|
289
|
+
CHECK (warned IN (0, 1)),
|
|
290
|
+
created_at TEXT NOT NULL DEFAULT (${NOW}),
|
|
291
|
+
updated_at TEXT NOT NULL DEFAULT (${NOW}),
|
|
292
|
+
-- A written reason is owed on the top tier (3.11).
|
|
293
|
+
CHECK (tier <> 'max' OR reason IS NOT NULL),
|
|
294
|
+
-- An element capture is the one that names an element.
|
|
295
|
+
CHECK ((kind = 'element') = (selector IS NOT NULL)),
|
|
296
|
+
-- Never absolute (1.7a). Both spellings of a root, because the check has to
|
|
297
|
+
-- hold whichever platform wrote the row.
|
|
298
|
+
CHECK (path NOT LIKE '/%' AND path NOT LIKE '_:%' AND path NOT LIKE '\\%')
|
|
299
|
+
) STRICT
|
|
300
|
+
`;
|
|
301
|
+
/**
|
|
302
|
+
* `comparisons` — what one diff did, under the numbers in force at the time
|
|
303
|
+
* (§1.9).
|
|
304
|
+
*
|
|
305
|
+
* A table rather than a ledger entry for three reasons, and the third is the
|
|
306
|
+
* one that settles it: the ledger is the one thing in this design that may be
|
|
307
|
+
* trimmed (§1.1), so folding this in would let a future decision to trim it
|
|
308
|
+
* silently destroy the tuning history. The other two: a rerun answers a
|
|
309
|
+
* different question once any of the three settings has moved, and the
|
|
310
|
+
* references to a capture, a target and a lease are real foreign keys here and
|
|
311
|
+
* unenforceable inside a blob on a ledger row.
|
|
312
|
+
*
|
|
313
|
+
* **All three settings are copied rather than referenced**, because all three
|
|
314
|
+
* are mutable and all three determined the output — snapshotting one and
|
|
315
|
+
* referencing the others would be a record that is half-true.
|
|
316
|
+
*/
|
|
317
|
+
const COMPARISONS = `
|
|
318
|
+
CREATE TABLE comparisons (
|
|
319
|
+
id TEXT PRIMARY KEY,
|
|
320
|
+
source_capture_id TEXT NOT NULL REFERENCES captures (id),
|
|
321
|
+
-- The capture the caller named. A missing target returns the picture with
|
|
322
|
+
-- an explanation rather than a refusal (1.9), so a row is only written when
|
|
323
|
+
-- a diff actually ran and the target was found.
|
|
324
|
+
target_capture_id TEXT NOT NULL REFERENCES captures (id),
|
|
325
|
+
claim_id TEXT NOT NULL REFERENCES claims (id),
|
|
326
|
+
at TEXT NOT NULL DEFAULT (${NOW}),
|
|
327
|
+
-- The three settings actually applied.
|
|
328
|
+
colour_tolerance REAL NOT NULL CHECK (colour_tolerance >= 0),
|
|
329
|
+
minimum_region_area INTEGER NOT NULL CHECK (minimum_region_area >= 0),
|
|
330
|
+
maximum_regions INTEGER NOT NULL CHECK (maximum_regions > 0),
|
|
331
|
+
-- The raw count and its share, before regions are worked out. What
|
|
332
|
+
-- distinguishes "nothing moved" from "the threshold ate it".
|
|
333
|
+
changed_pixels INTEGER NOT NULL CHECK (changed_pixels >= 0),
|
|
334
|
+
changed_ratio REAL NOT NULL CHECK (changed_ratio BETWEEN 0 AND 1),
|
|
335
|
+
-- True when at least one region survives filtering, not when any pixel
|
|
336
|
+
-- differs. Stored rather than derived from the region list because the
|
|
337
|
+
-- definition is the thing every caller branches on and it has to have one
|
|
338
|
+
-- answer.
|
|
339
|
+
changed INTEGER NOT NULL
|
|
340
|
+
CHECK (changed IN (0, 1)),
|
|
341
|
+
-- One entry per changed area with its position, size and two crop paths.
|
|
342
|
+
-- No separate region count: it is the length of this list.
|
|
343
|
+
regions TEXT NOT NULL,
|
|
344
|
+
overlay_path TEXT NOT NULL,
|
|
345
|
+
-- A truncated result that does not say so is a lie about completeness.
|
|
346
|
+
truncated INTEGER NOT NULL DEFAULT 0
|
|
347
|
+
CHECK (truncated IN (0, 1)),
|
|
348
|
+
created_at TEXT NOT NULL DEFAULT (${NOW}),
|
|
349
|
+
updated_at TEXT NOT NULL DEFAULT (${NOW}),
|
|
350
|
+
CHECK (overlay_path NOT LIKE '/%' AND overlay_path NOT LIKE '_:%'
|
|
351
|
+
AND overlay_path NOT LIKE '\\%')
|
|
352
|
+
) STRICT
|
|
353
|
+
`;
|
|
354
|
+
/**
|
|
355
|
+
* `feedback` — the tenth tool's table, and the one built to be removed
|
|
356
|
+
* (§3.16).
|
|
357
|
+
*
|
|
358
|
+
* Its own table rather than an event kind, on three arguments: the ledger
|
|
359
|
+
* records what the service *did* and this records what a caller *thought*, so
|
|
360
|
+
* folding it in would make every count over `events` start by excluding a kind
|
|
361
|
+
* that is not an event; the planned removal has to stay a deletion rather than
|
|
362
|
+
* becoming a migration over a retired kind other rows still use; and the two
|
|
363
|
+
* have opposite lifecycles — the ledger is written on every call by every
|
|
364
|
+
* process and trimmed, this is written rarely and read by hand.
|
|
365
|
+
*
|
|
366
|
+
* **No lease is required, and that is the point rather than a convenience.** A
|
|
367
|
+
* caller whose claim was refused is the caller most likely to have something
|
|
368
|
+
* worth recording, so requiring a lease would silence exactly the population
|
|
369
|
+
* the tool exists to hear from.
|
|
370
|
+
*/
|
|
371
|
+
const FEEDBACK = `
|
|
372
|
+
CREATE TABLE feedback (
|
|
373
|
+
-- Borrows the ledger's cursor discipline: a counter key, so reading
|
|
374
|
+
-- "everything since I last looked" is the same one query it is everywhere.
|
|
375
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
376
|
+
at TEXT NOT NULL DEFAULT (${NOW}),
|
|
377
|
+
session_id TEXT,
|
|
378
|
+
claim_id TEXT REFERENCES claims (id),
|
|
379
|
+
-- The caller's last operation, read from the ledger, so it names what was
|
|
380
|
+
-- actually attempted rather than what the caller remembers attempting.
|
|
381
|
+
last_event_id INTEGER REFERENCES events (id),
|
|
382
|
+
-- The refusal that was hit, if the caller's last event was a denial.
|
|
383
|
+
last_guard TEXT,
|
|
384
|
+
-- The axis is not satisfaction: it is whether the service moved the
|
|
385
|
+
-- caller's actual work forward or got in its way (3.16).
|
|
386
|
+
rating INTEGER NOT NULL
|
|
387
|
+
CHECK (rating BETWEEN 1 AND 5),
|
|
388
|
+
-- A small fixed set, chosen to be greppable. 'worked-well' is in it
|
|
389
|
+
-- deliberately: a list with no positive value collects only complaints.
|
|
390
|
+
category TEXT NOT NULL
|
|
391
|
+
CHECK (category IN (
|
|
392
|
+
'refusal-unclear', 'no-path', 'worked-around',
|
|
393
|
+
'surprised-me', 'worked-well'
|
|
394
|
+
)),
|
|
395
|
+
-- The floor is deliberate: twenty characters is roughly the shortest useful
|
|
396
|
+
-- sentence, and it stops a reflexive one-word row.
|
|
397
|
+
note TEXT NOT NULL
|
|
398
|
+
CHECK (length(note) BETWEEN 20 AND 2000),
|
|
399
|
+
created_at TEXT NOT NULL DEFAULT (${NOW}),
|
|
400
|
+
updated_at TEXT NOT NULL DEFAULT (${NOW})
|
|
401
|
+
) STRICT
|
|
402
|
+
`;
|
|
403
|
+
/**
|
|
404
|
+
* The two partial indexes, and they are two rather than three.
|
|
405
|
+
*
|
|
406
|
+
* **A change in a count, stated rather than corrected quietly** (`SCHEMA.md`
|
|
407
|
+
* §1.11, §9.2). Three partial indexes were built and exercised on the SQLite
|
|
408
|
+
* version this design targets, and that measurement stands — it covered one
|
|
409
|
+
* index more than the design contains. The third enforced one canonical
|
|
410
|
+
* picture per view, browser, kind and breakpoint, and it went with the concept
|
|
411
|
+
* it enforced. A reader who remembers "three, verified" and finds two with no
|
|
412
|
+
* explanation has to work out whether a measurement failed, whether an index
|
|
413
|
+
* was dropped for being wrong, or whether somebody miscounted; none of those
|
|
414
|
+
* happened.
|
|
415
|
+
*
|
|
416
|
+
* **Two rules a reader may look for and will not find.** "One live lease per
|
|
417
|
+
* tab" is structural — a tab's lease reference is set when the row is created,
|
|
418
|
+
* is never null and never changes, so there is nothing for an index to refuse.
|
|
419
|
+
* And **"one live lease per session" is gone entirely**, not enforced
|
|
420
|
+
* differently: a lease is one tab, so a session that wants three tabs holds
|
|
421
|
+
* three leases, and the rule is incompatible with that model. What it would
|
|
422
|
+
* have caught as a side effect — two callers accidentally sharing one session
|
|
423
|
+
* identity — is not caught anywhere, and that is named as lost rather than
|
|
424
|
+
* quietly dropped.
|
|
425
|
+
*/
|
|
426
|
+
const PARTIAL_INDEXES = `
|
|
427
|
+
-- One live tab row per physical driver tab. The rule lives at the write
|
|
428
|
+
-- because the staleness is in the read that came before it: two callers both
|
|
429
|
+
-- read "nothing there", both reads were true when made, and nothing re-checks
|
|
430
|
+
-- the second by the time its write lands. Across separate processes this is
|
|
431
|
+
-- not one option among several — there is no shared process to hold a lock in.
|
|
432
|
+
CREATE UNIQUE INDEX one_row_per_physical_tab
|
|
433
|
+
ON tabs (browser_id, driver_tab_id) WHERE state IN ('opening', 'open', 'closing');
|
|
434
|
+
|
|
435
|
+
-- Not a uniqueness rule: this makes the capacity count an index-only scan.
|
|
436
|
+
-- The count of live claims is read inside the transaction every arbitration
|
|
437
|
+
-- call opens, with every other caller on the machine waiting behind it, so
|
|
438
|
+
-- the answer coming out of the index without touching the table is what keeps
|
|
439
|
+
-- the serialised section short.
|
|
440
|
+
CREATE INDEX live_claims
|
|
441
|
+
ON claims (state) WHERE state IN ('queued', 'active');
|
|
442
|
+
`;
|
|
443
|
+
/**
|
|
444
|
+
* The ordinary indexes (`MILESTONES.md` #7).
|
|
445
|
+
*
|
|
446
|
+
* At a tab budget of fifteen the live set is tens of rows, so most of these
|
|
447
|
+
* change nothing measurable on live data. They exist for the historical rows,
|
|
448
|
+
* which are the part that grows without bound — with one exception, the
|
|
449
|
+
* sweep's scan, which is hot rather than historical.
|
|
450
|
+
*/
|
|
451
|
+
const INDEXES = `
|
|
452
|
+
-- Not a query index: the target of the composite foreign key on tabs that
|
|
453
|
+
-- stops a tab naming a browser its own lease did not. Free, given id is
|
|
454
|
+
-- already unique.
|
|
455
|
+
CREATE UNIQUE INDEX claims_id_browser ON claims (id, browser_id);
|
|
456
|
+
|
|
457
|
+
-- The sweep's scan: everything live and past its expiry, in one index range.
|
|
458
|
+
-- Read on every arbitration call.
|
|
459
|
+
CREATE INDEX claims_state_expires ON claims (state, expires_at);
|
|
460
|
+
|
|
461
|
+
-- Head of queue, first in first out. Separate from the sweep's index because
|
|
462
|
+
-- they order by different columns, and a scan that has to sort gets slower as
|
|
463
|
+
-- history accumulates.
|
|
464
|
+
CREATE INDEX claims_state_created ON claims (state, created_at);
|
|
465
|
+
|
|
466
|
+
-- A session's own history, and the query the admission transaction needs when
|
|
467
|
+
-- it reads a session's other live leases.
|
|
468
|
+
CREATE INDEX claims_session_created ON claims (session_id, created_at DESC);
|
|
469
|
+
|
|
470
|
+
-- The ownership check, and everything release and the sweep do.
|
|
471
|
+
CREATE INDEX tabs_claim ON tabs (claim_id);
|
|
472
|
+
|
|
473
|
+
-- A slice read; one lease's whole history; the capture and diff rollups; and
|
|
474
|
+
-- which rule refuses most, small because denials are rare.
|
|
475
|
+
CREATE INDEX events_at ON events (at);
|
|
476
|
+
CREATE INDEX events_claim_id ON events (claim_id, id);
|
|
477
|
+
CREATE INDEX events_kind_at ON events (kind, at);
|
|
478
|
+
CREATE INDEX events_guard ON events (guard) WHERE guard IS NOT NULL;
|
|
479
|
+
|
|
480
|
+
-- Listing, and the rollup.
|
|
481
|
+
CREATE INDEX captures_claim ON captures (claim_id);
|
|
482
|
+
CREATE INDEX captures_taken_at ON captures (taken_at);
|
|
483
|
+
|
|
484
|
+
-- The diffs run from one capture, and the diffs run against one — which is
|
|
485
|
+
-- what tuning reads.
|
|
486
|
+
CREATE INDEX comparisons_source ON comparisons (source_capture_id, at DESC);
|
|
487
|
+
CREATE INDEX comparisons_target ON comparisons (target_capture_id);
|
|
488
|
+
|
|
489
|
+
-- Reading it back, most recent first, filtered by kind.
|
|
490
|
+
CREATE INDEX feedback_at ON feedback (at DESC);
|
|
491
|
+
CREATE INDEX feedback_category_at ON feedback (category, at DESC);
|
|
492
|
+
`;
|
|
493
|
+
/**
|
|
494
|
+
* The two browser rows, created by the first schema step (§1.2d).
|
|
495
|
+
*
|
|
496
|
+
* This is what makes "exactly two" hold from the bottom: the check constraint
|
|
497
|
+
* and the primary key cap the table at two, and this seed floors it at two.
|
|
498
|
+
*/
|
|
499
|
+
const SEED_BROWSERS = `
|
|
500
|
+
INSERT INTO browsers (id, state) VALUES ('regular', 'stopped'), ('private', 'stopped');
|
|
501
|
+
`;
|
|
502
|
+
/** Every statement this step runs, in the order it runs them. */
|
|
503
|
+
export const STEP_ONE_SQL = [
|
|
504
|
+
BROWSERS,
|
|
505
|
+
CLAIMS,
|
|
506
|
+
TABS,
|
|
507
|
+
EVENTS,
|
|
508
|
+
CAPTURES,
|
|
509
|
+
COMPARISONS,
|
|
510
|
+
FEEDBACK,
|
|
511
|
+
PARTIAL_INDEXES,
|
|
512
|
+
INDEXES,
|
|
513
|
+
SEED_BROWSERS,
|
|
514
|
+
];
|
|
515
|
+
export const stepOne = {
|
|
516
|
+
version: 1,
|
|
517
|
+
summary: 'The whole schema: seven tables, the two partial indexes, and the two browser rows.',
|
|
518
|
+
apply: (db) => {
|
|
519
|
+
for (const statement of STEP_ONE_SQL) {
|
|
520
|
+
db.exec(statement);
|
|
521
|
+
}
|
|
522
|
+
},
|
|
523
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step two: the one row that is not an environment variable.
|
|
3
|
+
*
|
|
4
|
+
* **A new step rather than an edit to step one**, which is the rule rather
|
|
5
|
+
* than a preference (`CLAUDE.md`, `SCHEMA.md` §1.2d): step one has run
|
|
6
|
+
* somewhere, and editing a step that has run means two installations
|
|
7
|
+
* reporting the same version with different schemas — a difference nothing
|
|
8
|
+
* reports until something breaks far from the cause.
|
|
9
|
+
*
|
|
10
|
+
* ── Why this table exists at all, when §1.10 deletes the settings table ──
|
|
11
|
+
*
|
|
12
|
+
* It is a **check**, not a configuration surface, and the difference is that
|
|
13
|
+
* nothing can write to it through any caller-reachable path. §1.10:
|
|
14
|
+
*
|
|
15
|
+
* > Several processes arbitrate against the tab budget simultaneously
|
|
16
|
+
* > (§1.0a) [...] Each admits callers against its own belief, each is
|
|
17
|
+
* > internally consistent, and **the ceiling silently stops being a
|
|
18
|
+
* > ceiling.**
|
|
19
|
+
*
|
|
20
|
+
* The budget stays an environment variable. This row is what makes several
|
|
21
|
+
* processes' beliefs about it comparable, and `src/store/budget.ts` is the
|
|
22
|
+
* only thing that reads or writes it.
|
|
23
|
+
*
|
|
24
|
+
* **One row, kept by the database rather than by a convention.** `only_row`
|
|
25
|
+
* is pinned to a single value by a check and made unique by the primary key,
|
|
26
|
+
* so a second row cannot be inserted even by a statement written somewhere
|
|
27
|
+
* this design never sees. That is what stops a one-row check quietly becoming
|
|
28
|
+
* a key-value store, which is the shape §1.10 rejected.
|
|
29
|
+
*/
|
|
30
|
+
const TAB_BUDGET = `
|
|
31
|
+
CREATE TABLE tab_budget (
|
|
32
|
+
only_row INTEGER PRIMARY KEY
|
|
33
|
+
CHECK (only_row = 1),
|
|
34
|
+
-- The same bound the environment carries, recorded by whichever process
|
|
35
|
+
-- opened this store first. Never adopted and never overwritten after that:
|
|
36
|
+
-- a later process that disagrees refuses to start (7.2).
|
|
37
|
+
tabs INTEGER NOT NULL
|
|
38
|
+
CHECK (tabs > 0),
|
|
39
|
+
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
40
|
+
updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
|
41
|
+
) STRICT
|
|
42
|
+
`;
|
|
43
|
+
/** Every statement this step runs. */
|
|
44
|
+
export const STEP_TWO_SQL = [TAB_BUDGET];
|
|
45
|
+
export const stepTwo = {
|
|
46
|
+
version: 2,
|
|
47
|
+
summary: 'The tab-budget agreement row: one value, one row, no caller-reachable write path.',
|
|
48
|
+
apply: (db) => {
|
|
49
|
+
for (const statement of STEP_TWO_SQL) {
|
|
50
|
+
db.exec(statement);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Step three: the queue's order becomes a counter rather than a clock.
|
|
3
|
+
*
|
|
4
|
+
* **A new step rather than an edit to step one**, per the rule that a step
|
|
5
|
+
* which has run somewhere is history (`CLAUDE.md`, `SCHEMA.md` §1.2d).
|
|
6
|
+
*
|
|
7
|
+
* ── The defect this fixes, because it is not obvious from the outside ────
|
|
8
|
+
*
|
|
9
|
+
* §1.5 orders the queue by `created_at`, tie-broken by `id`, and says exactly
|
|
10
|
+
* what the tie-break buys: "two requests in the same millisecond share a
|
|
11
|
+
* `created_at`, and without a tie-break the front of the queue flips between
|
|
12
|
+
* reads". That reasoning is sound and the tie-break does deliver a **stable**
|
|
13
|
+
* order.
|
|
14
|
+
*
|
|
15
|
+
* What it does not deliver is the other promise §2.5 makes:
|
|
16
|
+
*
|
|
17
|
+
* > **A caller's position only ever improves**, and it improves by exactly
|
|
18
|
+
* > one each time a tab comes free.
|
|
19
|
+
*
|
|
20
|
+
* **Measured, and it is reproducible in a few hundred rounds:** three callers
|
|
21
|
+
* arriving inside one millisecond share a `created_at`, so the order between
|
|
22
|
+
* them is decided by `id` — which is a random identifier, unrelated to
|
|
23
|
+
* arrival. A caller whose identifier sorts low is told *position 1* while
|
|
24
|
+
* a caller that genuinely arrived earlier is later told *position 1* as well,
|
|
25
|
+
* and the first caller's next answer is *position 2*. **Its position got
|
|
26
|
+
* worse**, which is the one thing §2.5 promises cannot happen, and it costs
|
|
27
|
+
* exactly the trust the queued response is trying to build when it asks a
|
|
28
|
+
* caller to schedule a check and wait.
|
|
29
|
+
*
|
|
30
|
+
* ── Why a counter and not a finer clock ─────────────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* A higher-resolution timestamp makes ties rarer without making them
|
|
33
|
+
* impossible, which turns a reproducible defect into a rare one — the worst
|
|
34
|
+
* of the available outcomes, because the promise stays false and the evidence
|
|
35
|
+
* goes away. A counter allocated by the database at insert has no ties by
|
|
36
|
+
* construction, in the same way and for the same reason the ledger's cursor
|
|
37
|
+
* has none (§1.6).
|
|
38
|
+
*
|
|
39
|
+
* **`created_at` keeps its own job.** It is when the lease was asked for,
|
|
40
|
+
* which is a fact about the lease that a person reads; it simply stops being
|
|
41
|
+
* the thing that decides who is next.
|
|
42
|
+
*/
|
|
43
|
+
const ARRIVAL = `
|
|
44
|
+
ALTER TABLE claims ADD COLUMN arrival INTEGER
|
|
45
|
+
`;
|
|
46
|
+
/**
|
|
47
|
+
* Existing rows get an arrival consistent with the order they would have been
|
|
48
|
+
* served in under the previous rule.
|
|
49
|
+
*
|
|
50
|
+
* A store stepped from version two has live queue entries whose relative
|
|
51
|
+
* order callers have already been told. Backfilling in `created_at`, `id`
|
|
52
|
+
* order preserves every answer already given; assigning arbitrarily would
|
|
53
|
+
* reorder a queue that people are waiting in.
|
|
54
|
+
*/
|
|
55
|
+
const BACKFILL = `
|
|
56
|
+
UPDATE claims
|
|
57
|
+
SET arrival = (
|
|
58
|
+
SELECT count(*)
|
|
59
|
+
FROM claims AS earlier
|
|
60
|
+
WHERE (earlier.created_at, earlier.id) <= (claims.created_at, claims.id)
|
|
61
|
+
)
|
|
62
|
+
`;
|
|
63
|
+
/**
|
|
64
|
+
* The counter itself: one row, holding the highest arrival handed out.
|
|
65
|
+
*
|
|
66
|
+
* A table rather than `AUTOINCREMENT` on the column, because `arrival` is
|
|
67
|
+
* being added to a table that already has a primary key — a second
|
|
68
|
+
* autoincrementing column is not something the engine offers — and because
|
|
69
|
+
* the allocation has to happen inside the arbitration transaction, where it
|
|
70
|
+
* is serialised with everything else by construction (§1.0a).
|
|
71
|
+
*/
|
|
72
|
+
const SEQUENCE = `
|
|
73
|
+
CREATE TABLE claim_arrival (
|
|
74
|
+
only_row INTEGER PRIMARY KEY
|
|
75
|
+
CHECK (only_row = 1),
|
|
76
|
+
next INTEGER NOT NULL
|
|
77
|
+
CHECK (next >= 0)
|
|
78
|
+
) STRICT
|
|
79
|
+
`;
|
|
80
|
+
/** Start above whatever the backfill used, so no arrival is ever reused. */
|
|
81
|
+
const SEED_SEQUENCE = `
|
|
82
|
+
INSERT INTO claim_arrival (only_row, next)
|
|
83
|
+
VALUES (1, (SELECT coalesce(max(arrival), 0) FROM claims))
|
|
84
|
+
`;
|
|
85
|
+
/**
|
|
86
|
+
* The order the queue is read in, now with no ties to break.
|
|
87
|
+
*
|
|
88
|
+
* It supersedes nothing at read time — `claims_state_created` still serves
|
|
89
|
+
* the historical queries that order by when a lease was asked for.
|
|
90
|
+
*/
|
|
91
|
+
const INDEX = `
|
|
92
|
+
CREATE INDEX claims_state_arrival ON claims (state, arrival)
|
|
93
|
+
`;
|
|
94
|
+
/** Every statement this step runs, in order. */
|
|
95
|
+
export const STEP_THREE_SQL = [
|
|
96
|
+
ARRIVAL,
|
|
97
|
+
BACKFILL,
|
|
98
|
+
SEQUENCE,
|
|
99
|
+
SEED_SEQUENCE,
|
|
100
|
+
INDEX,
|
|
101
|
+
];
|
|
102
|
+
export const stepThree = {
|
|
103
|
+
version: 3,
|
|
104
|
+
summary: 'The queue orders by an arrival counter, so a position can never get worse.',
|
|
105
|
+
apply: (db) => {
|
|
106
|
+
for (const statement of STEP_THREE_SQL) {
|
|
107
|
+
db.exec(statement);
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
};
|