From b5d3c597d7dacda949dfce89921248b54c410dc2 Mon Sep 17 00:00:00 2001 From: katfang Date: Thu, 16 Jul 2026 15:46:50 -0400 Subject: [PATCH 1/4] React Presence Library changes * Change MouseArrow to not implicitly rely on tailwind. * Don't require children for MouseTracker --- reboot/std/react/presence/index.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/reboot/std/react/presence/index.tsx b/reboot/std/react/presence/index.tsx index 642b4f49..4e1499a2 100644 --- a/reboot/std/react/presence/index.tsx +++ b/reboot/std/react/presence/index.tsx @@ -118,8 +118,9 @@ const MouseArrow: FC<{ id: string; arrow: ReactNode }> = ({ id, arrow }) => { return (
= ({ arrow, className, style, children }) => { const { subscriberId, subscriberIds } = usePresenceContext(); From aa2c705e443e3168620d07dbc88f151e9adce304 Mon Sep 17 00:00:00 2001 From: katfang Date: Thu, 16 Jul 2026 15:47:53 -0400 Subject: [PATCH 2/4] Quick Fixes * Queue tests: comment fix * OrderedMap docs: fix broken link --- tests/reboot/std/collections/queue/v1/queue_tests.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/reboot/std/collections/queue/v1/queue_tests.py b/tests/reboot/std/collections/queue/v1/queue_tests.py index 78f2ce17..b7cbd5ef 100644 --- a/tests/reboot/std/collections/queue/v1/queue_tests.py +++ b/tests/reboot/std/collections/queue/v1/queue_tests.py @@ -4,7 +4,7 @@ from reboot.aio.tests import Reboot from reboot.protobuf import as_int, as_str, from_int, from_str, pack, unpack -# Import used in Queue documentation, so we want to keep them +# Import used in Queue documentation, so we want to keep them separate. # isort: off from reboot.std.collections.queue.v1.queue import Queue from reboot.std.item.v1.item import Item From 7094dc9845d8cfcac109242bf30213d7e8f2a93d Mon Sep 17 00:00:00 2001 From: katfang Date: Thu, 16 Jul 2026 15:50:30 -0400 Subject: [PATCH 3/4] Presence docs --- .../docs/library_services/presence.mdx | 488 ++++++++++++++++++ documentation/sidebars.js | 1 + tests/reboot/std/presence/presence_tests.py | 12 +- tests/reboot/std/presence/presence_tests.ts | 4 +- 4 files changed, 501 insertions(+), 4 deletions(-) create mode 100644 documentation/docs/library_services/presence.mdx diff --git a/documentation/docs/library_services/presence.mdx b/documentation/docs/library_services/presence.mdx new file mode 100644 index 00000000..5163db05 --- /dev/null +++ b/documentation/docs/library_services/presence.mdx @@ -0,0 +1,488 @@ +import Tabs from "@theme/Tabs"; +import TabItem from "@theme/TabItem"; + +# Presence + +Tracks which subscribers (e.g. users, browser tabs, or other clients) are +currently connected, automatically detecting when a connection has dropped. + +The library is composed of three servicers: +- `Presence`: Each `Presence` instance (identified by an `ID`) tracks a set of + currently online subscribers. To track online presence in different instances + (e.g. multiple chat rooms or shared documents), use a different `ID` for each + `Presence` instance. +- `Subscriber`: Represents an individual client connection. +- `MousePosition`: Optional servicer for tracking and syncing mouse cursor + position in real time. + +This library also has build in [React](#react) support so you can quickly drop +these features into your React app. + + + +## Imports and set up + +To use `Presence`, include its library when starting up your `Application`. + + + +```py +from reboot.std.presence.v1.presence import presence_library + +async def main(): + application = Application( + servicers=[MyServicer], + libraries=[presence_library()], + ) + await application.run() +``` + + +```ts +import { presenceLibrary } from "@reboot-dev/reboot-std/presence/v1"; + +new Application({ + servicers: [MyServicer], + libraries: [presenceLibrary()] + initialize, +}).run(); +``` + + + +### Authorizer + +By default, `Presence` allows all calls coming internally (e.g. from +your Reboot backend) or externally (e.g. from your React app). However, +this can be overridden by providing your own +[authorizer](/learn_more/auth#authorizers). + + + +```py +presence_authorizer = Presence.Authorizer( + ... # !!! TODO: FILL OUT, also, which of these are actually necessary for react? +) + +subscriber_authorizer = Subscriber.Authorizer( + ... +) + +mouse_position_authorizer = MousePosition.Authorizer( + ... +) + +application = Application( + servicers=[MyServicer], + libraries=[presence_library( + presence_authorizer=presence_authorizer, + subscriber_authorizer=subscriber_authorizer, + mouse_position_authorizer=mouse_position_authorizer, + )], +) +``` + + +```ts +const presenceAuthorizer = new Presence.Authorizer({ + ... // !!! TODO: FILL OUT, also, which of these are actually necessary for react? +}); + +const subscriberAuthorizer = new Subscriber.Authorizer({ + ... +}); + +const mousePositionAuthorizer = new MousePosition.Authorizer({ + ... +}); + +const application = new Application({ + servicers: [MyServicer], + libraries: [presenceLibrary({ + presenceAuthorizer, + subscriberAuthorizer, + mousePositionAuthorizer, + })], + initialize, +}); + +``` + + + +## React + +Once you've [set up](/learn_more/call/from_react) your React app to call +into your Reboot API, you can use the `Presence` React library to quickly +add presence tracking in your React app. + +First, add `@reboot-dev/reboot-std-api` and +`@reboot-dev/reboot-std-react` to your `package.json`. + +```bash +!!! TODO: what about reboot-react or reboot-std +npm install -S @reboot-dev/reboot-std-api +npm install -S reboot-dev/reboot-std-react +``` + +You may also want to, in defining your Reboot application, wish to [override +the default authorizer](#authorizer). + +The [`Presence`](#presence-component) and +[`MouseTracker`](#mousetracker-component) components allow you to quickly add +these features to your app. However, you can also call the library +[methods](#direct-library-usage) directly from React. See [Call your API from +React](/learn_more/call/from_react) to learn more about how to do this. + +### Presence Component + +The `Presence` React component is used to +1. Track the current client who is online, identified by a `subscriberId`. +2. Receive a list of subscribers by ID who are currently connected. + +Place any components that need presence data in a `Presence` component. +The children components can then call `usePresenceContext()` to get +the list of online subscribers by ID. + +#### Properties of <Presence> +* `presenceId` is the `string` ID of the `Presence` instance you want to track +the subscriber for. +* `subscriberId` is the `string` ID of the `Subscriber` who is connecting. + +#### Properties returned from `usePresenceContext()` +* `subscriberId` is the `string` ID `Subscriber` who is connecting. +* `subscriberIds` is a `string[]` of `Subscribers` who are connected. + +```tsx +import { + Presence, + usePresenceContext, +} from "@reboot-dev/reboot-std-react/presence"; + +// ... + + + + + +// ... + +const SubscriberList: FC<{}> = () => { + const { subscriberId, subscriberIds } = usePresenceContext(); + + return ( +
    + {subscriberIds.map((id) => ( +
  • {id === subscriberId ? `${id} (you)` : id}
  • + ))} +
+ ); +}; +``` + +### MouseTracker Component + +The `MouseTracker` component can be added as a child to the `Presence` +component to sync mouse positions across clients. + +The `arrow` prop defines how the other synchronized cursors will appear. + +```tsx + + ↖} + /> + +``` + + +## Direct Library Usage + +If you want to use the `Presence` library directly, such as access the +information server side, you can call the Reboot methods directly as usual. + +### Imports + +To use `Presence`, `Subscriber` or `MousePosition` servicers, import them where +you would like to use it. + + + + + + +```py +from reboot.std.presence.v1.presence import ( + MousePosition, + Presence, + Subscriber, +) +``` + + + + + + + +```ts +import { MousePosition } from "@reboot-dev/reboot-std/presence/mouse_tracker/v1"; +import { Subscriber } from "@reboot-dev/reboot-std/presence/subscriber/v1"; +import { Presence } from "@reboot-dev/reboot-std/presence/v1"; +``` + + + + + +## Presence Methods + +Each `Presence` instance tracks a set of currently online subscribers. You +can have as many `Presence` instances as you like, each with a unique ID +(e.g. one per room, channel, or document). + +### Getting a reference + + + +```py +presence = Presence.ref("my-room") +``` + + +```ts +const presence = Presence.ref("my-room"); +``` + + + +### Subscribe + +Register a subscriber as present. The subscriber's [`status()`](#status) must +already be `present` or you will receive a `FailedPrecondition` error. + +If the subscriber disconnects, they will automatically be removed from the list +of online subscribers. + + + +```py +await presence.subscribe( + context, + subscriber_id="user-abc", +) +``` + + +```ts +await presence.subscribe(context, { + subscriberId: "user-abc", +}); +``` + + + +### List + +Return the IDs of all the subscribers currently connected. + + + +```py +response = await presence.list(context) +print(response.subscriber_ids) +``` + + +```ts +const { subscriberIds } = await presence.list(context); +console.log(subscriberIds); +``` + + + +## Subscriber Methods + +Each `Subscriber` represents a single connected client. + +You may want to use one `Subscriber` per user or per connection. For example, +if you want to list which users are in a chat room without listing the user +twice if they are connected via computer or mobile device, use one `Subscriber` +per user. If instead you want to show the user as appearing 5 times when they +have 5 browser tabs open to your app, you would want to use one `Subscriber` for +each of those browser tab connections. + +One `Subscriber` can be added to multiple `Presence` instances (e.g. to show +a user is generally online and in specific chat rooms). + +### Getting a reference + + + +```py +subscriber = Subscriber.ref("user-abc") +``` + + +```ts +const subscriber = Subscriber.ref("user-abc"); +``` + + + +### Create + +Creates the `Subscriber` or ensures that it has already been created. +This needs to be called before calling [`connect()`](#connect), and it is +recommended to make this call before any [`connect()`](#connect) attempt. + + + +```py +await subscriber.create(context) +``` + + +```ts +await subscriber.create(context); +``` + + + + +### Connect + +Used to initiate a subscription and detects when the subscriber is no longer +present. + +This method runs indefinitely, that is, it does return. It also requires a +unique nonce for every call. + + + +```py +nonce = uuid.uuid4().hex + +# TODO !!! CHECK THIS + +# connect() runs indefinitely; run it concurrently with toggle(). +asyncio.gather( + subscriber.connect(context, nonce=nonce), + subscriber.toggle(context, nonce=nonce), +) +``` + + +```ts +// TODO !!! CHECK THIS + +const nonce = uuidv4(); + +// connect() runs indefinitely; run it concurrently with toggle(). +const abortController = new AbortController(); +Promise.all([ + subscriber.connect( + { signal: abortController.signal }, + { nonce }, + ), + subscriber.toggle(context, { nonce }), +]); +``` + + + +### Toggle + +Required to confirm that [`connect()`](#connect) for a given `nonce` is live. +The [`status()`](#status) of the `Subscriber` is not set to present *until* `toggle()` call +is made. + +Requires the same `nonce` that was used for the [`connect()`](#connect) call. + +See [`connect()`](#connect) for concurrent call example. + +### Status + +Returns the `status` of the `Subscriber`. `present` is true if any of the +`Subscriber`'s connections (made via a [`connect()`](#connect) and +[`toggle()`](#toggle)) are still live. Otherwise returns false. + + + +```py +response = await subscriber.status(context) +print(response.present) # True if connected +``` + + +```ts +const { present } = await subscriber.status(context); +console.log(present); // true if connected +``` + + + +## MousePosition Methods + +`MousePosition` is an optional Servicer that stores a client's current mouse +cursor coordinates. + +The easiest way get started using `MousePosition` is to use the same `ID` +as the `Subscriber`, and to use one `Subscriber` per connection. + +### Getting a reference + + + +```py +mouse_position = MousePosition.ref("user-abc") +``` + + +```ts +const mousePosition = MousePosition.ref("user-abc"); +``` + + + +### Update + +Set the current mouse position. + + + +```py +await mouse_position.update(context, left=120, top=240) +``` + + +```ts +await mousePosition.update(context, { left: 120, top: 240 }); +``` + + + +### Position + +Returns the current mouse position. + + + +```py +response = await mouse_position.position(context) +print(response.left, response.top) +``` + + +```ts +const { left, top } = await mousePosition.position(context); +console.log(left, top); +``` + + diff --git a/documentation/sidebars.js b/documentation/sidebars.js index 44ab122e..1224c4b8 100644 --- a/documentation/sidebars.js +++ b/documentation/sidebars.js @@ -145,6 +145,7 @@ const sidebars = { "library_services/mailgun", "library_services/oauth_token_manager", "library_services/ordered_map", + "library_services/presence", "library_services/pubsub", "library_services/queue", "library_services/sorted_map", diff --git a/tests/reboot/std/presence/presence_tests.py b/tests/reboot/std/presence/presence_tests.py index 812d93f2..21381567 100644 --- a/tests/reboot/std/presence/presence_tests.py +++ b/tests/reboot/std/presence/presence_tests.py @@ -14,13 +14,19 @@ from reboot.aio.external import ExternalContext from reboot.aio.memoize import MemoizeServicer from reboot.aio.tests import Reboot + +# Import used in Presence documentation, so we want to keep them separate. +# isort: off from reboot.std.presence.v1.presence import ( - ListResponse, MousePosition, - PositionResponse, Presence, - StatusResponse, Subscriber, +) +# isort: on +from reboot.std.presence.v1.presence import ( + ListResponse, + PositionResponse, + StatusResponse, presence_library, ) from typing import Optional diff --git a/tests/reboot/std/presence/presence_tests.ts b/tests/reboot/std/presence/presence_tests.ts index 9558cb31..aa95a8bf 100644 --- a/tests/reboot/std/presence/presence_tests.ts +++ b/tests/reboot/std/presence/presence_tests.ts @@ -9,7 +9,9 @@ import { import { errors_pb } from "@reboot-dev/reboot-api"; import { MousePosition } from "@reboot-dev/reboot-std/presence/mouse_tracker/v1"; import { Subscriber } from "@reboot-dev/reboot-std/presence/subscriber/v1"; -import { Presence, presenceLibrary } from "@reboot-dev/reboot-std/presence/v1"; +import { Presence } from "@reboot-dev/reboot-std/presence/v1"; +// eslint-disable-next-line +import { presenceLibrary } from "@reboot-dev/reboot-std/presence/v1"; import { strict as assert } from "node:assert"; import { test } from "node:test"; From cdb6cd68f947c2069706b64c44e5eec79ce7b3e2 Mon Sep 17 00:00:00 2001 From: Kat Fang Date: Tue, 11 Aug 2026 21:46:00 +0000 Subject: [PATCH 4/4] Documentation for Presence library. * Fixed authorizer examples. * Test for subscriber connection in TS. * Subscriber full example code in docs. * Wording updates. --- .../docs/library_services/presence.mdx | 215 ++++++++++++------ tests/reboot/std/presence/BUILD.bazel | 3 + tests/reboot/std/presence/presence_tests.ts | 79 +++++++ .../reboot/std/presence/subscriber_connect.ts | 22 ++ 4 files changed, 250 insertions(+), 69 deletions(-) create mode 100644 tests/reboot/std/presence/subscriber_connect.ts diff --git a/documentation/docs/library_services/presence.mdx b/documentation/docs/library_services/presence.mdx index 5163db05..b7fce559 100644 --- a/documentation/docs/library_services/presence.mdx +++ b/documentation/docs/library_services/presence.mdx @@ -43,7 +43,7 @@ import { presenceLibrary } from "@reboot-dev/reboot-std/presence/v1"; new Application({ servicers: [MyServicer], - libraries: [presenceLibrary()] + libraries: [presenceLibrary()], initialize, }).run(); ``` @@ -52,25 +52,47 @@ new Application({ ### Authorizer -By default, `Presence` allows all calls coming internally (e.g. from -your Reboot backend) or externally (e.g. from your React app). However, -this can be overridden by providing your own -[authorizer](/learn_more/auth#authorizers). +By default, `Presence` allows internal calls (e.g. from your Reboot backend) +and external calls with a [verified token](/learn_more/auth#token-verification). +However, this can be overridden by providing an +[authorizer rule](/learn_more/auth#authorizer-rules) for all servicers in the +presence library: ```py -presence_authorizer = Presence.Authorizer( - ... # !!! TODO: FILL OUT, also, which of these are actually necessary for react? +application = Application( + servicers=[MyServicer], + libraries=[presence_library( + authorizer=allow_if(any=[has_verified_token]) + )], ) +``` + + +```ts +const application = new Application({ + servicers: [MyServicer], + libraries: [presenceLibrary({ + authorizer: allowIf({ all: [hasVerifiedToken] }) + })], + initialize, +}); -subscriber_authorizer = Subscriber.Authorizer( - ... -) +``` + + -mouse_position_authorizer = MousePosition.Authorizer( - ... -) +or by providing specific service [authorizers](/learn_more/auth#authorizers): + + + +```py +presence_authorizer = Presence.Authorizer( ... ) + +subscriber_authorizer = Subscriber.Authorizer( ... ) + +mouse_position_authorizer = MousePosition.Authorizer( ... ) application = Application( servicers=[MyServicer], @@ -84,17 +106,11 @@ application = Application( ```ts -const presenceAuthorizer = new Presence.Authorizer({ - ... // !!! TODO: FILL OUT, also, which of these are actually necessary for react? -}); +const presenceAuthorizer = new Presence.Authorizer({ ... }); -const subscriberAuthorizer = new Subscriber.Authorizer({ - ... -}); +const subscriberAuthorizer = new Subscriber.Authorizer({ ... }); -const mousePositionAuthorizer = new MousePosition.Authorizer({ - ... -}); +const mousePositionAuthorizer = new MousePosition.Authorizer({ ... }); const application = new Application({ servicers: [MyServicer], @@ -112,17 +128,18 @@ const application = new Application({ ## React -Once you've [set up](/learn_more/call/from_react) your React app to call -into your Reboot API, you can use the `Presence` React library to quickly -add presence tracking in your React app. +The `Presence` library can be easily dropped into your React app to quicklly +add presence tracking. -First, add `@reboot-dev/reboot-std-api` and -`@reboot-dev/reboot-std-react` to your `package.json`. +Once you've [set up](/learn_more/call/from_react) your app to call +into your Reboot API, install `@reboot-dev/reboot-std`, +`@reboot-dev/reboot-std-api`, and `@reboot-dev/reboot-std-react` to your +`package.json`. ```bash -!!! TODO: what about reboot-react or reboot-std +npm install -S @reboot-dev/reboot-std npm install -S @reboot-dev/reboot-std-api -npm install -S reboot-dev/reboot-std-react +npm install -S @reboot-dev/reboot-std-react ``` You may also want to, in defining your Reboot application, wish to [override @@ -145,7 +162,7 @@ The children components can then call `usePresenceContext()` to get the list of online subscribers by ID. #### Properties of <Presence> -* `presenceId` is the `string` ID of the `Presence` instance you want to track +* `id` is the `string` ID of the `Presence` instance you want to track the subscriber for. * `subscriberId` is the `string` ID of the `Subscriber` who is connecting. @@ -213,7 +230,7 @@ you would like to use it. +(CODE:src=../../../tests/reboot/std/presence/presence_tests.py&lines=20-24) --> ```py @@ -228,7 +245,7 @@ from reboot.std.presence.v1.presence import ( +(CODE:src=../../../tests/reboot/std/presence/presence_tests.ts&lines=11-13) --> ```ts @@ -341,6 +358,7 @@ const subscriber = Subscriber.ref("user-abc"); Creates the `Subscriber` or ensures that it has already been created. This needs to be called before calling [`connect()`](#connect), and it is recommended to make this call before any [`connect()`](#connect) attempt. +It is safe to call this method multiple times. @@ -358,44 +376,12 @@ await subscriber.create(context); ### Connect -Used to initiate a subscription and detects when the subscriber is no longer -present. - -This method runs indefinitely, that is, it does return. It also requires a -unique nonce for every call. +Used to initiate a subscription. This method runs indefinitely and determines +when the subscriber is no longer present due to a dropped connection (e.g. when +a user closes the browser tab). - - -```py -nonce = uuid.uuid4().hex +See the [example code](#complete-example) below for how to use `connect()`. -# TODO !!! CHECK THIS - -# connect() runs indefinitely; run it concurrently with toggle(). -asyncio.gather( - subscriber.connect(context, nonce=nonce), - subscriber.toggle(context, nonce=nonce), -) -``` - - -```ts -// TODO !!! CHECK THIS - -const nonce = uuidv4(); - -// connect() runs indefinitely; run it concurrently with toggle(). -const abortController = new AbortController(); -Promise.all([ - subscriber.connect( - { signal: abortController.signal }, - { nonce }, - ), - subscriber.toggle(context, { nonce }), -]); -``` - - ### Toggle @@ -405,7 +391,7 @@ is made. Requires the same `nonce` that was used for the [`connect()`](#connect) call. -See [`connect()`](#connect) for concurrent call example. +See the [example code](#complete-example) below for how to use `toggle()`. ### Status @@ -428,6 +414,97 @@ console.log(present); // true if connected +### Complete Example + +The following is a complete example of how to create and connect a `Subscriber` +to a `Presence` instance. + + + + + + +```py +connect_failed = False + +async def connect(): + nonlocal connect_failed + try: + await subscriber_ref.Connect(context, nonce=nonce) + except: + connect_failed = True + +connect_task = asyncio.create_task(connect()) + +# Retry `Toggle` and `Subscribe` as long as connection hasn't failed. +attempt_num = 0 +while not connect_failed: + try: + print(f"Testing attempt: {attempt_num}") + await subscriber_ref.idempotently( + f"Attempt {attempt_num}", + ).Toggle(context, nonce=nonce) + except Subscriber.ToggleAborted as aborted: + if isinstance(aborted.error, NotFound): + print("Retrying Toggle") + attempt_num += 1 + continue + raise + + await presence_ref.Subscribe( + context, subscriber_id=subscriber_ref.state_id + ) + + # We've successfully subscribed and don't need to retry! + break +``` + + + + + +```ts +const nonce = uuidv4(); +let connectFailed = false; + +await subscriberRef.idempotently().create(context); + +subscriberRef + .connect(context, { nonce }) + .catch((_) => { + connectFailed = true; + }); + +let attempt = 0; +while (!connectFailed) { + try { + await subscriberRef + .idempotently(`attempt-${attempt}`) + .toggle(context, { nonce }); + } catch (e) { + if ( + e instanceof Subscriber.ToggleAborted && + e.error instanceof errors_pb.NotFound + ) { + attempt++; + continue; + } else { + break; + } + } + + await presenceRef.subscribe(context, { + subscriberId: subscriberRef.stateId, + }); + break; +} +``` + + + ## MousePosition Methods `MousePosition` is an optional Servicer that stores a client's current mouse diff --git a/tests/reboot/std/presence/BUILD.bazel b/tests/reboot/std/presence/BUILD.bazel index e23fb162..6c4f9df2 100644 --- a/tests/reboot/std/presence/BUILD.bazel +++ b/tests/reboot/std/presence/BUILD.bazel @@ -16,6 +16,7 @@ ts_project( name = "presence_tests_ts", srcs = [ "presence_tests.ts", + "subscriber_connect.ts", ":package.json", ], declaration = True, @@ -33,6 +34,8 @@ ts_project( "//:node_modules/@reboot-dev/reboot-api", "//:node_modules/@reboot-dev/reboot-std", "//:node_modules/@types/node", + # Required to get uuid package. + "//tests/reboot:greeter_js_reboot", ], ) diff --git a/tests/reboot/std/presence/presence_tests.ts b/tests/reboot/std/presence/presence_tests.ts index aa95a8bf..ca270c04 100644 --- a/tests/reboot/std/presence/presence_tests.ts +++ b/tests/reboot/std/presence/presence_tests.ts @@ -6,6 +6,7 @@ import { TokenVerifier, allow, } from "@reboot-dev/reboot"; +import { fork } from "child_process"; import { errors_pb } from "@reboot-dev/reboot-api"; import { MousePosition } from "@reboot-dev/reboot-std/presence/mouse_tracker/v1"; import { Subscriber } from "@reboot-dev/reboot-std/presence/subscriber/v1"; @@ -14,6 +15,7 @@ import { Presence } from "@reboot-dev/reboot-std/presence/v1"; import { presenceLibrary } from "@reboot-dev/reboot-std/presence/v1"; import { strict as assert } from "node:assert"; import { test } from "node:test"; +import * as uuid from "uuid"; class EmptyTokenVerifier extends TokenVerifier { async verifyToken( @@ -171,4 +173,81 @@ test("Use Presence Servicers", async (t) => { }); } ); + + await t.test("Subscriber connection", async (t) => { + await rbt.up( + new Application({ + libraries: [presenceLibrary()], + tokenVerifier: new EmptyTokenVerifier(), + }), + { + // needed so URL starts with http: + localEnvoy: true, + } + ); + + let context = rbt.createExternalContext("test-connect"); + let subscriberRef = Subscriber.ref("connect-test-subscriber"); + + await subscriberRef.idempotently().create(context); + let nonce = uuid.v4(); + + // Connect the subscriber. The following would work if we could cancel + // the promise/RPC so the test could end. However, because we can't, we use + // a subprocess instead. Left here to grab for documentation. + + // let connectFailed = false; + // const promise = subscriberRef + // .connect(context, { nonce }) + // .catch((_) => { + // connectFailed = true; + // }); + + const subprocess = fork( + "./tests/reboot/std/presence/subscriber_connect.js", + [rbt.url(), subscriberRef.stateId, nonce] + ); + + let attempt = 0; + while (true) { + try { + await subscriberRef + .idempotently(`attempt-${attempt}`) + .toggle(context, { nonce }); + } catch (e) { + if ( + e instanceof Subscriber.ToggleAborted && + e.error instanceof errors_pb.NotFound + ) { + attempt++; + continue; + } else { + break; + } + } + + await Presence.ref("connect-test").subscribe(context, { + subscriberId: subscriberRef.stateId, + }); + break; + } + + let { present } = await subscriberRef.status(context); + assert(present); + + // Tell the child process it can exit. + subprocess.send(""); + + await new Promise((resolve, reject) => { + subprocess.on("exit", (code, signal) => { + if (code === 0) { + resolve(); + } else if (signal === null) { + reject(new Error(`Child exited with code ${code}`)); + } else { + reject(new Error(`Child exited with signal ${signal}`)); + } + }); + }); + }); }); diff --git a/tests/reboot/std/presence/subscriber_connect.ts b/tests/reboot/std/presence/subscriber_connect.ts new file mode 100644 index 00000000..cc168c4a --- /dev/null +++ b/tests/reboot/std/presence/subscriber_connect.ts @@ -0,0 +1,22 @@ +import { ExternalContext } from "@reboot-dev/reboot"; +import { Subscriber } from "@reboot-dev/reboot-std/presence/subscriber/v1"; + +const args = process.argv.slice(2); +const url = args[0]; +const subscriberId = args[1]; +const nonce = args[2]; + +const context = new ExternalContext({ name: "subscriber-connect", url }); +const subscriber = Subscriber.ref(subscriberId); + +subscriber.connect(context, { nonce }); + +await new Promise((resolve) => { + process.once("message", () => { + resolve(); + }); +}); + +// Need to explicitly exit because the call to `testLongRunningWriter` +// should still be outstanding. +process.exit(0);