Skip to content

Commit aa0ccdc

Browse files
KrisBraunclaude
andauthored
feat(twister): add NewLink.updateOnly to update an existing thread only (#329)
Adds an opt-in `updateOnly` flag to `NewLink`. When set, `saveLink()` / `saveLinks()` apply the link only as an update to a thread that already exists for its `source`/`sources`, and never create a new one; when no matching thread is found the link is skipped and `saveLink()` returns null. Use it for signals that are only meaningful as an update to an item the user already has. The Google calendar connector now sets it on the note it emits for a cancelled occurrence of a recurring event: previously, when the recurring series itself had not been imported yet (e.g. a newly split series whose confirmed master had not synced, so only its cancelled future occurrences arrived), that note materialized a titleless, scheduleless "cancelled" thread for an event the user never saw. With updateOnly the cancellation is only ever added to the event's existing thread. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c517c4e commit aa0ccdc

4 files changed

Lines changed: 30 additions & 0 deletions

File tree

‎.changeset/link-update-only.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@plotday/twister": minor
3+
---
4+
5+
Added: `NewLink.updateOnly` — save a link only as an update to a thread that already exists for its `source`/`sources`, never creating a new one. When no matching thread is found the link is skipped and `saveLink()` returns `null`. Use it for signals that only make sense as an update to an item the user already has (e.g. a calendar cancellation), so they never materialize a standalone thread for something that was never imported.

‎connectors/google/src/calendar/sync.test.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1061,6 +1061,10 @@ describe("prepareEventInstanceFn — stale occurrence cancellations", () => {
10611061
(n as { key?: string }).key?.startsWith("cancellation-")
10621062
)
10631063
).toBe(true);
1064+
// The cancellation link is emitted as update-only: the platform applies it
1065+
// only if the recurring master's thread already exists, and skips it (no
1066+
// phantom "cancelled" thread) when the master was never imported.
1067+
expect((saved[0] as { updateOnly?: boolean }).updateOnly).toBe(true);
10641068
});
10651069
});
10661070

‎connectors/google/src/calendar/sync.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,14 @@ export async function prepareEventInstanceFn(
533533
meta: { syncProvider: "google", syncableId: calendarId },
534534
scheduleOccurrences: [cancelledOccurrence],
535535
notes: [cancelNote],
536+
// A cancelled occurrence is only meaningful as an annotation on the
537+
// recurring master the user already has. When Plot never imported that
538+
// master (e.g. a newly split series whose confirmed master hasn't synced
539+
// yet, so only its cancelled future occurrences arrived), creating a
540+
// thread here would surface a titleless, scheduleless "cancelled" thread
541+
// for an event the user never saw. `updateOnly` makes the platform apply
542+
// this only if the master's thread already exists, and skip it otherwise.
543+
updateOnly: true,
536544
// Don't flip the thread unread when the user cancelled the occurrence
537545
// themselves. Cancelled instances are usually sparse (no organizer /
538546
// attendees), so this rarely fires — best-effort, never over-suppresses.

‎twister/src/plot.ts‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,6 +1366,19 @@ export type NewLink = Partial<
13661366
* {@link Connector.autoThreading}.
13671367
*/
13681368
autoThread?: AutoThreadConfig | null;
1369+
/**
1370+
* Apply this link only as an UPDATE to a thread that already exists for its
1371+
* `source`/`sources`; never CREATE a new thread. When no matching thread is
1372+
* found, `saveLink()`/`saveLinks()` skip the link entirely and return
1373+
* `null` for it.
1374+
*
1375+
* Use for signals that are only meaningful as an update to an item the user
1376+
* already has — most notably a calendar cancellation, which should annotate
1377+
* an event that was imported, never materialize a standalone "cancelled"
1378+
* thread for an occurrence the user never saw. Leave undefined/false for
1379+
* normal links, which create the thread when absent.
1380+
*/
1381+
updateOnly?: boolean;
13691382
};
13701383

13711384
/**

0 commit comments

Comments
 (0)