Skip to content

Commit e1de3db

Browse files
committed
Add session lifecycle truapi interface
1 parent c606f76 commit e1de3db

11 files changed

Lines changed: 371 additions & 1 deletion

File tree

docs/rfcs/0016-product-session-restore.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ enum SessionLifecycleEvent {
194194
struct SessionLifecycleRequest {
195195
event_id: SessionLifecycleEventId,
196196
reason: SessionLifecycleReason,
197-
deadline_ms: Option<u32>,
197+
deadline_ms: Option<TimestampMs>,
198198
}
199199

200200
enum SessionLifecycleReason {

js/packages/truapi/src/generated/client.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,80 @@ export class ResourceAllocationClient {
11201120
}
11211121
}
11221122

1123+
/**
1124+
* Product session lifecycle operations.
1125+
*
1126+
* Default methods return [`CallError::HostFailure`] with an `unavailable`
1127+
* reason. Hosts override when they support product session restore.
1128+
*/
1129+
export class SessionClient {
1130+
constructor(private readonly transport: TrUApiTransport) {}
1131+
1132+
/**
1133+
* Subscribe to host lifecycle signals so a product can checkpoint semantic
1134+
* session state through scoped local storage before suspend, eviction, or
1135+
* close.
1136+
*
1137+
* ```truapi-client-example
1138+
* import {
1139+
* type Client,
1140+
* type HostSessionLifecycleSubscribeError,
1141+
* type HostSessionLifecycleSubscribeItem,
1142+
* type Subscription,
1143+
* type SubscriptionError,
1144+
* } from "@parity/truapi";
1145+
*
1146+
* export function watchSessionLifecycle(truapi: Client): Subscription {
1147+
* return truapi.session
1148+
* .sessionLifecycleSubscribe({
1149+
* request: { replayCurrentState: true },
1150+
* })
1151+
* .subscribe({
1152+
* next: (event: HostSessionLifecycleSubscribeItem) =>
1153+
* console.log(event),
1154+
* error: (error: SubscriptionError<HostSessionLifecycleSubscribeError>) =>
1155+
* console.error(error),
1156+
* complete: () => console.log("completed"),
1157+
* });
1158+
* }
1159+
* ```
1160+
*/
1161+
lifecycleSubscribe({
1162+
request,
1163+
}: {
1164+
request: T.HostSessionLifecycleSubscribeRequest;
1165+
}): ObservableLike<
1166+
T.HostSessionLifecycleSubscribeItem,
1167+
T.HostSessionLifecycleSubscribeError
1168+
> {
1169+
return createObservable<
1170+
T.HostSessionLifecycleSubscribeItem,
1171+
T.HostSessionLifecycleSubscribeError
1172+
>({
1173+
transport: this.transport,
1174+
ids: W.SESSION_LIFECYCLE_SUBSCRIBE,
1175+
payload: T.VersionedHostSessionLifecycleSubscribeRequest.enc({
1176+
tag: "V1",
1177+
value: request,
1178+
}),
1179+
decodeItem: (payload) =>
1180+
(
1181+
T.VersionedHostSessionLifecycleSubscribeItem.dec(payload) as {
1182+
tag: "V1";
1183+
value: T.HostSessionLifecycleSubscribeItem;
1184+
} & T.VersionedHostSessionLifecycleSubscribeItem
1185+
).value,
1186+
decodeInterrupt: (payload) =>
1187+
(
1188+
T.VersionedHostSessionLifecycleSubscribeError.dec(payload) as {
1189+
tag: "V1";
1190+
value: T.HostSessionLifecycleSubscribeError;
1191+
} & T.VersionedHostSessionLifecycleSubscribeError
1192+
).value,
1193+
});
1194+
}
1195+
}
1196+
11231197
/** Signing operations. */
11241198
export class SigningClient {
11251199
constructor(private readonly transport: TrUApiTransport) {}
@@ -1526,6 +1600,7 @@ export interface TrUApiClient {
15261600
readonly permissions: PermissionsClient;
15271601
readonly preimage: PreimageClient;
15281602
readonly resourceAllocation: ResourceAllocationClient;
1603+
readonly session: SessionClient;
15291604
readonly signing: SigningClient;
15301605
readonly statementStore: StatementStoreClient;
15311606
readonly system: SystemClient;
@@ -1567,6 +1642,7 @@ export function createClient(
15671642
permissions: new PermissionsClient(versionedTransport),
15681643
preimage: new PreimageClient(versionedTransport),
15691644
resourceAllocation: new ResourceAllocationClient(versionedTransport),
1645+
session: new SessionClient(versionedTransport),
15701646
signing: new SigningClient(versionedTransport),
15711647
statementStore: new StatementStoreClient(versionedTransport),
15721648
system: new SystemClient(versionedTransport),

js/packages/truapi/src/generated/types.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1580,6 +1580,48 @@ export const VersionedHostRequestResourceAllocationResponse: S.Codec<VersionedHo
15801580
}),
15811581
);
15821582

1583+
/** Versioned envelope for [`HostSessionLifecycleSubscribeError`]. */
1584+
export type VersionedHostSessionLifecycleSubscribeError = {
1585+
tag: "V1";
1586+
value: HostSessionLifecycleSubscribeError;
1587+
};
1588+
1589+
export const VersionedHostSessionLifecycleSubscribeError: S.Codec<VersionedHostSessionLifecycleSubscribeError> =
1590+
S.lazy(
1591+
(): S.Codec<VersionedHostSessionLifecycleSubscribeError> =>
1592+
S.indexedTaggedUnion({
1593+
V1: [0, HostSessionLifecycleSubscribeError] as const,
1594+
}),
1595+
);
1596+
1597+
/** Versioned envelope for [`HostSessionLifecycleSubscribeItem`]. */
1598+
export type VersionedHostSessionLifecycleSubscribeItem = {
1599+
tag: "V1";
1600+
value: HostSessionLifecycleSubscribeItem;
1601+
};
1602+
1603+
export const VersionedHostSessionLifecycleSubscribeItem: S.Codec<VersionedHostSessionLifecycleSubscribeItem> =
1604+
S.lazy(
1605+
(): S.Codec<VersionedHostSessionLifecycleSubscribeItem> =>
1606+
S.indexedTaggedUnion({
1607+
V1: [0, HostSessionLifecycleSubscribeItem] as const,
1608+
}),
1609+
);
1610+
1611+
/** Versioned envelope for [`HostSessionLifecycleSubscribeRequest`]. */
1612+
export type VersionedHostSessionLifecycleSubscribeRequest = {
1613+
tag: "V1";
1614+
value: HostSessionLifecycleSubscribeRequest;
1615+
};
1616+
1617+
export const VersionedHostSessionLifecycleSubscribeRequest: S.Codec<VersionedHostSessionLifecycleSubscribeRequest> =
1618+
S.lazy(
1619+
(): S.Codec<VersionedHostSessionLifecycleSubscribeRequest> =>
1620+
S.indexedTaggedUnion({
1621+
V1: [0, HostSessionLifecycleSubscribeRequest] as const,
1622+
}),
1623+
);
1624+
15831625
/** Versioned envelope for [`HostSignPayloadError`]. */
15841626
export type VersionedHostSignPayloadError = {
15851627
tag: "V1";
@@ -2743,6 +2785,53 @@ export const RuntimeType: S.Codec<RuntimeType> = S.lazy(
27432785
}),
27442786
);
27452787

2788+
/** Host-assigned stable identifier for one lifecycle event. */
2789+
export type SessionLifecycleEventId = string;
2790+
2791+
export const SessionLifecycleEventId: S.Codec<SessionLifecycleEventId> = S.lazy(
2792+
(): S.Codec<SessionLifecycleEventId> => S.str,
2793+
);
2794+
2795+
/** Reason for a lifecycle checkpoint request. */
2796+
export type SessionLifecycleReason =
2797+
| "AppSwitcher"
2798+
| "HostBackgrounded"
2799+
| "HostTerminating"
2800+
| "MemoryPressure"
2801+
| "UserClosedProduct"
2802+
| "HostPolicy";
2803+
2804+
export const SessionLifecycleReason: S.Codec<SessionLifecycleReason> = S.lazy(
2805+
(): S.Codec<SessionLifecycleReason> =>
2806+
S.Status(
2807+
"AppSwitcher",
2808+
"HostBackgrounded",
2809+
"HostTerminating",
2810+
"MemoryPressure",
2811+
"UserClosedProduct",
2812+
"HostPolicy",
2813+
),
2814+
);
2815+
2816+
/** Details for a single lifecycle checkpoint request. */
2817+
export interface SessionLifecycleRequest {
2818+
/** Host-assigned event id for de-duplicating repeated notifications. */
2819+
eventId: SessionLifecycleEventId;
2820+
/** Reason the host is asking the product to checkpoint state. */
2821+
reason: SessionLifecycleReason;
2822+
/** Best-effort deadline for checkpoint completion. */
2823+
deadlineMs?: TimestampMs;
2824+
}
2825+
2826+
export const SessionLifecycleRequest: S.Codec<SessionLifecycleRequest> = S.lazy(
2827+
(): S.Codec<SessionLifecycleRequest> =>
2828+
S.Struct({
2829+
eventId: SessionLifecycleEventId,
2830+
reason: SessionLifecycleReason,
2831+
deadlineMs: S.Option(TimestampMs),
2832+
}) as S.Codec<SessionLifecycleRequest>,
2833+
);
2834+
27462835
/** Shape for borders and backgrounds. */
27472836
export type Shape =
27482837
/** Border radius value. */
@@ -2946,6 +3035,13 @@ export const Theme: S.Codec<Theme> = S.lazy(
29463035
(): S.Codec<Theme> => S.Status("Light", "Dark"),
29473036
);
29483037

3038+
/** Milliseconds since the Unix epoch. */
3039+
export type TimestampMs = bigint;
3040+
3041+
export const TimestampMs: S.Codec<TimestampMs> = S.lazy(
3042+
(): S.Codec<TimestampMs> => S.u64,
3043+
);
3044+
29493045
/** 32-byte statement topic. */
29503046
export type Topic = HexString;
29513047

@@ -3993,6 +4089,52 @@ export const HostRequestResourceAllocationResponse: S.Codec<HostRequestResourceA
39934089
}) as S.Codec<HostRequestResourceAllocationResponse>,
39944090
);
39954091

4092+
/** Error from session lifecycle subscription setup. */
4093+
export type HostSessionLifecycleSubscribeError =
4094+
/** The host does not support product session lifecycle events. */
4095+
| { tag: "Unsupported"; value?: undefined }
4096+
/** Catch-all. */
4097+
| { tag: "Unknown"; value: GenericErr };
4098+
4099+
export const HostSessionLifecycleSubscribeError: S.Codec<HostSessionLifecycleSubscribeError> =
4100+
S.lazy(
4101+
(): S.Codec<HostSessionLifecycleSubscribeError> =>
4102+
S.TaggedUnion({ Unsupported: S._void, Unknown: GenericErr }),
4103+
);
4104+
4105+
/** Lifecycle event emitted by the host before a product transition. */
4106+
export type HostSessionLifecycleSubscribeItem =
4107+
/** The product should checkpoint state before it is suspended. */
4108+
| { tag: "WillSuspend"; value: SessionLifecycleRequest }
4109+
/** The product should checkpoint state before its WebView may be evicted. */
4110+
| { tag: "WillEvict"; value: SessionLifecycleRequest }
4111+
/** The product should checkpoint state before it is closed. */
4112+
| { tag: "WillClose"; value: SessionLifecycleRequest };
4113+
4114+
export const HostSessionLifecycleSubscribeItem: S.Codec<HostSessionLifecycleSubscribeItem> =
4115+
S.lazy(
4116+
(): S.Codec<HostSessionLifecycleSubscribeItem> =>
4117+
S.TaggedUnion({
4118+
WillSuspend: SessionLifecycleRequest,
4119+
WillEvict: SessionLifecycleRequest,
4120+
WillClose: SessionLifecycleRequest,
4121+
}),
4122+
);
4123+
4124+
/** Request to subscribe to host session lifecycle events. */
4125+
export interface HostSessionLifecycleSubscribeRequest {
4126+
/** Ask the host to replay the current lifecycle state when one is active. */
4127+
replayCurrentState: boolean;
4128+
}
4129+
4130+
export const HostSessionLifecycleSubscribeRequest: S.Codec<HostSessionLifecycleSubscribeRequest> =
4131+
S.lazy(
4132+
(): S.Codec<HostSessionLifecycleSubscribeRequest> =>
4133+
S.Struct({
4134+
replayCurrentState: S.bool,
4135+
}) as S.Codec<HostSessionLifecycleSubscribeRequest>,
4136+
);
4137+
39964138
export type HostSignPayloadError =
39974139
| { tag: "FailedToDecode"; value?: undefined }
39984140
| { tag: "Rejected"; value?: undefined }

js/packages/truapi/src/generated/wire-table.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,10 @@ export const STATEMENT_STORE_CREATE_PROOF_AUTHORIZED = {
306306
request: 132,
307307
response: 133,
308308
} as const satisfies RequestFrameIds;
309+
310+
export const SESSION_LIFECYCLE_SUBSCRIBE = {
311+
start: 162,
312+
stop: 163,
313+
interrupt: 164,
314+
receive: 165,
315+
} as const satisfies SubscriptionFrameIds;

js/packages/truapi/src/playground/codegen/services.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,18 @@ export const services: ServiceInfo[] = [
365365
},
366366
],
367367
},
368+
{
369+
name: "Session",
370+
methods: [
371+
{
372+
name: "lifecycle_subscribe",
373+
type: "subscription",
374+
description:
375+
'Subscribe to host lifecycle signals so a product can checkpoint semantic\nsession state through scoped local storage before suspend, eviction, or\nclose.\n\n```truapi-client-example\nimport {\n type Client,\n type HostSessionLifecycleSubscribeError,\n type HostSessionLifecycleSubscribeItem,\n type Subscription,\n type SubscriptionError,\n} from "@parity/truapi";\n\nexport function watchSessionLifecycle(truapi: Client): Subscription {\n return truapi.session\n .sessionLifecycleSubscribe({\n request: { replayCurrentState: true },\n })\n .subscribe({\n next: (event: HostSessionLifecycleSubscribeItem) =>\n console.log(event),\n error: (error: SubscriptionError<HostSessionLifecycleSubscribeError>) =>\n console.error(error),\n complete: () => console.log("completed"),\n });\n}\n```',
376+
requestDescription: "HostSessionLifecycleSubscribeRequest",
377+
},
378+
],
379+
},
368380
{
369381
name: "Signing",
370382
methods: [

rust/crates/truapi/src/api/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub mod payment;
1010
pub mod permissions;
1111
pub mod preimage;
1212
pub mod resource_allocation;
13+
pub mod session;
1314
pub mod signing;
1415
pub mod statement_store;
1516
pub mod system;
@@ -25,6 +26,7 @@ pub use payment::Payment;
2526
pub use permissions::Permissions;
2627
pub use preimage::Preimage;
2728
pub use resource_allocation::ResourceAllocation;
29+
pub use session::Session;
2830
pub use signing::Signing;
2931
pub use statement_store::StatementStore;
3032
pub use system::System;
@@ -42,6 +44,7 @@ pub trait TrUApi:
4244
+ Permissions
4345
+ Preimage
4446
+ ResourceAllocation
47+
+ Session
4548
+ Signing
4649
+ StatementStore
4750
+ System
@@ -62,6 +65,7 @@ impl<T> TrUApi for T where
6265
+ Permissions
6366
+ Preimage
6467
+ ResourceAllocation
68+
+ Session
6569
+ Signing
6670
+ StatementStore
6771
+ System
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
//! Unified [`Session`] trait.
2+
3+
use crate::versioned::session::{
4+
HostSessionLifecycleSubscribeError, HostSessionLifecycleSubscribeItem,
5+
HostSessionLifecycleSubscribeRequest,
6+
};
7+
use crate::wire;
8+
use crate::{CallContext, CallError, Subscription};
9+
10+
/// Product session lifecycle operations.
11+
///
12+
/// Default methods return [`CallError::HostFailure`] with an `unavailable`
13+
/// reason. Hosts override when they support product session restore.
14+
pub trait Session: Send + Sync {
15+
/// Subscribe to host lifecycle signals so a product can checkpoint semantic
16+
/// session state through scoped local storage before suspend, eviction, or
17+
/// close.
18+
///
19+
/// ```truapi-client-example
20+
/// import {
21+
/// type Client,
22+
/// type HostSessionLifecycleSubscribeError,
23+
/// type HostSessionLifecycleSubscribeItem,
24+
/// type Subscription,
25+
/// type SubscriptionError,
26+
/// } from "@parity/truapi";
27+
///
28+
/// export function watchSessionLifecycle(truapi: Client): Subscription {
29+
/// return truapi.session
30+
/// .sessionLifecycleSubscribe({
31+
/// request: { replayCurrentState: true },
32+
/// })
33+
/// .subscribe({
34+
/// next: (event: HostSessionLifecycleSubscribeItem) =>
35+
/// console.log(event),
36+
/// error: (error: SubscriptionError<HostSessionLifecycleSubscribeError>) =>
37+
/// console.error(error),
38+
/// complete: () => console.log("completed"),
39+
/// });
40+
/// }
41+
/// ```
42+
#[wire(start_id = 162)]
43+
async fn lifecycle_subscribe(
44+
&self,
45+
_cx: &CallContext,
46+
_request: HostSessionLifecycleSubscribeRequest,
47+
) -> Result<
48+
Subscription<HostSessionLifecycleSubscribeItem>,
49+
CallError<HostSessionLifecycleSubscribeError>,
50+
> {
51+
Err(CallError::unavailable())
52+
}
53+
}

0 commit comments

Comments
 (0)