فهرست مستندات

اعلان‌های پلتفرم#

رجیستری مالک یک projection ماندگار News است. منبع یک Occurrence تغییرناپذیر را گزارش می‌کند؛ رجیستری آن را روی Subject جاری که شخص می‌تواند به آن برگردد reduce می‌کند و وضعیت seen/dismissed متعلق به گیرنده است. Desktop Shell مرکز اعلان کامل حساب را نمایش می‌دهد. Widget داخل Environment فقط projection همان Environment را می‌خواند.

تماس ورودی یک ردیف News نیست و push خارجی هم Inbox دوم نیست. ADR-0131 وضعیت زندهٔ CallAlert و Delivery به‌ازای هر دستگاه را جدا نگه می‌دارد. APIهای عمومی این صفحه فعلاً mobile/web push، ایمیل یا پیامک ارائه نمی‌کنند.

کجا پیاده شده است#

  • packages/contracts/src/notifications.ts — policy بازبینی‌شدهٔ event، Occurrence، acknowledgement، Inbox، treatment و قرارداد publish قدیمی.
  • services/registry/src/notifications/subject-routes.ts — signal اپِ باز، workload تفویض‌شده و acknowledgement دقیق Subject.
  • services/registry/src/repo/sql/notification-subjects.ts — reducer اتمیک Occurrence به Subject، receipt رخداد و watermarkهای acknowledgement.
  • services/registry/src/notifications/routes.ts — خواندن Environment و مسیرهای additive قدیمی.
  • services/registry/src/notifications/device-routes.ts — Center کامل حساب روی device plane، read، dismiss و credential کانال کنترل حساب.
  • services/registry/src/notifications/treatment.ts — تصمیم گیرنده میان announce | file | mute.
  • services/registry/src/notifications/retention.ts — پاک‌سازی محدود ردیف‌های working منقضی‌شده.

«منبع» App یا Environment است؛ «مبدأ» HTTP origin است. جدول واژگان در docs/product/glossary.md قرار دارد.

چهار مفهوم با چهار طول عمر#

مفهوممعنیطول عمر
Occurrenceیک واقعیت تغییرناپذیر منبع با eventId پایدار در محدودهٔ همان منبعreceipt فقط برای idempotency retry نگه داشته می‌شود
Subjectردیف جاری Center برای یک گفت‌وگو، سند، job، دعوت یا رخداد مستقلprojection کاری، معمولاً تا ۳۰ روز
Attentionوضعیت ماندگار گیرنده برای revision جاری Subject که هنوز رسیدگی نشدهتا seen، dismiss، resolve یا expiry
Pulseدرخواست نمایش همین حالای یک episode واجد شرایط Attentionفقط زنده؛ هرگز از list یا reconnect بازسازی نمی‌شود

این یک reducer کوچک در برابر platform_notifications است، نه event store عمومی. history پیام، log job، history تماس و audit همچنان متعلق به محصول منبع است.

policy رخداد همراه revision تغییرناپذیر بازبینی می‌شود#

نویسندهٔ Occurrence جدید باید روی declaration دقیق رخداد، policy بازبینی‌شده داشته باشد. هیچ signal نمی‌تواند آن را تغییر دهد:

{
  "notifications": {
    "eventTypes": [
      {
        "type": "document.changed",
        "label": "Document changed",
        "policy": {
          "family": "generic",
          "subject": {
            "mode": "keyed",
            "revision": "source_monotonic"
          },
          "effects": ["raise", "refresh", "resolve"],
          "preferenceKey": "document.changed",
          "pulse": "none",
          "presentation": { "kind": "source" }
        }
      }
    ],
    "backgroundDelivery": "none"
  }
}

معنای فیلدهای بازبینی‌شده:

  • family خانوادهٔ بستهٔ نمایش پلتفرم است. در قرارداد جدید جای category انتخابی caller را به‌عنوان policy می‌گیرد؛ کلید صدا یا urgency نیست.
  • subject.mode برای یک موضوع پایدار دامنه keyed و برای یک ردیف مستقل به‌ازای هر event برابر per_occurrence است. revision حالت keyed از دامنهٔ authoritative منبع می‌آید، نه از ساعت.
  • effects transitionهای مجاز raise، refresh و resolve را محدود می‌کند. حالت per-occurrence فقط raise دارد.
  • preferenceKey هویت پایدار ترجیح در سطح event است.
  • pulse یکی از new_attention | none است؛ متن منبع باید none باشد.
  • presentation یا محتوای محدود منبع است یا template نام‌دار و بازبینی‌شدهٔ پلتفرم.

platform_template: message_waiting فقط به family message تعلق دارد و فقط برای revision first-party که خود پلتفرم بازبینی کرده پذیرفته می‌شود. منبع event و در صورت نیاز actorSub جفتی App را می‌دهد؛ رجیستری عنوان را از roster خودِ گیرنده می‌سازد، اگر نام مجاز پیدا نشد متن عمومی فارسی می‌گذارد و action معنایی open_app را خودش می‌سازد. منبع برای این template عنوان، body، preview، tone یا category نمی‌فرستد.

هر رخداد message خودکار صدای پلتفرم نمی‌گیرد. متن منبع، حتی برای App first-party، به‌طور پیش‌فرض سقف file دارد. template بازبینی‌شده و نوشته‌شدهٔ پلتفرم می‌تواند پیش‌فرض announce داشته باشد؛ گیرنده همچنان می‌تواند آن را پایین بیاورد و mute منبع همیشه floor است.

Signal کردن Occurrence از App باز#

App حساب‌دار با قابلیت platform.notifications.publish.self@1 می‌فرستد:

POST /v1/apps/notifications/signal
Authorization: Bearer <app-session>
Content-Type: application/json

SDK عمومی این route را با appClient.signalNotificationOccurrence(occurrence) می‌پوشاند.

نمونهٔ Subject کلیددار با متن منبع:

{
  "eventId": "edit_01J9Y7K6",
  "eventType": "document.changed",
  "occurredAt": "2026-09-04T10:42:17.000Z",
  "effect": "raise",
  "subjectKey": "document:doc_42",
  "revision": 81,
  "content": {
    "kind": "source",
    "title": "Document updated",
    "body": "The latest changes are ready.",
    "action": { "kind": "open_app" }
  }
}

برای template بازبینی‌شدهٔ پیام، content چنین است:

{ "kind": "platform_template", "actorSub": "aact_..." }

App session گیرنده، revision منبع و scope حساب یا Environment جاری را تعیین می‌کند. caller هیچ raw account subject یا artwork mutable منبعی نمی‌فرستد. پاسخ 202 { "accepted": true } است و treatment یا وضعیت گیرنده را افشا نمی‌کند.

occurredAt زمان تغییرناپذیر رخداد در منبع است و رجیستری زمان دریافت خودش را جداگانه ثبت می‌کند. این route هم‌حساب رخداد قدیمی‌تر از ۳۰ روز یا بیش از پنج دقیقه جلوتر از ساعت رجیستری را رد می‌کند؛ route تفویض‌شده پاسخ accepted-only را حفظ می‌کند و همان مورد را no-op می‌گیرد. retry باید eventId و occurredAt اولیه را بدون تغییر نگه دارد.

در policy نوع keyed، subjectKey و revision هر دو اجباری‌اند. در per_occurrence هر دو حذف می‌شوند و رجیستری Subject را از eventId می‌سازد. refresh و resolve همیشه Subject کلیددار می‌خواهند. اگر یک شناسهٔ دامنه میان چند گیرنده مشترک است، منبع باید برای هر گیرنده یک Subject key پایدار، opaque و recipient-scoped بسازد؛ شناسهٔ خام گفت‌وگو، سند یا job مشترک نباید به رجیستری برسد.

Effectنتیجه برای revision تازه‌تر
raiseAttention را می‌سازد یا دوباره باز می‌کند و ممکن است یک Pulse بازبینی‌شده بسازد
refreshpresentation جاری را بدون بازکردن Attention مصرف‌شده به‌روز می‌کند
resolveSubject فعال متعلق به منبع را می‌بندد، بدون اینکه ادعا کند گیرنده آن را خوانده

منبع در retry باید دقیقاً همان eventId را تکرار کند. retry دقیق، revision قدیمی یا revision پوشش‌داده‌شده با watermark همگی no-op پذیرفته‌شده‌اند: seen را جابه‌جا نمی‌کنند، expiry را جلو نمی‌برند، version را زیاد نمی‌کنند و Pulse یا invalidation نمی‌فرستند.

Signal از بک‌اند App#

ارسال تفویض‌شدهٔ پس‌زمینه با اتصال صریح از App session احرازشده آغاز می‌شود:

const delegation = await appClient.createNotificationDelegation();
await sendOnceToAppBackend({
  delegationToken: delegation.delegationToken,
  expiresAt: delegation.expiresAt,
});

revision باید backgroundDelivery: "delegated" داشته باشد و هر دو قابلیت platform.notifications.publish.self@1 و platform.notifications.connect.background.self@1 را درخواست کند. token را روی کانال HTTPS احرازشدهٔ App منتقل کنید، مانند credential نگه دارید، log نکنید، rotate کنید و هنگام disconnect revoke کنید.

Backend آن delegation opaque را با Project token تاریخ‌دار دارای scope notifications:publish ترکیب می‌کند:

import { createAppNotificationWorkloadClient } from "@al-amr/backend";

const notifications = createAppNotificationWorkloadClient({
  issuer: process.env.AL_AMR_REGISTRY_URL!,
  projectToken: process.env.AL_AMR_PROJECT_TOKEN!,
});

await notifications.signal(storedDelegationToken, occurrence);

این متد POST /v1/workloads/apps/notifications/signal را فراخوانی می‌کند. رجیستری می‌تواند credential workload نامعتبر، scope کم یا request بدشکل متعلق به caller را رد کند. پس از پذیرش این مرز بیرونی، همهٔ outcomeهای وابسته به گیرنده همان 202 { "accepted": true } هستند: delegation گم‌شده/revoked، رضایت قدیمی، revision عوض‌شده و reduction موفق از هم قابل تشخیص نیستند. هیچ count، treatment، read state یا وجود گیرنده برنمی‌گردد.

یک outbox پایدار را در همان transaction نوشتن دامنه commit کنید و همان event identity را تا acceptance retry کنید. acceptance یعنی handoff امن انجام شده؛ یعنی نه اینکه شخص کارت را دیده یا صدا را شنیده است.

فقط محتوایی را acknowledge کنید که App واقعاً نمایش داده#

وقتی App محتوای authoritative منبع را تا revision مشخص نشان داده، watermark Subject را جلو می‌برد:

await appClient.acknowledgeNotificationSubjects({
  acknowledgements: [
    {
      eventType: "document.changed",
      subjectKey: "document:doc_42",
      throughRevision: 81,
    },
  ],
});

مسیر HTTP برابر POST /v1/apps/notifications/acknowledge است و request دقیقاً ۱ تا ۱۰۰ acknowledgement یکتا دارد. source و recipient از App session می‌آیند. رجیستری بیشینهٔ throughRevision را حتی پیش از رسیدن Subject یا Occurrence تاخیردار نگه می‌دارد؛ پس delivery دیررس کار دیده‌شده را زنده نمی‌کند. acknowledgement قدیمی هرگز raise تازه‌تر را مصرف نمی‌کند.

پاسخ فقط 202 { "accepted": true } است. باز شدن App، کلیک روی chrome Shell یا موفقیت AppHost acknowledgement نیست؛ باید product UI واقعاً محتوا را تا revision اعلام‌شده نمایش داده باشد.

فعال‌کردن یک ردیف Center از میزبان مورداعتماد Desktop می‌خواهد مقصد معنایی بستهٔ آن را اجرا کند. اجرای دقیق فقط جایی تضمین می‌شود که Desktop یا AppHost اختیار همان نوع مقصد را داشته باشد. مقصد پشتیبانی‌نشده یک نتیجهٔ صریح می‌دهد و Attention را نگه می‌دارد؛ پذیرفتن launch هرگز به‌جای presentation تأییدشده توسط منبع حساب نمی‌شود.

Shell Center و Environment Widget دو خوانندهٔ متفاوت‌اند#

Desktop Main مورداعتماد از device plane استفاده می‌کند:

  • GET /v1/account/notifications?limit=…&cursor=… — Center کامل حساب؛
  • POST /v1/account/notifications/read — seen کردن ردیف‌های مشخص؛
  • POST /v1/account/notifications/dismiss — برداشتن ردیف‌های مشخص از feed کاری.

Runtime محیط مسیرهای موازی /v1/runtime/notifications، /read و /dismiss را دارد، اما list فقط به Environment جاری محدود است. title، body، source، Subject key و CallAlert کامل حساب فقط به‌دلیل حضور همان شخص وارد Environment نمی‌شوند.

list از keyset pagination با cursor opaque استفاده می‌کند:

{
  "notifications": [],
  "unseenCount": 0,
  "attentionCount": 0,
  "unreadCount": 0,
  "nextCursor": "opaque-if-more"
}

unseenCount همهٔ Subjectهای فعال دیده‌نشده، از جمله mute را می‌شمارد. attentionCount، mute را حذف می‌کند و badge Shell و attention محیط را می‌سازد. unreadCount برای Widget تغییرناپذیر alias قدیمی attentionCount است تا mute به badge تبدیل نشود. countها query مستقل کل feed هستند و با اندازهٔ page محدود نمی‌شوند.

ردیف V1 می‌تواند projectionVersion، attentionVersion و subjectRevision را به schema tolerant Inbox اضافه کند. echo سخت‌گیرانهٔ publish قدیمی برای bundleهای App تغییرناپذیر دست‌نخورده می‌ماند.

actionهای V1 Center همان platform version رندرشده را می‌فرستند:

{
  "notificationIds": ["ntf_..."],
  "projectionVersions": { "ntf_...": 4 }
}

رجیستری فقط projection مطابق را به‌روز می‌کند؛ read یا dismiss قدیمی revision تازه‌تر Subject را مصرف نمی‌کند. map اختیاری raisedAt fallback timestamp برای readerهای immutable Environment است.

dismiss تصمیم مصرف‌شدنی گیرنده دربارهٔ Subject جاری است، نه ترجیح ماندگار منبع. Attention همان episode را مصرف و ردیف را از Center معمولی حذف می‌کند. raise جدیدتر می‌تواند آن را باز کند؛ refresh نمی‌تواند. ردیف dismissed یا expired با retention محدود پاک می‌شود و در feed عادی برنمی‌گردد.

Realtime hint بدون محتواست؛ Pulse reconciliation نیست#

notification_created و account_inbox_changed هیچ title، source، family، Subject، count یا action ندارند. فقط خواندن مجاز را schedule می‌کنند. Occurrence تکراری hint ندارد. client بعد از reconnect و روی clock آهسته هم reconcile می‌کند.

اولین snapshot authoritative پس از boot، sign-in یا reconnect baseline ساکت است. list، pagination، poll و جایگزینی cache قدیمی هرگز از diff ردیف‌ها Pulse نمی‌سازند. Pulse اتمیک همراه episode تازهٔ Attention تصمیم گرفته می‌شود و فقط درخواست delivery زنده است.

attentionVersion اولین projection پلتفرم در نوبت جاری Attention است. پیام‌های بعدیِ دیده‌نشده و refresh، مقدار projectionVersion را جلو می‌برند ولی این هویت را حفظ می‌کنند. projectionVersion در Pulse زنده با همین attentionVersion تطبیق می‌یابد؛ بنابراین رسیدن پیام دوم پیش از خواندن Pulse اول، تنها اعلان این نوبت را حذف نمی‌کند. بازشدن نوبت تازه هویت جدید می‌گیرد و Pulse قدیمی آن را اعلام نمی‌کند. برای projection قدیمیِ بدون این فیلد، تطبیق دقیق projectionVersion حفظ می‌شود. migration مقدار فعلی را ساکت backfill می‌کند و Pulseهای گذشته را بازسازی نمی‌کند.

Shell مالک کارت transient حساب، badge، policy OS notice و صدای attention پلتفرم است. Widget مالک chrome یا صدای حساب نیست. فقط attention: "none" | "waiting" | "urgent" را بدون count، family، source، title یا Subject به Environment گزارش می‌کند.

یک arbiter متعلق به Main هر هویت Pulse را فقط یک‌بار مصرف می‌کند. وقتی پنجره در پس‌زمینه است OS notice صریحاً silent است، چون همان arbiter tone را پخش کرده؛ در foreground نیز همان Pulse یک tone دارد و کارت Shell جای OS notice را می‌گیرد. با فعال‌شدن producer عمومی تماس در ADR-0133، lease تماس ورودی بر Pulse اولویت دارد و Pulse مغلوب تماس بعداً replay نمی‌شود. Main پایان تماس، انقضا و سکوت محلی را از همان lease مدیریت می‌کند.

Treatment و ترجیح‌ها#

هر Subject یک treatment دارد که هنگام reduction revision تازه حل می‌شود:

Treatmentنتیجهٔ ماندگارامکان Pulse زنده
announceSubject قابل‌دیدن و Attentionفقط زیر policy بازبینی‌شدهٔ new_attention
fileSubject قابل‌دیدن و Attentionندارد
muteSubject قابل‌دیدن و شاید unseen؛ بدون badge یا attention محیطهرگز

ترجیح‌ها تصمیم گیرنده روی origin رجیستری‌اند: per-source و در صورت نیاز per preferenceKey بازبینی‌شده. mutedUntil سکوت زمان‌دار است. mute سطح source کفی است که event تازه نمی‌تواند دور بزند. عوض شدن preference، episode قبلاً reduceشده را replay، retract یا reclassify نمی‌کند.

مسیرهای additive قدیمی#

قرارداد publish اولیه برای callerهای immutable باقی می‌ماند:

  • Environment فعال: POST /v1/runtime/notifications/publish؛
  • App باز و environment-hosted: POST /v1/apps/notifications/publish؛
  • workload اپ: POST /v1/workloads/apps/notifications/publish؛
  • workload محیط: POST /v1/workloads/environments/notifications/publish.

payload قدیمی category، title، body/action معنایی اختیاری، dedupeKey و TTL دارد. category هیچ اختیار family، صدا یا Pulse تازه‌ای نمی‌دهد. این مسیرها projection قدیمی را مستقیم می‌نویسند و ordering یا watermark V1 نمی‌گیرند. POST /v1/apps/notifications/read فقط برای Appهای immutable به‌عنوان acknowledgement source-wide باقی مانده؛ کد first-party جدید از Subject دقیق استفاده می‌کند.

platform.notify.companion@1 مسیر محدود و platform-authored اعلان companion است و fan-out رضایت‌گرفتهٔ Environment همچنان accepted-only با handoff ماندگار می‌ماند. هیچ‌کدام متن دلخواه، raw account subject یا نتیجهٔ قابل‌مشاهدهٔ وابسته به recipient را به caller نمی‌دهند. مرز اختیار در ADR-0071، ADR-0087 و ADR-0109 آمده است.

routeهای App V1 به معنی وجود occurrence signal برای Environment نیستند. frontend و backend محیط تا اضافه‌شدن API صریح و بازبینی‌شده برای همان authority plane، مسیر publish Runtime و Grant کوتاه‌عمر exact-endpoint فعلی را نگه می‌دارند.

نشان منبع، live alert و delivery خارجی#

ردیف Center از artwork revision مدیریت‌شده توسط رجیستری استفاده می‌کند و هرگز publisher URL را fetch نمی‌کند. descriptor رسانه shape: "icon" | "cover" دارد، چون دو ترکیب بازبینی‌شده قابل‌تعویض نیستند.

CallAlert زنده حقیقت backend با deadline است، نه Notification Subject، unread count یا Pulse. ADR-0133 این مسیر account/device را با سرویس عمومی تماس پلتفرم و App مستقل تماس فعال می‌کند. GET /v1/account/call-alerts با assertion دستگاه projection محدود شامل revision، مالک بازبینی‌شده، نام تماس‌گیرنده، deadline و عمل بازکردن App را می‌دهد. hint فاقد محتواست؛ boot و reconnect بی‌صدا هستند. Main انقضا و lease مشترک صدای توجه را مدیریت می‌کند. هم‌ساز «مشاهدهٔ تماس» و «بی‌صدا در این دستگاه» دارد؛ پاسخ یا رد فقط در App انجام می‌شود. حالت‌های پایان، پاسخ در دستگاه دیگر، رد و انقضا فوراً کارت را پاک می‌کنند. backend تماس ازدست‌رفته را به‌صورت Occurrence ساکت call.missed از reducer موجود Subject ثبت می‌کند. Runtime مهمان و Widget محیط به اعلان تماس حساب دسترسی ندارند و هیچ actorKey محدود به محیط به هویت حساب تبدیل نمی‌شود.

push خارجی عمداً در این قراردادها پیاده نشده است. provider آینده projection زندهٔ واجد شرایط را per-device مصرف می‌کند؛ نمی‌تواند source of truth شود یا Inbox را scrape کند.

همچنین سرویس‌های میزبان App، مرجع API، مرجع SDK، ADR-0130 و ADR-0131 را ببینید.