src/photos/photos.oxl
use super.model.*
use super.limits { THUMB_SIZES }
// Хранилище файлов (в Стандарте `BlobStore` из `provide` в cluster.oxl); здесь локальный актор узла.
pub actor Blobs {
var files: Map<str, u64> = {:}
msg put(name: str, size: u64) { files.insert(name, size) }
msg count() -> u64 { files.len() as u64 }
msg has(name: str) -> bool { files.get(name) != none }
}
dep blobs: ActorRef<Blobs>
// ОТКЛОНЕНИЕ (DEVIATIONS.md #10): `Image` — тип внешней зависимости `imagekit`, здесь картинка — `bytes`, а «декодирование» проверяет
// длину (меньше четырёх байт — не картинка). `makeThumbs` и `moderate` — простые функции вместо `photos/ml.oxl`.
@error pub(api) data BadImage { pub(api) reason: str }
@error pub(api) data Rejected { pub(api) reason: str }
pub(api) type PhotoId = distinct u64
// Топик «фото готово»: на него подписываются клиенты и индексатор
@retain(count: 1000) pub(api) topic PhotoEvents: PhotoReady
pub(api) data PhotoReady { pub(api) id: PhotoId; pub(api) owner: UserId; pub(api) thumbs: List<str> }
data Photo { id: PhotoId; owner: UserId; image: bytes }
data Thumbs { id: PhotoId; urls: List<str> }
// Чистая функция: проверяет формат и создаёт Photo или возвращает BadImage
fn decodePhoto(owner: UserId, b: bytes) -> Photo | BadImage {
if b.len() < 4 { fail BadImage{ reason: "unsupported format" } }
Photo{ id: PhotoId(cluster.ids.next() as u64), owner: owner, image: b }
}
// По §10.6 значение `respond` (здесь `PhotoId`) передаётся следующему шагу, поэтому шаги после него работают с
// идентификатором (так и в V.5, D-5116/D-5334). Владелец — параметр пайплайна `owner`.
fn makeThumbs(id: PhotoId, sizes: List<u32>) -> Thumbs io {
print("photo-step makeThumbs id={id.raw} node={cluster.self().name}") // приёмка проверяет по журналу, на каком узле шаг выполнился
var urls: List<str> = []
for s in sizes { urls.push("thumb/{id.raw}/{s}") }
Thumbs{ id: id, urls: urls }
}
// Модерация: файлы, чей id кратен 13, отклоняются
fn moderate(t: Thumbs) -> Thumbs | Rejected io {
print("photo-step moderate id={t.id.raw} node={cluster.self().name}")
if t.id.raw % 13 == 0 { fail Rejected{ reason: "not allowed" } }
t
}
// Пайплайн: каждый шаг может выполняться на своём узле, данные передаются от узла к узлу напрямую, клиент получает только respond.
pub(api) pipeline uploadPhoto(owner: UserId, img: bytes) -> PhotoId {
(o, b) => decodePhoto(o, b) // 1. на текущем узле; плохой файл — ошибка клиенту
-> p => { blobs.put("orig/{p.id.raw}", p.image.len() as u64); p } // 2. сохраняем оригинал
-> respond it.id // 3. ответ клиенту: id фото; дальше — фон
-> id => makeThumbs(id, THUMB_SIZES) on gpu // 4. превью на узле с GPU
-> moderate on ml durable // 5. модерация; с этого шага прогресс в журнале
-> t => emit PhotoEvents(PhotoReady{ id: t.id, owner: owner, thumbs: t.urls }) // 6. сообщаем подписчикам
} on_error(e, step) { print("photo pipeline failed at {step}: {e}") } // ошибки после respondSource: examples/shop/src/photos/photos.oxl