From 055e0d5a70fe2b2edd6e596c261193f64054df6d Mon Sep 17 00:00:00 2001
From: William Killerud
Date: Fri, 24 May 2024 15:58:10 +0200
Subject: [PATCH 1/2] docs: the bridge, browser, and store that's WIP
As part of improving hybrid applications using Podium
---
docs/api/bridge.md | 115 +++++++++++++++++
docs/api/browser.md | 15 ++-
docs/api/store.md | 158 +++++++++++++++++++++++
docs/guides/assets.md | 9 ++
docs/guides/client-side-communication.md | 110 ++++++++++++++--
docs/guides/hybrid.md | 111 ++++++++++++++++
sidebars.js | 1 +
static/img/client-side-messaging.png | Bin 0 -> 249490 bytes
8 files changed, 501 insertions(+), 18 deletions(-)
create mode 100644 docs/api/bridge.md
create mode 100644 docs/api/store.md
create mode 100644 docs/guides/hybrid.md
create mode 100644 static/img/client-side-messaging.png
diff --git a/docs/api/bridge.md b/docs/api/bridge.md
new file mode 100644
index 0000000..228f2d9
--- /dev/null
+++ b/docs/api/bridge.md
@@ -0,0 +1,115 @@
+---
+title: "@podium/bridge"
+---
+
+This package is a bridge designed to pass [JSON-RPC 2.0](https://www.jsonrpc.org/specification) messages between a web application and a native web view.
+
+## Usage
+
+To install:
+
+```sh
+npm install @podium/bridge
+```
+
+Import the bridge in your client-side bundle:
+
+```js
+import "@podium/bridge";
+```
+
+You should probably send messages via [@podium/browser]. That said, the bridge is available on `window['@podium'].bridge`.
+
+```js
+/** @type {import("@podium/bridge").PodiumBridge} */
+const bridge = window["@podium"].bridge;
+
+// You can listen for incoming messages, which can either be RpcRequest or RpcResponse
+bridge.on("global/authentication", (message) => {
+ const request =
+ /** @type {import("@podium/bridge").RpcRequest<{ token?: string }>} */ (
+ message
+ );
+
+ if (typeof request.token === "string") {
+ // logged in
+ } else {
+ // logged out
+ }
+});
+
+// You can trigger notifications (one-way messages)
+bridge.notification({
+ method: "global/authentication",
+ params: { token: null },
+});
+
+// And you can call methods and await the response
+/** @type {import("@podium/bridge").RpcResponse<{ c: string }>} */
+const response = await bridge.call({
+ method: "document/native-feature",
+ params: { a: "foo", b: "bar" },
+});
+```
+
+## API
+
+### `bridge.on`
+
+Add a listener for incoming messages for a given method name.
+
+```js
+import "@podium/bridge";
+
+/** @type {import("@podium/bridge").PodiumBridge} */
+const bridge = window["@podium"].bridge;
+
+bridge.on("global/authentication", (message) => {
+ const request =
+ /** @type {import("@podium/bridge").RpcRequest<{ token?: string }>} */ (
+ message
+ );
+
+ if (typeof request.token === "string") {
+ // logged in
+ } else {
+ // logged out
+ }
+});
+```
+
+### `bridge.notification`
+
+Send a [notification](https://www.jsonrpc.org/specification#notification) (one-way message).
+
+```js
+import "@podium/bridge";
+
+/** @type {import("@podium/bridge").PodiumBridge} */
+const bridge = window["@podium"].bridge;
+
+bridge.notification({
+ method: "global/authentication",
+ params: { token: null },
+});
+```
+
+### `bridge.call`
+
+Send a [request](https://www.jsonrpc.org/specification#request_object) and await a [response](https://www.jsonrpc.org/specification#response_object).
+
+```js
+import "@podium/bridge";
+
+/** @type {import("@podium/bridge").PodiumBridge} */
+const bridge = window["@podium"].bridge;
+
+/** @type {import("@podium/bridge").RpcResponse<{ c: string }>} */
+const response = await bridge.call({
+ method: "document/native-feature",
+ params: { a: "foo", b: "bar" },
+});
+```
+
+[@podium/browser]: /docs/api/browser
+[@podium/store]: /docs/api/store
diff --git a/docs/api/browser.md b/docs/api/browser.md
index 21fca41..7cdddc4 100644
--- a/docs/api/browser.md
+++ b/docs/api/browser.md
@@ -4,6 +4,9 @@ title: "@podium/browser"
---
The `@podium/browser` module is a client-side library designed to simplify communication between a podlet and the layout, and between podlets.
+The module also supports applications running in a [hybrid application](/docs/guides/hybrid#client-side-communcication) when used with [@podium/bridge](/docs/api/bridge).
+
+For an API designed as reactive state, see [@podium/store](/docs/api/store).
## Installation
@@ -55,11 +58,11 @@ Publish an event for a channel and topic combination. Returns the event object p
This method takes the following arguments:
-| option | default | type | required | details |
-| ------- | ------- | -------- | -------- | ------------------------- |
-| channel | `null` | `string` | `true` | Name of the channel |
-| topic | `null` | `string` | `true` | Name of the topic |
-| payload | `null` | any | `false` | The payload for the event |
+| option | default | type | required | details |
+| ------- | ------- | -------- | -------- | ------------------------------------------------------------------------------- |
+| channel | `null` | `string` | `true` | Name of the channel. Podium reserves `system` and `view` for built-in features. |
+| topic | `null` | `string` | `true` | Name of the topic. |
+| payload | `null` | any | `false` | The payload for the event. |
Examples:
@@ -77,7 +80,7 @@ This method takes the following arguments:
| option | default | type | required | details |
| -------- | ------- | ---------- | -------- | --------------------------------------------------------- |
-| channel | `null` | `string` | `true` | Name of the channel |
+| channel | `null` | `string` | `true` | Name of the channel. |
| topic | `null` | `string` | `true` | Name of the topic |
| callback | `null` | `Function` | `true` | Callback function to be invoked. Receives an event object |
diff --git a/docs/api/store.md b/docs/api/store.md
new file mode 100644
index 0000000..65336b1
--- /dev/null
+++ b/docs/api/store.md
@@ -0,0 +1,158 @@
+---
+title: "@podium/store"
+---
+
+This is a client-side library that provides a reactive data store using [nanostores](https://github.com/stores/stores) on top of [@podium/browser](https://github.com/podium-lib/browser)'s `MessageBus`. It includes some ready-made stores and helpers for you to make reactive stores for your own events.
+
+By using reactive state backed by `MessageBus` you can seamlessly share state between different parts of a Podium application. If a podlet changes the value of shared state, all other podlets using that value can update.
+
+
+
+## Usage
+
+To install:
+
+```sh
+npm install @podium/store
+```
+
+Use an [included store](#included-stores) in your client-side application:
+
+```js
+// store/user.js
+import { $authentication } from "@podium/store";
+import { computed } from "nanostores";
+
+// You can optionally make a computed value based on the included store
+export const $loggedIn = computed($authentication, (authentication) =>
+ Boolean(authentication.token)
+);
+
+// Colocating actions with the store makes it easier to
+// see what can trigger updates.
+export function logIn(token) {
+ $authentication.set({ token });
+}
+
+export function logOut() {
+ $authentication.set({ token: null });
+}
+```
+
+Use the reactive store to do minimal updates of your UI when state changes. Nanostores supports multiple view frameworks:
+
+- [React and Preact](https://github.com/stores/stores?tab=readme-ov-file#react--preact)
+- [Lit](https://github.com/stores/stores?tab=readme-ov-file#lit)
+- [Vue](https://github.com/stores/stores?tab=readme-ov-file#vue)
+- [Vanilla JS](https://github.com/stores/stores?tab=readme-ov-file#vanilla-js)
+
+This example shows a React component:
+
+```js
+// components/user.jsx
+import { useStore } from "@nanostores/react";
+import { $loggedIn } from "../stores/user.js";
+
+export const User = () => {
+ const loggedIn = useStore($loggedIn);
+ return
{loggedIn ? "Welcome!" : "Please log in"}
;
+};
+```
+
+This is the same component in Lit:
+
+```js
+// components/user.js
+import { StoreController } from "@nanostores/lit";
+import { $loggedIn } from "../stores/user.js";
+
+class User extends LitElement {
+ loggedInController = new StoreController(this, $loggedIn);
+
+ render() {
+ return html`
`;
+ }
+}
+
+customElements.define("a-user", User);
+```
+
+### Create your own reactive state
+
+By using the [included helper](#mapchannel-topic-initialvalue) you can make your reactive state sync between the different parts of a Podium application.
+
+## API
+
+### `$authentication`
+
+Type: [`map`](https://github.com/stores/stores?tab=readme-ov-file#maps)
+
+```js
+import { $authentication } from "@podium/store";
+```
+
+### `atom(channel, topic, initialValue)`
+
+Create your own [`atom`](https://github.com/nanostores/nanostores?tab=readme-ov-file#atoms) that syncs between parts of a Podium application using the [MessageBus](https://github.com/podium-lib/browser).
+
+This method requires the following arguments:
+
+| option | type | details |
+| ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
+| channel | `string` | Name of the channel |
+| topic | `string` | Name of the topic |
+| payload | `object` | The initial value. Replaced if [`peek(channel, topic)`](https://github.com/podium-lib/browser?tab=readme-ov-file#peekchannel-topic) returns a value. |
+
+```js
+import { atom } from "@podium/store";
+
+const $reminders = atom("reminders", "list", []);
+```
+
+### `map(channel, topic, initialValue)`
+
+Create your own [`map`](https://github.com/nanostores/nanostores?tab=readme-ov-file#maps) that syncs between parts of a Podium application using the [MessageBus](https://github.com/podium-lib/browser).
+
+This method requires the following arguments:
+
+| option | type | details |
+| ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
+| channel | `string` | Name of the channel |
+| topic | `string` | Name of the topic |
+| payload | `object` | The initial value. Replaced if [`peek(channel, topic)`](https://github.com/podium-lib/browser?tab=readme-ov-file#peekchannel-topic) returns a value. |
+
+```js
+import { map } from "@podium/store";
+
+const $user = map("user", "profile", { displayName: "foobar" });
+```
+
+### `deepMap(channel, topic, initialValue)`
+
+Create your own [`deepMap`](https://github.com/nanostores/nanostores?tab=readme-ov-file#deep-maps) that syncs between parts of a Podium application using the [MessageBus](https://github.com/podium-lib/browser).
+
+This method requires the following arguments:
+
+| option | type | details |
+| ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
+| channel | `string` | Name of the channel |
+| topic | `string` | Name of the topic |
+| payload | `object` | The initial value. Replaced if [`peek(channel, topic)`](https://github.com/podium-lib/browser?tab=readme-ov-file#peekchannel-topic) returns a value. |
+
+```js
+import { deepMap, listenKeys } from "@podium/store";
+
+export const $profile = deepMap({
+ hobbies: [
+ {
+ name: "woodworking",
+ friends: [{ id: 123, name: "Ron Swanson" }],
+ },
+ ],
+ skills: [["Carpentry", "Sanding"], ["Varnishing"]],
+});
+
+listenKeys($profile, ["hobbies[0].friends[0].name", "skills[0][0]"]);
+```
diff --git a/docs/guides/assets.md b/docs/guides/assets.md
index 17ba3ae..7cd8166 100644
--- a/docs/guides/assets.md
+++ b/docs/guides/assets.md
@@ -124,3 +124,12 @@ Using the [shadow DOM](https://developer.mozilla.org/en-US/docs/Web/API/Web_comp
With `podlet.css()` the end result is a `` tag in the HTML document's ``. If your podlet's content renders inside a shadow DOM that CSS won't be able to reach the podlet.
With a declarative shadow DOM you have to include your own `` to the CSS from inside the shadow DOM.
+
+### Islands architecture
+
+Podium works well with [islands architecture](https://jasonformat.com/islands-architecture/) where interactivity on the client is handled by small, isolated applications.
+
+Especially when building your layout:
+
+- Consider how JavaScript libraries you use handle external content (external in the sense that it is not generated by your library).
+- Be mindful of how much of the document your JavaScript library hydrates.
diff --git a/docs/guides/client-side-communication.md b/docs/guides/client-side-communication.md
index 9506293..db288e0 100644
--- a/docs/guides/client-side-communication.md
+++ b/docs/guides/client-side-communication.md
@@ -1,25 +1,111 @@
---
-title: Client-side communication in a Podium app
+title: Client-side communication
---
-Podium provides a client side library called [@podium/browser] that includes a [MessageBus](/docs/api/browser#messagebus). The message bus simplifies passing data between different podlets' client-side JavaScript.
+Podium's micro frontend architecture extends all the way to the browser. Each podlet can bring [client-side JavaScript applications](/docs/guides/assets) to add interactivity to the HTML, essentially an [islands architecture](https://jasonformat.com/islands-architecture/).
-## The use case
+Say `InputPodlet` contains an input field where a user can input a new reminder and `ListPodlet` contains a list of all reminders.
-If podlet A contains an input field where a user can input a new reminder and podlet B contains a list of all reminders.
+![Two boxes stacked vertically where the first represents an input field, and the second a list of reminders](/img/client-side-messaging.png)
-When a user inputs a new reminder in podlet A they would expect that the reminders list in podlet B is updated immediately with the new reminder.
+When a user inputs a new reminder in `InputPodlet` they would expect that the reminders list in `ListPodlet` is updated immediately with the new reminder. However, since they are two separate applications `InputPodlet` can't directly add entries to `ListPodlet`'s state.
-`MessageBus` provides a simple mechanism for a podlet to publish events or to subscribe to receive events, in the browser.
+## Podium client-side libraries
-In the example above:
+Podium offers client side libraries to help communication between different applications running in the browser:
-- Podlet B would subscribe to receive new reminder events.
-- Podlet A would publish a new reminder event.
-- Podlet B would update on receiving the new reminder event.
+- [@podium/browser](#podiumbrowser) includes a message bus and methods to publish and subscribe to messages.
+- [@podium/store](#podiumstore) offers a reactive state API backed by the message bus, letting you sync state with ease.
-## Getting Started
+The libraries are designed to make communication between podlets loosely coupled and resilient.
-See the documentation for [@podium/browser] to get started.
+### `@podium/browser`
+
+This library contains the message bus that is the backbone of client-side communication in Podium. Podlets (or the layout) publish and subscribe to messages on the bus for loosely coupled synchronization of state.
+
+:::tip
+
+[@podium/store](#podiumstore) offers an alternative API that uses this message bus under the hood. You can use whichever you prefer.
+
+:::
+
+In the example above, when `InputPodlet` accepts a new item it also publishes a message on the bus.
+
+```js
+// input-podlet.js
+import { MessageBus } from "@podium/browser";
+
+const messageBus = new MessageBus();
+
+messageBus.publish("reminders", "newReminder", {
+ title: "Buy milk",
+});
+```
+
+`ListPodlet` subscribes to the same `reminders` channel and `newReminder` topic that `InputPodlet` publishes to. When nes messages arrive, `ListPodlet` updates its internal state.
+
+```js
+// list-podlet.js
+import { MessageBus } from "@podium/browser";
+
+const messageBus = new MessageBus();
+
+const reminders = [];
+
+// ListPodlet listens for new reminders published on the message bus and updates its state
+messageBus.subscribe("reminders", "newReminder", (event) => {
+ const reminder = event.payload;
+ reminders.push(reminder);
+});
+```
+
+See [@podium/browser] for API documentation.
+
+### `@podium/store`
+
+This library adds a reactive state API using [nanostores]. It sets up publishing and subscribing for you behind the scenes, leaving you with a reactive variable you read from and write to that will stay in sync between applications.
+
+:::tip
+
+Using `@podium/store` you can trigger minimal UI updates using a [nanostores integration] for a given UI library.
+
+:::
+
+Keeping with our example, when `InputPodlet` accepts a new item updates a reactive `atom` holding the list of reminders.
+
+```js
+// input-podlet.js
+import { atom } from "@podium/store";
+
+/** @type {import("@podium/store").WritableAtom} */
+const $reminders = atom("reminders", "list", []);
+
+// Replace the existing value with a new list to trigger reactive updates
+$reminders.set([...$reminders.value, "Buy milk"]);
+```
+
+`ListPodlet` would set up the same `atom` and use either a [nanostores integration] with an existing UI library, or subscribe and handle changes manually.
+
+```js
+// list-podlet.js
+import { atom } from "@podium/store";
+
+/** @type {import("@podium/store").WritableAtom} */
+const $reminders = atom("reminders", "list", []);
+
+// Perhaps fetch stored reminders from an API,
+// or populate the state with data included in a server-side render
+$reminders.set(storedReminders);
+
+// Update the UI when the reminders list changes
+$reminders.subscribe((value) => {
+ console.log(value);
+});
+```
+
+See [@podium/store] and [nanostores] for API documentation.
[@podium/browser]: /docs/api/browser
+[@podium/store]: /docs/api/store
+[nanostores]: https://github.com/nanostores/nanostores
+[nanostores integration]: https://github.com/nanostores/nanostores?tab=readme-ov-file#integration
diff --git a/docs/guides/hybrid.md b/docs/guides/hybrid.md
new file mode 100644
index 0000000..a7ff0ad
--- /dev/null
+++ b/docs/guides/hybrid.md
@@ -0,0 +1,111 @@
+---
+title: Hybrid apps
+---
+
+Podium includes features for developers of hybrid applications, where parts of a (typically mobile) application are built using web technologies.
+
+- Specification of HTTP headers that are passed to podlets on [the context](/docs/guides/context).
+- Client-side library for sending [messages between web and native](#client-side-communcication).
+- Developer tools extension for browsers to simulate HTTP headers set by a native webview.
+
+## Initial request
+
+Layouts and podlets need to be able to adapt to requests from a hybrid web view. To support this, Podium specifies a set of HTTP headers that the web view includes in requests:
+
+### Hybrid HTTP headers
+
+| Header | Example | Description |
+| ------------------------- | ------------------------------ | ----------------------------------------------------------------------------------------- |
+| `x-podium-app-id` | `com.yourcompany.app@1.2.3` | To identify clients in logs |
+| `x-podium-base-font-size` | `1rem` | To set base font size variable in CSS based on accessibility settings in the native host. |
+| `x-podium-device-type` | `hybrid-ios`, `hybrid-android` | To give hints to the server what should be included in the response. |
+| `Authorization` | `Bearer eyJhbGciOiJIU...` | Optional. Signifies a logged-in user. |
+
+#### Podium developer tools extension
+
+Get the extension to make it easier to set these HTTP headers when developing locally:
+
+- [Firefox](https://addons.mozilla.org/en-US/firefox/addon/podium-developer-tools/)
+- [Chromium](https://chromewebstore.google.com/detail/podium-development-extens/jdlcejoeifgnnnckhnhapbmgieajaipl) based browsers
+
+## Client-side communcication
+
+[@podium/bridge] is a module that sets up a JSON RPC bridge for communication between a native application and a web application running in a webview.
+[@podium/browser](/docs/guides/client-side-communication#podiumbrowser)'s message bus taps into this bridge to publish and subscribe to messages.
+
+You use the API from `@podium/browser` or [@podium/store](/docs/guides/client-side-communication#podiumstore),
+and messages seamlessly get sent across the bridge for you.
+
+```js
+// Include this once, preferably in your layout before loading applications,
+// and before importing `@podium/browser` and `@podium/store`.
+import "@podium/bridge";
+```
+
+See [@podium/bridge] for API documentation.
+
+### Message format
+
+When you use the bridge with `@podium/browser` and `@podium/store`, behind the scenes, `channel`, `topic` and `payload` are combined to form a valid [JSON RPC 2.0](https://www.jsonrpc.org/specification) message.
+Here's an example:
+
+```js
+import "@podium/bridge";
+import { MessageBus } from "@podium/browser";
+
+const messageBus = new MessageBus();
+
+messageBus.publish("system", "authentication", { token: null });
+```
+
+`"system"` and `"authentication"` are combined to `"system/authentication"`, and the `payload` argument is used as `params` in JSON RPC terms.
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "system/authentication",
+ "params": { "token": null }
+}
+```
+
+The same goes for `@podium/store`:
+
+```js
+import "@podium/bridge";
+import { atom } from "@podium/store";
+
+const $auth = atom("system", "authentication", { token: null });
+```
+
+### Reserved message names
+
+Podium reserves these topics for built-in features:
+
+- `system`
+- `view`
+
+### Message contracts
+
+#### `system/authentication`
+
+Logged out:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "system/authentication",
+ "params": { "token": null }
+}
+```
+
+Logged in:
+
+```json
+{
+ "jsonrpc": "2.0",
+ "method": "system/authentication",
+ "params": { "token": "eyJhbGciOiJIU..." }
+}
+```
+
+[@podium/bridge]: /docs/api/bridge
diff --git a/sidebars.js b/sidebars.js
index 67ede71..1a034e5 100644
--- a/sidebars.js
+++ b/sidebars.js
@@ -39,6 +39,7 @@ const sidebars = {
{ type: "doc", id: "guides/redirects" },
{ type: "doc", id: "guides/layout-development" },
{ type: "doc", id: "guides/podlet-development" },
+ { type: "doc", id: "guides/hybrid" },
],
},
{
diff --git a/static/img/client-side-messaging.png b/static/img/client-side-messaging.png
new file mode 100644
index 0000000000000000000000000000000000000000..a5ab21bb803073652f43c9517e4dd4b73ff702f6
GIT binary patch
literal 249490
zcmY(q1ymhDlQ0UwU4ly>xVvj`3-0dD#ogT@5G=SuaMxVi-7oGC++FWacE8pLpP5KQNRK*$E#Nw55o=2n0mk
z52ROPn2%>tQ)v}>2ncT~2#CN?2#DtoRNx^5gc}P4#IX?s1m71Z-&5Vu4|D!^1l-72EfI#~E?}miP$in-O
zrfsdJ<*FtBozK((z+`OZU}DbX32^+!1tH+c_W=UTU5!aS0e1E-e4c`2|3$&~0sliY
zlac-xiL0$3nU=f~shESaIVl$t3lj^O5F#llserSY1)qwzV{J*k4SOxxJ`4pY4%|A5%
zlP|<7@L!bwKiq%x2r&Os{{QLBe_Q%r*bh^M5Cxe3_q7Qj;u~zkK|p+ikP#PA^MpL@
zK=@@K<#e%Lm}o%)Xg1ev#&Njryg+IO2KNjOBB4?lBaEQ5p)(;sw{C|+UsLMUf9u8E
z^j(AdsRxf&vxMhhG#1Z&X}_y(KrvQtH};b=L!Uk4;XS~ueCpO`!(}58L1ycvB-ipU
z@F45>UnxI7J{Dr+EN00r;boZ!vPS*xVw3>|9wGX5m8u)y)+`mT1-R?$_
z44lya(997%7H@rEo{O!*t;zn6dxcbf>^~eI;$Br8jaEqCR50jqp#Hfxynx
z)Kman=P!p$HhCy{mo3d?sMQr_e~F3*dYOB|84<9dRagDfc0BX8ZfaRmmH@ZvL{cv0?s(u66l4j&OE%rph(4
zwPm#R!xxNtKWBA*ae=nGyE_N#ItqGhftn6)u$Vv;6chwAojo~}UJAoOg>XMhI5;>Q
zo}Ru=eKl#Hheai=A>_7iY|f8sWhS(eTIUFOf1O@gSqVJQ-0w+c)a@;ii}Jguu^
zO&TF~U-~+pwS1@+bH}thLRfXf+)s*47%5w++rlL9u;#M8;d6mbyw~zPDo&G~c`uNHEDp2V
z6+1=~bSUy)jeN%a6t3)$nn(Og@L`Spv~K7NXe6@-Y}pYqwMu8(MMH2j?fS0ozD$`n
zZ_@Snqr|lQ*@z=x*(xLYZ|;L2K-4eS>o2}-W?3>*?N}-!5&!_``YBKlP*9lbcdyMZ
zpYTG_Ncb28GH)%3uKGArVmacx1=^kh9;<|O^fxl>+UywgD8Z(bM!9~M`I}S2AnS(O?^Papcazr^CtLh
zGvc!XPmhI$aEMGR*8olU-dAjEPz+yI
z7JG%p+>sQ8-h3bXsf93v?nV^Dy_|Wa_^%{AZ^KbkvhZ0&!4+N46_-bf0V}Z+!7xa6
z+drD{W=n<$QMeY9vGvqS+p#)l8pD_*uNP0
z<=1Hl6Vfw60gFm%r>V2No>B{UHVJ7rjYUYB6E${I%zrb1XjWwiuYbynE=_K$IzyDL0jpipDTQSJ)JVx4wy6ULTCf!Z%P*jk
zSgvqAx3M24j3Ci-Imk2W(SjK+=)-w%H2h(xtus9(o?z{?%fTWiTsxJL*K&`8bWL%t
z9T-ahk)yMAa{xRPcLd8pfl_Avx6Bod2@o|D*X0*|r>Fhxx7=qPt$D)U49g3&t}DVW
zjL!GFn#{DXD=`4t8KY$5%p``Kp3yk_SYhUd?&DxUOTZ
zh|7F*1Co-3AC2BB!ucbL6}FnxG=!L6c^!58ndQuDM;dxw#48?IuumTcbpV$J5f$w`
z8we}Rw1tRS2cLH*Ha;Z)bCYBCjIY^Kh0E=pS5q6^l$5OT)zif-RiDTWRo+2CbmH%8
z#zWiy{;FNS)o_=SS~It|=AQV9v(n16!jom{puLO>yD%#O<^6j{8kohajdjB=JXboMVodkg}7&3u3^d|?Ke4qeD@
zx{-O6*k;PtScfbEcRPLfOc>qOo!Wtz-My?MEl$0nST~~?pz{KVz
zm%&0~YU`8D)v?=W^XL
zUm#mKB3J;N=gIjTIhw2PN#fAqYyT2Zgg{as%tueZX&iK9s0>*+luG0-ZeK345TJ|6
zBbDniTz-9*;eM9Xxzyf