فهرست مستندات
اعلانهای پلتفرم#
رجیستری مالک یک 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 منبع میآید، نه از ساعت.effectstransitionهای مجاز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 تازهتر |
|---|---|
raise | Attention را میسازد یا دوباره باز میکند و ممکن است یک Pulse بازبینیشده بسازد |
refresh | presentation جاری را بدون بازکردن Attention مصرفشده بهروز میکند |
resolve | Subject فعال متعلق به منبع را میبندد، بدون اینکه ادعا کند گیرنده آن را خوانده |
منبع در 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 زنده |
|---|---|---|
announce | Subject قابلدیدن و Attention | فقط زیر policy بازبینیشدهٔ new_attention |
file | Subject قابلدیدن و Attention | ندارد |
mute | Subject قابلدیدن و شاید 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 را ببینید.