actr-framework 0.5.9

Actor-RTC framework core (stub for code generation testing)
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
//! Adapter glue between the user's [`Workload`] impl and the Component
//! Model `Guest` trait generated by wit-bindgen.
//!
//! The `entry!` macro emits a zero-sized `__ActrEntryAdapter` struct in
//! user-crate scope and implements [`generated::exports::actr::workload::workload::Guest`][g]
//! for it. Each trait method delegates to one of the helpers in this
//! module — keeping the macro body terse and the translation logic
//! reviewable here rather than inside a macro expansion.
//!
//! [g]: super::generated::exports::actr::workload::workload::Guest
//!
//! # Singleton workload
//!
//! The generated `Guest` trait methods are **associated functions** on a
//! ZST, not instance methods. actr models a workload as a single owned
//! struct (`impl Workload for MyActor`), so we store a user-constructed
//! instance in a module-scoped `OnceLock`. The `entry!` macro initialises
//! the cell lazily on first access — this matches the pre-Phase-1 semantics
//! where `actr_init` is called once before the first `actr_handle`.
//!
//! # Per-dispatch `WasmContext`
//!
//! Under the 0.2.0 async world each adapter method constructs a fresh
//! [`super::WasmContext`] from the explicit `invocation-ctx` the host
//! threads into every export ([`WasmContext::from_invocation`][fi]). The
//! 0.1.0 getter imports (`get-self-id` / `get-caller-id` / `get-request-id`)
//! are gone; identity arrives as data. The token is carried into every
//! outbound host import so the host routes the right bridge under concurrent
//! in-flight calls.
//!
//! [fi]: super::context::WasmContext::from_invocation

use std::sync::OnceLock;

use actr_protocol::{ActrError, Direction, RpcEnvelope};
use bytes::Bytes;

use crate::workload::{
    BackpressureEvent, CredentialEvent, ErrorCategory, ErrorEvent, PeerEvent, WebRtcPeerStatus,
};
use crate::{MessageDispatcher, Workload};

use super::context::{WasmContext, proto_actr_error_to_wit, wit_actr_error_to_proto};
use super::generated::actr::workload::types as wit_types;

// ─────────────────────────────────────────────────────────────────────────────
// Singleton workload cell
// ─────────────────────────────────────────────────────────────────────────────

/// Typed cell that the `entry!` macro populates on first access.
///
/// The macro generates a module-local `OnceLock<W>` (one per compiled
/// guest) and routes every `Guest` trait method through helpers in this
/// module. We expose the cell + accessor as a generic type so the macro
/// stays small and readable.
pub struct WorkloadCell<W: 'static> {
    cell: OnceLock<W>,
}

impl<W: 'static> WorkloadCell<W> {
    /// Create an empty cell. `const`-constructible so it can live in a
    /// `static` with no runtime init cost.
    pub const fn new() -> Self {
        Self {
            cell: OnceLock::new(),
        }
    }

    /// Return the stored workload, constructing it lazily the first time
    /// this is called. `init` is a zero-argument constructor so the
    /// `entry!` macro can wrap any expression (`Default::default()`, a
    /// specific struct literal, etc.).
    pub fn get_or_init<F: FnOnce() -> W>(&self, init: F) -> &W {
        self.cell.get_or_init(init)
    }
}

impl<W: 'static> Default for WorkloadCell<W> {
    fn default() -> Self {
        Self::new()
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Error translation
// ─────────────────────────────────────────────────────────────────────────────

/// Map an [`ActrError`] to the WIT `actr-error` variant the host expects
/// as the error arm of every fallible export.
pub(crate) fn actr_error_to_wit(e: ActrError) -> wit_types::ActrError {
    proto_actr_error_to_wit(e)
}

/// Map a WIT `actr-error` variant back into [`ActrError`]. Used when the
/// adapter needs to translate host-reported errors that surface through
/// an export's return value (not currently used, but pairs with
/// [`actr_error_to_wit`] and keeps the surface symmetrical).
pub(crate) fn actr_error_from_wit(e: wit_types::ActrError) -> ActrError {
    wit_actr_error_to_proto(e)
}

// ─────────────────────────────────────────────────────────────────────────────
// Event-type translation (WIT ⇄ actr_framework)
// ─────────────────────────────────────────────────────────────────────────────

fn peer_event_from_wit(e: wit_types::PeerEvent) -> PeerEvent {
    PeerEvent {
        peer: super::context_helpers::actr_id_from_wit(&e.peer),
        relayed: e.relayed,
        status: e.status.map(webrtc_peer_status_from_wit),
    }
}

fn webrtc_peer_status_from_wit(s: wit_types::WebrtcPeerStatus) -> WebRtcPeerStatus {
    match s {
        wit_types::WebrtcPeerStatus::Idle => WebRtcPeerStatus::Idle,
        wit_types::WebrtcPeerStatus::Connecting => WebRtcPeerStatus::Connecting,
        wit_types::WebrtcPeerStatus::Connected => WebRtcPeerStatus::Connected,
        wit_types::WebrtcPeerStatus::Recovering => WebRtcPeerStatus::Recovering,
    }
}

fn error_category_from_wit(c: wit_types::ErrorCategory) -> ErrorCategory {
    match c {
        wit_types::ErrorCategory::HandlerPanic => ErrorCategory::HandlerPanic,
        wit_types::ErrorCategory::HandlerError => ErrorCategory::HandlerError,
        wit_types::ErrorCategory::SignalingFailure => ErrorCategory::SignalingFailure,
        wit_types::ErrorCategory::TransportFailure => ErrorCategory::TransportFailure,
        wit_types::ErrorCategory::DataChunkDeliveryUncertain => {
            ErrorCategory::DataChunkDeliveryUncertain
        }
    }
}

fn timestamp_from_wit(t: wit_types::Timestamp) -> std::time::SystemTime {
    std::time::UNIX_EPOCH + std::time::Duration::new(t.seconds, t.nanoseconds)
}

fn error_event_from_wit(e: wit_types::ErrorEvent) -> ErrorEvent {
    ErrorEvent {
        source: actr_error_from_wit(e.source),
        category: error_category_from_wit(e.category),
        context: e.context,
        timestamp: timestamp_from_wit(e.timestamp),
    }
}

fn credential_event_from_wit(e: wit_types::CredentialEvent) -> CredentialEvent {
    CredentialEvent {
        new_expiry: timestamp_from_wit(e.new_expiry),
    }
}

fn backpressure_event_from_wit(e: wit_types::BackpressureEvent) -> BackpressureEvent {
    BackpressureEvent {
        queue_len: e.queue_len as usize,
        threshold: e.threshold as usize,
    }
}

fn rpc_envelope_from_wit(envelope: wit_types::RpcEnvelope) -> RpcEnvelope {
    RpcEnvelope {
        request_id: envelope.request_id,
        route_key: envelope.route_key,
        payload: if envelope.payload.is_empty() {
            None
        } else {
            Some(Bytes::from(envelope.payload))
        },
        direction: Some(Direction::Request as i32),
        ..Default::default()
    }
}

// ─────────────────────────────────────────────────────────────────────────────
// Dispatch helpers — called from the generated Guest trait impl
// ─────────────────────────────────────────────────────────────────────────────

/// Bridge from the generated `dispatch(envelope)` export into the
/// user's [`MessageDispatcher`].
///
/// Reads the singleton workload, builds a fresh [`WasmContext`] from
/// the host's per-call context accessors, decodes the WIT envelope,
/// and invokes `<W::Dispatcher as MessageDispatcher>::dispatch`. The
/// handler's `ActorResult<Bytes>` is translated into the WIT
/// `result<list<u8>, actr-error>` shape.
pub async fn run_dispatch<W: Workload>(
    workload: &W,
    envelope: wit_types::RpcEnvelope,
    inv: wit_types::InvocationCtx,
) -> Result<Vec<u8>, wit_types::ActrError> {
    let ctx = WasmContext::from_invocation(inv);
    let envelope = rpc_envelope_from_wit(envelope);
    match <W::Dispatcher as MessageDispatcher>::dispatch(workload, envelope, &ctx).await {
        Ok(bytes) => Ok(bytes.to_vec()),
        Err(e) => Err(actr_error_to_wit(e)),
    }
}

// ── on_start / on_ready / on_stop / on_error (4, fallible) ───────────────
//
// Each lifecycle hook is inlined rather than funnelled through a
// closure-taking `run_lifecycle_hook` helper. Rust's async lifetimes
// would otherwise force an HRTB shape that fights with `&ctx` crossing
// await points; inlining here keeps the type-check trivial.
//
// The native host installs a synthetic `InvocationContext` while invoking
// package lifecycle exports. Read it through the same context imports as
// dispatch so wasm, dynclib, and linked workloads observe matching
// `self_id` / `caller_id` / `request_id` semantics.

pub async fn run_on_start<W: Workload>(
    workload: &W,
    inv: wit_types::InvocationCtx,
) -> Result<(), wit_types::ActrError> {
    let ctx = WasmContext::from_invocation(inv);
    match workload.on_start(&ctx).await {
        Ok(()) => Ok(()),
        Err(e) => Err(actr_error_to_wit(e)),
    }
}

pub async fn run_on_ready<W: Workload>(
    workload: &W,
    inv: wit_types::InvocationCtx,
) -> Result<(), wit_types::ActrError> {
    let ctx = WasmContext::from_invocation(inv);
    match workload.on_ready(&ctx).await {
        Ok(()) => Ok(()),
        Err(e) => Err(actr_error_to_wit(e)),
    }
}

pub async fn run_on_stop<W: Workload>(
    workload: &W,
    inv: wit_types::InvocationCtx,
) -> Result<(), wit_types::ActrError> {
    let ctx = WasmContext::from_invocation(inv);
    match workload.on_stop(&ctx).await {
        Ok(()) => Ok(()),
        Err(e) => Err(actr_error_to_wit(e)),
    }
}

pub async fn run_on_error<W: Workload>(
    workload: &W,
    event: wit_types::ErrorEvent,
    inv: wit_types::InvocationCtx,
) -> Result<(), wit_types::ActrError> {
    let ctx = WasmContext::from_invocation(inv);
    let event = error_event_from_wit(event);
    match workload.on_error(&ctx, &event).await {
        Ok(()) => Ok(()),
        Err(e) => Err(actr_error_to_wit(e)),
    }
}

// ── Signaling (3, infallible) ─────────────────────────────────────────────
//
// `on_signaling_connecting` / `on_signaling_connected` take `Option<&C>`;
// the hosted Component has no way to signal "no context" because the
// context accessors always consult the active invocation. For now we
// always pass `Some(&ctx)` — matches the behaviour during reconnects,
// which is where these hooks actually surface. The initial-connection
// case (where the pre-Phase-1 host passed `None`) does not fire through
// the guest runtime path.

pub async fn run_on_signaling_connecting<W: Workload>(workload: &W, inv: wit_types::InvocationCtx) {
    let ctx = WasmContext::from_invocation(inv);
    workload.on_signaling_connecting(Some(&ctx)).await;
}

pub async fn run_on_signaling_connected<W: Workload>(workload: &W, inv: wit_types::InvocationCtx) {
    let ctx = WasmContext::from_invocation(inv);
    workload.on_signaling_connected(Some(&ctx)).await;
}

pub async fn run_on_signaling_disconnected<W: Workload>(
    workload: &W,
    inv: wit_types::InvocationCtx,
) {
    let ctx = WasmContext::from_invocation(inv);
    workload.on_signaling_disconnected(&ctx).await;
}

// ── WebSocket (3, infallible) ─────────────────────────────────────────────

pub async fn run_on_websocket_connecting<W: Workload>(
    workload: &W,
    event: wit_types::PeerEvent,
    inv: wit_types::InvocationCtx,
) {
    let ctx = WasmContext::from_invocation(inv);
    let event = peer_event_from_wit(event);
    workload.on_websocket_connecting(&ctx, &event).await;
}

pub async fn run_on_websocket_connected<W: Workload>(
    workload: &W,
    event: wit_types::PeerEvent,
    inv: wit_types::InvocationCtx,
) {
    let ctx = WasmContext::from_invocation(inv);
    let event = peer_event_from_wit(event);
    workload.on_websocket_connected(&ctx, &event).await;
}

pub async fn run_on_websocket_disconnected<W: Workload>(
    workload: &W,
    event: wit_types::PeerEvent,
    inv: wit_types::InvocationCtx,
) {
    let ctx = WasmContext::from_invocation(inv);
    let event = peer_event_from_wit(event);
    workload.on_websocket_disconnected(&ctx, &event).await;
}

// ── WebRTC P2P (3, infallible) ────────────────────────────────────────────

pub async fn run_on_webrtc_connecting<W: Workload>(
    workload: &W,
    event: wit_types::PeerEvent,
    inv: wit_types::InvocationCtx,
) {
    let ctx = WasmContext::from_invocation(inv);
    let event = peer_event_from_wit(event);
    workload.on_webrtc_connecting(&ctx, &event).await;
}

pub async fn run_on_webrtc_connected<W: Workload>(
    workload: &W,
    event: wit_types::PeerEvent,
    inv: wit_types::InvocationCtx,
) {
    let ctx = WasmContext::from_invocation(inv);
    let event = peer_event_from_wit(event);
    workload.on_webrtc_connected(&ctx, &event).await;
}

pub async fn run_on_webrtc_disconnected<W: Workload>(
    workload: &W,
    event: wit_types::PeerEvent,
    inv: wit_types::InvocationCtx,
) {
    let ctx = WasmContext::from_invocation(inv);
    let event = peer_event_from_wit(event);
    workload.on_webrtc_disconnected(&ctx, &event).await;
}

// ── Credential (2, infallible) ────────────────────────────────────────────

pub async fn run_on_credential_renewed<W: Workload>(
    workload: &W,
    event: wit_types::CredentialEvent,
    inv: wit_types::InvocationCtx,
) {
    let ctx = WasmContext::from_invocation(inv);
    let event = credential_event_from_wit(event);
    workload.on_credential_renewed(&ctx, &event).await;
}

pub async fn run_on_credential_expiring<W: Workload>(
    workload: &W,
    event: wit_types::CredentialEvent,
    inv: wit_types::InvocationCtx,
) {
    let ctx = WasmContext::from_invocation(inv);
    let event = credential_event_from_wit(event);
    workload.on_credential_expiring(&ctx, &event).await;
}

// ── Mailbox (1, infallible) ───────────────────────────────────────────────

pub async fn run_on_mailbox_backpressure<W: Workload>(
    workload: &W,
    event: wit_types::BackpressureEvent,
    inv: wit_types::InvocationCtx,
) {
    let ctx = WasmContext::from_invocation(inv);
    let event = backpressure_event_from_wit(event);
    workload.on_mailbox_backpressure(&ctx, &event).await;
}

pub async fn run_on_data_chunk(
    chunk: wit_types::DataChunk,
    sender: wit_types::ActrId,
    _inv: wit_types::InvocationCtx,
) -> Result<(), wit_types::ActrError> {
    // The registered stream callback receives (chunk, sender) only; it has
    // no fresh `Context`, so the invocation-ctx is accepted for contract
    // uniformity but not consumed on this path. The host temporarily aliases
    // the registering context's retired token to this DataChunk invocation's
    // live bridge while the callback runs.
    super::context::dispatch_registered_stream(chunk, sender)
        .await
        .map_err(actr_error_to_wit)
}

// (`complete_sync` was removed with the M4 switch to the 0.2.0 async world:
// the guest exports are now real `async func`, driven to completion by the
// host's `Store::run_concurrent` region rather than by an in-guest poll
// loop. Each adapter helper above is a plain `async fn` awaited directly by
// the generated `Guest` impl.)