@drawbridge/drawbridge-agents 0.1.59 → 0.1.60
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.
|
@@ -1,102 +1,235 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: drawbridge-add-connection
|
|
3
|
-
description: Use
|
|
3
|
+
description: Use for ANY work touching connections, vendors or hooks in drawbridge-utils — adding or checking a manifest, adding a vendor or its credentials, changing auth shape or scopes, adding a hook/link/step, or moving a vendor's API calls out of another repo. Also use before answering questions about how connections work.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
#
|
|
6
|
+
# Connections
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
skill is the order of work that fills it honestly.
|
|
8
|
+
**Invoke this before touching a manifest, a vendor file, or any code that calls a
|
|
9
|
+
vendor's API. Not after. Darren, 2026-09-13: "never make a connection without
|
|
10
|
+
using that skill."**
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
## Which job is this?
|
|
13
|
+
|
|
14
|
+
| What you are doing | Go to |
|
|
15
|
+
|---|---|
|
|
16
|
+
| adding a whole new vendor connection | **A. New connection** |
|
|
17
|
+
| adding credentials for a vendor | **B. Vendor** |
|
|
18
|
+
| a repo calls a vendor's API directly | **C. Vendor code outside a manifest** |
|
|
19
|
+
| checking an existing connection | **D. Check** |
|
|
20
|
+
|
|
21
|
+
They share one law, and it is the reason this file exists.
|
|
22
|
+
|
|
23
|
+
## THE LAW
|
|
24
|
+
|
|
25
|
+
**A vendor's facts live on its manifest. Nothing else.** Not a constant in
|
|
26
|
+
drawbridge-sync, not a client in drawbridge-api, not an env var read at a call
|
|
27
|
+
site. If a line of code knows a vendor's endpoint, header, payload shape, error
|
|
28
|
+
string or id format, it belongs in a hook.
|
|
29
|
+
|
|
30
|
+
**A vendor is NAMED by the connection that spends it.** `vendors : [ … ]` on the
|
|
31
|
+
manifest. That is what makes the provider screen able to say what a credential is
|
|
32
|
+
for, and what makes `vendorSettings()` hand a hook its fields.
|
|
33
|
+
|
|
34
|
+
**Grep before you invent.** A key, a field, a hook or a collection that already
|
|
35
|
+
has a shape must reuse it. Read the contract and the scaffold first, every time.
|
|
36
|
+
|
|
37
|
+
## STOP — the three mistakes that have actually been made
|
|
38
|
+
|
|
39
|
+
Each of these was made by someone who had read this file. They are here as
|
|
40
|
+
gates, not trivia.
|
|
41
|
+
|
|
42
|
+
**1. Inventing a key instead of using the mechanism.** A vendor card said nothing
|
|
43
|
+
about what its credentials were for, so a `usage` key was added to the vendor
|
|
44
|
+
scaffold and to all eight vendor files — when the answer was that the manifest
|
|
45
|
+
that spends it had not named it in `vendors`. The screen derives that. One list
|
|
46
|
+
entry, no new key. *(2026-09-13. Earlier: a flat `provider` string was invented
|
|
47
|
+
when `provider : { slug, id }` was already in five collections.)*
|
|
48
|
+
|
|
49
|
+
> **Before adding any key, answer out loud: what already answers this? If the
|
|
50
|
+
> answer is "a manifest would, if it declared the vendor / hook / field it
|
|
51
|
+
> already can", do that instead.**
|
|
18
52
|
|
|
19
|
-
|
|
53
|
+
**2. "This vendor has no connection, so it has no manifest."** Bright Data was
|
|
54
|
+
written as a vendor nothing named, on the grounds that no merchant links a Bright
|
|
55
|
+
Data account. Neither does anyone link our SendGrid account. **Who a credential
|
|
56
|
+
belongs to is not who spends it.** Drawbridge's own vendors are spent by the
|
|
57
|
+
`drawbridge` manifest and named in its `vendors` list — SendGrid, Twilio, HubSpot,
|
|
58
|
+
Bright Data. A vendor no manifest names is a bug, not a category.
|
|
59
|
+
|
|
60
|
+
**3. Leaving the vendor's calls behind.** Bright Data became a provider row and
|
|
61
|
+
its API calls stayed in `drawbridge-sync/lib/scrape.js` for another day — an
|
|
62
|
+
endpoint constant, a bearer header and a request body inline in a 3,798-line
|
|
63
|
+
file. Moving the credential is half the job. **The calls are the job.**
|
|
64
|
+
|
|
65
|
+
## A. New connection
|
|
66
|
+
|
|
67
|
+
**Copy `_scaffold.js`. Answer every key. Delete nothing.**
|
|
68
|
+
`drawbridge-utils/lib/connections/_scaffold.js` is three things at once: the file
|
|
69
|
+
you copy, the reference for what each key may hold, and **the schema `build()`
|
|
70
|
+
reads**. A manifest that omits a key or invents one cannot be imported — it fails
|
|
71
|
+
before any test runs, in every repo that pulls the package, naming the key.
|
|
72
|
+
|
|
73
|
+
So "did I miss something" is answered by the suite. What follows is judgement,
|
|
74
|
+
which a schema cannot check.
|
|
75
|
+
|
|
76
|
+
**A manifest ships complete: every hook the vendor can serve, every link its
|
|
77
|
+
dashboard exposes, every scope those need, copy humanized, walked on dev.
|
|
78
|
+
Nothing is deferred for a deadline. Darren, 2026-09-11: "Don't skip shit, do this
|
|
79
|
+
right, our production launch deadline is somewhat flexible." If the work does not
|
|
80
|
+
fit the time, the time moves.**
|
|
20
81
|
|
|
21
82
|
1. **Read the vendor twice, and write down where.** Fetch, never recall — a
|
|
22
|
-
consent url
|
|
23
|
-
|
|
83
|
+
consent url written from memory pointed at the api host and stalled every
|
|
84
|
+
merchant on a blank page.
|
|
24
85
|
- **API reference**: auth mechanisms and who the vendor says each is for
|
|
25
86
|
(your own account vs accounts outside your organization — Drawbridge is
|
|
26
|
-
always the second), partner or public-app program, OAuth
|
|
87
|
+
always the second), partner or public-app program, OAuth endpoints and
|
|
27
88
|
transport, token lifetime, refresh, every endpoint a hook will call.
|
|
28
|
-
- **Dashboard docs
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
`
|
|
35
|
-
|
|
36
|
-
2. **Scope for the whole manifest, not today's hooks.**
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
| vendor's shape |
|
|
89
|
+
- **Dashboard docs**: the page url for each object we hold an id for, where
|
|
90
|
+
the vendor says that id appears, the vendor's own names for things, and what
|
|
91
|
+
a disconnect really does on their side. This read fills `urls.*` and the
|
|
92
|
+
merchant copy.
|
|
93
|
+
|
|
94
|
+
Both urls go on **the vendor file's `urls`**, beside its `scopes` link.
|
|
95
|
+
`build()` throws if a `review` key reappears on a manifest.
|
|
96
|
+
|
|
97
|
+
2. **Scope for the whole manifest, not today's hooks.** Request every scope any
|
|
98
|
+
hook in the contract could need for this vendor, whether or not it ships now.
|
|
99
|
+
Scopes are app-level at Klaviyo and Attentive: adding one later costs every
|
|
100
|
+
connected merchant a re-consent. Declare them in `auth.oauth.scopes` —
|
|
101
|
+
**without it a vendor cannot detect drift**, which is the bug this whole
|
|
102
|
+
rework came out of.
|
|
103
|
+
|
|
104
|
+
3. **The auth SHAPE says which kind it is. There is no `auth.type` to pick.**
|
|
105
|
+
All five options are present and exactly one is non-false:
|
|
106
|
+
|
|
107
|
+
| the vendor's shape | the truthy key |
|
|
47
108
|
|---|---|
|
|
48
|
-
| installed from the vendor's app store, never redirects back | `install` |
|
|
49
|
-
| registered app, merchant consents on the vendor's screen | `oauth` |
|
|
50
|
-
| the vendor offers ONLY per-account credentials | `keys` |
|
|
51
|
-
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
the
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
109
|
+
| installed from the vendor's app store, never redirects back | `auth.install` |
|
|
110
|
+
| registered app, merchant consents on the vendor's screen | `auth.oauth` |
|
|
111
|
+
| the vendor offers ONLY per-account credentials | `auth.keys` |
|
|
112
|
+
| Drawbridge mints the credential, no third party | `auth.generated` |
|
|
113
|
+
| nothing to authenticate | `auth.none` |
|
|
114
|
+
|
|
115
|
+
The other four are `false` **carrying the reason**. `type` is INJECTED by
|
|
116
|
+
`build()` from the shape; leave it out — a declared `type` that disagrees
|
|
117
|
+
with the shape throws.
|
|
118
|
+
|
|
119
|
+
4. **Implement every hook the vendor can serve.** The only `false` is "the vendor
|
|
120
|
+
has no endpoint for this", with the reason and the page that shows the absence.
|
|
121
|
+
Time is never a reason.
|
|
122
|
+
|
|
123
|
+
**`lifecycle.health` is REQUIRED of every `oauth` and `install` connection** —
|
|
124
|
+
both end in a token somebody else can revoke, expire or narrow, and none of
|
|
125
|
+
those tells us.
|
|
126
|
+
|
|
127
|
+
**A hook describes its effects; it never performs them.** It receives
|
|
128
|
+
`( props, options )` and returns the answer envelope; `perform()` does the
|
|
129
|
+
writing. **Read `lib/connections/_hooks.js` before writing your first one** —
|
|
130
|
+
it carries a complete annotated hook and runs it.
|
|
131
|
+
|
|
132
|
+
5. **Write the merchant copy from the two reads, then run `humanizer:humanizer`
|
|
133
|
+
on it before commit.** Excerpt, description, guide, confirm, error copy: the
|
|
134
|
+
vendor's own names, the help centre's navigation path, what disconnect really
|
|
135
|
+
does. `content.errors` names the vendor in both arms — a merchant connecting
|
|
136
|
+
two accounts in one sitting needs to know which one failed.
|
|
137
|
+
|
|
138
|
+
6. **Let the enforcement check you.** `build()` refuses, at import: a missing or
|
|
139
|
+
invented top-level key, a missing hook, a hook prop its slot does not carry, an
|
|
140
|
+
unknown trigger, an oauth vendor declining `lifecycle.health`, two truthy auth
|
|
141
|
+
kinds, a step whose type disagrees with its position. Trust a named failure
|
|
142
|
+
over your memory of these steps.
|
|
143
|
+
|
|
144
|
+
7. **Walk it on dev.** Connect a real account, pick the resource, sync one contact
|
|
145
|
+
and one opted-out contact, break the grant and confirm the error reaches the
|
|
146
|
+
card, the page and the checklist, reconnect and confirm it clears *immediately*,
|
|
147
|
+
then open each `urls.*` link. **A green suite is not the walk.** Record it in
|
|
148
|
+
the release doc for the change that carries the manifest — there is no
|
|
149
|
+
`review.verified` any more.
|
|
150
|
+
|
|
151
|
+
## B. Vendor
|
|
152
|
+
|
|
153
|
+
One vendor, one file: `lib/connections/vendors/<vendor>.js`, carrying `fields`,
|
|
154
|
+
`icon`, `name`, `slug` and `urls`. However many connections spend it.
|
|
155
|
+
|
|
156
|
+
**Then name it.** Add the slug to the `vendors` list of every manifest that
|
|
157
|
+
spends it. **This is not optional bookkeeping** — it is what makes the provider
|
|
158
|
+
screen say who spends the credential, and what makes `vendorSettings({ slug })`
|
|
159
|
+
return the fields to that connection's hooks.
|
|
160
|
+
|
|
161
|
+
- A vendor merchants connect → named by that connection (`klaviyo`, `shopify`).
|
|
162
|
+
- **A vendor DRAWBRIDGE spends → named by the `drawbridge` manifest**, beside
|
|
163
|
+
SendGrid, Twilio, HubSpot and Bright Data. There is no such thing as a vendor
|
|
164
|
+
with no connection; there are vendors no MERCHANT connects.
|
|
165
|
+
- Each field's `credential` names the env var it falls back to. That is the only
|
|
166
|
+
place that name is written — `requires` derives from it, and so does the test
|
|
167
|
+
env fixture.
|
|
168
|
+
|
|
169
|
+
A vendor that is named by nothing renders a credential form saying nothing about
|
|
170
|
+
what it powers. If you find yourself wanting a key to explain a vendor, you have
|
|
171
|
+
found an unnamed vendor instead.
|
|
172
|
+
|
|
173
|
+
## C. Vendor code outside a manifest
|
|
174
|
+
|
|
175
|
+
You found an endpoint, a token header, a payload shape or a vendor's error string
|
|
176
|
+
in drawbridge-sync, drawbridge-api, drawbridge-webhooks or anywhere else.
|
|
177
|
+
|
|
178
|
+
**It becomes a hook.** Not "later", not "it has no connection", not "it needs a
|
|
179
|
+
dependency the package cannot have".
|
|
180
|
+
|
|
181
|
+
1. **Which manifest spends this vendor?** If a merchant connects it, theirs. If
|
|
182
|
+
Drawbridge pays for it, `drawbridge`. Name it in `vendors` if it is not there.
|
|
183
|
+
2. **Which domain?** Read `HOOKS` in `contract.js` first. If nothing fits, add a
|
|
184
|
+
domain — with the reasoning for why it is not one of the existing ones, and
|
|
185
|
+
slots named for what every vendor of that kind does rather than for what this
|
|
186
|
+
one calls it. Every other manifest then answers the new domain `false` with a
|
|
187
|
+
reason; `build()` will not let you forget one.
|
|
188
|
+
3. **Split vendor from Drawbridge, explicitly.** This is the judgement call and
|
|
189
|
+
it is the whole design. The vendor's facts move; our policy stays. From the
|
|
190
|
+
Bright Data move: the endpoint and the unblocking request moved; the tier
|
|
191
|
+
ladder that chooses between renderers, and the extraction that runs on whatever
|
|
192
|
+
comes back, did not.
|
|
193
|
+
4. **A dependency the package must not carry is not an exemption.** Published
|
|
194
|
+
utils is imported by app-web and share, so it holds no browser, no BullMQ, no
|
|
195
|
+
socket server. The hook answers the part the vendor knows and the shell does
|
|
196
|
+
the rest — `scrape.browser` answers an ENDPOINT and the shell drives it. An
|
|
197
|
+
injected `fetcher` covers plain HTTP.
|
|
198
|
+
5. **Sweep, and pin the sweep.** A test that greps this repo for the vendor's
|
|
199
|
+
endpoint and env names, failing on any that survive. Both
|
|
200
|
+
`connection-slug-neutral.test.js` (api) and `commerce-slug-neutral.test.js`
|
|
201
|
+
(sync) are the pattern, exemptions named with reasons and each one re-checked
|
|
202
|
+
for whether it is still earning its place.
|
|
203
|
+
|
|
204
|
+
## D. Check
|
|
205
|
+
|
|
206
|
+
Same steps read as questions: do the requested scopes cover every hook the
|
|
207
|
+
contract lists, does each `urls.*` entry open, does the vendor file carry its
|
|
208
|
+
api/dashboard/scopes links, **is the vendor named by the connection that spends
|
|
209
|
+
it**, **is any of its wire detail still outside the manifest**, was the copy
|
|
210
|
+
humanized, has it been walked. A gap is a fix, not a note.
|
|
87
211
|
|
|
88
212
|
## Red flags — stop
|
|
89
213
|
|
|
90
214
|
- "Minimum scopes for now" — re-consent for every merchant later.
|
|
91
|
-
- "
|
|
92
|
-
- "
|
|
93
|
-
- "Verified in the header comment" — `review.verified` is the record.
|
|
94
|
-
- "Flip that hook later", "skipped because of the deadline" — nothing is
|
|
95
|
-
skipped. Say the time is short and keep going.
|
|
215
|
+
- "Humanizer if time" — it is a step, not a polish.
|
|
216
|
+
- "Flip that hook later", "skipped because of the deadline" — nothing is skipped.
|
|
96
217
|
- "Keys as an interim, OAuth after the demo" — the OAuth build is the work.
|
|
218
|
+
- "I'll add `review.verified`" — the key is gone and `build()` throws on it.
|
|
219
|
+
- "The hook can just write it" — a hook holds no controller. Describe the write.
|
|
220
|
+
- "`false` because nobody has asked for it yet" — `false` is a decision about the
|
|
221
|
+
VENDOR, with a reason.
|
|
222
|
+
- **"This vendor has no connection"** — it has no MERCHANT. Name it on
|
|
223
|
+
`drawbridge`.
|
|
224
|
+
- **"I'll add a key for that"** — what already answers it? Almost always a
|
|
225
|
+
manifest declaring something it already can.
|
|
226
|
+
- **"The credentials moved, that's the migration"** — the calls are the job.
|
|
97
227
|
|
|
98
228
|
## Notes
|
|
99
229
|
|
|
100
230
|
- A fact that must hold across repos goes through `drawbridge-record-contract`.
|
|
101
|
-
- If the vendor's list endpoint filters by name server-side, the field must
|
|
102
|
-
|
|
231
|
+
- If the vendor's list endpoint filters by name server-side, the field must NOT
|
|
232
|
+
declare `search : false`; a test pins that for Attentive.
|
|
233
|
+
- The reference doc's tables are GENERATED from the registry
|
|
234
|
+
(`lib/connections/reference.js`) and a test fails if they drift. Regenerate,
|
|
235
|
+
never hand-edit.
|
|
@@ -519,6 +519,12 @@ api's identity/organization routes); the merged keys keep the old names. Reading
|
|
|
519
519
|
`providerSettings({ slug : 'drawbridge' })` anywhere finds no row — it was split on dev
|
|
520
520
|
2026-09-08 and prod is seeded per-vendor. Detail: `drawbridge-docs/reference/providers.md`.
|
|
521
521
|
|
|
522
|
+
- **The embedded app's install paths ARE `INSTALL_ENDPOINTS`** (utils `lib/connections/contract.js`
|
|
523
|
+
↔ drawbridge-shopify-app `app/drawbridge.server.ts`). The api's `POST /connection/:vendor/*`
|
|
524
|
+
allows exactly those thirteen and 404s anything else; shopify-app does not depend on utils, so
|
|
525
|
+
`app/drawbridge.server.test.ts` carries the list verbatim and fails on drift. Rename one side,
|
|
526
|
+
rename both and the test — a mismatch is an embedded app whose organization list silently 404s.
|
|
527
|
+
|
|
522
528
|
## Import surfaces
|
|
523
529
|
|
|
524
530
|
- Import names against a package's **actual exports** — a missing export resolves to `undefined`
|
|
@@ -527,10 +533,11 @@ api's identity/organization routes); the merged keys keep the old names. Reading
|
|
|
527
533
|
|
|
528
534
|
## Shopify sales channel
|
|
529
535
|
|
|
530
|
-
- `@drawbridge/shopify
|
|
531
|
-
message `'Shopify product not found'`**
|
|
536
|
+
- The Shopify storefront client (`@drawbridge/drawbridge-utils/shopify/storefront`, formerly its
|
|
537
|
+
own `@drawbridge/shopify` package) throws the **exact message `'Shopify product not found'`**
|
|
538
|
+
from `getProduct`/`getProductVariantsPage` when the Storefront API can't see a product
|
|
532
539
|
(unpublished from the Drawbridge channel / deleted); transport and GraphQL failures throw
|
|
533
|
-
`'Shopify storefront error: …'`.
|
|
540
|
+
`'Shopify storefront error: …'`. The Shopify manifest's `commerce.product` hook
|
|
534
541
|
string-matches the not-found message to deactivate the product and send ResourceFeedback
|
|
535
542
|
instead of retrying. Reword the message → unpublished products retry-loop forever and
|
|
536
543
|
merchants get no feedback; drawbridge-api's shopifyProduct route relies on the not-found
|
|
@@ -546,7 +553,7 @@ api's identity/organization routes); the merged keys keep the old names. Reading
|
|
|
546
553
|
- **Feedback must forward the product's own `updatedAt`, never a fresh timestamp.** Shopify
|
|
547
554
|
versions feedback on `productUpdatedAt` and refuses a payload older than the version it holds,
|
|
548
555
|
so stamping `new Date()` records a version ahead of the product and every later send comes back
|
|
549
|
-
`'Feedback for a later version of this resource was already accepted'`.
|
|
556
|
+
`'Feedback for a later version of this resource was already accepted'`. The admin client
|
|
550
557
|
reads it (costing one extra `read_products` query per send) so callers cannot get this wrong.
|
|
551
558
|
- **ResourceFeedback messages are format-validated by Shopify, not by us**: one message, ≤ 100
|
|
552
559
|
characters, leading capital, trailing period, not all caps. A breach fails the whole send.
|
|
@@ -570,7 +577,7 @@ api's identity/organization routes); the merged keys keep the old names. Reading
|
|
|
570
577
|
throttling, or a field missing on the API version land in a **top-level `errors`** — a bare
|
|
571
578
|
string for a 401, an array of objects otherwise — with a null `data`. Reading only `userErrors`
|
|
572
579
|
turns an auth failure into a silent success.
|
|
573
|
-
- `SHOPIFY_REQUIRED_SCOPES` (`@drawbridge/
|
|
580
|
+
- `SHOPIFY_REQUIRED_SCOPES` (`@drawbridge/drawbridge-utils` `lib/shopify/constants.js`) must stay in lockstep with
|
|
574
581
|
`[access_scopes]` in drawbridge-shopify-app's `shopify.app.*.toml` (both files). drawbridge-api
|
|
575
582
|
diffs each store's granted scopes against it to flag "update permissions" on the connections
|
|
576
583
|
page. A scope added to the toml but not the constant silently escapes the check; one added to
|
|
@@ -27,8 +27,9 @@ worked fine.
|
|
|
27
27
|
## Coordinating peer dependencies on bump
|
|
28
28
|
|
|
29
29
|
When bumping any `@drawbridge/*` package, audit every sibling package's
|
|
30
|
-
`peerDependencies` for a stale pin.
|
|
31
|
-
`"@drawbridge/
|
|
30
|
+
`peerDependencies` for a stale pin. Consumer pins are version-exact (e.g.
|
|
31
|
+
`"@drawbridge/drawbridge-utils": "0.0.183"`; components' peer on utils is the
|
|
32
|
+
one range, `>=0.0.118`), so bumping the package in consumer
|
|
32
33
|
apps while leaving a sibling's peer dep on the old version triggers
|
|
33
34
|
`ERESOLVE` at install time.
|
|
34
35
|
|
|
@@ -37,9 +37,32 @@ quietly that dev served stale code the whole time.
|
|
|
37
37
|
- Add it to the release document in `drawbridge-docs/releases/` for the launch it
|
|
38
38
|
belongs to, with the ordering constraint stated: what has to be deployed before
|
|
39
39
|
it can run, and what breaks if it runs early.
|
|
40
|
-
- If the work needs a worker, enqueue the job the live path enqueues,
|
|
41
|
-
same
|
|
42
|
-
|
|
40
|
+
- If the work needs a worker, enqueue the job the live path enqueues, through the
|
|
41
|
+
same helper the live path uses rather than a copy of it, so the payload and the
|
|
42
|
+
job id shape cannot drift.
|
|
43
|
+
|
|
44
|
+
## Do not reach for a coalescing job id
|
|
45
|
+
|
|
46
|
+
A static job id looks like coalescing and is usually suppression. BullMQ drops an
|
|
47
|
+
add whose id still exists, and a queue that retains completed jobs will refuse a
|
|
48
|
+
re-add for as long as it keeps them — a week on the connection queue, a month for
|
|
49
|
+
failed ones. Worse, an id can be reused a layer down: `drawbridge-sync`'s
|
|
50
|
+
`queue/workflow.js` derives each step's job id from the orchestrator job's id, so
|
|
51
|
+
a static orchestrator id produced a static step id even though the orchestrator
|
|
52
|
+
job itself carried `removeOnComplete`.
|
|
53
|
+
|
|
54
|
+
That is not theoretical. It is what made a renamed segment never reach Mailchimp
|
|
55
|
+
or Klaviyo: the first register ran, and every later one for that segment was
|
|
56
|
+
dropped in silence for seven days.
|
|
57
|
+
|
|
58
|
+
**Make the id unique per dispatch and make the work idempotent instead.** A
|
|
59
|
+
register that writes the same row twice costs nothing. A register that is dropped
|
|
60
|
+
loses a change the merchant can see. Where a timestamp is the unique part, add a
|
|
61
|
+
counter too, because a loop dispatches many jobs inside one millisecond.
|
|
62
|
+
|
|
63
|
+
Reserve a static id for the case where running twice is genuinely harmful AND the
|
|
64
|
+
queue clears the id on both outcomes, and say in a comment which of the two you
|
|
65
|
+
are relying on.
|
|
43
66
|
|
|
44
67
|
## Media migrations, the one exception
|
|
45
68
|
|
package/package.json
CHANGED