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

مرجع SDK#

الامر کیت توسعه (SDK) و کارخواه اصلی مرورگر را مستقل از چارچوب نگه می‌دارد و آداپتورهای هر چارچوب را به‌صورت صریح ارائه می‌کند.

پکیجمسئولیت
@al-amr/contractsطرح‌واره‌های Zod، typeها، JSON Schema و OpenAPI تولیدشده
@al-amr/sdkنشست Runtime و اپ، Host Services، اعلان، حضور، تنظیمات و Grant
@al-amr/reactProvider، hook، سطح‌های Widget در Compact/Workspace، Inbox و Portal در DOM
@al-amr/r3fآداپتور صحنه و Portal برای React Three Fiber
@al-amr/backendابزارهای بررسی Plugin Grant و استعلام نشست اپ
@al-amr/cliجریان‌های ساخت، افزودن، اعتبارسنجی، آزمایش، انتشار و بازرسی

React و پیاده‌سازی پلاگین‌ها بخشی از کیت توسعهٔ اصلی (SDK) نیستند. طرح‌واره‌های پروتکل، شناسه‌های بازبینی‌شده و سقف‌های BackendComponent که میان بخش مرورگر و بک‌اند Multiplayer و Media مشترک‌اند در @al-amr/contracts قرار دارند تا هر دو سمت دقیقاً همان قرارداد باریک را اعتبارسنجی کنند. پکیج‌های پلاگین برای حفظ سازگاری این نمادهای پروتکل را دوباره صادر می‌کنند و همچنان مالک کارخواه و رفتار خود هستند. یک محیط (Environment) مستقل از چارچوب می‌تواند بدون React از کیت توسعهٔ اصلی استفاده کند.

قاعدهٔ اپ تعبیه‌شده#

App در سند bundle خودش createEmbeddedAppClient({ appId }) را می‌سازد و connect() را فراخوانی می‌کند. client نشست مقید به generation را فقط روی MessageChannel انتقال‌یافته از AppHost دسکتاپ می‌گیرد، توکن حامل App را در حافظه نگه می‌دارد و برای فراخوانی بک‌اند خود App، authorizationHeader() را ارائه می‌کند. اعتبارنامهٔ Runtime والد افشا نمی‌شود.

AppHost دسکتاپ پیش از انتقال channel، revision دقیق نگه‌داری‌شده، پنجرهٔ محصول و origin باندل alamr-app://<appId>/ را بررسی می‌کند. App نباید AlAmrWidget را import یا mount کند؛ پلتفرم مالک chrome ویجت و lifecycle محصول است.

نشست متصل‌شده displayMode و grantedCapabilities دقیق را می‌دهد. Compact برای همهٔ اپ‌ها اجباری و Workspace اختیاری است.

تغییر حالت دیگر سند App را نابود نمی‌کند: state حافظه از آن جان به در می‌برد و حالت تازه از راه client.subscribePresentation(...) می‌رسد. displayMode روی نشست امضاشده، حالتی است که launch با آن مجاز شده و هرگز جابه‌جا نمی‌شود؛ presentation.displayMode حالت جاری است. اپ همچنان باید انتظار راه‌اندازی دوباره را داشته باشد وقتی پلتفرم می‌گوید — تغییر نسخه یا بازیابی از خطا — پس state پایدار جای بک‌اند شماست.

client.presentation همچنین state: "presented" | "background" را گزارش می‌کند. در حال اجرا و بیرون از صفحه، حالتی است که اپ اکنون می‌تواند در آن باشد، اگر display.background: "persistent" اعلام کرده باشد. رسم را متوقف کن، پخش را ادامه بده، و به‌جای شمردن tick از ساعت بخوان: پلتفرم قول می‌دهد سند زنده بماند، نه اینکه کد تو سر وقت اجرا شود. تا وقتی صدا تولید می‌کنی client.setAudible(true) را صدا بزن — یک اطلاع است، نه یک مجوز.

نشست‌ها داخل SDK تمدید می‌شوند، پس session.id هر چند دقیقه عوض می‌شود. دادهٔ ذخیره‌شده را روی session.appActorSub کلید بزن.

برای قابلیت اختیاری‌ای که نشست متصل‌شده ندارد، یک عمل آشکار کاربر می‌تواند client.requestOptionalCapability("platform.media.microphone@1") را صدا بزند. SDK هیچ قابلیتی را grant نمی‌کند؛ رجیستری بررسی می‌کند manifest تغییرناپذیر اپ آن را پیشنهاد داده و میزبان دسکتاپ سطح رضایت متعلق به پلتفرم را باز می‌کند. App نمی‌تواند آن سطح را باز یا جعل کند. چون میزبان به user activation زنده نیاز دارد، این را از دل خود ژست کاربر صدا بزنید، نه پس از کار دیگر. هنگام بازگشت به Widget یا انتخاب دوبارهٔ App پایدار، اختیار دوباره سنجیده می‌شود و میزبان فقط وقتی Permissions Policy مرورگر عوض شده renderer محصول را دوباره می‌سازد. اجازهٔ خود مرورگر همچنان تصمیمی مستقل است.

MessagePort مقید به generation کانال Host Services باقی می‌ماند:

const context = await appClient.getSpatialContext();
const locations = await appClient.listSemanticLocations();
const captured = await appClient.captureCurrentLocation();
const assessment = await appClient.assessNavigation(captured);
const result = await appClient.commitNavigation(assessment.assessmentId);

محیط provider را با createSemanticEnvironmentHostAdapter() می‌سازد و آن را به <AlAmrWidget hostAdapter={hostAdapter} /> می‌دهد. پس از تغییر واقعیِ جا، refreshSpatialContext() را صدا می‌زند. ویجت فقط متدهای قرارداد و grant را broker می‌کند و state صحنه را حدس نمی‌زند.

دو گزینه می‌گویند بازدیدکننده کجاست، و هر دو اختیاری‌اند (ADR-0075):

  • currentZone(): { zoneId, label } | undefined — کدام ناحیهٔ محیط. اگر محیط یک فضای پیوسته است، اصلاً ندهیدش؛ اپ همین نبودن را درست نشان می‌دهد، به‌جای زونی که فقط برای پر کردن یک فیلد اجباری ساخته شده باشد.
  • currentLocation(): { locationId?, label } | undefined — کجای آن. تنها برای جایی که در spatial.semanticLocations اعلام شده locationId بگذارید؛ اتاق یا نیمکتی که محیط فقط می‌خواهد نامش را بگوید، با برچسبِ تنها برمی‌گردد و بدون CapturedLocationResolver قابل capture نیست.

هر دو برچسب همان چیزی است که بازدیدکننده می‌خواند: در زمان اجرا و به زبان او نوشته می‌شود. نامِ خودِ محیط میان آن‌ها نیست؛ آن روی توکن نشست و به گواهی رجیستری به اپ می‌رسد.

برای capture نقطهٔ دلخواه، یک CapturedLocationResolver با هر سه عملیات capture، assess و navigate فراهم کنید. SDK نوع‌های CapturedEnvironmentLocationRef و CapturedLocationAssessment را برای این مرز provider صادر می‌کند. assess باید locator مبهم را ورودی غیرقابل‌اعتماد بداند و نسخهٔ دقیق محیط، نسخهٔ resolver، زون، محدوده، دسترسی و قابلیت navigation را بررسی کند. این عملیات هم هنگام assessment و هم بلافاصله پیش از navigation commit اجرا می‌شود.

Capture و assessment navigation به عمل موقت کاربر نیاز دارند. Navigation یک جفت assess/commit کوتاه‌عمر و caller-bound است و اختیار حرکت در محیط می‌ماند. Captured locator opaque و revision-bound است؛ فقط موقعیت semantic اعلام‌شده می‌تواند از navigation دسکتاپ میان Environmentها عبور کند. commit موفق در همان Environment، Widget را نمی‌بندد و سند App را reload نمی‌کند.

برای state ذخیره‌نشده setBeforeExitHandler() را به‌کار ببرید. میزبان حداکثر یک ثانیه منتظر می‌ماند و مالک confirmation است. requestClose() از ویجت close می‌خواهد و clear() توکن memory-only، port و درخواست‌های pending را پاک می‌کند.

appClient.setBeforeExitHandler(async ({ reason }) => {
  const saved = await saveDraft(reason);
  return saved ? { disposition: "allow" } : { disposition: "confirm" };
});

API اعلان#

Runtime فعال فقط projection Environment جاری را می‌خواند:

const page = await client.listNotifications({ cursor, limit: 50 });
const notification = await client.publishNotification(request);
const stop = client.subscribeNotificationInvalidation(() => reconcile());

subscribeNotificationInvalidation وقتی صدا می‌زند که چیزی برای این نشست بلند شده باشد، و بیش از همین نمی‌گوید — فریم حضورِ پشتش یک شناسهٔ نشست دارد و هیچ اعلانی. با listNotifications دوباره بخوانید؛ و جمعشان کنید، چون رگبار پیام رگبار اشاره است. list، snapshot اول، page، poll و reconnect همگی reconciliation هستند و هیچ Pulse گذرایی را بازسازی نمی‌کنند.

ردیف V1 یک projectionVersion متعلق به پلتفرم دارد. version همان ردیفی را که UI واقعاً رندر کرده همراه seen یا dismiss بفرستید تا action قدیمی revision تازه‌تر Subject را مصرف نکند:

const item = page.notifications[0];
const versions =
  item.projectionVersion === undefined
    ? undefined
    : { [item.notificationId]: item.projectionVersion };

await client.markNotificationsRead([item.notificationId], undefined, versions);
await client.dismissNotifications([item.notificationId], undefined, versions);

آرگومان دوم fallback قدیمی { [notificationId]: createdAt } برای readerهای immutable است. رجیستری readAt خودش را برمی‌گرداند؛ در browser مهر نزنید. unseenCount ردیف unseen و mute را هم دارد، attentionCount آن را حذف می‌کند و unreadCount قدیمی alias همان attention است.

App باز با policy رخداد بازبینی‌شده از API occurrence استفاده می‌کند:

await appClient.signalNotificationOccurrence(occurrence);

await appClient.acknowledgeNotificationSubjects({
  acknowledgements: [{ eventType, subjectKey, throughRevision }],
});

call اول فقط { accepted: true } می‌دهد و retry دقیق باید همان eventId را تکرار کند. call دوم فقط پس از آنکه App محتوای authoritative را تا throughRevision واقعاً نشان داد watermark را جلو می‌برد؛ بازشدن App یا کلیک روی chrome Shell acknowledgement نیست. source و recipient از App session می‌آیند. signal به grant platform.notifications.publish.self@1 نیاز دارد.

وقتی شخص اعلان یک App را فشار می‌دهد، Shell آن App را باز یا focus می‌کند و سپس هدف معناییِ متعلق به source را روی کانال تثبیت‌شدهٔ AppHost می‌فرستد:

const stopActivation = appClient.subscribeActivation(({ source, target }) => {
  if (source === "notification" && target.kind === "notification_subject") {
    openAuthoritativeSubject(target.subjectKey);
  }
});

subjectKey برای Shell opaque و برای recipient scoped است؛ شناسهٔ خام گفت‌وگو، سند یا حساب نیست. اگر event میزبان پیش از mountشدن listener برسد، SDK فقط آخرین target را تا اولین subscribeActivation() نگه می‌دارد و همان‌جا یک‌بار مصرف می‌کند؛ subscribe دوبارهٔ effect آن press را replay نمی‌کند. App در cold launch باید بعد از آن هم activation را تا آماده‌شدن list authoritative نگه دارد، فقط با داده و permission جاری خودش resolve کند و تنها بعد از نمایش واقعی محتوا acknowledgement بدهد. پس launch ناموفق یا target ناشناخته/قدیمی، Attention پلتفرم را دست‌نخورده می‌گذارد.

برای background اپ، حساب متصل delegation می‌سازد یا revoke می‌کند:

const delegation = await appClient.createNotificationDelegation();
await appClient.revokeNotificationDelegation();

بک‌اند، secret یک‌بارنمایش delegation را با Project token دارای scope notifications:publish به createAppNotificationWorkloadClient() از @al-amr/backend می‌دهد.

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

const workload = createAppNotificationWorkloadClient({
  issuer: registryIssuer,
  projectToken,
});
await workload.signal(delegationToken, occurrence);

پس از پذیرش واقعیت‌های متعلق به caller، outcomeهای وابسته به recipient فقط accepted-only هستند. outbox ماندگار منبع همان eventId را retry می‌کند و پاسخ هیچ count، treatment یا recipient state نمی‌دهد.

appClient.publishNotification(request) و workload.publish(delegationToken, request) برای caller immutable به‌عنوان متد legacy و additive باقی می‌مانند. category انتخابی caller هیچ اختیار family، صدا یا Pulse نمی‌دهد.

backend محیط App delegation ندارد و Environment Backend Grant کوتاه‌عمر متصل به endpoint با notifications.publish@1.0.0 را همراه publish قدیمی فعلی به‌کار می‌برد.

navigateToEnvironment({ environmentId, focusLocationId? }) درخواست navigation تایپ‌شده‌ای به میزبان دسکتاپ می‌فرستد و با { ok: true } یا { ok: false, refusal } حل می‌شود. میزبان bundle دقیق فعال و نگه‌داری‌شدهٔ مقصد را resolve می‌کند و Registry، location اختیاری را با همان revision تطبیق می‌دهد. هیچ URL ناشری ساخته یا افشا نمی‌شود.

promise وقتی حل می‌شود که دربارهٔ سفر تصمیم گرفته شده باشد، نه وقتی درخواست رفته باشد: resolve کردن مقصد ممکن است bundle‌ای را که این دستگاه ندارد دانلود کند، پس فراخوان باید حالت انتظار نشان بدهد و برایش سقف زمانی نگذارد. در حالت موفق، سند فراخوان هم‌زمان با بالا آمدن مقصد نابود می‌شود؛ یعنی { ok: true } معنایش «این سند تمام شد» است، نه «حالت انتظار را پاک کن». refusal جملهٔ خودِ میزبان است و برای نشان دادن است، نه برای شرط گذاشتن روی آن.

میزبان این سه را رد می‌کند: سفر به همان محیطی که فراخوان در آن ایستاده، سفر دوم وقتی یکی در جریان است، و سفری که کمتر از دو ثانیه پس از سفرِ پذیرفته‌شدهٔ قبلی خواسته شود. این‌ها فقط سمت میزبان اجرا می‌شوند: navigator.userActivation که مهمان خودش گزارش می‌دهد دفاع نیست، چون مهر آن را خود مهمان می‌زند.

consumeEnvironmentLaunchIntent() نیمهٔ ورودی است و هیچ میزبانی هنوز آن را تحویل نمی‌دهد — هیچ‌جا کلاینت با launchIntent ساخته نمی‌شود، پس در هر build منتشرشده undefined برمی‌گرداند. confirmation و commit همچنان در مالکیت Widget و Environment‌اند.

قاعدهٔ Runtime#

Shell دسکتاپ bundle دقیق و نگه‌داری‌شدهٔ Environment را انتخاب و bridge مربوط به Runtime را inject می‌کند. SDK را با origin رجیستری و هویت ثبت‌شدهٔ Environment پیکربندی کنید؛ کد محصول نباید OAuth مرورگر را آغاز یا URIهای callback مجوز/خروج را اعلام کند. ورود حساب متعلق به Shell است.

SDK اعتبارنامهٔ Runtime تزریق‌شده را فقط در حافظه نگه می‌دارد و پس از خطای شبکه یا مجوز شخص را downgrade نمی‌کند. وضعیت connected تنها پس از پذیرفته‌شدن نشست میزبان، فعال‌شدن Runtime و نخستین heartbeat موفق Presence گزارش می‌شود.

client.snapshot و هر مقداری که client.subscribe() تحویل می‌دهد، مشاهده‌ای عمیقاً فقط‌خواندنی است. کیت توسعه کل snapshot زمان اجرا، از جمله مقادیر تودرتوی نشست، فعالیت و جزئیات را کپی و freeze می‌کند. مصرف‌کننده باید به‌جای تغییر یک snapshot مشاهده‌شده، گذارها را از طریق متدهای client درخواست کند؛ تکمیل دیرهنگام یک عملیات ناهمگام نمی‌تواند پس از release، خروج، supersession یا توقف، وضعیت قدیمی را دوباره برقرار کند.

snapshotهای تنظیمات پلاگین که متدهای تنظیمات client برمی‌گردانند یا از cache محلی خوانده می‌شوند نیز عمیقاً فقط‌خواندنی و freezeشده هستند. این تضمین مقادیر حل‌شده، همهٔ لایه‌های منبع، provenance تودرتو، نشانگرهای بازنگری و طرح‌وارهٔ تنظیمات را دربر می‌گیرد. تغییرات را از طریق setPluginSettings() یا resetPluginSettings() بنویسید تا اعتبارسنجی، هم‌زمانی ETag و انتشار واکنشی مرجع باقی بمانند.

اگر یک تغییر هم‌زمان، خروج، یا بازنشانی زمان اجرا خواندن درحال‌انجام تنظیمات را نامعتبر کند، آن خواندن به‌جای بازگرداندن snapshot نسل قدیمی با کد plugin_settings_load_stale رد می‌شود. خواندن را فقط پس از آماده‌شدن وضعیت جاری زمان اجرا دوباره اجرا کنید. اگر فراخواننده باید این نتیجهٔ قابل‌تلاش‌مجدد را تشخیص دهد، خطای صادرشدهٔ PluginSettingsStaleLoadError را دریافت کند.

محتوای تعاملی محیط را درون AlAmrRuntimeGate قرار دهید. برای هر هویت اصلی (principal)، فقط یک صفحه یا دستگاه می‌تواند اجارهٔ فعال زمان اجرا را در اختیار داشته باشد. صفحهٔ superseded محتوای پلاگین (Plugin) را از صفحه خارج می‌کند و تا زمانی که کاربر صریحاً «فعال‌کردن این صفحه» را انتخاب نکند غیرفعال می‌ماند؛ تمرکز صفحه به‌تنهایی مالکیت را پس نمی‌گیرد. تابع پاک‌سازی پلاگین باید هنگام از دست رفتن فعالیت، رسانه، socket، timer و دسترسی دستگاه را متوقف کند.

کیت توسعه در حالت غیرفعال، socket فعال حضور (Presence) را با یک کانال کنترل فقط‌خواندنی جایگزین می‌کند. به این ترتیب خروج سراسری مرورگر، بدون heartbeat، حضور، اجرای پلاگین یا پس‌گرفتن خودکار مالکیت فوراً دریافت می‌شود. قابل مشاهده شدن دوبارهٔ زبانه نیز وضعیت مرجع زمان اجرا را تطبیق می‌دهد تا سامانه از تعلیق پس‌زمینهٔ مرورگر یا frame ازدست‌رفتهٔ WebSocket بازیابی شود.

مرز بک‌اند Environment#

مرورگر با client.requestEnvironmentBackendGrant({ scopes, context }) مجوز کوتاه‌عمر می‌گیرد و فقط آن را به endpoint اعلام‌شده می‌فرستد. سرور از createEnvironmentBackendGrantVerifier در @al-amr/backend استفاده می‌کند. بررسی issuer، audience، endpoint، origin باندل میزبانی‌شده در دسکتاپ، scope، context و Runtime fence انجام می‌شود و introspection زنده حالت امن پیش‌فرض است؛ subject خام حساب افشا نمی‌شود.

قاعدهٔ بوت Environment#

آمادگی محیط حقیقتی محلی در کلاینت است و از snapshot مربوط به Runtime رجیستری جداست. کلاینت تازه در وضعیت booting شروع می‌کند؛ محیط وقتی صحنهٔ حیاتی‌اش mount شد client.signalEnvironmentReady() را صدا می‌زند و در صورت نیاز با client.reportEnvironmentBootProgress(0..1) پیشرفت واقعی گزارش می‌کند. AlAmrRuntimeGate سطح بوتِ برند را تا رسیدن این سیگنال نگه می‌دارد: تا وقتی Runtime فعال است ولی محیط آماده نیست، فرزندان زیر سطح mount می‌مانند (صحنه به بارگذاری ادامه می‌دهد)؛ حالت‌های superseded، signed_out و failed مثل قبل محتوای افزونه را unmount می‌کنند. سطح بوت عادی یک status غیرتعاملی است و بدون تأخیر حداقلی مصنوعی، بلافاصله پس از آمادگی کنار می‌رود؛ فقط حالت‌های استثنایی dialog هستند. محیط‌ها نباید صفحهٔ بارگذاری خودشان را بسازند و سیگنال آمادگی idempotent است.

chrome محیط#

EnvironmentHostAdapter.chrome یک پل اختیاری و در-فرایند برای رفتاری است که پشت یک نشانِ متعلق به پلتفرم می‌نشیند. این یک Host Service نیست: هیچ‌چیز روی آن از واسط اپ عبور نمی‌کند، serialize نمی‌شود و پشت گرنت قابلیت قرار نمی‌گیرد؛ پس نه ورودی contracts دارد و نه نسخهٔ پروتکل.

interface EnvironmentChromeState {
  readonly suppressed: boolean;
  readonly settingsAvailable: boolean;
}

interface EnvironmentChromeAdapter {
  getState(): EnvironmentChromeState;
  subscribe(listener: () => void): () => void;
  openSettings(): void;
}

ویجت نشان تنظیمات خود را تنها تا وقتی settingsAvailable درست است می‌کشد، و تا وقتی suppressed درست است هر نشان پلتفرم را پنهان می‌کند — محیطی که مودال خودش را باز کرده یا میان گذر Zone است صاحب صفحه است، و ویجت بالای تمام stacking context آن می‌نشیند. openSettings() فقط زمانی صدا زده می‌شود که نشان کشیده شده باشد؛ هر چه پس از آن بیاید از آنِ محیط است. ویجت هرگز تنظیمات محیط را رندر، بازرسی یا ذخیره نمی‌کند. پل را از طریق createSemanticEnvironmentHostAdapter({ chrome }) بدهید، یا کلاً حذفش کنید و محیط فقط نشان‌های کمتری بگیرد. ADR-0062 را ببینید.