@assembly-js/app-foundation 0.0.37 → 0.0.38
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/CLAUDE.md
CHANGED
|
@@ -1197,7 +1197,7 @@ Use `AssemblyBridge.header.*` for breadcrumbs, CTAs, and actions menu. See `.cla
|
|
|
1197
1197
|
- ❌ Don't hardcode mutation error messages like "Failed to save preference". Read `error` from the response body and surface it to the user.
|
|
1198
1198
|
- ❌ Don't `throw new Error` with a sentence the user should read from a server action. Production redacts that throw, so the toast shows a generic React error and the response is HTTP 500. Return `{ ok: false, message }` and toast `message`. Let `TokenInvalidError` and unexpected failures throw. See `assembly-action-results`.
|
|
1199
1199
|
- ❌ Don't `Promise.all` a list-sized write fan-out (one `createNotification` / `markNotificationRead` / update per recipient or record). Writes cap at 5 concurrent per token; firing a whole list 429s and silently drops the overflow. Use `sendThrottled` from `@assembly-js/app-foundation/server`. A fixed handful of calls is the only exception. See "Write rate limit" above.
|
|
1200
|
-
- ❌ Don't re-notify a still-true condition from a cron.
|
|
1200
|
+
- ❌ Don't re-notify a still-true condition from a cron. Mark a recipient sent only after `createNotification` succeeds, and clear that record when the condition ends unless the alert is once per billing period; keep those successful outcomes until the period resets, even if usage dips below the threshold. See "Recurring conditions" in `assembly-notifications`.
|
|
1201
1201
|
- ❌ Don't use title case — see "Labels, copy, and dropdowns" above.
|
|
1202
1202
|
- ❌ Don't render raw IDs or pass a raw ID string as `value` on `ComboboxItem` — see "Select and Combobox — base-ui differences" above.
|
|
1203
1203
|
- ❌ Don't write a scalar string to a custom field that has options. **All option-bearing custom fields are `multiSelect` in Assembly's schema** — even ones that LOOK like single-select dropdowns. The value must always be an array of option keys: `{ favorite_pizza: ['pepperoni'] }`, never `{ favorite_pizza: 'pepperoni' }`. A scalar string for a multiSelect causes a 400 with `invalid custom field option <value> provided`.
|
package/package.json
CHANGED
|
@@ -10,7 +10,9 @@ description: Use when the app must notify internal users or clients, or needs pe
|
|
|
10
10
|
|
|
11
11
|
## Mental model
|
|
12
12
|
|
|
13
|
-
Send a notification when something happens that the recipient should know about
|
|
13
|
+
Send a notification when something happens that the recipient should know about — action required (review, approve, respond) or awareness (status changes, completions, new messages). Skip routine reads, navigation, trivial state changes, and routine confirmations of the recipient's own actions.
|
|
14
|
+
|
|
15
|
+
For a required condition alert (a retainer reaching 90%, going over its allowance, or a budget crossing its limit), the audience is everyone specified by the requirement who has access, including the user whose action crossed the threshold. A sole admin logging usage must still receive the staff alert. Choose recipients by access; use the actor as sender without automatically removing them from the audience.
|
|
14
16
|
|
|
15
17
|
Every notification is three decisions: **audience** (internal users and/or clients), **channel** (in-product, email, or both), and **context on arrival** (what the recipient sees and can do). Get those right; the rest is mechanics.
|
|
16
18
|
|
|
@@ -338,17 +340,11 @@ Option 2 is cheaper (no list call) and more reliable. Pick option 1 when you don
|
|
|
338
340
|
|
|
339
341
|
A cron that scans for a condition still being true — unassigned for a day, overdue, no reply — selects the same rows on every tick. `createNotification` does not remember a previous send, so each tick creates another copy for every recipient. `sendThrottled` only spaces that flood out; it does not stop it.
|
|
340
342
|
|
|
341
|
-
|
|
343
|
+
Track each condition occurrence in the app's own table, keyed by subject and condition (include the billing period for a recurring allowance). Resolve the eligible audience before claiming the occurrence: an empty audience creates no sent marker.
|
|
342
344
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
if (await alreadyAlerted(thread.id, 'stale-unassigned')) continue;
|
|
347
|
-
await markAlerted(thread.id, 'stale-unassigned');
|
|
348
|
-
await sendThrottled(recipients, (u) =>
|
|
349
|
-
assembly.createNotification({ requestBody: { recipientInternalUserId: u.id, /* ... */ } }),
|
|
350
|
-
);
|
|
351
|
-
```
|
|
345
|
+
A concurrency claim reserves work; it is not proof of delivery. Keep pending claims retryable after a failure or an expired lease, and mark a recipient sent only after their `createNotification` succeeds. On partial failure, retry the failed recipients and skip those already sent. If there is only one recipient, track that outcome on the occurrence itself. Clear the occurrence when the condition ends if recurrence should alert again. For once-per-period threshold alerts, keep successful outcomes through dips below the threshold and use a new occurrence when the period resets.
|
|
346
|
+
|
|
347
|
+
A permanent marker written before fan-out can consume an alert without sending anything. Marking the whole occurrence sent after `sendThrottled` is also insufficient: it returns per-recipient outcomes, so check failures before declaring delivery complete.
|
|
352
348
|
|
|
353
349
|
Do not use `listNotifications` to decide. A sweep will not page the whole center, and a notification the recipient already read still has to count.
|
|
354
350
|
|
|
@@ -461,7 +457,7 @@ Each client gets their own badge and clears it independently. `recipientCompanyI
|
|
|
461
457
|
## What NOT to do
|
|
462
458
|
|
|
463
459
|
- Do not `Promise.all` a recipient-list fan-out. Writes cap at 5 concurrent; firing a whole list at once 429s and drops notifications. Use `sendThrottled` (see "Throttling fan-outs"). A fixed handful of calls is the only exception.
|
|
464
|
-
- Do not re-send a
|
|
460
|
+
- Do not re-send a condition alert to recipients already notified for that occurrence. Keep failed sends retryable and track successful outcomes (see "Recurring conditions").
|
|
465
461
|
- Do not use `recipientId` — deprecated. Use `recipientClientId` or `recipientInternalUserId`.
|
|
466
462
|
- Do not pass `recipientCompanyId` alone as a recipient. The API rejects it. To notify a company, fan out across its clients (see "Notifying a company").
|
|
467
463
|
- Do not omit the company id when a multi-company client is involved. Notifying a multi-company client needs `recipientCompanyId` (`400 "company ID is required when notifying a client"`); a multi-company client *sending* to an internal user needs `senderCompanyId` (`400 "sender company ID is invalid based on sender"`). Single-company members are auto-inferred. See "Company scoping, by recipient type."
|
|
@@ -144,7 +144,7 @@ Rules that fall out of this:
|
|
|
144
144
|
|
|
145
145
|
- **`cronRoute` is not optional.** The route is a public URL; without the wrapper anyone who guesses it can trigger the app's scheduled work under the app's own credentials. Never hand-roll the header check.
|
|
146
146
|
- **Use `getOfflineAssembly()`** for every platform call — there is no viewer and no cookie. `getSessionIdentity()` is `null` here, so a notification's `senderId` must be chosen explicitly (a system/admin internal user); see `assembly-notifications`.
|
|
147
|
-
- **A scan that notifies must remember what it already sent.** Selecting every row that still matches (unassigned, overdue, unanswered) and calling `createNotification` notifies that same set again on the next tick.
|
|
147
|
+
- **A scan that notifies must remember what it already sent.** Selecting every row that still matches (unassigned, overdue, unanswered) and calling `createNotification` notifies that same set again on the next tick. Mark a recipient sent only after `createNotification` succeeds, and clear that record when the condition ends unless the alert is once per billing period; keep those successful outcomes until the period resets, even if usage dips below the threshold. See `assembly-notifications` ("Recurring conditions").
|
|
148
148
|
- **A missed tick is harmless.** Vercel does not retry, but the row stays due until it is marked, so the next tick picks it up. Never mark a row done before its work succeeded.
|
|
149
149
|
- **A stuck row frees itself.** If Vercel stops the function mid-run, the catch never runs and the row stays `running`; the stale-claim clause lets the next tick reclaim it after 15 minutes. Keep `claimedAt` and that clause — without them one hang parks a row forever.
|
|
150
150
|
- **Never compute the next run by adding minutes to `now`.** Use `nextRunAfter`: it anchors to the scheduled time, skips missed occurrences, and keeps daily cadences on their local wall-clock time across daylight-saving changes.
|