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

سرویس‌های میزبان اپ و بافت مکانی#

Host Services تنها راه پشتیبانی‌شده برای خواندن بخش مجاز از بافت میزبان یا درخواست یک عمل از محیط است. App هیچ‌گاه کد Environment را import نمی‌کند، صحنه یا کروم پلتفرم را نمی‌خواند و توکن Runtime یا Plugin Grant نمی‌گیرد.

App revision
  createEmbeddedAppClient()
          │  versioned App frames
          ▼
desktop AppHost session ─ capability grant / generation / deadline
          │  validated Main↔Environment relay
          ▼
EnvironmentHostAdapter ── Environment-owned scene semantics

handshake اجرا یک کانال منطقی پایدار می‌سازد. در کلاینت دسکتاپ، window.alamr.appHost آن را از WebContentsView sandboxشدهٔ App بیرون می‌آورد و Main مالک AppHostProtocolSession و همان پروتکل frame نسخه‌دار SDK است. درخواست، پاسخ، خطا، لغو، رویداد و lifecycle همگی روی همین کانال و برای یک revision و نسل دقیق اجرا می‌شوند. هر frame حداکثر ۶۴ KiB است و App حداکثر هشت درخواست هم‌زمان دارد.

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

  • packages/contracts/src/host-services.ts — شناسهٔ قابلیت‌ها، شکل frameها و زمینهٔ مکانی که هر دو طرف این پروتکل با آن اعتبارسنجی می‌کنند.
  • packages/sdk/src/embedded-app.ts — نیمهٔ اپ: createEmbeddedAppClient، frameهای درخواست و پاسخ و چرخهٔ عمر، و هر فراخوان میزبانی که اپ می‌تواند بزند.
  • packages/sdk/src/app-client-port.ts — adapter کانال که MessagePort سند یا bridge دسکتاپ window.alamr.appHost را انتخاب می‌کند.
  • packages/sdk/src/app-host-protocol-session.ts — نشست منطقی و مستقل از framework میزبان برای launch، renewal، حصار نسل، dispatch محدود، presentation و beforeExit. مرورگر و Desktop هر دو port احراز‌شده را به همین implementation می‌دهند.
  • packages/sdk/src/host-service-dispatcher.ts و packages/sdk/src/environment-host.ts — نیمهٔ محیط، جایی که EnvironmentHostAdapter پاسخ می‌دهد.
  • packages/contracts/src/environment-bridge.ts، packages/bridge و packages/sdk/src/environment-host-service-relay.ts — رلهٔ نسخه‌دار و محدود به ۶۴ KiB در Desktop. Main فقط متدهای متعلق به Environment را پس از پذیرش policy می‌فرستد؛ Environment نتیجه‌های schema-checked و تغییر کامل context مکانی را بدون عبور function بین processها برمی‌گرداند.
  • packages/react/src/app-frame.tsx — adapter پذیرش مرورگر؛ source/origin iframe را می‌سنجد و portهای browser-only برای user activation، تب consent و companion picker را تزریق می‌کند، اما مالک launch یا renewal نیست.
  • platform/desktop/src/main/apps/app-view-manager.ts — adapter دسکتاپِ مقید به receipt که برای هر سند مستقل App یک AppHostProtocolSession مشترک می‌سازد و آن را به سرویس‌های میزبانِ Main-owned متصل می‌کند.
  • platform/desktop/src/main/apps/environment-host-adapter-provider.ts — proxy context-aware در Main برای Environment نصب‌شدهٔ فعلی. Home، یک Environment دیگر، سند جایگزین‌شده و surface آزادشده همگی fail-closed هستند و adapter صحنهٔ کهنه را قرض نمی‌گیرند.

قابلیت‌ها#

اپ قابلیت‌های بسته و نسخه‌بندی‌شده را در al-amr.app.json درخواست می‌کند:

{
  "capabilities": {
    "required": [
      {
        "id": "platform.environment.identity.read@1",
        "versionRange": "^1.0.0"
      },
      {
        "id": "host.spatial.context.read@1",
        "versionRange": "^1.0.0"
      }
    ],
    "optional": [
      {
        "id": "host.spatial.locations.read@1",
        "versionRange": "^1.0.0"
      }
    ]
  }
}

نبودن قابلیت required جلوی اجرای اپ را می‌گیرد. نبودن قابلیت optional باید به تجربه‌ای کوچک‌تر ولی سالم منجر شود. وابستگی هر قابلیت باید در همان سطح یا سطح قوی‌تر اعلام شود. رجیستری پیش از اجرای App، نسخهٔ منتشرشده، کاتالوگ قابلیت، نوع کاربر، وابستگی، Runtime و رضایت کاربر را بررسی می‌کند. درخواست مانیفست به معنی مجوز نیست؛ اپ باید client.snapshot.hostSession.grantedCapabilities را بررسی کند.

قابلیتافشا و فعال‌سازی
platform.identity.profile.read@1نام و آواتار حساب؛ فقط حساب
platform.environment.identity.read@1هویت امضاشدهٔ محیط فعال
host.spatial.context.read@1زون و موقعیت معنایی فعلی
host.spatial.locations.read@1موقعیت‌های پایدار اعلام‌شده توسط محیط
host.spatial.location.capture@1مرجع opaque برای جای فعلی
host.spatial.navigation.request@1درخواست حرکت از محیط
platform.notifications.publish.self@1signal در declaration بازبینی‌شدهٔ اعلان برای principal فعلی App
platform.notifications.connect.background.self@1واگذاری اعلان پس‌زمینه به بک‌اند اپ؛ فقط حساب
platform.companions.list@1خواندن‌های بعدی نام، چهره و شناسهٔ ناشناسِ مخصوص اپ تا زمان قطع؛ بدون موقعیت؛ فقط حساب
platform.companions.offer@1پیشنهاد دادن به یکی از همراهان، بدون اینکه اپ بداند او کیست
platform.notify.companion@1یک اعلان پلتفرمیِ بدون محتوای اپ برای یک همراه
platform.call@1برقراری تماس با یک همراه و دریافت تماس‌های او؛ فقط حساب؛ زنگ به‌شکل CallAlert زندهٔ حساب می‌رسد
platform.media.microphone@1اجازهٔ درخواست میکروفن به قاب همان اپ
platform.presence.roster.read@1چند نفر در این دنیا هستند؛ هرگز اینکه چه کسانی

فعال‌سازی کاربر خاصیت متد است نه قابلیت، و جدول بالا عمداً دیگر خلافش را ادعا نمی‌کند. HOST_SERVICE_METHOD_DEFINITIONS مقدار requiresUserActivation را به ازای هر متد نگه می‌دارد، و این تنها دانه‌بندی‌ای است که می‌تواند قاعدهٔ واقعی را بیان کند: host.spatial.navigation.request@1 برای assessNavigation ژست می‌خواهد و برای commitNavigation نمی‌خواهد، چون ژست متعلق به لحظه‌ای است که شخص درخواست کرده، نه لحظه‌ای که میزبان بر اساسش عمل می‌کند.

دسترسی به همراه در بک‌اند خود اپ#

platform.companions.list@1 فقط شناسهٔ جفتیِ aact_ مخصوص همان اپ و نمایش حل‌شده را می‌دهد؛ نه شناسهٔ حساب، نه محل حضور، نه محیط و نه کلید خام گراف همراهی. خودِ شکل aact_ اثبات نمی‌کند رابطه هنوز برقرار است.

پیش از یک نوشتن مستقیم برای شخص دیگر، اپ یکی از subjectهای همان فهرست رضایت‌داده را به POST /v1/apps/companions/reach می‌دهد. رجیستری یال زنده، امتناع‌ها و تعلیق را دوباره می‌سنجد و در صورت مجازبودن، اثبات امضاشده‌ای با عمر ۶۰ ثانیه می‌دهد. همهٔ ردهای مربوط به طرف مقابل فقط { "status": "unavailable" } هستند. بک‌اند خود اپ با createAppCompanionReachVerifier صادرکننده، App، تماس‌گیرنده و همراه دقیق را بررسی می‌کند و هرگز subject را فقط از روی شکلش قبول نمی‌کند.

این قابلیت عمومیِ همهٔ اپ‌هاست، نه API پیام‌رسان. هیچ محتوا یا شناسهٔ گفت‌وگویی از رجیستری عبور نمی‌کند و دادهٔ محصول طبق ADR-0068 در بک‌اند خود اپ می‌ماند.

اعلام محیط#

محیطی که سرویس مکانی ارائه می‌دهد باید واژگان پایدار و سرویس متناظر را با هم اعلام کند:

{
  "spatial": {
    "semanticLocations": [
      {
        "locationId": "garden-gate",
        "label": "Garden gate",
        "navigationPolicy": "confirm"
      }
    ]
  },
  "hostServices": {
    "protocolVersion": "1.0.0",
    "provides": [
      "host.spatial.context@1",
      "host.spatial.locations@1",
      "host.spatial.capture@1",
      "host.spatial.navigation@1"
    ]
  }
}

شناسهٔ موقعیت، معنای متعلق به محیط است؛ route یا مختصات نیست. اگر دادهٔ اپ به آن اشاره می‌کند، شناسه را پایدار نگه دارید. انتشار، snapshot تغییرناپذیر این واژگان را می‌سازد.

نبودِ زون در این اعلام عمدی است. محیط زونِ فعلی خود را در زمان اجرا روی EnvironmentSpatialContext می‌گوید، با همان برچسبی که بازدیدکننده‌اش می‌خواند؛ و محیطی که یک فضای پیوسته است هیچ زونی نمی‌فرستد — پس رجیستری هرگز فهرست زون نگه نمی‌دارد. استدلالش در ADR-0075 است.

Adapter را از state واقعی محیط بسازید و به ویجت بدهید:

import { createSemanticEnvironmentHostAdapter } from "@al-amr/sdk";

const hostAdapter = createSemanticEnvironmentHostAdapter({
  environmentId: manifest.environmentId,
  environmentRevisionId: () => currentRevisionId,
  spatial: manifest.spatial!,
  currentZoneId: () => currentZoneId,
  currentSemanticLocationId: () => currentLocationId,
  capturedLocationResolver: {
    capture: () => ({
      kind: "captured",
      environmentId: manifest.environmentId,
      environmentRevisionId: currentRevisionId,
      zoneId: currentZoneId,
      resolverId: "example.player-position",
      resolverVersion: "1.0.0",
      locator: worldNavigation.captureOpaqueLocator(),
    }),
    assess: (target) =>
      worldNavigation.canResolveCapturedPlace(target.locator)
        ? {
            presentation: { title: "Saved place" },
            minimumPolicy: "direct",
          }
        : undefined,
    navigate: (target) => worldNavigation.goToCapturedPlace(target.locator),
  },
  navigate: async (locationId) => {
    return await worldNavigation.goToSemanticLocation(locationId);
  },
  onWidgetStateChange: ({ capturesInput, occupiedRect }) => {
    controls.setWidgetCapture(capturesInput);
    camera.setWidgetSafeArea(occupiedRect);
  },
});
<AlAmrWidget hostAdapter={hostAdapter} />

وقتی preload دسکتاپ یا development host رلهٔ window.alamr.hostServices را فراهم کند، AlAmrWidget این adapter را خودکار به آن متصل می‌کند و هنگام unmount هم subscription درخواست و هم context مکانی را آزاد می‌کند. Environment بدون React می‌تواند همین اتصال را صریح بسازد: bindEnvironmentHostServices(window.alamr.hostServices, hostAdapter).

این hop اضافه authority را به renderer محیط منتقل نمی‌کند. Main پیش از relay هر متد متعلق به Environment همچنان capability اعطاشده توسط Registry، نسل فعلی App، context محیط فعلی، user activation، بودجهٔ درخواست، navigation assessment و deadline را بررسی می‌کند. پاسخ باید دقیقاً با id، generation و method درخواست برابر باشد. reload کار درحال‌پرواز را stale می‌کند و Environment غایب یا آزادشده با خطای retryable unavailable پاسخ می‌گیرد.

پس از تغییر واقعی زون یا موقعیت معنایی، hostAdapter.refreshSpatialContext() را صدا بزنید. Adapter یک visitId پایدار و sequence صعودی را نگه می‌دارد. موقعیت را قبل از ورود واقعی گزارش نکنید.

برای پشتیبانی از نقطه‌های دلخواهی که موقعیت معنایی authored نیستند، کل capturedLocationResolver شامل capture، assess و navigate را فراهم کنید. خروجی آن locator محدود و opaque به همراه شناسه/نسخهٔ resolver و نسخهٔ دقیق محیط است. در هر assess، locator را ورودی غیرقابل‌اعتماد بدانید و نسخه، زون، ساختار، محدوده، دسترسی و قابل‌حرکت‌بودن آن را بررسی کنید. SDK پیش از navigate در زمان commit دوباره assess را اجرا می‌کند تا capture و بازگشت دو مسیر امنیتی جدا نباشند. اپ مرجع را ذخیره می‌کند ولی locator آن را تفسیر نمی‌کند.

استفاده در اپ#

برای هر نسل سند App یک بار متصل شوید:

import { createEmbeddedAppClient } from "@al-amr/sdk";

const client = createEmbeddedAppClient({
  appId: manifest.appId,
  registryBaseUrl,
});

const session = await client.connect();
const mode = session.displayMode;
const context = await client.getSpatialContext();
const locations = await client.listSemanticLocations();

وقتی اپ به affordance مکانی زنده نیاز دارد subscribe کنید:

const unsubscribe = client.subscribeSpatialContext((nextContext) => {
  notes.showCurrentPlace(session.environmentId, nextContext.zoneId);
});

Capture و navigation باید از عمل مستقیم کاربر شروع شوند:

saveButton.addEventListener("click", async () => {
  const location = await client.captureCurrentLocation();
  await notes.save({ location });
});

goButton.addEventListener("click", async () => {
  const assessment = await client.assessNavigation(note.location);
  showAssessment(assessment); // The App may explain, but the host owns confirmation.
  const result = await client.commitNavigation(assessment.assessmentId);
  if (result.status === "unavailable") showUnavailable(result.reason);
});

Notes برای ساخت یا ویرایش یادداشت عادی به این capabilityهای مکانی نیاز ندارد. کتابخانه و پوشه‌هایش با actor جفتی اپ کلید می‌خورند. capture انتخاب صریح زمان ساخت است که provenance تغییرناپذیر می‌افزاید؛ هیچ‌گاه پوشه یا scope پیکرهٔ یادداشت نمی‌شود. یادداشت پیوست‌شده به محیط دیگر همچنان به‌عنوان context نمایش داده می‌شود، اما تا وقتی session.environmentId با محیط پیوست برابر نیست Notes نباید assessNavigation را ارائه کند.

Notes ویرایش آفلاین را فقط پس از پایدارشدن outbox همان actor تأیید می‌کند. پنجره‌های هم‌مبدأٔ اپ، تراکنش‌های کوتاه cache را با Web Lock انحصاریِ actor سری می‌کنند، پس از گرفتن قفل دوباره مقدار پایدار را می‌خوانند و با رویدادهای storage همگرا می‌شوند. قفل دوم برای همان actor فقط یک worker همگام‌سازی راه دور را انتخاب می‌کند؛ fetch هیچ‌گاه قفل cache را نگه نمی‌دارد، بنابراین شبکهٔ معطل نمی‌تواند ذخیرهٔ محلی پنجرهٔ دیگر را متوقف کند. اگر storage پایدار یا Web Locks موجود نباشد، Notes ادعای ذخیرهٔ محلی نمی‌کند. کار شبکه deadline و backoff نماییِ jitterدار دارد؛ teardown پذیرش mutation تازه را می‌بندد، mutationهای ازپیش‌پذیرفته را تا storage محلی drain می‌کند، transport را abort می‌کند و پاسخ دیررس را پیش از تغییر cache کنار می‌گذارد.

ارزیابی و commit عمداً جدا هستند. ارزیابی کوتاه‌عمر، متصل به caller و در commit دوباره بررسی می‌شود. اپ مستقیماً player، camera، route یا Zone را تغییر نمی‌دهد. commit مکانی موفق در همان محیط سند App را باز نگه می‌دارد؛ این حرکت داخل تجربهٔ فعلی است، نه transition چرخهٔ عمر App.

قواعد مرجع مکانی#

  • مرجع semantic شامل شناسهٔ محیط و موقعیت پایدار است و می‌تواند در deep link میان محیط‌ها توسط رجیستری بررسی شود.
  • مرجع captured opaque و متصل به نسخه است و داخل URL اجرا قرار نمی‌گیرد.
  • مختصات، pose بازیکن، camera transform، route خصوصی و شناسهٔ node صحنه هویت مکانی قابل‌حمل نیستند.
  • بافت مکانی raw account subject ندارد. دادهٔ اپ با actor جفتی و تأییدشدهٔ اپ کلید می‌خورد.
  • بافت مکانی هویت محیط هم ندارد. محیط بافت را از manifest خودش پر می‌کند، پس نسخه‌ای از هویت در آنجا حرفِ خود محیط دربارهٔ خودش می‌بود — و بافت روی host.spatial.context.read@1 سوار است، یعنی هویت را به اپی می‌داد که کاربرش platform.environment.identity.read@1 را رد کرده. هویت محیط فقط از session.environmentId می‌آید که رجیستری از رکورد خودش امضا می‌کند و پشت همان قابلیت نگه می‌دارد.

Lifecycle#

فقط برای کار ذخیره‌نشده handler بگذارید:

client.setBeforeExitHandler(async ({ reason }) => {
  if (!draft.dirty) return { disposition: "allow" };
  const saved = await draft.saveBeforeDeadline();
  return saved
    ? { disposition: "allow" }
    : { disposition: "confirm", message: exitMessage(reason) };
});

میزبان حداکثر یک ثانیه منتظر می‌ماند و خطا نمی‌تواند کاربر را محبوس کند. در cleanup، client.clear() را صدا بزنید.

beforeExit اکنون به‌مراتب کمتر از گذشته اجرا می‌شود. تغییر Compact/Workspace سند یا نشست تازه نمی‌سازد و آن را صدا نمی‌زند، و بستن پنل هم برای اپی که display.background: "persistent" اعلام کرده آن را صدا نمی‌زند — آن اپ park می‌شود، نه exit. همچنان برای بستن واقعی، برای replace وقتی سومین اپ در حال اجرا باید جا باز کند، و برای از دست رفتن Runtime اجرا می‌شود.

دو رویداد و دو متد میزبان برای اپ‌های park‌شده وجود دارد:

فریمجهتمعنا
host.presentation.changedمیزبان ← اپpresented یا background، حالت جاری، دیده‌شدن صفحه، توانایی پاسخ میزبان و ساعت دیواری میزبان
host.session.renewal.dueمیزبان ← اپنشست پنج‌دقیقه‌ای باید دوباره صادر شود
host.presentation.getاپ ← میزبانpresentation جاری، برای اپی که دیر مشترک شده
host.presentation.audibleاپ ← میزباناطلاع: این اپ صدا تولید می‌کند
host.session.renewاپ ← میزبانصدور کد launch تازه در برابر challenge و nonce تازه

هیچ‌کدام از این چهار قابلیتی نمی‌خواهد و هیچ‌کدام ژست کاربر لازم ندارد: ژست، تمدید را برای اپی که پشت پنل بسته است — تنها اپی که به آن نیاز دارد — ذاتاً ناممکن می‌کرد. تمدید، مراسم launch را کامل تکرار می‌کند، پس هر بررسی‌ای که رجیستری روی launch اول اجرا می‌کند دوباره اجرا می‌شود؛ به رجیستری هرگز گفته نمی‌شود که این فراخوانی یک تمدید است، و همین ندانستن، خودِ آن ویژگی است.

همچنین اپ‌ها، مرجع مانیفست، مرجع SDK و ADR-0046 را بخوانید.