Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/service-messaging-provenance-anchors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/service-messaging': patch
---

Provenance comments in `service-messaging` were re-anchored

Comment and docblock lines under `src/` that cited tracker numbers which no
longer resolve on GitHub now cite the commit in this repository's history that
decided the matter, and say in their own words what was decided. Comments
only: no type, schema, export, refusal text or runtime behaviour changes.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* tenant-classification contract this file pins (#10740) is unchanged —
* threaded `tenantId`, never `bypassTenantAudit`.
*
* [#11453] `SqlNotificationOutbox.ack` has since made the SAME move for the
* [commit 1a47a5368] `SqlNotificationOutbox.ack` has since made the SAME move for the
* same reason: its new status precondition ("this row must still be
* `in_flight`") is a compare-and-set, and a predicate on the by-id path is
* silently discarded, so it rides `multi: true` too. Its audit op is
Expand Down Expand Up @@ -89,10 +89,10 @@ let driver: SqlDriver;
let warns: Array<{ msg: string; meta: any }>;
/** Every `options` bag that reached `SqlDriver.update` — the `update` op only. */
let driverUpdates: Array<{ object: string; id: unknown; options: any }>;
/** Every `options` bag that reached `SqlDriver.updateMany` — `redeliver`'s op since #11009, the notification `ack`'s since #11453, and the HTTP `ack`'s since #17634. */
/** Every `options` bag that reached `SqlDriver.updateMany` — `redeliver`'s op since #11009, the notification `ack`'s since commit 1a47a5368, and the HTTP `ack`'s since #17634. */
let driverUpdateManys: Array<{ object: string; where: unknown; options: any }>;

/** The audit line for the PREDICATE op — `redeliver`'s write since #11009, the notification `ack`'s since #11453. */
/** The audit line for the PREDICATE op — `redeliver`'s write since #11009, the notification `ack`'s since commit 1a47a5368. */
const auditedUpdateMany = (object: string): boolean =>
warns.some((w) => w.msg.includes(`[tenant-audit] updateMany on tenant-scoped object "${object}"`));

Expand Down Expand Up @@ -226,7 +226,7 @@ describe('ack — the two dispatcher sites are a classified global sweep (update
//
// [#17634] The dispatcher's ack hands the claim credential, so it is a
// compare-and-set on the predicate path and the reading moves to the
// `updateMany` spy — the move the notification ack made in #11453. The
// `updateMany` spy — the move the notification ack made in commit 1a47a5368. The
// claim path writes there too (its reap and its atomic claim), so the
// filter names what an ACK write looks like: a scalar id bound to
// `in_flight` AND to the claiming node. That predicate IS the
Expand Down Expand Up @@ -283,7 +283,7 @@ describe('ack — the two dispatcher sites are a classified global sweep (update
]);
// ② Declared global, for both organizations' rows.
//
// [#11453] The ack's op is `updateMany` now, so the reading moves to
// [commit 1a47a5368] The ack's op is `updateMany` now, so the reading moves to
// that spy. The claim path writes there too (its reap and its atomic
// claim), so the filter names what an ACK write looks like — and that
// predicate is not incidental: `{ id: <scalar>, status: 'in_flight' }`
Expand Down
6 changes: 3 additions & 3 deletions packages/services/service-messaging/src/dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export class NotificationDispatcher {
//
// No partition lock is needed, and none was ever in force: the reap only
// moves rows already past their timeout, a claim only takes `pending`
// rows, and an ack whose claim was reaped matches nothing (#11859) — while
// rows, and an ack whose claim was reaped matches nothing (commit d9cf78eaa) — while
// the per-claim reap, run under partition p's lock, was already rewriting
// rows in every other partition.
//
Expand Down Expand Up @@ -368,7 +368,7 @@ export class NotificationDispatcher {
}

/**
* [#11453] Record one attempt's outcome, tolerating the ONE refusal a
* [commit 1a47a5368] Record one attempt's outcome, tolerating the ONE refusal a
* correct dispatcher can legitimately provoke.
*
* `ack()` now refuses a row that is not `in_flight`, and this loop can meet
Expand All @@ -389,7 +389,7 @@ export class NotificationDispatcher {
*/
private async ackAttempt(row: ClaimedDeliveryRecord, result: AckResult): Promise<void> {
try {
// [#11859] The record is handed back WHOLE: its (claimedBy,
// [commit d9cf78eaa] The record is handed back WHOLE: its (claimedBy,
// claimedAt) pair is the claim credential the store stamped, and
// the ack's compare-and-set binds it — this loop never needs to
// know or repeat its own nodeId.
Expand Down
4 changes: 2 additions & 2 deletions packages/services/service-messaging/src/email-channel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ describe('email channel', () => {
it('END TO END: one emit, one outbox, one dispatcher tick — the row lands `dead`, ⛔ not `success`', async () => {
// The unit assertions above are about a return value; THIS is the
// reading the card is written against — what an operator sees on
// `sys_notification_delivery`. Before #18424 this row read
// `sys_notification_delivery`. Before commit 879b51270 this row read
// `status: 'success'`, which is the silent half of the defect.
const data = fakeData();
const outbox = new MemoryNotificationOutbox(1);
Expand Down Expand Up @@ -580,7 +580,7 @@ describe('email channel', () => {
});
});

// ── #11741 — organization threading. This channel is the producer the
// ── Commit b706af987 — organization threading. This channel is the producer the
// ruling names as HOLDING an organization (`delivery.notification
// .organizationId`, the tenant stamp the outbox snapshots per delivery),
// so it threads that value into the email service's input on BOTH of its
Expand Down
18 changes: 9 additions & 9 deletions packages/services/service-messaging/src/email-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface EmailSenderSurface {
html?: string;
text?: string;
/**
* Structural mirror of `SendEmailInput.organizationId` (#11741) —
* Structural mirror of `SendEmailInput.organizationId` (commit b706af987) —
* the tenant stamp for `sys_email.organization_id`, threaded from
* `delivery.notification.organizationId` when the delivery holds
* one. Optional: an org-less delivery sends without it.
Expand All @@ -56,7 +56,7 @@ export interface EmailSenderSurface {
to: string | string[];
data?: Record<string, unknown>;
locale?: string;
/** Same tenant stamp as on `send` (#11741) — forwarded by the email service into the send it performs. */
/** Same tenant stamp as on `send` (commit b706af987) — forwarded by the email service into the send it performs. */
organizationId?: string;
}): Promise<{ id?: string; status?: string; error?: string } | unknown>;
/**
Expand All @@ -81,7 +81,7 @@ export interface EmailChannelOptions {
* Resolve the email service; `undefined` ⇒ there is no transport, which
* {@link MessagingChannel.isAvailable} reports as
* `transport_not_configured` and {@link MessagingChannel.send} REFUSES with
* the same reason (#18424). ⛔ Not a no-op success: a delivery nothing was
* the same reason (commit 879b51270). ⛔ Not a no-op success: a delivery nothing was
* sent for is never reported as delivered.
*/
getEmail(): EmailSenderSurface | undefined;
Expand Down Expand Up @@ -112,7 +112,7 @@ export interface EmailChannelOptions {

/**
* The ONE token both members of this channel use for "there is no transport"
* (#18424) — the reason `isAvailable()` already returns, reused verbatim so the
* (commit 879b51270) — the reason `isAvailable()` already returns, reused verbatim so the
* refusal `send()` writes onto the delivery row and the suppression fan-out
* records on `sys_notification.suppressed_channels` name the same condition.
*
Expand Down Expand Up @@ -160,7 +160,7 @@ const EMAIL_SHAPE = (s: string): boolean => {
* path) use that one resolution. A producer-set `payload.locale` — the
* pre-ruling single value for the whole notification — is no longer consulted.
*
* Failure is always REPORTED, never absorbed (#18424): no email service ⇒ a
* Failure is always REPORTED, never absorbed (commit 879b51270): no email service ⇒ a
* refusal carrying the declared `transport_not_configured` reason, graded
* `permanent` so the row dead-letters on attempt one; a recipient with no
* resolvable address ⇒ a reported failure. Either way the delivery row shows
Expand Down Expand Up @@ -273,7 +273,7 @@ export function createEmailChannel(opts: EmailChannelOptions): MessagingChannel
async send(ctx: MessagingChannelContext, delivery: Delivery): Promise<SendResult> {
const email = opts.getEmail();
if (!email) {
// [#18424] The SAME condition `isAvailable()` answers above, so
// [commit 879b51270] The SAME condition `isAvailable()` answers above, so
// it gets the same answer — a refusal naming
// `transport_not_configured`, ⛔ never `{ ok: true }`.
//
Expand Down Expand Up @@ -340,7 +340,7 @@ export function createEmailChannel(opts: EmailChannelOptions): MessagingChannel
to: address,
...(data !== undefined ? { data } : {}),
...(templateLocale ? { locale: templateLocale } : {}),
// #11741 — this channel HOLDS the organization (the
// Commit b706af987 — this channel HOLDS the organization (the
// tenant stamp the outbox snapshots per delivery), so
// it threads it for the sys_email.organization_id
// stamp. Absent stays absent — never fabricated.
Expand Down Expand Up @@ -380,7 +380,7 @@ export function createEmailChannel(opts: EmailChannelOptions): MessagingChannel
subject: rendered.subject,
...(rendered.html !== undefined ? { html: rendered.html } : {}),
...(rendered.text !== undefined ? { text: rendered.text } : {}),
// #11741 — same threading as the template arm above.
// Commit b706af987 — same threading as the template arm above.
...(n.organizationId ? { organizationId: n.organizationId } : {}),
});
const id = result?.id;
Expand All @@ -399,7 +399,7 @@ export function createEmailChannel(opts: EmailChannelOptions): MessagingChannel
// are `IEmailService.sendTemplate`'s own error codes plus this
// channel's missing-capability refusal above.
//
// [#18424] `transport_not_configured` joins them, and the grade is
// [commit 879b51270] `transport_not_configured` joins them, and the grade is
// driven rather than assumed: in the shipping composition the same
// condition already terminates a claimed row at once — the mount
// gate unmounts the channel and the dispatcher acks
Expand Down
2 changes: 1 addition & 1 deletion packages/services/service-messaging/src/http-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ export class HttpDispatcher {
* [#17634] Record one attempt's outcome with the claim credential this
* node's `claim()` stamped on the row, tolerating the ONE refusal a correct
* dispatcher can legitimately provoke — `NotificationDispatcher.ackAttempt`'s
* shape (#11453, #11859).
* shape (commits 1a47a5368, d9cf78eaa).
*
* A send slower than `claimTtlMs` lets the visibility-timeout reap return the
* row to `pending`, and another node — or this one, on a later tick —
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* #17634 — an HTTP ack proves OWNERSHIP of the claim it completes, not just
* that a row id exists: a late ack from a claim the visibility-timeout reap
* took back must not overwrite the live re-claim on `sys_http_delivery`. The
* notification outbox closed the same shape in #11859; this file pins the HTTP
* notification outbox closed the same shape in commit d9cf78eaa; this file pins the HTTP
* outbox against the same sequence.
*
* ## The reachable sequence this file replays — for real
Expand Down
4 changes: 2 additions & 2 deletions packages/services/service-messaging/src/http-outbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ export type HttpAckResult = HttpAckSuccess | HttpAckFailure;
* stamps on a row when {@link IHttpOutbox.claim} takes it — handed back to
* {@link IHttpOutbox.ack} so the outcome is written only while that claim still
* holds the row. It is exactly the pair the notification outbox's
* `ClaimedDeliveryRecord` guarantees for `INotificationOutbox.ack` (#11859), with
* `ClaimedDeliveryRecord` guarantees for `INotificationOutbox.ack` (commit d9cf78eaa), with
* the same meaning:
*
* - ownership is proven by ROUND-TRIPPING what `claim()` returned, never by the
Expand Down Expand Up @@ -649,7 +649,7 @@ export interface IHttpOutbox {
*
* [#17634] **Pass `claimed`** — the claim credential on the row {@link claim}
* returned (the row itself will do). With it, `ack` is the ownership-checked
* completion `INotificationOutbox.ack` performs (#11453, #11859):
* completion `INotificationOutbox.ack` performs (commits 1a47a5368, d9cf78eaa):
*
* ⛔ **Precondition: the row MUST still be held by that claim.** Two tests,
* both re-stated IN the conditional write: the row is `in_flight`, AND its
Expand Down
10 changes: 5 additions & 5 deletions packages/services/service-messaging/src/inbox-caller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/**
* Authenticated-caller scoping for the plugin-facing inbox surface
* (ADR-0030 Layer 5) — the write door (#10753) and the read door (#11452).
* (ADR-0030 Layer 5) — the write door (#10753) and the read door (commit 3b5f0360c).
*
* ## The shape this closes
*
Expand All @@ -22,7 +22,7 @@
* permission check sees it either. Unconstrained and undeclared, in both
* directions.
*
* The READ side has the same shape (#11452): `listInbox(userId, opts)` keys
* The READ side has the same shape (closed by commit 3b5f0360c): `listInbox(userId, opts)` keys
* its whole read — inbox rows joined with read-state — on the same free
* parameter, so an in-process caller could read ANY user's inbox titles,
* bodies and read-state. Both doors resolve their recipient here.
Expand Down Expand Up @@ -83,9 +83,9 @@ import type { ExecutionContext } from '@objectstack/spec/kernel';
* `ExecutionContext` the caller was handed, passed through whole.
*
* Passed WHOLE, deliberately: the measured defect family behind
* `assembleExecutionContext` (#6071, #6206, #6551) is "a field exists on
* `ExecutionContext`, one copy carries it, another silently does not". A
* hand-picked `{ userId }` slice here would be one more such copy.
* `assembleExecutionContext` (#6071, #6551, and the share-link envelope trim
* commit 8e13ca876 undid) is "a field exists on `ExecutionContext`, one copy
* carries it, another silently does not". A hand-picked `{ userId }` slice here would be one more such copy.
*/
export type InboxCaller = ExecutionContext;

Expand Down
4 changes: 2 additions & 2 deletions packages/services/service-messaging/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export { CHANNEL_UNAVAILABLE_REASONS } from './channel.js';
export type {
INotificationOutbox,
NotificationDeliveryRecord,
// [#11859] What claim()/claimDigest() hand out and ack() takes back — the
// [commit d9cf78eaa] What claim()/claimDigest() hand out and ack() takes back — the
// record carrying the claim credential the compare-and-set binds.
ClaimedDeliveryRecord,
DeliveryStatus,
Expand All @@ -116,7 +116,7 @@ export type {
ReapOptions,
AckResult,
} from './outbox.js';
// [#11453] `ack()`'s status precondition refuses with this, so a caller that
// [commit 1a47a5368] `ack()`'s status precondition refuses with this, so a caller that
// wants to distinguish "I lost the claim" from a transport fault can catch it.
export { NotificationAckError } from './outbox.js';
export { SqlNotificationOutbox, DELIVERY_OBJECT } from './sql-outbox.js';
Expand Down
10 changes: 5 additions & 5 deletions packages/services/service-messaging/src/memory-outbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class MemoryNotificationOutbox implements INotificationOutbox {
r.claimedBy = opts.nodeId;
r.claimedAt = now;
r.updatedAt = now;
// [#11859] The copy handed out carries the claim credential the
// [commit d9cf78eaa] The copy handed out carries the claim credential the
// two lines above just stamped — the record IS the credential.
out.push({ ...r, claimedBy: opts.nodeId, claimedAt: now });
}
Expand Down Expand Up @@ -117,7 +117,7 @@ export class MemoryNotificationOutbox implements INotificationOutbox {

async ack(claimed: ClaimedDeliveryRecord, result: AckResult): Promise<void> {
const id = claimed.id;
// [#11859] The runtime half of the ClaimedDeliveryRecord contract, for
// [commit d9cf78eaa] The runtime half of the ClaimedDeliveryRecord contract, for
// JS callers and casts: a record with no claim credential was not
// handed out by claim()/claimDigest() and is refused before any read.
if (typeof claimed.claimedBy !== 'string' || typeof claimed.claimedAt !== 'number') {
Expand All @@ -128,7 +128,7 @@ export class MemoryNotificationOutbox implements INotificationOutbox {
// to corrupt and no claim to lose. Unchanged, and declared on the
// interface so the two backends agree about it.
if (!r) return;
// [#11453] The status precondition. `ack` completes a delivery this
// [commit 1a47a5368] The status precondition. `ack` completes a delivery this
// caller CLAIMED; an unclaimed `pending` row (the ack-as-cancel trap)
// or an already-terminal one is refused, and nothing below runs — so a
// refused ack leaves status, attempts and error exactly as they were.
Expand All @@ -141,7 +141,7 @@ export class MemoryNotificationOutbox implements INotificationOutbox {
'DELIVERY_NOT_ELIGIBLE',
);
}
// [#11859] Ownership: the row is claimed, but not by the claim this
// [commit d9cf78eaa] Ownership: the row is claimed, but not by the claim this
// record came from — it was reaped and re-claimed while the send ran
// (possibly by this same store handing it to this same node again: the
// credential is the PAIR, so a later claim's `claimedAt` refuses the
Expand All @@ -155,7 +155,7 @@ export class MemoryNotificationOutbox implements INotificationOutbox {
}
const now = this.clock();
// Reached only for a genuinely claimed row, so this counts a real
// dispatch attempt and nothing else (#11453).
// dispatch attempt and nothing else (commit 1a47a5368).
r.attempts += 1;
r.lastAttemptedAt = now;
r.claimedBy = undefined;
Expand Down
Loading
Loading