From 5ce913aedc04d20d926ff71c147459666a6981fc Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Fri, 25 Jul 2025 12:46:15 +0700 Subject: [PATCH] Huly Network Signed-off-by: Andrey Sobolev --- .github/workflows/main.yml | 1 + README.md | 18 +- common/config/rush/pnpm-lock.yaml | 94 ++ network/README.md | 397 ++++++++ network/core/.eslintrc.js | 7 + network/core/config/rig.json | 5 + network/core/jest.config.js | 7 + network/core/package.json | 53 + network/core/src/__test__/dummy.ts | 38 + network/core/src/__test__/node.spec.ts | 160 +++ network/core/src/__test__/samples.ts | 43 + network/core/src/__test__/utils.ts | 31 + network/core/src/api/client.ts | 32 + network/core/src/api/discovery.ts | 25 + network/core/src/api/net.ts | 109 ++ network/core/src/api/node.ts | 47 + network/core/src/api/request.ts | 33 + network/core/src/api/timeouts.ts | 5 + network/core/src/api/transport.ts | 17 + network/core/src/api/types.ts | 11 + network/core/src/api/utils.ts | 10 + network/core/src/discovery/static.ts | 57 ++ network/core/src/index.ts | 10 + network/core/src/net/index.ts | 281 ++++++ network/core/src/node/node.ts | 347 +++++++ network/core/src/node/session.ts | 251 +++++ network/core/src/utils.ts | 51 + network/core/tsconfig.json | 12 + network/docs/Schema.png | Bin 0 -> 214102 bytes network/docs/api-reference.md | 950 ++++++++++++++++++ network/docs/readme.md | 208 ++++ network/todo.md | 22 + network/zeromq/.eslintrc.js | 7 + network/zeromq/.npmignore | 4 + network/zeromq/config/rig.json | 5 + network/zeromq/jest.config.js | 7 + network/zeromq/package.json | 42 + network/zeromq/src/__test__/dummy.ts | 36 + network/zeromq/src/__test__/node.spec.ts | 121 +++ network/zeromq/src/__test__/samples.ts | 43 + network/zeromq/src/__test__/transport.spec.ts | 91 ++ network/zeromq/src/index.ts | 2 + network/zeromq/src/node.ts | 109 ++ network/zeromq/src/transport/client.ts | 134 +++ network/zeromq/src/transport/index.ts | 2 + network/zeromq/src/transport/server.ts | 169 ++++ network/zeromq/src/types.ts | 21 + network/zeromq/tsconfig.json | 12 + rush.json | 10 + 49 files changed, 4146 insertions(+), 1 deletion(-) create mode 100644 network/README.md create mode 100644 network/core/.eslintrc.js create mode 100644 network/core/config/rig.json create mode 100644 network/core/jest.config.js create mode 100644 network/core/package.json create mode 100644 network/core/src/__test__/dummy.ts create mode 100644 network/core/src/__test__/node.spec.ts create mode 100644 network/core/src/__test__/samples.ts create mode 100644 network/core/src/__test__/utils.ts create mode 100644 network/core/src/api/client.ts create mode 100644 network/core/src/api/discovery.ts create mode 100644 network/core/src/api/net.ts create mode 100644 network/core/src/api/node.ts create mode 100644 network/core/src/api/request.ts create mode 100644 network/core/src/api/timeouts.ts create mode 100644 network/core/src/api/transport.ts create mode 100644 network/core/src/api/types.ts create mode 100644 network/core/src/api/utils.ts create mode 100644 network/core/src/discovery/static.ts create mode 100644 network/core/src/index.ts create mode 100644 network/core/src/net/index.ts create mode 100644 network/core/src/node/node.ts create mode 100644 network/core/src/node/session.ts create mode 100644 network/core/src/utils.ts create mode 100644 network/core/tsconfig.json create mode 100644 network/docs/Schema.png create mode 100644 network/docs/api-reference.md create mode 100644 network/docs/readme.md create mode 100644 network/todo.md create mode 100644 network/zeromq/.eslintrc.js create mode 100644 network/zeromq/.npmignore create mode 100644 network/zeromq/config/rig.json create mode 100644 network/zeromq/jest.config.js create mode 100644 network/zeromq/package.json create mode 100644 network/zeromq/src/__test__/dummy.ts create mode 100644 network/zeromq/src/__test__/node.spec.ts create mode 100644 network/zeromq/src/__test__/samples.ts create mode 100644 network/zeromq/src/__test__/transport.spec.ts create mode 100644 network/zeromq/src/index.ts create mode 100644 network/zeromq/src/node.ts create mode 100644 network/zeromq/src/transport/client.ts create mode 100644 network/zeromq/src/transport/index.ts create mode 100644 network/zeromq/src/transport/server.ts create mode 100644 network/zeromq/src/types.ts create mode 100644 network/zeromq/tsconfig.json diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7b21678e8d7..8b7f60674e6 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,6 +23,7 @@ on: env: CacheFolders: | communication + network common desktop desktop-package diff --git a/README.md b/README.md index 4dd9d922179..a7fe1b0a4d5 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,17 @@ If you want to interact with Huly programmatically, check out our [API Client](. You can find API usage examples in the [Huly examples](https://github.com/hcengineering/huly-examples) repository. +## Huly Virtual Network + +The platform features a distributed network architecture that enables scalable, fault-tolerant communication between accounts, workspaces, and nodes. The [Huly Virtual Network](./network/README.md) provides: + +- **Distributed Load Balancing**: Intelligent routing across multiple nodes using consistent hashing +- **Multi-Tenant Architecture**: Secure workspace isolation with role-based access control +- **Fault Tolerance**: Automatic failover and recovery mechanisms +- **Real-time Communication**: Event-driven architecture with broadcast capabilities + +For detailed information about the network architecture, deployment, and API reference, see the [Network Documentation](./network/README.md). + ## Table of Contents - [Huly Platform](#huly-platform) @@ -35,6 +46,7 @@ You can find API usage examples in the [Huly examples](https://github.com/hcengi - [Self-Hosting](#self-hosting) - [Activity](#activity) - [API Client](#api-client) + - [Huly Virtual Network](#huly-virtual-network) - [Table of Contents](#table-of-contents) - [Pre-requisites](#pre-requisites) - [Verification](#verification) @@ -106,6 +118,7 @@ This project uses GitHub Packages for dependency management. To successfully dow Follow these steps: 1. Generate a GitHub Token: + - Log in to your GitHub account - Go to **Settings** > **Developer settings** > **Personal access tokens** (https://github.com/settings/personal-access-tokens) - Click **Generate new token** @@ -113,13 +126,13 @@ Follow these steps: - Generate the token and copy it 2. Authenticate with npm: + ```bash npm login --registry=https://npm.pkg.github.com ``` When prompted, enter your GitHub username, use the generated token as your password - ## Fast start ```bash @@ -280,6 +293,7 @@ This guide describes the nuances of building and running the application from so #### Disk Space Requirements Ensure you have sufficient disk space available: + - A fully deployed local application in clean Docker will consume slightly more than **35 GB** of WSL virtual disk space - The application folder after build (sources + artifacts) will occupy **4.5 GB** @@ -303,6 +317,7 @@ Make sure Docker is accessible from WSL: Windows Git often automatically replaces line endings. Since most build scripts are `.sh` files, ensure your Windows checkout doesn't break them. **Solution options:** + - Checkout from WSL instead of Windows - Configure Git on Windows to disable auto-replacement: ```bash @@ -343,6 +358,7 @@ After these preparations, the build instructions should work without issues. When starting the application (`rush docker:up`), some network ports in Windows might be occupied. You can fix port mapping in the `\dev\docker-compose.yaml` file. **Important:** Depending on which port you change, you'll need to: + 1. Find what's using that port 2. Update the new address in the corresponding service configuration diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index e4fcbb4eaed..bc96e79ead3 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -820,6 +820,12 @@ importers: '@rush-temp/mongo': specifier: file:./projects/mongo.tgz version: file:projects/mongo.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(gcp-metadata@5.3.0(encoding@0.1.13))(snappy@7.2.2)(socks@2.8.3)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3)) + '@rush-temp/network': + specifier: file:./projects/network.tgz + version: file:projects/network.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3)) + '@rush-temp/network-zeromq': + specifier: file:./projects/network-zeromq.tgz + version: file:projects/network-zeromq.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3)) '@rush-temp/notification': specifier: file:./projects/notification.tgz version: file:projects/notification.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(@types/node@22.15.29)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3)) @@ -2341,6 +2347,9 @@ importers: yjs: specifier: ^13.6.23 version: 13.6.23 + zeromq: + specifier: ^6.5.0 + version: 6.5.0 zod: specifier: ^3.22.4 version: 3.24.2 @@ -5400,6 +5409,14 @@ packages: resolution: {integrity: sha512-2NrKTsPik2KN7fUtLuhOmZFyaailUgcbTQKLiXEvmcNdiCBYdpopLuCl7wU9l1MffhKvekpCb05WoB4os4ZSdA==, tarball: file:projects/mongo.tgz} version: 0.0.0 + '@rush-temp/network-zeromq@file:projects/network-zeromq.tgz': + resolution: {integrity: sha512-hXbmbcq2y/BwdXvrOeP5hpZYRqssUggoqp4cRGU5rFZRLRbQLDDU9k3AkCVi385ijUkp0EpsaDhzyEwgY4pXTw==, tarball: file:projects/network-zeromq.tgz} + version: 0.0.0 + + '@rush-temp/network@file:projects/network.tgz': + resolution: {integrity: sha512-NZhhGCoxzJydJ7hjYD8QJFl/iQPt6Vj6hrRsTsKBBO2iS25HRxjxZ1CTTTkbSU/3ZzKBZ8qrlJZuc4LIsyezCA==, tarball: file:projects/network.tgz} + version: 0.0.0 + '@rush-temp/notification-assets@file:projects/notification-assets.tgz': resolution: {integrity: sha512-vQTl0ZJNng9Y+dSKnBRwSbf1c6zVJp2xTUCyH0pB4DiJcco0GiHxu0vS1eVRM6X1sgCQ9w7bxX8S6Wyvp9IcOw==, tarball: file:projects/notification-assets.tgz} version: 0.0.0 @@ -8240,6 +8257,10 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cmake-ts@1.0.2: + resolution: {integrity: sha512-5l++JHE7MxFuyV/OwJf3ek7ZZN1aGPFPM5oUz6AnK5inQAPe4TFXRMz5sA2qg2FRgByPWdqO+gSfIPo8GzoKNQ==} + hasBin: true + co-body@6.1.0: resolution: {integrity: sha512-m7pOT6CdLN7FuXUcpuz/8lfQ/L77x8SchHCF4G0RBTJO20Wzmhn5Sp4/5WsKy8OSpifBSUrmg83qEqaDHdyFuQ==} @@ -11746,6 +11767,10 @@ packages: node-addon-api@6.1.0: resolution: {integrity: sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==} + node-addon-api@8.4.0: + resolution: {integrity: sha512-D9DI/gXHvVmjHS08SVch0Em8G5S1P+QWtU31appcKT/8wFSPRcdHadIFSAntdMMVM5zz+/DL+bL/gz3UDppqtg==} + engines: {node: ^18 || ^20 || >= 21} + node-api-version@0.2.0: resolution: {integrity: sha512-fthTTsi8CxaBXMaBAD7ST2uylwvsnYxh2PfaScwpMhos6KlSFajXQPcM4ogNE1q2s3Lbz9GCGqeIHC+C6OZnKg==} @@ -14492,6 +14517,10 @@ packages: zen-observable@0.8.15: resolution: {integrity: sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==} + zeromq@6.5.0: + resolution: {integrity: sha512-vWOrt19lvcXTxu5tiHXfEGQuldSlU+qZn2TT+4EbRQzaciWGwNZ99QQTolQOmcwVgZLodv+1QfC6UZs2PX/6pQ==} + engines: {node: '>= 12'} + zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} @@ -23385,6 +23414,62 @@ snapshots: - supports-color - ts-node + '@rush-temp/network-zeromq@file:projects/network-zeromq.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3))': + dependencies: + '@types/jest': 29.5.12 + '@types/node': 22.15.29 + '@types/uuid': 8.3.4 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3)) + prettier: 3.2.5 + ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) + typescript: 5.8.3 + uuid: 8.3.2 + zeromq: 6.5.0 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + + '@rush-temp/network@file:projects/network.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3))': + dependencies: + '@types/jest': 29.5.12 + '@types/node': 22.15.29 + '@types/uuid': 8.3.4 + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3) + '@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.8.3) + eslint: 8.56.0 + eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.56.0)(typescript@5.8.3))(eslint@8.56.0)(typescript@5.8.3))(eslint-plugin-import@2.29.1(eslint@8.56.0))(eslint-plugin-n@15.7.0(eslint@8.56.0))(eslint-plugin-promise@6.1.1(eslint@8.56.0))(eslint@8.56.0)(typescript@5.8.3) + eslint-plugin-import: 2.29.1(eslint@8.56.0) + eslint-plugin-n: 15.7.0(eslint@8.56.0) + eslint-plugin-promise: 6.1.1(eslint@8.56.0) + jest: 29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3)) + prettier: 3.2.5 + simplytyped: 3.3.0(typescript@5.8.3) + ts-jest: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(jest@29.7.0(@types/node@22.15.29)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3)))(typescript@5.8.3) + typescript: 5.8.3 + uuid: 8.3.2 + transitivePeerDependencies: + - '@babel/core' + - '@jest/types' + - babel-jest + - babel-plugin-macros + - esbuild + - node-notifier + - supports-color + - ts-node + '@rush-temp/notification-assets@file:projects/notification-assets.tgz(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(esbuild@0.24.2)(ts-node@10.9.2(@types/node@22.15.29)(typescript@5.8.3))': dependencies: '@types/jest': 29.5.12 @@ -32328,6 +32413,8 @@ snapshots: clone@1.0.4: {} + cmake-ts@1.0.2: {} + co-body@6.1.0: dependencies: inflation: 2.1.0 @@ -36541,6 +36628,8 @@ snapshots: node-addon-api@6.1.0: {} + node-addon-api@8.4.0: {} + node-api-version@0.2.0: dependencies: semver: 7.6.3 @@ -39674,6 +39763,11 @@ snapshots: zen-observable@0.8.15: {} + zeromq@6.5.0: + dependencies: + cmake-ts: 1.0.2 + node-addon-api: 8.4.0 + zip-stream@6.0.1: dependencies: archiver-utils: 5.0.2 diff --git a/network/README.md b/network/README.md new file mode 100644 index 00000000000..9c6759a88f5 --- /dev/null +++ b/network/README.md @@ -0,0 +1,397 @@ +# Huly Virtual Network + +A distributed, scalable network architecture for the Huly platform that enables fault-tolerant communication between accounts, workspaces, and nodes in a multi-tenant environment. + +## πŸš€ Overview + +The Huly Virtual Network is a sophisticated distributed system designed to handle enterprise-scale workloads with the following key capabilities: + +- **Distributed Load Balancing**: Intelligent routing of accounts across multiple nodes using consistent hashing +- **Multi-Tenant Architecture**: Secure isolation of workspaces with role-based access control +- **Fault Tolerance**: Automatic failover and recovery mechanisms +- **Horizontal Scaling**: Dynamic node discovery and elastic scaling +- **Real-time Communication**: Event-driven architecture with broadcast capabilities + +![Network Architecture](./docs/Schema.png) + +## πŸ—οΈ Architecture Components + +### Core Components + +```mermaid +graph TB + A[Client] -->|Connects| SM[Session Manager] + SM -->|Linked to| N1[Node 1] + SM -->|Routes to| N2[Node 2] + SM -->|Routes to| N3[Node 3] + + subgraph "Node 1" + N1[Node1] + SM[Session Manager] + W1[Workspace 1] + W2[Workspace 2] + end + + subgraph "Node 2" + N2[Node2] + W3[Workspace 3] + end + + subgraph "Node 3" + N3[Node3] + W4[Workspace 4] + end + + subgraph "Node 4" + N4[Node4] + W7[Sub-workspace] + W8[Sub-workspace] + end + + N1 -->|Manages| W1[Workspace 1] + N1 -->|Manages| W2[Workspace 2] + + N2 -->|Manages| W3[Workspace 3] + N3 -->|Manages| W4[Workspace 4] + + W1 -.->|Child| N4[Node4] + W3 -.->|Child| N4[Node4] + + N4 -.->|Manages| W7[Sub-workspace] + N4 -.->|Manages| W8[Sub-workspace] +``` + +### 1. Session Management Layer + +The **Session Manager** coordinates virtual client connections to node and perform and collect all reduce requests: + +- **Client Authentication**: Token-based authentication with workspace access validation +- **Session Lifecycle**: Connection establishment, maintenance, and cleanup +- **Load Distribution**: Intelligent routing based on account-to-node mapping +- **Real-time Events**: Rich communication with broadcast capabilities + +### 2. Node Architecture + +**Nodes** are computational units that handle distributed operations: + +```typescript +interface Node { + _id: NodeUuid + ask: (req: Request, options?: NodeAskOptions) => Promise + modify: (workspaceId: WorkspaceUuid, req: Request) => Promise> + ping: (workspaces: WorkspaceUuid[], processChildren: boolean) => Promise + broadcast: (req: Array>) => Promise + close: () => Promise +} +``` + +**Key Features:** + +- **Query Processing**: Distributed map-reduce operations across workspaces +- **Modification Handling**: Transactional updates with consistency guarantees +- **Health Monitoring**: Continuous ping/pong for node availability +- **Event Broadcasting**: Real-time message distribution + +### 3. Workspace Management + +**Workspaces** are isolated environments that contain application data: + +```typescript +interface Workspace { + _id: WorkspaceUuid + ask: (req: Request) => Promise> + modify: (req: Request) => Promise> + suspend: () => Promise + resume: () => Promise + close: () => Promise +} +``` + +**Lifecycle Management:** + +- **Lazy Loading**: On-demand workspace activation +- **Resource Optimization**: Automatic suspension of inactive workspaces +- **Hierarchical Organization**: Parent-child workspace relationships +- **Health Monitoring**: Continuous workspace health checks + +### 4. Discovery Services + +Three specialized discovery services manage system topology: + +#### Account Discovery + +Maps `AccountUuid` to accessible `WorkspaceUuid[]`: + +```mermaid +graph LR + A[Account UUID] --> AD[Workspace Discovery] + AD --> W1[Workspace 1
Owner] + AD --> W2[Workspace 2
Member] + AD --> W3[Workspace 3
Guest] +``` + +#### Node Discovery + +Distributes accounts across nodes using consistent hashing: + +```mermaid +graph LR + A[Account UUID] --> DHT[DHT] + DHT --> N1[Node 1] + DHT --> N2[Node 2] + DHT --> N3[Node 3] +``` + +#### Workspace Discovery + +Resolves workspace child relations: + +```mermaid +graph LR + W1[Parent Workspace] --> WD[Workspace Discovery] + WD --> CW1[Child Workspace 1] + WD --> CW2[Child Workspace 2] +``` + +## πŸ”„ Core Operations + +### Query Operations (Map/Reduce) + +Distributed query processing with intelligent routing: + +```mermaid +sequenceDiagram + participant C as Client + participant SM as Session Manager + participant PN as Personal Node + participant TN as Target Nodes + participant W as Workspaces + + C->>SM: Query Request + SM->>PN: Route to Personal Node + PN->>PN: Resolve Workspaceβ†’Node Mapping + PN->>TN: Distribute Query + TN->>W: Activate & Query Workspaces + W->>TN: Process Child Workspaces + TN->>TN: Map/Reduce Results + TN->>PN: Return Aggregated Results + PN->>C: Final Response +``` + +**Process Flow:** + +1. **Request Routing**: `AccountUuid` β†’ `NodeId` +2. **Workspace Resolution**: Personal node resolves workspace locations +3. **Distributed Execution**: Query distributed to target nodes +4. **Workspace Activation**: Lazy loading of required workspaces +5. **Hierarchical Processing**: Automatic handling of child workspaces +6. **Result Aggregation**: Map-reduce across distributed results +7. **Response Delivery**: Consolidated response to client + +### Modification Operations + +Transactional modifications with consistency guarantees: + +```mermaid +sequenceDiagram + participant C as Client + participant SM as Session Manager + participant PN as Personal Node + participant TN as Target Node + participant W as Workspace + + C->>SM: Modify Request + SM->>PN: Route to Personal Node + PN->>PN: WorkspaceId β†’ NodeId Resolution + PN->>TN: Forward Modification + TN->>W: Execute on Workspace + W->>TN: Confirm Transaction + TN->>PN: Return Response + PN->>C: Forward Response +``` + +### Broadcast Operations + +Efficient real-time message distribution: + +#### Account-Level Broadcast + +```mermaid +graph LR + M[Message] --> SM[Session Manager] + SM --> PN[Personal Node] + PN --> C1[Client 1] + PN --> C2[Client 2] + PN --> C3[Client 3] +``` + +#### Workspace-Level Broadcast + +```mermaid +graph TB + M[Workspace Message] --> SM[Session Manager] + SM --> N1[Node 1] + SM --> N2[Node 2] + SM --> N3[Node 3] + N1 --> C1[Client 1] + N1 --> C2[Client 2] + N2 --> C3[Client 3] + N3 --> C4[Client 4] +``` + +## πŸ“¦ Package Structure + +```text +network/ +β”œβ”€β”€ core/ # Core network interfaces and implementations +β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”œβ”€β”€ api/ # Core API definitions +β”‚ β”‚ β”‚ β”œβ”€β”€ types.ts # Base type definitions +β”‚ β”‚ β”‚ β”œβ”€β”€ node.ts # Node and Workspace interfaces +β”‚ β”‚ β”‚ β”œβ”€β”€ request.ts # Request/Response types +β”‚ β”‚ β”‚ β”œβ”€β”€ discovery.ts # Discovery service interfaces +β”‚ β”‚ β”‚ β”œβ”€β”€ client.ts # Client and Session interfaces +β”‚ β”‚ β”‚ β”œβ”€β”€ transport.ts # Transport layer interfaces +β”‚ β”‚ β”‚ β”œβ”€β”€ timeouts.ts # Timeout configurations +β”‚ β”‚ β”‚ └── utils.ts # Utility types and interfaces +β”‚ β”‚ β”œβ”€β”€ discovery/ # Discovery implementations +β”‚ β”‚ β”‚ └── static.ts # Static discovery service +β”‚ β”‚ β”œβ”€β”€ node/ # Node implementations +β”‚ β”‚ β”‚ β”œβ”€β”€ node.ts # Node implementation +β”‚ β”‚ β”‚ └── session.ts # Session management +β”‚ β”‚ β”œβ”€β”€ utils.ts # Utility functions +β”‚ β”‚ └── index.ts # Main exports +β”‚ └── package.json +β”œβ”€β”€ zeromq/ # ZeroMQ transport implementation +β”‚ β”œβ”€β”€ src/ +β”‚ β”‚ β”œβ”€β”€ transport/ # Transport implementations +β”‚ β”‚ β”‚ β”œβ”€β”€ client.ts # Client transport +β”‚ β”‚ β”‚ β”œβ”€β”€ server.ts # Server transport +β”‚ β”‚ β”‚ └── index.ts # Transport exports +β”‚ β”‚ β”œβ”€β”€ client/ # Client implementations +β”‚ β”‚ β”œβ”€β”€ node.ts # ZeroMQ node implementation +β”‚ β”‚ β”œβ”€β”€ types.ts # ZeroMQ specific types +β”‚ β”‚ └── index.ts # Main exports +β”‚ └── package.json +β”œβ”€β”€ docs/ # Documentation and diagrams +β”‚ β”œβ”€β”€ api-reference.md # Complete API reference +β”‚ └── Schema.png # Architecture diagram +└── README.md # This file +``` + +## πŸš€ Getting Started + +### Installation + +```bash +# Install the network package +npm install @hcengineering/network +``` + +### Basic Usage + +```typescript +import { StaticNodeDiscovery, StaticWorkspaceDiscovery, SessionManagerImpl } from '@hcengineering/network' + +// Initialize discovery services +const nodeDiscovery = new StaticNodeDiscovery([ + ['node1', { ... }], + ['node2', { ... }] +]) + +const workspaceDiscovery = new StaticWorkspaceDiscovery({ + user1: ['workspace1', 'workspace2'], + user2: ['workspace3'] +}) + +// Create session manager +const sessionManager = new SessionManagerImpl(nodeImpl, tickManager, workspaceDiscovery, nodeDiscovery) + +// Register client session +const client = await sessionManager.register('user1' as AccountUuid, 'session1') + +// Perform operations +const queryResult = await client.ask( + { + method: 'query-data', + collection: 'documents' + }, + { + workspace: ['workspace1' as WorkspaceUuid] + } +) + +const modifyResult = await client.modify('workspace1' as WorkspaceUuid, { + method: 'update', + collection: 'documents', + data: { status: 'updated' } +}) +``` + +## πŸ”§ Development + +### Building + +```bash +# Build the package +npm run build + +# Build and watch for changes +npm run build:watch +``` + +### Testing + +```bash +# Run tests +npm test + +# Run tests with coverage +npm run test:coverage +``` + +### Linting + +```bash +# Format code +npm run format + +# Validate code +npm run validate +``` + +## 🀝 Contributing + +1. **Fork the Repository**: Create your own fork of the Huly platform +2. **Create Feature Branch**: `git checkout -b feature/amazing-feature` +3. **Commit Changes**: `git commit -m 'Add amazing feature'` +4. **Push to Branch**: `git push origin feature/amazing-feature` +5. **Create Pull Request**: Submit your changes for review + +### Development Guidelines + +- **Code Style**: Follow TypeScript best practices and ESLint rules +- **Testing**: Maintain high test coverage for new features +- **Documentation**: Update documentation for API changes +- **Performance**: Consider performance implications of changes + +## πŸ“„ License + +This project is licensed under the Eclipse Public License 2.0 (EPL-2.0). See the [LICENSE](../LICENSE) file for details. + +## πŸ”— Related Projects + +- **[Huly Platform](https://github.com/hcengineering/platform)**: Main platform repository +- **[Huly Self-Host](https://github.com/hcengineering/huly-selfhost)**: Self-hosting deployment +- **[Huly Examples](https://github.com/hcengineering/huly-examples)**: API usage examples + +## πŸ“ž Support + +- **Documentation**: [Platform Documentation](../README.md) +- **Issues**: [GitHub Issues](https://github.com/hcengineering/platform/issues) +- **Discussions**: [GitHub Discussions](https://github.com/hcengineering/platform/discussions) +- **Twitter**: [@huly_io](https://twitter.com/huly_io) + +--- + +Built with ❀️ by the Huly Platform Contributors diff --git a/network/core/.eslintrc.js b/network/core/.eslintrc.js new file mode 100644 index 00000000000..ce90fb9646f --- /dev/null +++ b/network/core/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/network/core/config/rig.json b/network/core/config/rig.json new file mode 100644 index 00000000000..78cc5a17334 --- /dev/null +++ b/network/core/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "node" +} diff --git a/network/core/jest.config.js b/network/core/jest.config.js new file mode 100644 index 00000000000..2cfd408b679 --- /dev/null +++ b/network/core/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/network/core/package.json b/network/core/package.json new file mode 100644 index 00000000000..9190e5f2e50 --- /dev/null +++ b/network/core/package.json @@ -0,0 +1,53 @@ +{ + "name": "@hcengineering/network", + "version": "0.6.32", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "author": "Huly Platform Contributors", + "template": "@hcengineering/node-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent --forceExit", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --forceExit", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "simplytyped": "^3.3.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.8.3", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/node": "^22.15.29", + "@types/uuid": "^8.3.1" + }, + "dependencies": { + "@hcengineering/analytics": "^0.6.0", + "uuid": "^8.3.2" + }, + "repository": "https://github.com/hcengineering/platform", + "publishConfig": { + "registry": "https://npm.pkg.github.com" + }, + "exports": { + ".": { + "types": "./types/index.d.ts", + "require": "./lib/index.js", + "import": "./lib/index.js" + } + } +} diff --git a/network/core/src/__test__/dummy.ts b/network/core/src/__test__/dummy.ts new file mode 100644 index 00000000000..98a626d80ef --- /dev/null +++ b/network/core/src/__test__/dummy.ts @@ -0,0 +1,38 @@ +import type { Workspace } from '../api/node' +import type { Request, ResponseValue } from '../api/request' +import type { WorkspaceUuid } from '../api/types' + +export class DummyWorkspace implements Workspace { + _id: WorkspaceUuid + + data: any[] + + constructor (id: WorkspaceUuid) { + this._id = id + this.data = [`hello from ${this._id}`] + } + + async ask(req: Request): Promise> { + return { value: this.data, total: 1 } + } + + async modify (req: Request): Promise> { + if (req.data.action === 'broadcast') { + // Simulate broadcasting by adding to the workspace data + this.data.push(`broadcasted: ${req.data.data}`) + return { value: this.data, total: 1 } + } + if (req.data.action === 'add') { + // Simulate adding data to the workspace + this.data.push(req.data.data) + return { value: this.data, total: 1 } + } + return { value: ['done', this._id], total: 0 } + } + + async suspend (): Promise {} + + async resume (): Promise {} + + async close (): Promise {} +} diff --git a/network/core/src/__test__/node.spec.ts b/network/core/src/__test__/node.spec.ts new file mode 100644 index 00000000000..7672f1628dd --- /dev/null +++ b/network/core/src/__test__/node.spec.ts @@ -0,0 +1,160 @@ +import type { WorkspaceFactory } from '../api/node' +import { type NodeImpl } from '../node/node' +import type { SessionManagerImpl } from '../node/session' +import { DummyWorkspace } from './dummy' +import { nodes, simpleDiscovery, users, workspaces, wsDiscovery } from './samples' +import { prepare } from './utils' + +describe('node-ask', () => { + const wsFactory: WorkspaceFactory = async (workspaceId) => new DummyWorkspace(workspaceId) + + it('check ask for all workspaces', async () => { + const { sessionManager } = await prepare(wsFactory, wsDiscovery, simpleDiscovery, nodes.node2) + const client1 = await sessionManager.register(users.user1, 's1') + + const helloResp = await client1.ask('hello') + + expect(helloResp.value.map((it) => it)).toEqual([ + 'hello from ws1', + 'hello from ws10', + 'hello from ws2', + 'hello from ws3', + 'hello from ws7', + 'hello from ws8', + 'hello from ws9' + ]) + }) + + it('check ask for selected ws and all childs', async () => { + const { sessionManager } = await prepare(wsFactory, wsDiscovery, simpleDiscovery, nodes.node2) + const client1 = await sessionManager.register(users.user1, 's1') + + const helloResp = await client1.ask('hello', { workspace: [workspaces.ws1] }) + + expect(helloResp.value.map((it) => it)).toEqual([ + 'hello from ws1', + 'hello from ws10', + 'hello from ws7', + 'hello from ws8', + 'hello from ws9' + ]) + }) + it('check ask for single child ws of ws', async () => { + const { sessionManager } = await prepare(wsFactory, wsDiscovery, simpleDiscovery, nodes.node2) + const client1 = await sessionManager.register(users.user1, 's1') + + const helloResp = await client1.ask('hello', { workspace: [workspaces.ws7] }) + + expect(helloResp.value.map((it) => it)).toEqual(['hello from ws7']) + }) + it('check ask for childs of child workspaces', async () => { + const { sessionManager } = await prepare(wsFactory, wsDiscovery, simpleDiscovery, nodes.node2) + const client1 = await sessionManager.register(users.user1, 's1') + + const helloResp = await client1.ask('hello', { workspace: [workspaces.ws8] }) + + expect(helloResp.value.map((it) => it)).toEqual(['hello from ws10', 'hello from ws8', 'hello from ws9']) + }) + + it('check modify ws1', async () => { + const { sessionManager, manager } = await prepare(wsFactory, wsDiscovery, simpleDiscovery, nodes.node2) + const client1 = await sessionManager.register(users.user1, 's1') + + await client1.modify(workspaces.ws1, { action: 'add', data: 'h1' }) + const node4: NodeImpl = (await manager.node(nodes.node4)) as NodeImpl + expect(node4.workspaces[workspaces.ws1]).toBeDefined() + + expect((node4.workspaces[workspaces.ws1].workspace as DummyWorkspace).data.includes('h1')).toBeTruthy() + const helloResp = await client1.ask('hello', { workspace: [workspaces.ws1] }) + expect(helloResp.value.map((it) => it)).toEqual([ + 'hello from ws1', + 'h1', + 'hello from ws10', + 'hello from ws7', + 'hello from ws8', + 'hello from ws9' + ]) + }) + it('check modify ws9', async () => { + const { sessionManager } = await prepare(wsFactory, wsDiscovery, simpleDiscovery, nodes.node2) + const client1 = await sessionManager.register(users.user1, 's1') + + await client1.modify(workspaces.ws9, { action: 'add', data: 'h9' }) + const helloResp = await client1.ask('hello', { workspace: [workspaces.ws1] }) + expect(helloResp.value.map((it) => it)).toEqual([ + 'hello from ws1', + 'hello from ws10', + 'hello from ws7', + 'hello from ws8', + 'hello from ws9', + 'h9' + ]) + }) + it('check register to wrong node', async () => { + const { sessionManager } = await prepare(wsFactory, wsDiscovery, simpleDiscovery, nodes.node1) + await expect(sessionManager.register(users.user1, 's1')).rejects.toThrow( + 'Invalid host node for account user1, expected node1, got node2' + ) + }) + it('check broadcast', async () => { + const { sessionManager, manager } = await prepare(wsFactory, wsDiscovery, simpleDiscovery, nodes.node2) + const client1 = await sessionManager.register(users.user1, 's1') + + client1.onBroadcast = (response) => { + expect(response.data.value[0]).toBe('broadcasted1') + } + + const node5 = await manager.node(nodes.node5) + + await node5.broadcast([ + { + account: users.user1, + data: { value: ['broadcasted1'], total: 1 }, + _id: undefined, + nodeId: nodes.node5, + workspaceId: workspaces.ws1 + } + ]) + }) + + it('check broadcast', async () => { + const { sessionManager, manager } = await prepare(wsFactory, wsDiscovery, simpleDiscovery, nodes.node2) + const client1 = await sessionManager.register(users.user1, 's1') + + client1.onBroadcast = (response) => { + expect(response.data.value[0]).toBe('broadcasted1') + } + + const node5 = await manager.node(nodes.node5) + + await node5.broadcast([ + { + account: users.user1, + data: { value: ['broadcasted1'], total: 1 }, + _id: undefined, + nodeId: nodes.node5, + workspaceId: workspaces.ws1 + } + ]) + }) + + it('check warmup', async () => { + const { sessionManager, manager } = await prepare(wsFactory, wsDiscovery, simpleDiscovery, nodes.node2) + await sessionManager.register(users.user1, 's1') + await (sessionManager as SessionManagerImpl).waitWarmups() + expect(Object.keys(((await manager.node(nodes.node1)) as NodeImpl).workspaces)).toEqual(['ws3', 'ws8']) + expect(Object.keys(((await manager.node(nodes.node2)) as NodeImpl).workspaces)).toEqual(['ws9', 'ws10']) + expect(Object.keys(((await manager.node(nodes.node4)) as NodeImpl).workspaces)).toEqual(['ws1']) + expect(Object.keys(((await manager.node(nodes.node5)) as NodeImpl).workspaces)).toEqual(['ws2', 'ws7']) + }) + + it('check suspend/resume', async () => { + const { sessionManager, manager } = await prepare(wsFactory, wsDiscovery, simpleDiscovery, nodes.node2) + await sessionManager.register(users.user1, 's1') + await (sessionManager as SessionManagerImpl).waitWarmups() + expect(Object.keys(((await manager.node(nodes.node1)) as NodeImpl).workspaces)).toEqual(['ws3', 'ws8']) + expect(Object.keys(((await manager.node(nodes.node2)) as NodeImpl).workspaces)).toEqual(['ws9', 'ws10']) + expect(Object.keys(((await manager.node(nodes.node4)) as NodeImpl).workspaces)).toEqual(['ws1']) + expect(Object.keys(((await manager.node(nodes.node5)) as NodeImpl).workspaces)).toEqual(['ws2', 'ws7']) + }) +}) diff --git a/network/core/src/__test__/samples.ts b/network/core/src/__test__/samples.ts new file mode 100644 index 00000000000..4d09f291791 --- /dev/null +++ b/network/core/src/__test__/samples.ts @@ -0,0 +1,43 @@ +import type { AccountUuid, NodeUuid, WorkspaceUuid } from '../api/types' +import { StaticNodeDiscovery, StaticWorkspaceDiscovery } from '../discovery/static' + +export const workspaces = { + ws1: 'ws1' as WorkspaceUuid, + ws2: 'ws2' as WorkspaceUuid, + ws3: 'ws3' as WorkspaceUuid, + ws4: 'ws4' as WorkspaceUuid, + ws5: 'ws5' as WorkspaceUuid, + ws6: 'ws6' as WorkspaceUuid, + ws7: 'ws7' as WorkspaceUuid, + ws8: 'ws8' as WorkspaceUuid, + ws9: 'ws9' as WorkspaceUuid, + ws10: 'ws10' as WorkspaceUuid +} + +export const users = { + user1: 'user1' as AccountUuid, + user2: 'user2' as AccountUuid +} + +export const nodes = { + node1: 'node1' as NodeUuid, + node2: 'node2' as NodeUuid, + node3: 'node3' as NodeUuid, + node4: 'node4' as NodeUuid, + node5: 'node5' as NodeUuid +} + +export const simpleDiscovery = new StaticNodeDiscovery([ + [nodes.node1, {}], + [nodes.node2, {}], + [nodes.node3, {}], + [nodes.node4, {}], + [nodes.node5, {}] +]) + +export const wsDiscovery = new StaticWorkspaceDiscovery({ + [users.user1]: [workspaces.ws1, workspaces.ws2, workspaces.ws3], + [users.user2]: [workspaces.ws4, workspaces.ws5, workspaces.ws6], + [workspaces.ws1]: [workspaces.ws7, workspaces.ws8], + [workspaces.ws8]: [workspaces.ws9, workspaces.ws10] +}) diff --git a/network/core/src/__test__/utils.ts b/network/core/src/__test__/utils.ts new file mode 100644 index 00000000000..7b3ad293e8f --- /dev/null +++ b/network/core/src/__test__/utils.ts @@ -0,0 +1,31 @@ +import type { SessionManager } from '../api/client' +import type { NodeFactory, NodeManager, WorkspaceFactory } from '../api/node' +import type { NodeUuid } from '../api/types' +import type { StaticNodeDiscovery, StaticWorkspaceDiscovery } from '../discovery/static' +import { NodeImpl, NodeManagerImpl } from '../node/node' +import { SessionManagerImpl } from '../node/session' +import { TickManagerImpl } from '../utils' + +export async function prepare ( + wsFactory: WorkspaceFactory, + wsDiscovery: StaticWorkspaceDiscovery, + simpleDiscovery: StaticNodeDiscovery, + localNodeId: NodeUuid +): Promise<{ sessionManager: SessionManager, manager: NodeManager }> { + const workerFactory: NodeFactory = async (nodeId) => { + return new NodeImpl(nodeId, wsFactory, wsDiscovery, manager, new TickManagerImpl(20)) + } + + const manager = new NodeManagerImpl(workerFactory, simpleDiscovery) + + const localNode = await manager.node(localNodeId) + + const sessionManager = new SessionManagerImpl( + localNode as NodeImpl, + new TickManagerImpl(20), + wsDiscovery, + simpleDiscovery + ) + + return { sessionManager, manager } +} diff --git a/network/core/src/api/client.ts b/network/core/src/api/client.ts new file mode 100644 index 00000000000..0b461f4e3a6 --- /dev/null +++ b/network/core/src/api/client.ts @@ -0,0 +1,32 @@ +import type { Response, ResponseValue } from './request' +import type { AccountUuid, WorkspaceUuid } from './types' + +export type ClientBroadcast = (account: AccountUuid, response: Array>) => Promise + +export interface AskOptions { + workspace?: WorkspaceUuid[] +} + +export interface Client { + account: AccountUuid + sessionId: string // Unique session identifier for the client. + + ask: (req: T, options?: AskOptions) => Promise> + + modify: (workspaceId: WorkspaceUuid, req: T) => Promise> + + onBroadcast?: (response: Response) => void + + onClose?: () => void +} + +/** + * A Huly Network client, should work at same place as node. + */ +export interface SessionManager { + // Manage user sessions. + register: (account: AccountUuid, sessionid: string) => Promise + unregister: (sessionid: string) => Promise + + close: () => void +} diff --git a/network/core/src/api/discovery.ts b/network/core/src/api/discovery.ts new file mode 100644 index 00000000000..129d7edf771 --- /dev/null +++ b/network/core/src/api/discovery.ts @@ -0,0 +1,25 @@ +import type { AccountUuid, NodeUuid, WorkspaceUuid } from './types' + +export type NodeData = Record + +/** + * Provide interface for node discovery + */ +export interface NodeDiscovery { + byWorkspace: (workspace: WorkspaceUuid) => Promise + byAccount: (account: AccountUuid) => Promise + + list: () => Iterable + + stats: (node: NodeUuid) => Promise +} + +export interface WorkspaceDiscovery { + byAccount: (account: AccountUuid) => Promise + + byWorkspace: (workspace: WorkspaceUuid) => Promise +} + +export interface AccountDiscovery { + byWorkspace: (workspace: WorkspaceUuid) => Promise +} diff --git a/network/core/src/api/net.ts b/network/core/src/api/net.ts new file mode 100644 index 00000000000..aa4ad3eb07d --- /dev/null +++ b/network/core/src/api/net.ts @@ -0,0 +1,109 @@ +export type ContainerUuid = string & { _containerUuid: true } +export type ContainerKind = string & { _containerKind: true } +export type AgentUuid = string & { _networkAgentUuid: true } +export type ContainerEndpointRef = string & { _containerEndpointRef: true } + +export type ContainerState = | 'active' | 'stopped' | 'error' + +export interface ContainerRecord { + agentId: AgentUuid + uuid: ContainerUuid + state: ContainerState + kind: ContainerKind + endpoint: ContainerEndpointRef + lastVisit: number // Last time when container was visited +} + +export interface AgentRecord { + agentId: AgentUuid + // A change to containers + containers: ContainerRecord[] +} + +export interface StartRequest { + kind: ContainerKind + extra?: Record // Extra parameters for container start +} + +export type RequestData = string | ArrayBufferLike + +export interface ConnectionManager { + connect: (endpoint: ContainerEndpointRef) => Promise +} + +/** + * Interface to Huly network. + */ +export interface Network { + /* + * Registe or reregister agent in network. + * On every network restart agent should reconnect to network. + */ + register: (uuid: AgentUuid, agent: NetworkAgent, containers: ContainerRecord[]) => Promise + + agents: () => AgentRecord[] + + // A full uniq set of supported container kinds. + kinds: () => ContainerKind[] + + // Establish a recoverable connection to endpoint. + connect: (uuid: ContainerUuid) => Promise + + /* + * Get/Start of required container kind on agent + * Will start a required container on agent, if not already started. + */ + get: (uuid: ContainerUuid, request: StartRequest) => Promise + + list: (kind: ContainerKind) => ContainerRecord[] + + // Send some data to container, using proxy connection. + send: (target: ContainerUuid, data: RequestData) => Promise + + // ask for immediate termination for container + terminate: (uuid: ContainerUuid) => Promise +} + +export interface ContainerEvent { + added: ContainerRecord[] + deleted: ContainerRecord[] + updated: ContainerRecord[] +} + +export interface NetworkAgent { + uuid: AgentUuid + + // A supported set of container kinds supported to be managed by the agent + kinds: ContainerKind[] + + // Inform agent about other container events + onContainer: (event: ContainerEvent) => Promise + + // event handled from agent to network events. + onUpdate?: (event: ContainerEvent) => Promise + + // Agent will inform this callback when it is still alive. + onAlive?: () => void + + // Get/Start of required container kind on agent + get: (uuid: ContainerUuid, request: StartRequest) => Promise + + list: (kind: ContainerKind) => Promise + + // Send some data to container + send: (target: ContainerUuid, data: RequestData) => Promise + + // ask for immediate termination for container + terminate: (uuid: ContainerUuid) => Promise + + // ping container for being still used. + ping: (uuid: ContainerUuid) => Promise +} + +// A request/reponse interface to container. +export interface ContainerConnection { + send: (data: RequestData) => Promise + + // broadcast events. + on?: (data: RequestData) => Promise +} diff --git a/network/core/src/api/node.ts b/network/core/src/api/node.ts new file mode 100644 index 00000000000..def4d96a97b --- /dev/null +++ b/network/core/src/api/node.ts @@ -0,0 +1,47 @@ +import type { AskOptions } from './client' +import type { NodeDiscovery } from './discovery' +import type { Request, RequestAkn, Response, ResponseValue } from './request' +import type { NodeUuid, WorkspaceUuid } from './types' + +export interface NodeAskOptions extends AskOptions { + target?: WorkspaceUuid[] +} + +export interface Node { + _id: NodeUuid + + ask: (req: Request, options?: NodeAskOptions) => Promise + + modify: (workspaceId: WorkspaceUuid, req: Request) => Promise> + + ping: (workspaces: WorkspaceUuid[], processChildren: boolean) => Promise + + /** + * Inform clients about some request/Response + */ + broadcast: (req: Array>) => Promise + + close: () => Promise +} + +export interface NodeManager extends NodeDiscovery { + node: (node: NodeUuid) => Promise +} + +export type NodeFactory = (node: NodeUuid) => Promise + +export interface Workspace { + _id: WorkspaceUuid + + ask: (req: Request) => Promise> + + modify: (req: Request) => Promise> + + suspend: () => Promise // Suspend any system resources, be ready for a resume before any new requests. + + resume: () => Promise // A restore state and be able to respond for user actions. + + close: () => Promise +} + +export type WorkspaceFactory = (workspaceId: WorkspaceUuid) => Promise diff --git a/network/core/src/api/request.ts b/network/core/src/api/request.ts new file mode 100644 index 00000000000..4dd87558c96 --- /dev/null +++ b/network/core/src/api/request.ts @@ -0,0 +1,33 @@ +import type { AccountUuid, NodeUuid, WorkspaceUuid } from './types' + +export type RequestId = string & { __requestId: true } + +export interface Request { + _id: RequestId + account: AccountUuid + + // Workspace filter + workspace?: WorkspaceUuid | WorkspaceUuid[] + + workspaces: Record // A list of already processed workspaces. + data: T +} + +export interface RequestAkn { + // A list of nodes we need to retrieve data from, or retry to ask again if required. + workspaces: Record +} + +export interface ResponseValue { + value: T[] + total: number +} + +export interface Response { + _id: RequestId | undefined + account: AccountUuid + + nodeId: NodeUuid + workspaceId: WorkspaceUuid + data: ResponseValue +} diff --git a/network/core/src/api/timeouts.ts b/network/core/src/api/timeouts.ts new file mode 100644 index 00000000000..2da846db846 --- /dev/null +++ b/network/core/src/api/timeouts.ts @@ -0,0 +1,5 @@ +export const timeouts = { + pingTimeout: 30, // seconds + retryTimeout: 250, // ms + closeWorkspaceTimeout: 5 * 60 // 5 minutes +} diff --git a/network/core/src/api/transport.ts b/network/core/src/api/transport.ts new file mode 100644 index 00000000000..91b9775ea86 --- /dev/null +++ b/network/core/src/api/transport.ts @@ -0,0 +1,17 @@ +import type { RequestId } from './request' +import type { AccountUuid, NodeUuid } from './types' + +export interface ClientTransport { + request: (clientId: AccountUuid, reqId: RequestId, body: any) => Promise + subscribe: (account: AccountUuid) => void + unsubscribe: (account: AccountUuid) => void + close: () => Promise +} + +export interface ServerTransport { + nodeId: NodeUuid + + request: (target: NodeUuid, body: any) => Promise + send: (target: NodeUuid, reqId: RequestId | undefined, body: any) => Promise + close: () => Promise +} diff --git a/network/core/src/api/types.ts b/network/core/src/api/types.ts new file mode 100644 index 00000000000..84bc4bc2b4e --- /dev/null +++ b/network/core/src/api/types.ts @@ -0,0 +1,11 @@ +/** + * A unique identifier for a workspace. + */ +export type WorkspaceUuid = string & { __workspaceUuid: true } + +/** + * A unique identifier for an account. + */ +export type AccountUuid = string & { __accountUuid: true } + +export type NodeUuid = string & { __nodeUuid: true } diff --git a/network/core/src/api/utils.ts b/network/core/src/api/utils.ts new file mode 100644 index 00000000000..acc662945ce --- /dev/null +++ b/network/core/src/api/utils.ts @@ -0,0 +1,10 @@ +export type TickHandler = (tick: number, tps: number) => void | Promise + +export interface TickManager { + now: () => number + register: (handler: TickHandler) => void + unregister: (handler: TickHandler) => void + tick: () => void + tps: number + nextHash: () => number +} diff --git a/network/core/src/discovery/static.ts b/network/core/src/discovery/static.ts new file mode 100644 index 00000000000..3d68aa9b6d8 --- /dev/null +++ b/network/core/src/discovery/static.ts @@ -0,0 +1,57 @@ +import type { NodeData, NodeDiscovery, WorkspaceDiscovery } from '../api/discovery' +import type { AccountUuid, NodeUuid, WorkspaceUuid } from '../api/types' + +/** + * Returns a hash code for a string. + * (Compatible to Java's String.hashCode()) + * + * The hash code for a string object is computed as + * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] + * using number arithmetic, where s[i] is the i th character + * of the given string, n is the length of the string, + * and ^ indicates exponentiation. + * (The hash value of the empty string is zero.) + * + */ +function hashName (name: string): number { + return [...name].reduce((hash, c) => (Math.imul(31, hash) + c.charCodeAt(0)) | 0, 0) +} + +export class StaticNodeDiscovery implements NodeDiscovery { + constructor (private readonly nodes: Array<[NodeUuid, T]>) {} + async byWorkspace (workspace: WorkspaceUuid): Promise { + const hcode = hashName(workspace) + const node = this.nodes[Math.abs(hcode) % this.nodes.length] // This is a simple hash-based selection + return await Promise.resolve(node[0]) + } + + async byAccount (account: AccountUuid): Promise { + const hcode = hashName(account) + const node = this.nodes[Math.abs(hcode) % this.nodes.length] // This is a simple hash-based selection + return await Promise.resolve(node[0]) + } + + list (): Iterable { + return this.nodes.map(([id]) => id) + } + + async stats (node: NodeUuid): Promise { + const found = this.nodes.find(([id]) => id === node) + if (found == null) { + throw new Error(`Node ${node} not found`) + } + return found[1] + } +} + +export class StaticWorkspaceDiscovery implements WorkspaceDiscovery { + constructor (private readonly workspaces: Record) {} + + async byAccount (account: AccountUuid): Promise { + return this.workspaces[account] ?? [] + } + + async byWorkspace (workspace: WorkspaceUuid): Promise { + return this.workspaces[workspace] ?? [] + } +} diff --git a/network/core/src/index.ts b/network/core/src/index.ts new file mode 100644 index 00000000000..c7196f9d819 --- /dev/null +++ b/network/core/src/index.ts @@ -0,0 +1,10 @@ +export * from './api/discovery' +export * from './api/node' +export * from './api/request' +export * from './api/transport' +export * from './api/types' +export * from './api/utils' +export * from './discovery/static' +export * from './node/node' +export * from './node/session' +export * from './utils' diff --git a/network/core/src/net/index.ts b/network/core/src/net/index.ts new file mode 100644 index 00000000000..f3dbc7f0f4a --- /dev/null +++ b/network/core/src/net/index.ts @@ -0,0 +1,281 @@ +import type { + AgentRecord, + AgentUuid, + ConnectionManager, + ContainerConnection, + ContainerEndpointRef, + ContainerEvent, + ContainerKind, + ContainerRecord, + ContainerState, + ContainerUuid, + Network, + NetworkAgent, + RequestData, + StartRequest +} from '../api/net' + +interface ContainerRecordImpl { + record: ContainerRecord + endpoint: ContainerEndpointRef | Promise +} + +interface AgentRecordImpl { + api: NetworkAgent + containers: Map + + lastSeen: number // Last time when agent was seen +} + +export class NetworkImpl implements Network { + idx: number = 0 + + _agents = new Map() + + _containers = new Map() + + constructor (readonly cntMgr: ConnectionManager) {} + + agents (): AgentRecord[] { + return Array.from( + this._agents.values().map(({ api, containers }) => ({ + agentId: api.uuid, + containers: Object.values(containers).map(({ record }) => record) + })) + ) + } + + kinds (): ContainerKind[] { + return Array.from( + this._agents + .values() + .map((it) => it.api.kinds) + .flatMap((it) => it) + ) + } + + list (kind: ContainerKind): ContainerRecord[] { + return Array.from(this._agents.values()) + .flatMap((it) => Array.from(it.containers.values())) + .filter((it) => it.record.kind === kind) + .map((it) => it.record) + } + + async send (target: ContainerUuid, data: RequestData): Promise { + const agentId = this._containers.get(target) + if (agentId === undefined) { + throw new Error(`Container ${target} not found`) + } + const agent = this._agents.get(agentId) + if (agent === undefined) { + throw new Error(`Agent ${agentId} not found for container ${target}`) + } + const container = agent.containers.get(target) + if (container === undefined) { + throw new Error(`Container ${target} not registered on agent ${agentId}`) + } + await agent.api.send(target, data) + } + + async register (uuid: AgentUuid, agent: NetworkAgent, containers: ContainerRecord[]): Promise { + const newContainers = new Map( + containers.map((record) => [ + record.uuid, + { + record, + endpoint: record.endpoint + } + ]) + ) + + const containerEvent: ContainerEvent = { + added: [], + deleted: [], + updated: [] + } + + // Register agent record + const oldAgent = this._agents.get(uuid) + if (oldAgent !== undefined) { + oldAgent.api = agent // Just update API, in case of reconnect. + oldAgent.lastSeen = Date.now() + // Check if some of container changed endpoints. + for (const rec of containers) { + const oldRec = oldAgent.containers.get(rec.uuid) + if (oldRec !== undefined) { + if (oldRec.record.endpoint !== rec.endpoint) { + oldRec.endpoint = rec.endpoint // Update endpoint + containerEvent.updated.push(rec) + } + } + } + // Handle remove of containers + for (const oldC of oldAgent.containers.values()) { + if (newContainers.get(oldC.record.uuid) === undefined) { + containerEvent.deleted.push(oldC.record) + this._containers.delete(oldC.record.uuid) // Remove from active container registry + } + } + } + + const containersToShutdown: ContainerUuid[] = [] + + // Update active container registry. + for (const rec of containers) { + const oldAgentId = this._containers.get(rec.uuid) + if (oldAgentId === undefined) { + containerEvent.added.push(rec) + this._containers.set(rec.uuid, uuid) + } + if (oldAgentId !== uuid) { + containersToShutdown.push(rec.uuid) + } + } + + // update agent record + + this._agents.set(uuid, { + api: agent, + containers: newContainers, + lastSeen: Date.now() + }) + + void this.sendEvent(containerEvent) + + // Send notification to all agents about containers update. + return containersToShutdown + } + + async sendEvent (event: ContainerEvent): Promise { + for (const agent of Object.values(this._agents)) { + if (agent.api.onContainer !== undefined) { + try { + await agent.api.onContainer(event) + } catch (err: any) { + console.error(`Error in agent ${agent.api.uuid} onContainer callback:`, err) + } + } + } + } + + async get (uuid: ContainerUuid, request: StartRequest): Promise { + const existing = this._containers.get(uuid) + if (existing !== undefined) { + const containerImpl = this._agents.get(existing)?.containers?.get(uuid) + if (containerImpl !== undefined) { + if (containerImpl.endpoint instanceof Promise) { + return await containerImpl.endpoint + } + return containerImpl.endpoint + } + } + // Select agent using round/robin and register it in agent + const agent = Array.from(this._agents.values())[++this.idx % this._agents.size] + + const record: ContainerRecordImpl = { + record: { + uuid, + agentId: agent.api.uuid, + state: 'starting', + kind: request.kind, + lastVisit: Date.now(), + endpoint: '' as ContainerEndpointRef // Placeholder, will be updated later + }, + endpoint: agent.api.get(uuid, request) + } + agent.containers.set(uuid, record) + this._containers.set(uuid, agent.api.uuid) + + // Wait for endpoint to be established + const endpointRef = await record.endpoint + record.endpoint = endpointRef + record.record.state = 'active' + return endpointRef + } + + async terminate (uuid: ContainerUuid): Promise { + const containerAgent = this._containers.get(uuid) + if (containerAgent == null) { + return + } + + const agent = this._agents.get(containerAgent) + if (agent == null) { + return + } + + const container = agent.containers.get(uuid) + if (container == null) { + return + } + this._containers.delete(uuid) // Remove from active container registry + container.record.state = 'stopping' + await Promise.all([ + agent.api.terminate(uuid), + this.sendEvent({ + added: [], + deleted: [container.record], + updated: [] + }) + ]) + agent.containers.delete(uuid) + } + + async connect (uuid: ContainerUuid): Promise { + const containerAgent = this._containers.get(uuid) + if (containerAgent == null) { + throw new Error(`Container ${uuid} not found in active registry`) + } + + const agent = this._agents.get(containerAgent) + if (agent == null) { + throw new Error(`Agent for container ${uuid} not found`) + } + + const container = agent.containers.get(uuid) + if (container == null) { + throw new Error(`Container ${uuid} not found in agent ${containerAgent}`) + } + if (container.endpoint instanceof Promise) { + container.endpoint = await container.endpoint + } + return await this.cntMgr.connect(container.endpoint) + } +} + +export interface Container { + uuid: ContainerUuid + + send: (data: RequestData) => Promise + + state: ContainerState + + onState?: (state: ContainerState) => void + + endpoint: ContainerEndpointRef +} + +export type ContainerFactory = (uuid: ContainerUuid) => Promise + +export class AgentImpl implements NetworkAgent { + _containers = new Map>() + constructor (readonly uuid: AgentUuid, readonly factory: Record Promise>) { + + } + + async get (uuid: ContainerUuid, request: StartRequest): Promise { + let current = this._containers.get(uuid) + if (current instanceof Promise) { + current = await current + } + if (current !== undefined) { + return current.endpoint + } + + let container: Container | Promise = this.factory[request.kind]() + this._containers.set(uuid, container) + container = await container + this._containers.set(uuid, container) + return container.endpoint + } +} diff --git a/network/core/src/node/node.ts b/network/core/src/node/node.ts new file mode 100644 index 00000000000..69019aaaa31 --- /dev/null +++ b/network/core/src/node/node.ts @@ -0,0 +1,347 @@ +import type { ClientBroadcast } from '../api/client' +import type { NodeData, NodeDiscovery, WorkspaceDiscovery } from '../api/discovery' +import type { Node, NodeAskOptions, NodeFactory, NodeManager, Workspace, WorkspaceFactory } from '../api/node' +import type { Request, RequestAkn, Response, ResponseValue } from '../api/request' +import { timeouts } from '../api/timeouts' +import type { AccountUuid, NodeUuid, WorkspaceUuid } from '../api/types' +import type { TickManager } from '../api/utils' +import { groupByArray } from '../utils' + +class WorkspaceSession { + workspace: Workspace | Promise + tick: number + lastUse: number + state: 'ready' | 'suspended' + + constructor (workspace: Workspace | Promise, tick: number, lastUse: number, state: 'ready' | 'suspended') { + this.workspace = workspace + this.tick = tick + this.lastUse = lastUse + this.state = state + } + + async getWorkspace (): Promise { + if (this.workspace instanceof Promise) { + this.workspace = await this.workspace + } + return this.workspace + } + + async suspend (): Promise { + const ws = await this.getWorkspace() + if (this.state === 'ready') { + this.workspace = ws.suspend().then(() => ws) + await this.workspace + this.state = 'suspended' + } + } + + async resume (): Promise { + const ws = await this.getWorkspace() + if (this.state === 'suspended') { + this.workspace = ws.resume().then(() => ws) + await this.workspace + this.state = 'ready' + } + } +} + +export class NodeImpl implements Node { + workspaces: Record = {} + constructor ( + readonly _id: NodeUuid, + readonly workspaceFactory: WorkspaceFactory, + readonly workspaceDiscovery: WorkspaceDiscovery, + readonly discovery: NodeManager, + readonly tickManager: TickManager, + readonly onClose?: () => Promise + ) { + this.tickManager.register(async (tick, tps) => { + this.handleWorkspaceClose(tick, tps) + }) + } + + onClientBroadcast?: ClientBroadcast + + handleWorkspaceClose (tick: number, tps: number): void { + const now = this.tickManager.now() + for (const [wsid, { workspace, tick: wstick, lastUse }] of Object.entries(this.workspaces)) { + if (tick % tps === wstick && !(workspace instanceof Promise)) { + if (now - lastUse > timeouts.closeWorkspaceTimeout) { + // Not used for 5 minutes, close it + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete this.workspaces[wsid as WorkspaceUuid] + void workspace.suspend().catch() + } + } + } + } + + async workspace (workspaceId: WorkspaceUuid): Promise { + let workspace = this.workspaces[workspaceId] + if (workspace == null) { + // Create and store the promise immediately to prevent race conditions + const wrk = this.workspaceFactory(workspaceId) + const tick = this.tickManager.nextHash() + workspace = new WorkspaceSession(wrk, tick, this.tickManager.now(), 'ready') + this.workspaces[workspaceId] = workspace + } + if (workspace.state === 'suspended') { + await workspace.resume() + } + + return workspace + } + + async ask(req: Request, options?: NodeAskOptions): Promise { + const result: RequestAkn = { + workspaces: {} + } + const workspaces = options?.target ?? (await this.workspaceDiscovery.byAccount(req.account)) + + const byNode = await this.groupWorkspaces(workspaces) + + // For self workspaces we need to resolve child workspaces. + await this.includeChildWorkspaces(byNode, options) + + const promises: Array> = [] + for (const [node, _workspaces] of byNode.entries()) { + const workspaces = _workspaces.filter((ws) => req.workspaces[ws] == null) + if (workspaces.length === 0) { + continue + } + + if (node === this._id) { + const localWorkspaces = this.getFilteredWorkspaces(workspaces, options) + + for (const ws of localWorkspaces) { + req.workspaces[ws] = this._id + result.workspaces[ws] = this._id + } + + void this.askLocal(req, localWorkspaces).catch((err) => { + console.error('failed to ask local workspaces', err) + }) + } else { + const wrk = await this.discovery.node(node) + promises.push(this.askTo(req, wrk, workspaces, result, options)) + } + } + await Promise.all(promises) + return result + } + + private getFilteredWorkspaces (workspaces: WorkspaceUuid[], options: NodeAskOptions | undefined): WorkspaceUuid[] { + let localWorkspaces = workspaces + if (options?.workspace !== undefined) { + const wsSet = new Set(options?.workspace) + localWorkspaces = workspaces.filter((it) => wsSet.has(it)) + } + return localWorkspaces + } + + private async groupWorkspaces (workspaces: WorkspaceUuid[]): Promise> { + const byNode = new Map() + for (const workspace of workspaces) { + const node = await this.discovery.byWorkspace(workspace) + byNode.set(node, (byNode.get(node) ?? []).concat(workspace)) + } + return byNode + } + + private async includeChildWorkspaces ( + byNode: Map, + options?: NodeAskOptions + ): Promise { + const selfWorkspace = byNode.get(this._id) ?? [] + if (selfWorkspace.length > 0) { + let optionsSet: Set | undefined + if (options?.workspace !== undefined) { + // We need to enhance options to include child workspaces + optionsSet = new Set(options.workspace) + } + for (const ws of selfWorkspace) { + const childWs = await this.workspaceDiscovery.byWorkspace(ws) + if (options?.workspace !== undefined && optionsSet !== undefined && optionsSet.has(ws)) { + options.workspace.push(...childWs) + } + for (const cws of childWs) { + const node = await this.discovery.byWorkspace(cws) + byNode.set(node, (byNode.get(node) ?? []).concat(cws)) + } + } + } + } + + async askTo( + req: Request, + wrk: Node, + workspaces: WorkspaceUuid[], + result: RequestAkn, + options?: NodeAskOptions + ): Promise { + const response = await wrk.ask(req, { ...options, target: workspaces }) + const localWorkspaces = new Set( + this.getFilteredWorkspaces(Object.keys(response.workspaces) as WorkspaceUuid[], options) + ) + if (localWorkspaces.size > 0) { + for (const [ws, nodeId] of Object.entries(response.workspaces)) { + if (!localWorkspaces.has(ws as WorkspaceUuid)) { + continue + } + result.workspaces[ws as WorkspaceUuid] = nodeId + req.workspaces[ws as WorkspaceUuid] = nodeId + } + } + } + + async askLocal(req: Request, workspaces: WorkspaceUuid[]): Promise { + const targetNode = await this.discovery.byAccount(req.account) + const target = targetNode === this._id ? this : await this.discovery.node(targetNode) + for (const ws of workspaces) { + const worker = await this.workspace(ws) + const data = await (await worker.getWorkspace()).ask(req) + await target.broadcast([ + { + _id: req._id, + account: req.account, + workspaceId: ws, + nodeId: this._id, + data + } + ]) + } + } + + async modify(workspaceId: WorkspaceUuid, req: Request): Promise> { + const wsNode = await this.discovery.byWorkspace(workspaceId) + + if (wsNode === this._id) { + const wrk = await this.workspace(workspaceId) + return await (await wrk.getWorkspace()).modify(req) + } + const node = await this.discovery.node(wsNode) + return await node.modify(workspaceId, req) + } + + async broadcast(req: Array>): Promise { + const byAccount = groupByArray(req, (it) => it.account) + for (const [account, values] of byAccount.entries()) { + const nodeId = await this.discovery.byAccount(account) + if (this._id === nodeId) { + // Broadcast to local clients + await this.onClientBroadcast?.(account, values) + } else { + // Broadcast to remote node + const wrk = await this.discovery.node(nodeId) + await wrk.broadcast(values) + } + } + } + + async ping (workspaces: WorkspaceUuid[], processChildren: boolean): Promise { + const wsSet = new Set(workspaces) + + if (processChildren) { + const toProcess = Array.from(wsSet) + + while (toProcess.length > 0) { + const ws = toProcess.pop() + if (ws === undefined) { + break + } + const childWs = await this.workspaceDiscovery.byWorkspace(ws) + for (const cws of childWs) { + if (!wsSet.has(cws)) { + wsSet.add(cws) + toProcess.push(cws) + } + } + } + } + + const byNode = await this.groupWorkspaces(Array.from(wsSet)) + + for (const [node, workspaces] of byNode.entries()) { + if (node === this._id) { + // Ping local workspaces + for (const ws of workspaces) { + const wrk = await this.workspace(ws) + wrk.lastUse = this.tickManager.now() + } + } else { + const wrk = await this.discovery.node(node) + await wrk.ping(workspaces, false) + } + } + } + + async close (): Promise { + for (const { workspace } of Object.values(this.workspaces)) { + if (workspace instanceof Promise) { + await workspace.then(async (w) => { + await w.close() + }) + } else { + await workspace.close() + } + } + await this.onClose?.() + } +} + +export class NodeManagerImpl implements NodeManager { + nodes: Record> = {} + constructor ( + private readonly nodeFactory: NodeFactory, + readonly discover: NodeDiscovery + ) {} + + async node (node: NodeUuid): Promise { + let wrk = this.nodes[node] + if (wrk instanceof Promise) { + wrk = await wrk + } + if (wrk == null) { + wrk = this.nodeFactory(node) + this.nodes[node] = wrk + try { + wrk = await wrk + this.nodes[node] = wrk + } catch (err: any) { + console.error('Error creating worker for node', node, err) + throw err + } + } + return wrk + } + + async close (): Promise { + for (const wrk of Object.values(this.nodes)) { + if (wrk instanceof Promise) { + await wrk.then(async (w) => { + await w.close() + }) + } else { + await wrk.close() + } + } + this.nodes = {} + } + + byAccount: (account: AccountUuid) => Promise = async (account) => { + return await this.discover.byAccount(account) + } + + byWorkspace: (workspace: WorkspaceUuid) => Promise = async (workspace) => { + return await this.discover.byWorkspace(workspace) + } + + list: () => Iterable = () => { + return this.discover.list() + } + + stats: (node: NodeUuid) => Promise = async (node) => { + return await this.discover.stats(node) + } +} diff --git a/network/core/src/node/session.ts b/network/core/src/node/session.ts new file mode 100644 index 00000000000..975bfb55f8f --- /dev/null +++ b/network/core/src/node/session.ts @@ -0,0 +1,251 @@ +import { v4 as uuid } from 'uuid' +import { type AskOptions, type Client, type SessionManager } from '../api/client' +import type { NodeDiscovery, WorkspaceDiscovery } from '../api/discovery' +import type { Node } from '../api/node' +import type { Request, RequestAkn, RequestId, Response, ResponseValue } from '../api/request' +import { timeouts } from '../api/timeouts' +import type { AccountUuid, WorkspaceUuid } from '../api/types' +import type { TickManager } from '../api/utils' +import type { NodeImpl } from './node' + +interface RequestData { + request: Request + time: number + responses: Array> + akn: RequestAkn | undefined + promise: Promise> + + resolve: (value: ResponseValue) => void + reject: (err: Error) => void +} +class SessionImpl implements Client { + requests = new Map>() + + onClose?: () => void + onBroadcast?: ((response: Response) => void) | undefined + + lastOp: number = performance.now() + + constructor ( + readonly account: AccountUuid, + readonly sessionId: string, + readonly localNode: Node, + readonly tick: number + ) {} + + async ask(req: T, options?: AskOptions): Promise> { + this.lastOp = performance.now() + + const requestId = uuid() as RequestId + const request: Request = { + _id: requestId, + account: this.account, + data: req, + workspaces: {} + } + + let resolveRequest = (value: ResponseValue): void => {} + let rejectRequest = (_: Error): void => {} + + const rdata: RequestData = { + request, + time: Date.now(), + akn: undefined, + responses: [], + resolve: () => {}, + reject: () => {}, + promise: new Promise>((resolve, reject) => { + resolveRequest = resolve as RequestData['resolve'] + rejectRequest = reject as RequestData['reject'] + }) + } + this.requests.set(requestId, rdata) + rdata.resolve = resolveRequest + rdata.reject = rejectRequest + + rdata.akn = await this.localNode.ask(request, { ...(options ?? {}), target: undefined }) + + this.checkResponses(rdata, rdata.responses) + + return await rdata.promise + } + + async modify(workspaceId: WorkspaceUuid, req: T): Promise> { + this.lastOp = performance.now() + + const requestId = uuid() as RequestId + const request: Request = { + _id: requestId, + account: this.account, + data: req, + workspaces: {} + } + return await this.localNode.modify(workspaceId, request) + } + + checkResponses (rdata: RequestData, responses: Array>): void { + for (const response of responses) { + if (response._id == null) { + continue + } + if (rdata.akn?.workspaces[response.workspaceId] !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete rdata.akn.workspaces[response.workspaceId] + } + } + if (rdata.akn !== undefined && Object.keys(rdata.akn.workspaces).length === 0) { + rdata.responses.sort((a, b) => a.workspaceId.localeCompare(b.workspaceId)) + + // Flatten all response values properly + const allValues = rdata.responses.flatMap((r) => r.data.value) + const totalCount = rdata.responses.reduce((sum, r) => sum + r.data.total, 0) + + rdata.resolve({ value: allValues, total: totalCount }) + this.requests.delete(rdata.request._id) + } + } + + handleResponse(responses: Array>): void { + for (const response of responses) { + if (response._id == null) { + // This is a broadcast response, call the callback if it exists. + this.onBroadcast?.(response) + continue + } + + const rdata = this.requests.get(response._id) + if (rdata == null) { + console.warn('Response for unknown request', response._id, response) + continue + } + + rdata.responses.push(response) + + this.checkResponses(rdata, [response]) + } + } + + close (): void { + this.onClose?.() + } + + async retryIfNeeded (time: number): Promise { + for (const [, rdata] of this.requests.entries()) { + if (time - rdata.time > timeouts.retryTimeout) { + const wsretry = Array.from(Object.keys(rdata.akn?.workspaces ?? {})) as WorkspaceUuid[] + if (wsretry.length > 0) { + await this.localNode.ask(rdata.request, { target: wsretry }) + } + } + } + } +} + +export class SessionManagerImpl implements SessionManager { + clients = new Map() + clientsByUuid = new Map() + + rid: number = 0 + warmupRequests = new Map>() + + constructor ( + readonly node: NodeImpl, + readonly tickManager: TickManager, + + readonly workspaceDiscovery: WorkspaceDiscovery, + readonly nodeDiscovery: NodeDiscovery + ) { + this.node.onClientBroadcast = async (account: AccountUuid, response: Array>) => { + for (const session of this.clientsByUuid.get(account) ?? []) { + session.handleResponse(response) + } + } + tickManager.register(async (tick, tps) => { + // Retry failed requests + const time = performance.now() + for (const c of this.clients.values()) { + if (tick % tps === c.tick) { + await c.retryIfNeeded(time) + } + } + // Every 30 seconds, do a ping + if (tick % (tps * timeouts.pingTimeout) === 0) { + void this.doPing(Array.from(this.clientsByUuid.keys())).catch((err) => { + console.error('Error pinging accounts', err) + }) + } + }) + } + + async doPing (accounts: AccountUuid[]): Promise { + try { + const workspaces = new Set() + for (const account of accounts) { + const wss = await this.workspaceDiscovery.byAccount(account) + for (const ws of wss) { + workspaces.add(ws) + } + } + await this.node.ping(Array.from(workspaces), true) + } catch (err) { + console.error('Error pinging accounts', err) + } + } + + async register (account: AccountUuid, sessionId: string): Promise { + const accountNode = await this.nodeDiscovery.byAccount(account) + if (accountNode !== this.node._id) { + throw new Error( + 'Invalid host node for account ' + account + ', expected ' + this.node._id + ', got ' + accountNode + ) + } + const oldSession = this.clients.get(sessionId) + oldSession?.close() + + const session = new SessionImpl(account, sessionId, this.node, this.tickManager.nextHash()) + this.clients.set(sessionId, session) + this.clientsByUuid.set(account, (this.clientsByUuid.get(account) ?? []).concat(session)) + + const id = ++this.rid + this.warmupRequests.set(id, this.warmupAccount(account, id)) + + return session + } + + async warmupAccount (account: AccountUuid, id: number): Promise { + try { + const workspaces = await this.workspaceDiscovery.byAccount(account) + await this.node.ping(workspaces, true) + } catch (err: any) { + console.error(err) + } finally { + this.warmupRequests.delete(id) + } + } + + async unregister (sessionid: string): Promise { + const session = this.clients.get(sessionid) + session?.onClose?.() + this.clients.delete(sessionid) + if (session !== undefined) { + this.clientsByUuid.set( + session.account, + (this.clientsByUuid.get(session.account) ?? []).filter((it) => it.sessionId !== sessionid) + ) + } + await Promise.resolve() + } + + close (): void { + for (const session of this.clients.values()) { + session.close() + } + } + + async waitWarmups (): Promise { + const promises = Array.from(this.warmupRequests.values()) + if (promises.length > 0) { + await Promise.all(promises) + } + } +} diff --git a/network/core/src/utils.ts b/network/core/src/utils.ts new file mode 100644 index 00000000000..7bacfe2ebdd --- /dev/null +++ b/network/core/src/utils.ts @@ -0,0 +1,51 @@ +import type { TickHandler, TickManager } from './api/utils' + +export function groupByArray (array: T[], keyProvider: (item: T) => K): Map { + const result = new Map() + + array.forEach((item) => { + const key = keyProvider(item) + + if (!result.has(key)) { + result.set(key, [item]) + } else { + result.get(key)?.push(item) + } + }) + + return result +} + +/** + * Handles a time unification and inform about ticks. + */ +export class TickManagerImpl implements TickManager { + handlers: TickHandler[] = [] + + hashCounter: number = 0 + + _tick: number = 0 + + constructor (readonly tps: number) {} + + now (): number { + return performance.now() + } + + nextHash (): number { + return ++this.hashCounter % this.tps + } + + register (handler: TickHandler): void { + this.handlers.push(handler) + } + + unregister (handler: TickHandler): void { + this.handlers = this.handlers.filter((h) => h !== handler) + } + + async tick (): Promise { + this._tick++ + await Promise.all(this.handlers.map((h) => h(this._tick, this.tps))) + } +} diff --git a/network/core/tsconfig.json b/network/core/tsconfig.json new file mode 100644 index 00000000000..c6a877cf6c3 --- /dev/null +++ b/network/core/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/network/docs/Schema.png b/network/docs/Schema.png new file mode 100644 index 0000000000000000000000000000000000000000..bcdbb8d42a4c6184b630732faa6f9447db9dc3e7 GIT binary patch literal 214102 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 z zw=H-tsn_|U&Kc#8@F0?3MG|G=)tVb_7v8MZs2-xnLuD8$QOD;h- zz7)DO-Zjtf9JYt^y1e>_XI0KhxkDV-NO^McCWu9#Rh7uOk+!0*2|SCu|92*K>g|L;moe{ukKIpOK-|vTj*_l5+|SaG?wG#SJlGrpzr|>3Dp9cm?{HU zKZUB}2=5?aq(55)oW#KgSbWkYeJ6xqris#1p``3WjFhXrb(K1EqVLPHyQfmRX%hoH zfkXk~99XMU_SpAXu+sL&wh;Ahh)w9p $!V2h}XcqGD)kzY(?$qyXg0|AQk~)eMStWF;8dRgr2R_ znAePrpbbs$-*`nvZhPss3R$5g`GN3I#An5y%}+nCne0{Li|9u3W#1_TZ@!ds|M~C` zC~|QkLR&o#GDT3`_c7ap&nM5ivN~d8ok*>N52{W;Y(tV(e*K}`TJ=JwMe)=tpc!aY zJl-B!T#u1kb3kMU2Pm%arkGh(P5XjpW=-1wyLC=;!I*T|=yTvg7Te=GHn?NuD{Sh8 z$@xI3iA^zshrk`8byp(yxCXh&Qpv7o;|Uuaq|1AQpKg~ zcX*cIv#;#<40Ep!_LUq}CQ3{ukBTkJ8w$3WLXfu9!e9Pr*qe{;I4s;<3%jn_&Ubai z0tdPs;@%}zB^BP6D*0Tws17WMru2FS>mQ`dL-wZ0`+K&MijbS%e118T7l9XMKclNA z zBpYAbx;-jXk71u^LvM2Y%v|P4$Lh){LyE>+CP;u5(wS09C)k?VXEZ#Fg%pdrV2BTm zZus`AvGKv!>BlZv2Cxr|=F|p=Hd&q+@oeHVe*2Ays!h-6h_pm^hEx7=O(!5tkUtB$ zPL*C+YvdoEUYE9`Q$#m`$OtH!;8pnb(OM%)f≦WYiL(tbE`JtHC?VwV$XyMM!24 z$;7kTvME?~;K_RyTcD8+)G>z0a|UheObng5PeORO&4_`KOWd^z=lLG_554x-+3rOw zF919YMX#kN-VQq0c1)=sUddU8KI~ih!RqE5!4cqq5-r?%JuiApD6?9ry@U@r?o;&P&O2He{P!qyfQLH(Kc*1a7Ua8Z0 z!JwP=*n+EsWo*l>W?pnx#_pJ@?AMifZ$t82#k)gZiAjOms)hnSz22hLsPaKg-t<{U zh%0pW2a=?x0<@plWb9qMZHZ4Jn&~1zL-8$)bB(0KN#ToCA8gk$n&xe z|B>tc$Co(Wl^vOz8A_4)x2YQdZ<@qRy2{Bd=Osu)RpyTsr?lAWxOWO57E$7U>o3w^ zYa~{B-d0C#a&P=-PmJQh@4y+^D3zfQ*UAyYeM08xk^R$-fZ|R6qm4F{)XQQj46!54 zwYHD|ytCRJn{?(eYPcuU%+TIP*+=3Zw0GNFl&NwR+R5B&cvg@K{3%+^Tz3)IvlDIKZDp5BpE4eyISxwmVrm zfzN`jQK{GTbFnH;Sv4u@s?^&hSsm|;RVk3hbAPiTXSdI=0vNF&MT3SpzxYa=C=k-1`n=sRfBd@A8G6PCnxY__%(9`@68>}DTh)_=sp)o+C{LNij^jQ_W$*Xw9&@N z7m;k{sW$0|NLN>dpuVWk%CtyL*d(-AfzWxx8wEf1Zmz)vw@ zG8sCrI}g+&?rY3A+3DWm_pwzu?L9jhpPYQZ^nE#1$~=kB z%;|wYJ>qn+!)RPAF6NuL3zIfo^X-qR5n%3$14F|-pHiiGE+sjDFgr%~rTTq1N3FzQY{uXA#_A02DKgVUK0}mg?t~rZclFV1vuji|b;SWj&5FNkbeA(~++c z;#HL4y7RBeE?AMHtM0vz!IkDC3C6rdvQ>&%;q!chJj#fBGbO@ z#yBBI+BE*~;AxNMUNLyDrrC0{G}d-B0M;}iw6qk`b?Mp5l&Y@~|NEaW{b|L#=+C`h zR9Q*+M{$tnW#(4pw^P8Z+DJ1nx1j*9<((SQrXx?-V%$^gEoOpO99$hCPI7^5u<#Ee$U zDz%tuBHY}3C*oR-YB6`SbvDig+FrVF(R_l6?Oy^#U-_TH`^Eq0^Xbg@V%L?8wn}>B zi1qn}bNQO-PG_mvQ^6#z-Sam93l;6}OfHbq$^ZOLVc&I@>{(s(kF~3oGDBu0u&}9W zj}HB`2H38qun%U+^ZC%T`BL*rZ`Cbaf|g=Am&zL+zRn8!F>|QobeejO<9uS~J?kIi zz{MFFWfNtVN|Qiy?9$3;RO0P)-@GUha}DReI}Oz$sXy)e)*i>5uqo2GH-A>RP0Hd+ ztW7g{M0EdoQ@Q%&-3=XY<*hi-_X`yb`|oWnb+>zI^t0L>N& z_`qW}MH5RIu-=T|14mY+;6P8emKQa+WN{E>-FI3!i7uyYpnJvu{fkgdUqCWY-zwv0 zo0>XwpY9AvShD-Dgp!tRJl@&XgO;Go#8CV!HLJioI=ocWgmqafJEJcws5Y@3GmrlpCbXN0-xi~ z>V$Y9^@uC7=2vP@=@9A&*+07D)a|V4>;^t;Y&}}+tw@g=a_w5W-F1K11kB3qLI?dd z3lldJaQ~@V<4COSO9+a~V09#Ir`at`JN;e|>tWg~+(`xxYv^&W{Z!(FGdRQqD)g+D z+IK%`w3q4~GW&w~#BdX6-pv|cF zX^1Gtk*;qWnICC5-AD(M~cc$U>83ir5|mC)=F_4JC?dr0tU^?S`5p)1YgcM$V4=UGM^^esc4|(S%{pAWRX( z%Y1**lLmzx6P#M|Try%k`58UKq$SicQxUQz`#)rn>=lVyJE%R*^;0!tq*z{iz87BtercwU@9# zH9lSl%Aod3;wIoIuV^^rrFBTjQN=!YeClQ5Tg4|n<41DX0^Xo8_I@Yue#fV+>F)g; z5=+?*FQ@Xc=M^AN**wAuV-nfC`;~nGSTngkHG^H?odo@p+wkdVu>d#an>w|+8EWWW zr_apcwmWMnMMQ&?%;1CNS`7SGOZ2E!==e z`*_39`>O(c^Az9{((*e;u{rR^F0(aD84yY~_lJew_rLTh9tz*QaE(F~NB5XkfUHt< ztq_sH5}xd9w-3%n;zx9OQxmY0B{NlD%b5s?gMdfmFxeJ-$4E_8kfw?o+{!;!*9jFq z!O4D2YJa=ity%MJH9k=l6fO3XPjS&&CIkHQKgO5RDAo_hZN8*CL<|UI^!d@0OJnjWhlj&b*lkD0wkEGGqB;|*(i&sYmww3T@V-!77S%BA>7oJ|qjZexvUbXk# zn~O*jK2`7N{v@8Tpex*&Q}Vf;{OM&vtl~j|Sz^w*sX~G(bEJHwV1(?FtqBVcrseWW zug(lomOH}soJjQOEgXZYtSI%zEOdh9j+^#g2WK=z#|3zL^BHm27Ll-Hj|$k0zUCXt z5SHrnrSi6_VF`MO1!;Bb`OjMcvBSNFx>$iy>@5?zu4~l-17w#*bduJ>g(GdxnX6qD zxR_hAEGR%oWFC=Wk=ln`9L@+eRh7?==G;EHp zln3>O{WU2Y5Lt9xNu=-mw#4ld^pIv($~r)AqD~K+PP-1}Lb;LJj_MwKIGkAUoaOg8 zpnK%|)Tuip5e)P=9s1_EPh&N$3cQ{tJFl$YkZ{oUHpHg`4LLMd0!aO9HUTOi<-)@L z`DH=4UmRnEY=k7-o_7LB*YSlvht(Q>ut0bWLs=X}AaTa~2#Jv0x>=k^XR@)?rNa)b z;CLR9@ZU|R=6gQKLdhh<4<#k@!!L^kel-vKs9OfRR8&Zl#;pnM+_4(@Ay>vqaTY^e z#~ZOBXrQ_fu6b9@9`HL`%6a_m5Nc4jX6_9ui;3T}?3~^}6R0{@mgD{mP+}kPN)QOm z)M93XR+{YNW0oO7iluDPqO}h38EXp>oWTw&Hb%9zDJ$O2>Q4I_$#uRWomWfY2M_h8 z)NFPHX{{9aE)$gJN}m_zE0bRX74oarl$Z@&8w-)mS7xY)5wP9iy=Wv{J}-)gDmBJT z6`>5Y)XRI6IUN!zKeWcyqpwTK!}55q%RD3*XxFfV#cjH>hcRk z;Wh~xpYHQIDvh`rbanwt`l)3^rYIySW3$YAt?Bf{2CK!M@XX_O_&f(}HIDk@vE^lH z6Zs$K86+xE`IimBaE$x!{_H!Dy<0}+J_duyKX9~d6+jKbvE#)5G(d?DT~+Bf>olNQ zx#Oi1eLrleSRV}5(#wnp^vyfoeE#uxgCF%>vkTd(sv{x)oBnE;yrb`!D7uCNT|5{5;^Yr4)WYN}(_3)^-oPa$7> ztHdNEy0nwQ4&thv>r@o^Zf2))ytUPFHN4nzr)d@W&&gMO9o?;Ks0Bq_qrD| z`m?i^0@pRR)PW$RN7BqQwj`Q>oe^ieC zj>zk36WO7y-mwc6I^OiqzhiVoYA^M*s#?wK>Thy!(eukP!wqj+lVCIo0o(jhOP#Gx zmY0L1_kM;(&tJ?hb$FZL+pJPwlV#WL|Q42e5NO&a#Wig9O2$4FnK%nCHg0dbd#wcBg z1QYcHbdDM33mT~6ck0T#pO5S|mfD7b?fCjUs(r@~Z+!@yNqoxLa-EhJmT86hT@AK+ zP~F_bA0ywWXsRR6xgA%pwfxrgrV#_tE#wfalc>9I)EIcsxi}roeK?##GwF;1=>)i* z8!tdYRh@O%Eb*9cWBbtw>QkrteIYDnHJ@UOXD=u^#Yvi2|au1dC zLj*LQ3{}z|W=IBl*?Xjl6D!|r(Tc__CCkzDnd<}eQOIuDXuoyFe07BDm~>AW@ynag zDuEaT9s-*`n)hz1A7M#e)nnToPcM1dNod{paoWRTrmU- z_gR?5^tqP5aTyhiA$Ar}C}?@qJ58CSaGQAzTSjd`+d6vsz|Wo^dIz~r^^U+1yLv>& zpEU>fTbFFjPhwA9X#%9a@1SR4TR(`)Rl!<2^{xJ17hQCFwb$$^nGsv#GOhkfK~3`u zjkGqQTIJYP`&(vW4VgYoOKnx3ESUSM+gS4V52bcWr|wiWd#_?&9SDFk@@=41`;;1 zHC`w@s&{|^mTIS;n-Kc$ruZF`6uv!hosq@-A(s#ZsN-XAv`|I7Pn^vs#>a++Ads5? zjnUjn zCQ#f;=g5(*%_|^n9&r=hVir;%rR8Fc>s)Op)uD_|?Dr%$F8&EfQ7?ch$e2Kd*gp7l z0n7ngeTT);552vPoIL%208afFrxI?+u?7?43k-q@_qJ@Psp(+g^0hJM@P3&#RoI8R zv5MPza?E+d4DX2zyB0I^CWg++W*Z9%7grGc$Vu6#8fPIg>*jvQltY#5(PF3kk$xmn7G=LCr<%mym1%JEkG8ZymS-cI|4{IUJKeLKnG|l5 zq)Q9h@R>^l8A{eUeTbD!ucE(`QZ$#aOOkw1B@ouPX-AxgQJ?&bR~;XAcmg$vG566} zkbu>m+Z$=a33oNu;7B3ITD|IGY_roN*{0uJPv$ebF(u&Z3fpqxCRZrpaI<*@@3`tY zNPYW=Q9;G!Pz;Tsrx0?e`Sg=&<>U8Yo6O8Z3e2rSlH^v@-)2Oi* zQ^b4#2h8}!$G(jTj(>M5@qpmOR>lGaC+4OG4&Ux=-|eQ|z>V)@P{ zuTP^AMLd-3-rDiIo*q-4IludX1Y*AECKfB}BI%L}zKaFmB-c*+sr@2aFVJZ<0LJj* zQ)*NV1{l&zeu3G?Tm^I!^YWjk-+lQ=$5=80V9K~TLF_ZNr{(u=g}7tBi|9J|y)+2KCG*nC!&tIWE<>=mLCW8IE;nrTXIt(k<<=x@3p^>!mxw*XGDkXsLSRAa~CKqTCj1*=b zzA4m2?Xt4i{YawuG@RL@osSn^te*TjszACtZ%i*Lwu3Mf1`mxoTCOM#6k!y&2H!^d z4%NOIc{^$xk9#9)AXc+@ZKyV+{CBX^w3Nx!bdMi5RYwy&>PM?bYC^j2wJj#BFC$k! zsL%>HdL+aqdj6hBxOV?-MnnE+a2)RpK;YicyqCFozf77`_%%wHXUDlES${v_c0>%s zCD(J1sn6yBTgm_QfMr>ht5j^zW9fnt(3Xg{bxN9;!5KF<4txWc7k2L{<}(;@QVPl6 zn*l^I3Dma7Ys{XOtAh{U{E8a&i5{yrU=d77bj3Z!f2J6Cc*~?LeCLIV-*Zw0!#hfc zM0J(1B=t8HHts_ysjHjYy(j)ZIyE1b} zW+*1)XJ?+V$(IhN#{A^6V*@pBGsr(l34M3c8txXk>pL3iQt;;sQ(F7yykO&?O!4Vk z?~Cu4)o%)#q{tk6p0DT#|KLnl*y@!a8ZoLOWb1#UZF0EnzG&SuuEl||%WF<%E;=zz zn*;IEbPdj!W5@SZ>3#R*PMpM%%7v8+uN{KEh8&jfHFVS-sk@DjyQ^BV{+Lx37!Xq1 z_3wUj+OgVz_nMTUG>RhNCjg=?5vE^|m1s#W( zPoPOux_k4bU-+AM*t;rbv?;yX>oU%PcJ5G1fu@ry`O8*#NIT34d6J+dfMIZI+G`an zs@2D)s$A}H;$aud%hjwl2GDKfICvE4S1~2T>$0JdUj~l$%q!!AUcpn3c_8okr^pjHwL#*fL@g^ROn=zkLcm9bDS&S654WJ zKtk2V!T!4(>pXmRnLc9%u>c=ZYva88;-PFoj^)rph#>A(gDG6^(LR>J zS~boKw&+HIOe&V4kM)+r@Na@D3FXyc%6D|xoF1z1hR;@eEUW@*5=oQCPW|D%1HtVd zxaJ1mlT1zw3sf7_VUxd;apwn#Y6l9PPc>u)H|D*8b{DYHXN-mIV8BYvzbc?B9dAz@ zQgZ$p`d(9EwHRiGjomkHc}^5RY+V~M7jh}Dw3}V&eclE;-D>vdeC!%tYG9iR*%61W z!yn0(;~OGt!ikT@PXnhWd3Px+mxD}S?Z*i*rgA?ub0zJz6)Pv00qJ~) z6fmY>qk!;pY1}iRTe2xn*_i}An6|0dqq^)QP;BcGZ!rcR-Wl8#oK`blVGD;ZgnQc< z&_9>82o&};T4AGejM%I6yQ6}AX&LPqhW>8R_l}_{JL99Y2Z=f4?cE5%0@Oy+mCk$8 z_9mYY3P$YNq52V32M}6{I_vxyiMj!AFcWrZkGUjWu;`WpgUd}>Swl6-R?i!^VFrY{ z)R+`aJ~N@!A$)WAy62&>BSPAV^0jB(l+5K>Ku(w-DIP1kQjmjD8G05`mr%JE(P>@V?*c1OZa<}J~|x(6#JDN4sN$OK)6-UUTOBu=(GCE8XxkcUF+lcCV zdV_DgWETJkdBgmDl5_c{h{dd^MqDB0NvB*S!(K&wQh{c?fzby)Lj8L08|$Q*Z}d+O zz3aVYJ*I@GOc(T{FisSn?eot&#RZns1#sJqj^+>8iDPA!SJOQGK`B*OzkAHn*FliL zujeRv@U!qFM%0ZX{;&U7JjdY6HQ?#-q^}Hu+n72hW>ym z8dfatC&1rRRJTCJTZu)9e&2u{{zKAc1gr>9>(St+#vcjcp-mo_OgVBKMd0pN2utSB zY1@hnrJZ5DG!6aBS}wXVXFBo@?>;TXeH(7-2Ot@KTZHhD)Rg=8ish&vp!1k-y*9=L z3`^PHpSBM)+kIp>5OheYR17-!Sh88kDd$^qd@$A&llyBrJA>vNavw>*q_u3TQ#Lr0 z>~@CZz=OUF5`-#AF1EL*I}602Tu^u^(aB zXjf|;G)!p+I=v>zx)bbC8~saBCH`wTGryvtOX^0gU#q|#aX>%y{9ziQ&Q{jYx+!1N z%$kZB~<7R=UBrs@I%K9#tb(2?Jg4y(vHMox7+CC z143J{GYOB6TEIkJu1sbM!g z5!ZaV|2x*jmGb47E=zVT?YOYQLpWFrx(J5|^icL8VNT*zmeod1p*zzaDC zT?oNIOR1IUqWfqK5YYLzE&%GM^2{>!noecfXO=TQx9Z>xBm`Pyxx9oawD#uQ`VX%NMXD3g#f8FAL$A8Q=6EmFjE z-?qa#35Yf6m7u`^T&TLh&)q*P8g(~Uom-s?tzB*!Mlan-KYeN4gE19i$BuIooRe*88d?27@a&v@XE313$;) zKbgh}qf8VH@)yx*^=sg7&TTZF!GDJ-{*>1#f68km@)iDQ1aVOIZu-Ey=-P*8P_`DR zEg&>O`I+{71hI(`f&t7)W9V&%sel5Fo~&zSFisUObn#Mp%QqziQ4*dhthC*|Ryk(z zKG9bQiPp{OPP_Z^H894c>XL(|-n9XAB+v3!t^}K4SI;dNL>d!3dVC(m&_ zu1suV2#N=UIXo^ZrXDG5pO3`=rbx3+a!u)YSu66j&gEM1K6&xX#1+(eI{E$?|2EhS z@U040xrKM)&H$~W>N9g)OJ2u=rB~fYul-H_#ydt~E7p~7%$CAZFZ;9Lp6YUN=2_H- z;Blepl3*e9(*~AK9YUjc<^MtYM4%ai-(A)hPT68d9U$ z^DFYr=@0b}-c#e5F;#={;kgyp@Ju@zc?(QdOm%`jn?jjhb)NUzyK2fmeVcr_E5^k7 zZZb*H0`+-NGyG_F{ea);thVhCRG{N39hHtw>ghD9`Ym&5qmwY1!-gw0?^+i*th0jT zHZv-ocCM~YUOoXBiz9V03n*~}`aMaIID0s}>QePN5b|ij(Re}pbX$CDwovBc^e0M& zn1a%w?XSeV5;5v~|LeT4nGd(Os*lB0Xjc-x0?ux4pYCI*^Gj29Iq!Ce{YKc;UBTEy zXWV0U4-dW)Su!BcU_A#C*3W9m6> zPLC8*H00U=A!UX}&rMO}^3MG74Hds}2%!O(4Oa9c0KE=>1#JiCTeb=yh8l88$$)Kc zO1${`PYYs+52LArs1&{~6d9wm^{(%p?i1|7rxI0}!Pe(WDjq>`F_R|8Xv!p_v%Gme zgDlYOd!${}jYJMefEIf_l~2I0|XgM?hB6@3TOV(RkTeP?9EB*1@e6&7ZsC@E;G z>PbE?yorgt&iAWNO)iiB1Ft20D+sz8VU3*)-0B%k+*|`#Qna7TU1T;hPeaBu3uNy| zosmZaYAUD>Fca8Z3lW2S-IwX*4an9*!)V_kWM(uFa*j0yM)ME zGk;qmM$-k^c6_qx(p;KcxfU-P@f>f3zivvzzxS}KQq++ibi0Q-C&Y9_y)BGM4A^k7 ztRvJkZ|I7$hvdJO{4_Y_n%?}W?_fz&@3OLh&l)MSx=QKObsmbhaZji##I#2o6d|d;rK&#upBbP7bwxv#mk

lPcXvwiy;DITZ-_d%TGTdJ<$_Rr5_V~UPx|E2KD&H zuGES(`JT!oF@}ufJ!kt}ArVLCQ2duR~`mVcvQl`s|3OXb5Q2Z$LA>recr5G6}a>L4$k68 zj{7q!NuB#Gg-PDn#N{@0rmT;XE+UDQZ4;2ZrZK!LZ}EAGjmN~RrVEqXo-f2#CL;K##Ag^duzj0}lBq$ymqAw^u2}aZ|2981Sg4%ej$UsI;diUKzt4J#^mi@4 z@17~bVhQO37-03S56?Rh0?%35%Z?z!NXlG%;6mmFCHe2pkM_%N`_8c2!P1uqgzy?d zISEXYO~#EgmG{XMG#O6!Kuuf^zax>PI|+kqOtrv2%5v|P1TBwR!52N6GMkQJ8*zIz zLW;UGMtIM}CQp~xZ6d_?w{JM4d%ZL|p;TxuR0zLGY%+KzQ>8=_L>bKp&{8)FSY)!@ z9e4g!V)l0XU4-OGBzq1s!PXzTAl5ArWcT5Lna;;yUK*}2g@3X3U@CM-tk3b6kGY;X zK2v$sZy%UYn1vkgMDl)=y?(fZx7X!w$*HtRtq_i0fJ#8?Pd;px>MY_ON0lL~W@JPB zuxLj>>0bA#v@I_Hvwr|i4A@_s2{}D(5q!QY#{UBS zBTZImWP=%MT6BK9UxltG6D$;wU-T}?rXot4vMezbB=9OV0RiZbhHa*AM9W&8$vGzW zHWmO?^C5UZR?nlJxO=1g+*LzJD~VBZqa39P;^S93(pWSrCVA| z(4w8OIZensYH1$+beU?oHJPXOsk-IzvY*}{N1qL#!blM>rL%dTT}Uez+Ru^L7~SJY z_vo?$jD$Xe(>GSqFD2VuPJpYM0^|K3t4IdsH+V8U#lsvkfvOCVo&?6#1M-v-Kt{dK zV>=hLRx<;U1Lu7l{uy;kOX(QR?lY{jzjIDy50HR#w>%md@_nd)DPW~vlkRZt*4NmcZRZPI@BK0Xc@e3=XrUr+ zI=$il43EJWS;+s#-dBc2*|lpwA}WXq0)mQ^5|SfGNViBM4MQm1-9re1hzO{3BPrbs z3=G}f14BvYFmw&^-GI-#pZ&i3JHFrFaqNG?!OXqZeXX^wb=7%ZV6vzpipd~m(|u@? zdQ!>t>Qv9v2)r$NHmC_n&(%wBeD~d)hkq<6Ja4~eytNxc z-R&)rWFQSjD8}+mg+G}e#<(+)=)=z|!FY+r&U>|Z(juI=#W+L6KZ}%1-rJNL(7vXQ z*mtt3klxx0#RQE}qCpz3yLY!u$*aY5irhx>n(fBN)d34mC!?S8h&TAm1E~G96_>Sk zpRg2CZ}U<=lvQAsgz97x6lkdf`6OhuAvwD1dN-_B=gnS^$zyQ2vCbKs>*ct6cLQhU zQS{bi4_DP3gI*XbOixU5iMRyqrTw`o>^xrEYRtEmA&S`!y|o2~yA$hAo#u{JLP<|5 zE`gkXVeN?~gW-mL9;GZkK+>eGlvDB|z+(u5_gEUG^eAb*_WORm%754&>ZL*$#*CNN zq(ZM3T`r?$t@Jf($2luKvKXj1%mOKUB)J zwe-wbTjP%qIm#<*L&|kD_ffwzX!+W7TP*US(y|WVvhD-1RmMTfT%b&Oe<$gD!f-2j z4N=ne!>KP>WdEYnv)$4eK$&tF{ba@yGpQYs3s;i6O)FraMB}~HOe$fv!q4D<*q47&v+%jdL22uSyV108yEnhVkRff&D9bUV zU^o&`w*#edgA`2+rZ8E#Yma0sHAw?3;2nT@k>9^tx)mO0cal4O`U=H7sa2&Q&3@My z7u^Qn{}W~V$u!-WGha=O{ye1!7xM7Y>KjsrC?%YuZuG&I=Bm%P04tw0_5BDp?|=z7@DE z#DIbkb#ZMaZ73Tn@Ewj|corr-&cdpqytQN<)-pjgtUWdeRv_Fl0u&lG&un+EbztQo z=yEFF$)$3*IMQ~VOm>rOhB)i2<@{niKl`iX{Ek(;8csjbNa>CB*_vOjsHY1US zag5P=yhD!^?^C{#0eRxI&ChwurBmv(Zvzru?^;a4G={S1WuT?A;%d%XVX`APH9IeA z_kp-Y+KN}Y1bMJodIB-~g@*bOGVFMwbyQF>`{ij)l`XmE^NbzR5Kfl@PJib~C-p%` z(_&}-p}eO+(Vm-ud$jX@MyNQq%g1n)Gdv#dlNGlPk|GlS*oVuk(jX+ zrYak~2znA$EWgq$&Bq{Wkkd`T4Yvt3DJEG^tSsG7t=^EYUQ~lkT)0AgyOS*Gj!b1W zI&T=B7yH&;&{u4=klnIL^&CNarQ#V9C%cGuz^Jm9@BvWcAV&P+sHmLp$&1SEbUbo2~1hV+9d(^-rX2EIl(WAGjEDm~|zqMQp# zs>j0e|I#p?X=zQLmu5nx(VVo}R4aRjwQQ}?&exnf(5`$6c6fO0B`4P*UvVfy|+HTy! zfpUfDc)cC+`60K_8WqjbF$ZKq?Y`hx(Wlb`cLZXIE~2ZlPy zS$HxG#sf#PQO{5ct@uY|K)z?7=m;}?UXzBUuw_`3oJH9B^^x*O>YUSgBz9>gO)u?w_d~jC%YiZ>XhsUG$71+o#b6x>#88+MFl3zqMyPH$?Wz=S4XO+KlbBZs4Y;~1tW>~@a#`I*G+0P}(Nry;py98Q_c*oOYJ zIX9i2baW)^8g9?GL#q^L7P&D6%&GEuH<9RwPWU3zWt;FFm`#A;JqH;Gz7yJf#81>2Nm zi>x|!WrI5SATpTco>b%{G~CDr=C?yh5#`cqSTo?oqUWGmT2ZABq!++? zzQ~;X@3cu1gf{uPL0so%5X2yt=QN&`I=(pQ_>5NhpSTSq1nq6kdMz?0GU$#QhMte5Ea&!tJ$nPv@7p3QwwE5<5Oq%C~ zXvS*sbNh&o^cuW@4%I|1Ja>Y-<8@w?NSILuq+e0*UBl_h!e+!bNL0>pjJNmZGwKL` zL{48tPM-*7eznpdiv%Eq&q?M7ekQvU=ArGpNmZ%2nC_asRhU=&r459E#lyipAjh8J__e)>%UoP%eMdIPofPSqr(XTS3P zFW7`G=HkOk^I;0V%>#fN_wv zy%E5V8|CD~%*Rf`i#-x$a zXbecMtOH>T`2iFV^4tTEDmxb6sIaLN`LMdpESMHVmB%k~Lk9dkIT5pVcd!VS`LUgm zed`+&ufS`wLRFZ}$|*N%R@zGzNgxASr#LZ;79-#_k7(@UA={_!0zkUFvq0PgQ-?4B z*MPaLO-7(40GrB1G|n)Ac?Eb0@(;r3DR|u8S(MULcRbJFMSf$mRbN&J=xXj0}})Y!DJ(1*!sSP#VxF)b36TJWPZL?V=oh8LgBgFAM;2rZvKiD zDF%ChJTR1hyX_A2Yt3-jQE`k^dTQplZ{IyXI1(HAk%RN(@}YfMIKL9)pk>9jwSYA~ z{fO6b*(o8rkoe}G9e0%$jy3=)J#)?hzB0#YH?>9*UfT%FCx8Kj-@RXgH24T$tz50; zm?r>!BTWD7+lH{p)==OfkgOV8h|&`rV)@Iu?N?jrtw%mIk1oy6BbZj9nQ$jX_6@NW=Rhg@HCz&65lpr$f**@*%b>+Nb|IV3KrwWzS5!?zLgGbzfU4 z6*>oTO+pKAEUamV(JN5QD3`FTV$uO1bdGgw_8&IXbzHxDO-dL*A=+GWmK7CPxdiGR zfu}k8o+3I&w{!a_O5@2*L@(Y}_!d0@6w27qO6hHRL10-5!~oaY0D=L5Igy`vR#uSl zk+cSAVhw6>--=>-F#+D5SI9cKRK9R9)xf1?Cv|YuMM3cZo1=xs`rD8IX*u_F33|f= zc{bz!Q0L$V^~bAyW(}}^m6081vp#Vdk`^HicM=KAoU;#bb-2(%%NH3I6rdUO%z-;} zRj`-;cuMS22EYQ_ExlhqTg@q02TGyNn-F7_EfLP7M|+0X5>zGie=QA@aO4PV?kPm= z_Hcq_^%InBcpJht1Py$*TemIf{tT5xMO|0WA2X7apS zhW@D|X&068u~rVJYgaF4`mkW65^myCRBPoMAJlaXn0EIy?wF?+()z`AjluKVF&%+I zFjr&AV=m88sqdNis+D{F^f8=XCx_!e)#nd3fbDg1P{=%L+Onh@Rxuy4O?kPawPdL_ zUWJ1b0s(He1c0tn`6URxJMbNF*5?ZJAQF50FwxdsE)~;t4XKXyRdL=I7s~YB4nEu?&gQvj4 z@?eG4a9zNYmZsUsxJE2v&+QWskN`m!vgE|c&IAe9JRD}1Xm5U3s|@S78E^Krm)?2# z;;_F}jU({|2^dCx<(bN3ZcWSOR2B02K@Yr07bL&{$`@AgMF;_)=T2vVSngpDfT=(7 zgjSCOxr}=n?m;Z&%g9Ux%I2#jOn!td?etVW{#Ab>K;U3wQ-vzun({e3T<%XT*r92~ zWLzV8kK%E9YW%a>LJqR&3%zACNO73(hu$q8H*I`8=A}t2w1CU7?Hj|z{;X;Yz^`q7 z;Cj9*>M;{n6TF)cF2T3f?s#u(hNDDM?nmtM*5{n$-kO|bZuSI)&aCV zsmIS%O$x)EU^d9Kv>=H{m*YJ04DF+a%OfLHse4rEGHS&KnkRmyFA(E1He6 zcBh9g^az=v!27eP#*O=plERt;_<;orqdM~XtsvoK3S3j?>t4#ka|zT6d*aDTV6yEM ztGwK`Qm@H0Qaum54*DdI?Viu~pPiioxyG7hyCHbqzS`j19Qd$sj8<7?qGM~A0zD7G z`P2AUJW&U66x^7VkZw@x-OyP0x!VkXdXVEiER$&fV@p=(JLb-`n9&g*K#|g82f*w9 zwnxYl{}Sq4JttWeQ@PE{u#xr+uY<4hV_8DD)67TJ1CVdeBNx`ObN}x=v`6=7mbKXum+pRy4f=>K;yV&)V~idikv7XjKPwAfjL{b1is|bSYJVywEqbHBa&>A=bc>ZF4N1QJ=Nh}F1_)@=2Je4U+K=e>*GE;+N>^dsz2Z~7U~mR#m4RzG3Gx=e z5sT#Dk7^K;ggYPCMchc($RT!J9l9GQ_?Z@$?M-a+WW{K>6Clk~f zj#=*vrT-!TpwMkNjf`_E!Upa&Cx~61ZWXFz7^h`6H$yD{Z)AEK=4pfY1F-N1Bg*F}gO;u${{Cs=QWKI$rUM)G#X?X4$<=xm1W1hxtd z(9E+kv6z+H7Q}8>#* z%hTtYUf*6mXVm??`_DUD*(l@W?3%}lgQD}wzmC!6MDWyqG&xpg zU3;b3Zsvnm6fV0RINtoa)!h}p4&+kcVU>^yVIQ+n%gp?A4c`-i=v&TuSZSa2HGMv) zpexb6QPU$9EM#Y=_aPOnZ>)6EJ)JuAsIpM$Kz+f@pgM+`FJhzIBF<`Ng@YFKwWEl@ zW->r&I}brnpQ5<7cXtz`Bwe$mCDEI0c&+#Rap%Is$~mYu%z%5Ub8;A><~TjF8Z|y( zXW$*V5HfhTH@;!3|F-h#@3#m9)Qkv+%V6LlsQw_Af| z+)-A?9#@9vUp|Wjzh+Hr9DY2r4#r*nSRQ0^MpU!>Ox^1|FxHW(b5{NfpVhHrUxowC z84+o-j;OEq#o_B9+T8F<3sJo@T$OE5mX-7D`9_1_^L3S5O!+;-HUEtUx{QthIo{{| z$tHGFCzXq<6pCcxFKzDtgG-?>`GAOY52GD??fcx5?K%ga#|jS2Vb2l#>&Yfz5%!C7 zpg2LvFZBlbyCFyI!E?@AAVmfjYoRgAe!?$}VFd@ABZ*xJE-T+J=*KJ%9PDpbIhtFr zng9vCI4h0UW~qmH?J^Zpw(2T(j!VYEo{7GB56U1Vw=O?dGX^K=@24}-(L95nbo; zHa^b0it*N7GC{#8wG;1vLA)9x66fZ&y&JXKNk;+#?Tyw)NpbMiU|A`Uddibyff9%6 zJL$x(E0`+SOWtLDz2{m?>OCY3>~TsHQvK!4Hr1kstS$rN-gXjbC2Ic$-HtkR()XR5 zow=|$X{KDX%O?zePcztCq9lryC5;-9pb(d^7wT(YaQKVqo;nxCd%x$peD>aD4^smLoZ0pXW%@MUt`pyXAWk45qPW6- zegH9<9((Cx;%s7K5@qBzg&&~2!LyXO)0HsBcfg0{A$>yFAitIwYPb^->oS3Pa9!-x zLdfb5AvAfz17{YP)GML3MVg|#Z!4azh@@gHZ4Z9W1i2a^Kg{}`;KDvZjWm~LpoNM5 z$XZCx?Xe@tYzM;jT?+TZz18AZ!+FZi>yjJ9Q6sWK`^Gp+Q)CSoy&--&YE`U_1n)cV z7qAe%P}4R@ALe-k%6<}lgYD`9OrF6e*m&Gim<8!$fla2+=xI4^e7zB|LZYU3gP3My zrA$>(2R~B0f(RRMIzI@rVDDBk4w~cy@I>h#l9#Uu7jM1^3QE=cI%U|6T!Q#T+ zHUi5wv1dK2=&W(8Mh{z8kUcN%iS4fB346gA+m;~gLd}-BN_9v^Pv9P=GDrq zQ=a|4>gaZdRc>{E+faddkg-EvPjaYLR!^9i>wYzX8hZgupAJ-|)6?yq{vi9=jG6M9 zNnwJ+so)VOBPWU5lMQo&Zzc-VVFd1d5yQR!=aDX5L5@xjTWDTEb((Q556Vh39XmQq z_H$OZd`%v|O&jt!7d861h=ab=z|1O^!sxw?k?+*(vZI zr<;{^5zK96%*_xq&Z+vQ01CkyoLxdcS(~ z`pZQsPd0{8S{yNsrhO={^hqAMe5G2kEgZHF{?%A340oQPIs8T*RhiMQQ^b- z>8L!7Qv$`misD1zRrJ&O`-T0*5riD(L>r04$3>>l<6>&d+7K24lc_gaUJ6qR2Q`+U zSh#&^&Al1h9dq{ub|G;0;Q(WixGvLUOh%4Tl>g8`0_*7V`%_BmA|o<{QCp5h zDBiY26=!58JGd+=VUT|_k^2)-7v5Hr_b922=XH=ip~ykyE4|V|`&fO(0{D4M#nuT% z^O_>*ev(ZmE3^`Y^ydpVnJx=83EkrojEi}flFn6jk{;=o+u0-3fZDIDZCQK}y zPm%5Pftg9k1q+d&)J8bp&wh?v_kbAgyAKS+6yr;p2#o4h4p9g%cQ zS(1boG*&Tn#^DD>mB{JlKJLt4LMp|2cOJOsH9-_lRy+4UlFi0^1KLAeIKklx$$sGK zZN5ySRKmVfxUY2(s+=tlwI=d(WDF%kSf!E4n^;bB^U_LkBG~L5tyJqBJpQVRG$0Ri zi|^(rhH=c>wofv9g96r+C*$DbhZ`u>Vsu-AxA$>1TsTL`}xbcvUPtb${d8n!~8T(Tl zU^)?2l8q1~gI?iIWK_)O$B^5JrBN*R-P0)V&OlE~%-pokEEKu&5E7zAuO5^{L~=Nj z`W2=$@0BHR7aB>TyUM=^ELJBK(LKFKwpM9*5)YT(ZuuG`UAJ3lR?88XTukY8d&Uo3 zY<3EoF>fn7l95R?Mts~C2wz&_sERbIJ76< z)@Pw#NlEZy$CZSW6yz868ZTZmd5DjJ>G9;C#qxBrMs?& zkQX4myFFLFLOQ`_ZQ0heM*68uY*f?$snczOZ|XlH;|2v?wtt0Hyfx(!3w*=++R4;p z%8m{e{6syu@z)#t-#T^Q1GK3{HW1*LcY6f{f@Np4IMqi|IL*}nI|j{LdJ>c)Zy@Uu zdHiIi0)<$yLTdm4p37bG&aax91!J#@)kN%!c+8)y^5th>!lECVaS=aV{F=#g_ zsyZ?9uIH}ooZV|@bh*OUgWumZ(KH1_t%SWD_ffNg(sI*V`cBKKZd4ihKHP>BkYUb$@m}j-@BCncy z!?%6YlixuEBRWN}#K^CXVF9M&*_ryGa8>wF+P-mhukPHGK((<`_5SVh__S`!J0cdN z$DKix#H7ug2?x`%pY6X7e+v7o)OyOuV>IQxvG=Rt?(uw(v#y;5{t zkX8X0a>_AtDahGN7E#Za6uV^kpwasj!B33eStF?`SIz@P{rb8DOdamcmGfuhv&D^Gm4;4GC!OIe#ry6+h&T~wfCO_DU-)rE~ znf=QLhNCi>GNc+jw{z{Ot1r95^Tpj(3C- zlDMDE=l=P%IoAHKmq&JAqtbSC<+X46Z0r+w)pnQ2XmyOg#;BW(HIN zj6FL#&lE7*4pm!HTZwqEq4JciFxnPcFHOy?DL$l`W`JV>vXPO!*SXL1ldb$yh=hzf z4DsU_zT8)Fa8eo>&c9Z-uIq9nSzI||kRt$7c?!zYFF#rxbGv)Q44wI_LOj)>7+@g!0h)kYQoOZ3%@cx3>K>@zR2}4a78HLhZ)f#k z04a^tPCsW>TG8OyIl~MN%cx0&VVfd@=$t26E-k!PxXc){4g<$2}JHIs*}-+4c*1ZzWKV~V>R=^M3cqHSK3jFg2qaR z5=4Nwh-Ui5X=a*XJ-WU~nK3rADfH(=Gh^Yn7`?aH4_+^)?F~nSM!dL;7d}rAmO1Qb zp8hzXXJdX9>>VUB#r3v`OVkqRbzQd}hV+{;h_Q@41g&K*5ZOR#V9G2a>LDvX_YA?- ziV%6TF%h>`hSMRw^t}nvYvAqi<)WqQ>AmA{?wvguABI@fLIVpya3CBIS4P zr0N>!B8?Y8Yn^!Zje>SkbLk7+%jMrr=Vc(IBgTX(le?cr1WlX$_zKjmnW^bXHcAas zWM!gjHodr5@I_eQ6{eZ%d-Mv`8vMZm)k8^OezdE+7e9GU2%d}C7DGPxjd)OK<{&h> z!mCGMSoAo7?gx3X6->>^3y;WquTXM*uV!@P4v`Y~otZioIT~&ff8C(f5l-c`JdF}& zR?SqXr`MWs;(XXeRMsGcRsjQX@lr}md#MU#MnE0*$Fw^PZvK2r@_sEn;LPe&Ow**c6 zk0V|Tb&D6bs}^`p2IXY?>EfR5psx^2DK{7!+siU(e6812l6|`R1-hAtts|uXhBM2 z*&~A0;hOAel*(C$vPrw?2kD9B?w_WdfQDd>Uetyj&i1n#8-tH_Rv(LVwqPp)>CZ7 z&Fen!eJE2ijn*DU87ItAN*3C!Rst_L_Uo&XbPnZbQT+wqzE{iT$JVbDu3M{QN83V( z&dW5~A2MNHOmEFj2hrj@C~mHrk2>pY`hm2JOpw7q%gQoe%CKsIyEAyo{Q?cFQ&Wsy z!kNh}yo+1|dn-I)*0kGNQ}&ygV;5F@zSZ=!ysjftbU=*k9*w@#s{uPy1PQ3WV&^zp z)#~v9RJquV<)E_p@o?+ybbYRf#2pcEfGxUntfZp^)n_5<+7wwg`jrXDYZK z=~nlc3^#%(Qw`cTkO+?bwZFRHe-L)!W~?@vBl99?i9!#pe#+;kjQ%B<6k?_)iZVl_ zS~wpmdD4A7(#a0DAcwU&INqL&e2uk2L=Bv^cSVZEXZZXI3sLE6 zU*P7xAA+1w>sbxTOMZooFn@4X(!XB_ zFXDL@l>1yg*UNz^v=wllJYw4Q=T0X0VfqU7EhLn*fJ5T#%%R0vQj04l_S$G?!zeR; z@u$GRCl3-|$b6+Ow2+RIaQ+MiEP{^hvE%=sKITS-G1P-@Kdo} z%wj7G@3fn$C;MpQV2gczQrP$cn+0WA#&Y-ufi}k zy4s}}x7TXeIr%zADzq)dUX%c-F#nCodD>TYUzWD#5TE>QH{eG`cC1s9mW%NOG=5zQ zFa^-$x( zgd({-q%^cTU=%n#TyoAZcXLtTR}w;na{3VGlE~(*Ziua71Qvq1o&Ut?t}14 zIw}#PIL?ym@s{{w2e9YZ(+bj^V|$vg-FlgC4mnmwg18?E3my+Laz>Wi%cV$<+#X+= z%^jU2dGV;vjGDQ`wVX20C_wvxZ)9aK z3d%h?3StAnJ1rkg;LkoAHiJvjy29j=7P4ZujOcvf4+4wqf~AB$*>(jF)8>G{r8BbOx754tjTRrb-RiO>rT?1?AXV%z2yWdh+j)N0J|&bn@%;h1S{E>j zB8=DOp8f4Di~co}Pb@w3cTLBiJqg^v+(oXMU6NBcBD$t#Ra5-$ zzMu>u^2fhddP5UJC!UkCmL zv*va3D82Ku8tUaKfwTSQi=1z&ctK9L!SW3vZQ|otf`KRNhm$#y)HY=wEl;N9vRIYP zPY$TpEqvcr2Wau5PmmGaX6L-na}_sl>somn6w=*{D^gYQbOEV z{laYUX6$xs9cI)d7fXYi=sOxW+h$p!S zyYGb{0)iN1s7mWc)MJmP)r#gR7v06JT%IwnPo68ZV;Tjueqw=0>!PUqmd6K>7M(Hp zQ+U3<&P$|$g0BVJxN{!CM390LtJ`yIH=H1N96tq`oVa06!Bq;!yKq6IOd$>I!4ip~ z$mIZ2lKmYOCC2JcDpSfpHEMr^nUrAUgC zu5~uP#>82@$DdONxA6bWJ%JDr>Qd^$3v|FcPh!ah91EsnR`Ye*i?q3@{NB=em@2Sw zcT3jGb#e&Wl?Muu=!bY|Z_V(<7H-Y!sVi?mi)pl1gUTF@iiegnhbe;0Y3*3xQE~i_gq|dz zltpL5Yyy%Gmc9s-KiwVS!naaJPR|pNbQ8pNF4Le9N5qU}4o;1MXsSnzW3G zJRX9y$r%X3YL|-bt|A1dCtkJSvsZ0@EZPp*cTl!J@q+(4K7fJClJJ3F$t%vi&J&0e zEx&j3wEP~jIAhRx=wzukDec>!mvnnZkjSG&wkvY&{joKfD#Glm3T^AtpG#-Tq7@6T z#_27Na_f@W;sk%-#M8&B_h!;jH9<^QT&W?4jzq<>9QI+hDW3fs9t?i1U13ZlsQrmo zepRx+Oy%kB6(uuWTS$1TDZ(IZ)cu| zQ;55V{{>UHyFjqSL_Xvxwp*ce=^)Q4Yy_dh=9ig!p1;pC$JJbYXF>Bth-ooM5;MJp4mHU<2chi+gT#K{PMAqg(E!P8G@{9n%g`#S@=HVScvn=S_5 zNMe3)?d#P>Sn;%!bT#&hR(B;r zSad2$tO7#=?qPGf`%9O91ExD&F|9PPWgle`{TZqq+K@*x)V#x7TB|c*-z(R}*Tr8= z!)q~JT`DDe+_*tqZQ)d|%jC%sSAecgkUKd}gVc=V3&0T*Juhc^4pR7>Y6))YCG5FI zNSa!@!`NR(FpLwz;7Aac1~uLD@_yGM9g*{M8Ig<-8per*v^K!2A^vSSJrg$P5i z0Q+S*Bv7#c!yU4ZQ*$ve|5vHlmE(6|^(d0~%=5s@!sq+zeJLvAn2BK;-_AdGkZ6?} zjP_KIhYWsPUo=O0uQq~;DL6Yvub&#BetJ@~j z(%mni9hco97>wUTw$E9{&=zCJG9*S8TZAF)W*}bN9gEmEYU57(fo>i7`&H|A=I?Tk zmWuypZnZogA9KUA>)cGVE~FRU9lqe&M9 zm=}Wj<1Al(^f8u7R*fcu+~I6xlb+YARoHo`S!uwt$-eV~EI#35b#Njt^w2{d4nNia z?Xc#`lq#bVCyRf=7EU5oi+%-ilaSDaePFW5*Qe0wj%XgBOJw30q5(XFM*BP^M0FEP zy82$kLj~GVwbNfer3aXzSW%b5C#Mtr(xOUDR|q@$qx1eTt3@HK#h;vzk@zEJ@^Sll*TxXjo}czat~FjfBV4+UpQk(E z#b%Z+gMua zkzMsR{BM$84UHxmg~d=lg59qjVAa|SN-%igv#@LD_XpwWOAsmg{IK9 zihxy!-|u=k&deh)tHa@_<>8d!piv~w`^g8Q8aC+!Tska%oDlBCH_K#La$@hcSgG%f z3PSQFc)7ZHNasro*Ofh+M#U@0pi~a;HZHe~A~#?@!ydi8{_j$x>WjwKPE%g5@8xdh z=b)22X4}>6q9`^9R(RJKA*NVI)i;34+z)hFRIpqru@u}PJGH_I^SVbl{#k>ut#FB5}G=tnC@+(_lp0%Nee#^e3syo8}sJ_;U{^KPq{j2M3;Qnd>4E1#YHgIcD4+d z39PHWUmMyOO6hU`rdF|VUfS*RKZ`;Lgy*zz{{5kU`DD=l#!Bc}U#V1W-1M-w+*X)r z^uBw0Zt1u`TUz|`>wPCU#7YasUAJduxEtsPjX#}p*4}^l^wNDWwFV+lvExh)uK(D% zmmlAWQ2*|Ap>0nWW1f<8H!*y8Qu}@;>c)*bI~V-_FNBERK&AwM=*-#H@+K&5aOlWQ z_bd6OOk+R&)u1zIyU6^XlNH6L8LfPiS=v+y(BvUKwyaz?iUS%+h2#{9Pp&%Wc7GMt z${pM+m0fINpguumZT3VFHBNS0w%xh124fj~xVjy+@8JFC@(tjcE~3-rL4nx=EJ24q89#b^TX8B zr*q0o+mz;AjrSeQuOzf_At4M?)8W5MC^?%Qz9*oRQ6d;z+6$PT%WM&01=GSJG3i$7(e2a`6Bo7N%pL8auL2RHZ4JD1vDPNuH* zFAkI@57~d$ZI}PtuKs44X~nEv?V!LH>eLkqjpb#zlO(|B7$?Zb40oIEBkAQ!{dnBXIiCTk4+DEVne(u@4;ePhWF@rt06t;POi9+t~7I3U2L>OP~@|M2fi z)#r=*rZ5bkBMEXOM1>!)pKs*D@-pP{DZf4;Yv300OGoH2IIfRQNng3~rb<|x;(UBn z9dxqyX4WAMqf1kF5nf!>6Xwk&%x&W#M(lU7{$v)3Hsj7=7E1ql{D49}BHJ1HB0QHb zd!9P=a1?)e3W01j+1AFd&EdFbV!aeWe)*jTcdU@VZeBerSe}Xn+07|sjr>YLLHQ&j z$xYoAy~W+gqio{WYDx8y<+l}Lxd4a$;)>`i#yKSQi{xZ7zKWXt^M#|maViw)r+NJE zk-#*TO@9I{5LphVTv&fs$|B5Gnj5&*@(Idcy$kBI$l+{kpqsrB*~g)OVc7cICi?b{ zUPgOqy;iR))O!AqK=s~KPpvgEPbkV6r?sN3>*uRqjALQOA6*u5{Dm$3 z?1QcPV|w#HXZs&J>v7PBAUoC`sS1bwok}@zY7U6CYir9{f`1ZAg(-a~pB|km;`Lc)X@K&7u5?J+zLcO6VCxG zOk;_{g>X|Pe~$1_W%`DY4Bc!(EALcy!&4HEb~*CP`%5^PAB&Yq#v_b?UStD;N5m{! zLiJO=esH^VaLTzxmM$fszV7m^lR1Dbm~NMH)xG+k_E0h#?lPv**N@9>nBh%@^0$M8 zAUDP+*UV&?`5ac_fO|#m!;99zfB9_VRwynd<#aH;`QX4NFY`{iP8rxoz6rh^)fE3V zjw2SVzx5x2{`+Gz6kFSN@Vc(Roe;U!-M>ad+f#uvtv}Nv!52E`_2x3BDr^Q+-Y@}I z!$FpEZm*f!P;)~{SDDR!?k!VhEW$;ah83TJIEsf%pc#3Q zM3-$?m@9o~)##4AKDlp#$fY1uK70srIkr^ODuVP&#qa#?)OXiS8oLd2lK4C0>Ubfj zk`%iC&<@E(xJ|POLc78Kd;$-hXL{fpH2ZF*>s$R7nH`@S!5;*p9%z3Ha|Kq-Ze6HQ zQRn>R?>s-jFUo6Pf$BoKLw~{^|5yNy8`p5{*a6``2mO>!bT+f$6H45#nB%LhZ@7@% zP}m1?Ztip)KL?wt!&YL#4edf@0rD`Z>Z|n!8r0tZO;^Kg$pN$k{9D$4%r*|(Yq-wG z8~LL*c=ad-oB4${rndyFe_XRCyik@b;UDl^StlwH3Hqd4v+M;3QM(r`SE`s1Yod$$ zfmzRg9^vPqAFz32@Eki@MHcK7b*@ar$MVAZ@wWmUZ+CQ-^!W2Ov;Wzfe_`FF`(TTB z=jZrO8}iBoHzmYk{k>u+-KtMMjgFhA{Y?WHCRoyS61;Lk3C=Ut*CZNCFMX}#0qlyR z0*<%Gmz(hF$A7<{;5Aaz?8^s?L7`!na`idB|9oD8{^z}-e6J^2|G7#2{O%vZ|KE@Q zv1|TUB>yXtOEvJnr{%wCoBur*|NozhclC6A=Jkn!C4buW{pXr8%2IhPj!qrxTD1e+ z*i8U72)!x*BG#!`3!VYE+v&2C z7ZIwzi5=hG@;MAOC0FIS@;m-%t^9qol5buUFxXxCnz&K6-N8gTTIGUgVC*~c#zlEddwaY8M3xaia#1Hfbf8n-0{{lc1ZvxmsZvoT3wkdgGJx?mlKZ zQ)?~kAQZ01VODVe&+Gb9*upaO-$>SxqkgL8BLL*esUKk5#B}dZIP~7j14Hk{$Y!LM zyz{!4>>>oKNI0|oIoim77Cg&3&F1mCHKK-Yiaw%XR zW7#@^HDZ}UaGU5_PXb>76zDQHWw+DK4YKJp7~%D3s@B@Fy~M$W5!i~bda{E;4blaE z)eHcjKb=?SCp$tAfS;-N7-+D#p>QBpk z8;5ElqBU6U|FU8liZ~Tf36RmUl0Yc5u+X2=8JKMj&{FtZ!te)kk}fIg|eMQO?FakvRx0)$bnsME13`5~Qstu+Ywe;carVyMXG@TdfF zzCafpfVj@(qZ`r+0l)=2jiVG~2vyHl>RGS6#O@?v=7DB2%1Tf~)~SpDDrrlLYX_jU zgzN!KtYfXK3cFlQ3-|jd!T&>C@sFYvrp7Ysm)CVB+~8#{Y9ho?xXphjlPTQ^fS$cT z@NL`o5?#3oJI|%<V1%VNOT3QbcU|SQkb9XcpPp%aX6gvunJI*hq^P&K!f*^CJHL2gM zUcMEc-)goc@8mrCKA`*ph_N?|;~p7e@gIsa2RRVUSLRxH1(M(@>jfopJz~ z>A#mea==G;XJK--H0u5z+0rEUT9TqUM0;hDW#SM)2E*K;RU<$n>#0e(wj5D*mv0Yw-<0hR793F%O}yQG@|0Vx5IkVd+ty9ZFZyHj$2A*7}MciwyY-uHd~ zw-(Da>-lhI?(g1n_c>>uz4LoNI`Hgeg2$-y?~m~vo{e=KyYlxrCv_@?7uwGC%IapB z+CbI8P_}&d#z`k)M+|i+d8rH0{_yg~ot4>{8w?@S73PHOU>rZQpf6WDwE7a% zgf{v=68y{lL_WV6`KCnJ7R6Xa)%QiM9i+*0$-3VsCs|T z8utW zJ|0DzqB@Gk7F?+VO~X&3c2WkFia0%gmfhZLYf&@FtqLVBR`+)d(R7mj>I_-x_u9( z#@VWO?06oqTd1y0S6R~|2kqHvsWWARUm6jxuGDl9Kkg-yw?b3{!x%hmce})o#8L&yXvy=$ z6OUsUq6gfz(t_x_NccfuZJvSJ3ieP}!a+t9rC z!>W3blu?goqI?4-|7V&1j998<7~;jlH^xOW@;>kt?3I$Xb#)sjyB}eKk={T@g_&jB zlPK?pe_LM4S#*Do1 zuk6_${MTb?+JJgcJnDnx_(2OsW4^psI1N-U5cju+5~W-wfWmomcCyE3eh~-?7mE6j z1EQd+&ksx{Gdt&VQ4Y;nwZdeVnBJOZnG3p#H+W&!O$xz&gKSY{`Jv&+eFtwv;O6?nWm!J z2_^P2$UYsWJG@?aRL-*+YtWRhR%RDV0G#VZGTb&x+J&wzFYw6I?csfT!!jIqfFQ%$ zMHvU6;Ok#$IlVgd7Q9**QIP9hkr34Npu};TMPBoIGu5^gvMWGxgvJF%56CUkvLr2V zx(RYWoM&Ew=aZz_^=7cQSuyv5qsAz$M zZlFr?(Q)|rE5w)#qC3U*Ez+`h?|t+8J6I${oH(cK>SzDMg~#Q74~ z8@Suu>3XC@*dVI?KihmLcEc(d*?t1qedO5<`}8I$;q^X%B`upfymYrMlKwVu*!^5z6I=-om9;W zD!ikg(;dw*91-Q}Tnq?atP{oaxjCuYw&F)zA^~#ofKX;P&5Gs{f059xhIKu@L!x<( zo_il%?OGZpUI`FiwcPVKkpJSYnPo12;SS_Y^@cJeIxEupBLK2t3n2fC*!C;=0EqSP zm;3KQVXr`Lva<=7z*R%FyUCcKe0K&yaKv=)`+TK!qS?)gX`oTStSmsddEK?`Xd}1& z)y5h!WH2#38*hL4$TBkbRbqwz*+V?Io#T?d+Z5!b2@qe&+IX20iKz$O>P0XpJ&Aap z8A1ef&7I?bUOr=y0I7fb(RTrRAWJjK=}&l{JBIH|QN_H~Y%QhxXU65(`@uBdHUaqV zi%(xgo*#IWSwlB%nRKu%tM4bM&KRe^y-YhYHwd;@(cj)EPdGtvA2AgI48Yd5iZ-t0 ztW}s)7zIRv8Dmu3@FHPvw0S|c^bymwruJ!)i~gK#e-F(~s)N8PdGNPdAKV<^6gjj&_idrE>HtqCbzFG6B1J6a-x0pl-SJ(?ik7HK zYiYtsGEl*R7NtU^PrAbicYxkfjVtaaiV%m5k>@H|(EFug5GaL;*g4WAhG=w^(&x2b z-LRYhpEYEaEvDpxet$Ks|9zoW-RkJRIX#K<`Vggob_#SWN}};SZoMMMY=gXZvVCiQ z>bOhoym?^_)sFvdzqTc#N-tLgbmWjmcyTKi8A7Wy#$ zemu}O6V>+LqCfAOwBZL+-fxA!at)-6?hsGl+3#UtX(zUqeyPp9&h2@*pKSgy9T%X< zS3~eJqem=zKe@o2%i{U7nUuLQK`VGS!D-6OF~sDPC_!gM*#!#AjhnYS#8J8TI-anA zRK15}zXte!*RY)`zMzcD6b+paF4d}YiUW|-$Y~A=|45e__ehHJfjjV2>V&a5AMV&I zTev-24Oy?|4Vt0maz0e+JGVS(h3)bK^=^KcCararYDOJ!Fo`e#H;!0g0bO8h9_dqA zpcJM_Qh&3u$M9`+T2Ui+A%o9r-@39sZW<2s`mD+pE8`|#Oz8fAiKKclQq_jJcAHX~ z1f(htp>3_>@8>x7aD(XhJls889%(GHo!tHuU@G<5*y7>T3{%mQR94HV>b50*WFABW zk8& zRBGxOi7%zLPg8*&7aIhkk|rosy1r16ELMWbGkrx{zfd4)Tfy=4_B$H10)ys?A#7B(}Ou5A9xZ>EM zxosaQCh*lCEAPZq&w>9jdR(c8wGc>{!bgAjUR<8-u;>$wM-W=56BjJA(T7UA^S8if zfDofO>_|Op51^TTJQhmcHmUnpKbo;0PLnbgF=%OBqB5d^&ot_p;2mSCaQEp>8okmb(-C zsck0M&e(zFJB?Q!b6mr5lFYB?GjBOZCDrw5^_`E>Yu_vzI9o!Z9j&~n_(99fiIN(- z`UW}PU0+g3eW+-ww&{8%ar_ORDPNvIqus+gpk4MfNgEoG-(>-Os%mQgaHz`E%a%?2 zJPAOl{Z4ne{AZ6ljviZbYCR1Aa2ocvxg>yvHPJql)LxoOXX1}J!ji%A+wELQd^qm8 z*ZI`mAE}lMGuZPD_N&(vl`@t4I&mg1R8q)rf#c1${gLtCFTa#en2L(kh=~$|?d!Hv zh^HQ$GhyYZ*aqRKeARM0o>AKxNiM)Ks<&eBwQq^Dw)!)vAiIsCNbdE0f^PF2AOM6c zPmveBIn`9Bwm(=KggOpA#N^RmxHtj|cSVxO#S2B+Gw-~;(>u~Cei1`GqvX2H?Oe0p zxLu2|D|(fuT)@1y{DJQ1GY>EvS`vPU$#Hp_CHf2#>LFdSBeP1vihi@zx2{|;lhTN? z7QYOs8_A&pg^p285h8?F*2m?W+{hUwt3wam?7ZUoZB0TOi_-Pn9wl@ zX2Z38sU|E-7#{NPc(uugDKA^c~b_nW}z3 z2y101N^)Y)b^(+byBtlhGlYw@>+Md20zr%17YQ_+J0R-lZr75#IUJj!^SgO6p*<@r z!w;wH=q8f1))!$gzAxC7{VIh$(OtJIH?_j8Zy6(j*7i)4_(;sF5_|U68OQC{@Wx?T zo|X4+PQ`x$r|tS?XmF}|Z2R_T(nL6hUdzsC-FFV#N+C-cQ~ym`G+`9UG{5&Zkqh-{#0=0^v?%Ng zVrYn!8s~U5%~m+R2@qqCV@^NUv@lByLu4_fT{A%U#L_KtO`?Qak%}y~oVOvooZ~IL zkvwnVyCyEa*Wi|94KaZjvBKpd;t>tenvZ;_j=J~!{=s#L8)6<)jd4#HgQ z94%}5bkA)34m5{p?e4q|bf|X_usu|L_Msagsq*1f$$mzALXtv>)4J=r$IzB!;uQcD z(HH`_Du%csNGwTl0Z=J=Ae1?JeZh^IUo@WIHsJJs$lVK77H_4 zH)!(n9UaWr;Vb;{E;$=eeZ5oF)N@dYCDOk>#5?CrxATBPaMYM*2WDSvL~ftY>b%b= z<7>{u99pmICOtMOcS?K+O+G0{eYPUUZV-M)Ibq-t=8kX%`G1Uk_z zN^dfjPPsYtntX{IpMBv4^_eb~OtF8dpwRw6bv@dQ!`r5kO;ELktfP0B*``il4j&F( z+&5R5-viR247@+S-uM=6dwcJ5j_)M1Kc!|7C?S}w-RRH z0${DZl0gwaepY)@l18B(^{2v^?Wan7BmQ1fZTm{v`mZ)i@=ArM*sEsjq?nIYb;Ah@ z+=e92&qj+p@NeFJfOC4hLn^s@DbwhAD^bHJ_ZTQAF5m4|7&LcSfIq5^$g9H(7gN02K4Lro)V$pD-qS_>cW@h=@-HnZ{2beM)+ldoAV1gh8m1bonsnkO2cbJCvtw)&5{2G-^r* zY){giqLFH*hxm_#U3=EK@Xceucg$9;d);NACnIGrZ>6oH zuSDx2@!%sJauY4q+Hz|#%*tt2;jRNC57v8A`b>qYjbbE@ijR{d5@;y8dSjWM@B;bz zK$j|jeBg91)bslf-bxh^+UJb0S-IJ)CTe#lN1N9+5WQIpWa9@Tc|^%fCV%HK{{fZ1 zqKFAQT1UWH`=57c?n7BWqT9Ty)?$bogMJ#kofMIC0jGn zs<8qM{~CMvdmWDxN*Hhg@8l0IF;R5&-JxTJ!t0K+;V=PVQ=_- zX}B~RmjQtG_6*bSU%fi*QcMzT->W<)l4?;5Gg%kL=8Lo$Nb=e?+qhHF0tY}l-sLA~ z487FWd@^?SxPC`7SW-F6Bbg3JXx6-ms%;ximIKLi_w*8@a}zG^?oUPcyn@PG=nxm( zM7oOGrl5uy;fgH7CLZPt*Z`W#9TRcv|5&hpU<|#HCH~2cV3qxm8OrUQhO_O!u$;Zd^A!fYS;z4^tu0ccfig*U?Ig~c zxQhG^YXdFQ;9@Q8rlX-&K9wFkD~(-uuRI*nAmmA)xdNxv&TNZvZKn(%iS_aPk^Xlx zU(-ZR%dCJ~Atn%twNOmo&VZ3ovKj4@)9{BugnRLAl^A8QrKCG(@biBdTLT{}1% zY&|Rf!w09_2X5jkY_#Fe4^!QF#pv?BP@)+1dZ`((NK&bPZ(LG<-mRT$ zm1@LXj~xgSi^F{jmxDnuyxl&mp>D%mSPgSEz*auK444j}#E>LMelOrI|IhD?!1`BH z%^i2ojt`z--{iJaA9Jj>+eK4EAr(%5$csFVL(NwuBh=8zEejv7KHVBf6}I}aCrR7d z7DDo>yJv)C00hqz_Si7;J?~ zE-67pnKnK13RGUpVa*^>GNGT(QOL##!rS8vA5e+!ywM&*sK|$@zUPt~nHjUH0s4yO zTk4RPhb2Hok_L$5KB-{8=Q9yu4HPTIqgNHvSHI?$JTyz%PR%LTu#c?^r3^9XujZJ4 z%VIhyRaMK|tFN~MzT<5qn*A&}FbC=*uz1ZG&4eH|L(GDVqtyO*}ySu2kY zg|A_QPFf;I&b{O9S)SeH{TsPFWIEU`|Af{*}-)uB7bObIYkF7 zpCWCH>ne_mldb`@zHJ?Xd7N#f~O&^^Q;%tPHc0;q2HoU zx;25dWS8{ic{%7a_i!0qy~j+1k`c!x_cqE+=wzbk8_>o^CbrDAGeC4PCzdve3gpU zP_bPk0Or~U2WA6D!xXlsn%e!*$2|e&ykZ(JSq+M0v>R6)Bbe?zF6x(LsRQmv`+a6} z(ondR33addML)PK4bJLU+VmhMaR7d?80cF<(k1(~;DtNqqg0kuUsu|;i%J41*Wa-1 zxn5XuO##2SIcnoxFE6O;O7LV$3Q;rLCjo;+Bkg)D1{h<}9I}q*JR`F8ajKOaAXDkj zJ9BP3Bq}+(M5pGuD5ftB9EZ=2frNM8gtikD&epF}6y6fm(00fjk8U)_6{BVx_YwNd z=%8lZSYO-NpTwyn&m`%c2WKFg|KYffKv4*Eh4QxaZJCWVBBuU^9&jxtgVoaMiG>@y zY#;6KiDQZACvCPzTyLt&dLE9tZj^%HW2qDkT8B*^4>P9a^UbPQ*?Z?b~mUKcV&NK>W9rR!LufK=WwrgM}O#9bH^do z02wkf5YX*EOcgn0lMXUGy*&sY0cMX0<1weBZq~~kpP(1d71qr)7aQ9k{i0^>S>hcG z2V74V$_6TQ&z6l2CO>UUK`*|fffSZ4Wb)Ge;j|djy{3EHAMgJ&CH6w|edjcz-P_bh za5v|}cx83EpHLRM)Ow1H$a^upS;a&d+M>kXLs5}B*CNB~|ETnI;F_{7$YPhG-z8R6 zOeh;3cahI$hI&sxq~B=g<%Ddhs1%1f%va{?w;e^(Oz2Sh@<1~>x+&e~9JI9=kd=4s z`dVKe%)rKzUhBQ7j58%|>^V!FTyOh>CP^l#Ac!M$X8kKfO zd*<3dW^$t?r-Kx=8AG}8oUeDv+WvJUMH4NmU6C+IC0nLhzM6p19c=)p_z&3(cC<*M*mtGV|;_{4o zQpQSf?sAE9#`8i;YYT^|Tc7cN+Omw91SK@wHh!vBvjtg8Y9{AHh3)fE_Dbz~*A-4l zR_f6#(3&-yuCy#H*qT;-GYoAbtB=e;-PET_H3jKVf3I9oHfr?*vg7(Onn25G(rAAp zr;xXsebO+Z=LDXdOuO`;YTrS4w!h0e>$WDHmU?bFa^nRcIR$H)bFygRb zBR7xP%vvj0N~&c8{W`MQ_VRFpdD@)S9yk)*$N#as;5=oPZKT+LXQ5A=!)ra1=^v|l z&8v;#`*8w&HI#Oz8l5ixb!dc?3(s5=Cl>Ao%vy>H+k?IGr)QVqq%1+B{#~m{J>fgE57$x`(BeJ4CI~ zarEk}7t~a$#;459ZGHE8jzn(xpkv=%oecK{9e1>ST&zlBEo8sF_+rhQu2)$Ze)Ntj zR`5Yj8CC4K)KC$f!o$r}4@R%_eEvs69gq}sjnDKG!soi$UJ{I61>{<*Q&Tq%b?4M2 z<||vYRLwG@yrHBkAu*q0$9sr;K5bdYz%Hp0sK%ofh1iSW+NRPnT4Ui4*3_9_@UF4E z+=1d%`(SoeJgx@?|0bq(;)pzhCN0KPffx=i`rD7#X?s4i(ZuRTqiV3TIKv`EGK*~M zjVog`_f9}PL<7P)5#DU%-WKNqjI;GA_>?oP=NbA99e7WHJQ z#>>MZZMik(Us?d@4PeWijjp}(4#&8ESx%kvJnElZ6?vNLjpK^>){?dpU3425w`_Rca6P7I?7o)CM4qb?RaA@XAQaq_W}yw9#RxA@1tXan-3rMc)&kOn`-p zHD6lnSv20+b2flR9TV(qNV%ty?oVfc71{Pn#iTzZ8rkf=VJW4={yCEeA8KK&g z$+PC!P{9)ynE1jkbnEwb1;4d&&*q!NoDDAGK1IqpP#dZs&>xiT=7kt7Xkp>bQxreH z9B9J9;?Uq6YicDJ(XRaHheem5`vPvOCky`|10SgwNp8z@O{WznR#dZRJEl08Xyp!8 zGx}>X0${0F6I6@ND$)a8g1>Hn3{2Cw*BZ)-0;R286klU5nZNy$fRIAwRc`t;wV)o& z@>&YtD-Ux$cPuLb)kQ|EW;z#=Q3ZMJ7fsVL1g9EdU*JyfD96fDmTrM=RPDq}x7>;( z;&QyTxLVs@f=8g0|H#GTvw?bo1-*d@@vnrVoEGKuo5G8-i{yrq`t>YAUUBk29un;U zDwT9>%EjJ$`T-mw&XWV5;G0fj_W!ISQ6yR7#!xUzIE3547l*G&wL2vmz0??R&uO9N zBYPbt2-lAfQrl6T^dTshOO#$|Ed3FOcKk&uNPTW>i86niieRMCJ9;z0?3>KtP-P~* zg@qm(R*>2Q6TNc3Y`V3Xipf$Zo8jDB-1F%KtYcOLcgu@E`y~-HlrYXTp%ctif{Ar3 z$HaUKF|0S!?LrnBs#Hlnyo4)Ce{^rE>Hq zO;Z|08=h`lB96b~aTodb#e^y9trN~mBuy{E1C&29>m1f!_i_E9w3IT5GXa_@O-LKT zkJ*lhu~ZF?i$2`E;CEYkDnEVz7H%_Wn6uB_edBg3Um7H#M&9WU|8Y3)px_qV*Q}0M zqgaqlEkMK6D_M^p{FsYUVP7J}TgC8%`K4ZyB=2_5T zmDKCTtrHwDif`2?ZXo!T+@C+&LP?;E$fQtOO-tGMdE&^+ez=vEsJ~*ce|e!vgny+*MheYmPBAJ;;Cn9gtXK}&NVMyk-ju5i`b8%GsZ8@#wDs1uF7ch_4UlLtgeb%W(g9EEcXxDTYkkJ!ITHrXQf;Q z_&x+VLGltq9lUp zWqz9|M(DH}!Ym=JF(HJGC8ioy6+i7sYg>XAtkbR}mzLJn7~{7?TTP;m32n~NZqH4r zExFK*yAGnh<4E!`v?{ag5shk-^lg>O6n<9O)}%_qKGvt$HA1+5e?e&^8TVNK7Vb8$ zY%PSvxMx2G6gR)@Gz3`(h712SM+~OOP+{c(9DsOk12|9EY}PzmQahev>d(B|lA|Ho z|3PCgeRdsFthOpre09!TGEb9ZYY)g4)dfb!9LIXMaG_;Vj@8(}OU7;cSr1iu92zEu zmL%q{Br)spG&){L0Hm=XRZ9kREpSD3xcpfi zZA&H0h!q`MKhMG9a|FtwmbBmRyZa6~*{43A?O_K}g888J*zHaa)}0S$eSF6=*2cX< zbdUawQgol*VL6tV!i_%f+~Jt{Q2^D|G3xB69XU$W8ZF_*$RO1md6n4wd!dkLT9S-l zhl0K)cDb=H1nsHkwdKa|%K82;s2M2ca-`cYROb8|Fz(|P&g{!U%x~>Jpp6eOqE9$1 zKA)n_FAb#~a~bi+daJDhJv=V-n_w^dxZzTNaPoeNiBzFh?vSHT$YT2lB+TAkoGNyKF*B(99Rt0LiBHp@hs@wHje{Cn(8qxE`s+b zU)Xj5Oco&#K~?YC&v)zqh*}qOpe8nkt5xez(){P|m7}82Ph>7lmYMhi&Y;KMSFamI zNN{>(Y=5{j`=upsO94WD`RINse; zHN(T`1Ld;B59-69cU7+OYa^5#E#KHkjiH?ins%VqZakjv9wZ$uw-f4{9&em84o{UH5mqyON= z2O+y1{j(W9YpKi&M!V&X3($_(Tp8D}=Hv1G<6`-GF8wv8Svtrm{ble_l2ba8SfkMZ zia{EdA*V@Q&cnCrCr*WfeqTZWrl#Pco-YKq&t*eapMkcIMjNh_`p+pWT4j>B@o|~P z)9_aJ4agY6B|hV|*VRm(%pos{ih_>owaiPNcJwakZnu6Jlnfz@sVX__dR9o{u%9^L zS^l&B7D1hNiu0ri__*5+n*XQ*L_Pj~OkOJ&=*QFOIanVxpGUdAKAes10cz9j6E|ir zj=;vOzy|%qR%KN^KgnQ!k3Yb43MQbXn@BxIC){oTH>EDbNdVcg+qnbb{i*(_kB8vk z2Dbb0==`006hH*0zV?a!HwM3kYKMC8qmA^tOKOnhlYCuthH@7|tSV6`rNOSmdNl(d z&Jf{szv?8TpwFT0qCBl7>mC!LWzTo7OJP_3C`YS!MN*V)kB|yPWkoAMJB&a(JoG^$ z8GqnNOE)7?4#tKR8i|tI`ISlR_dOzy9ctwSlJ(L4rq;4XBKK%$+Sy&cuu=E0ZO8LD ziT3+54%mywtn)}f<5@KTFKLHNL%~K>(*yf@mHC(mx>)5+v)8l4%{ZXZsr4lftl7C^B2F+>TBn$8A!AV7pBI13qq>W|+eK22JHL zt$ipObQwBu}4$)ur%Ecpmd%`X&Cscj*@w zZCTuqMZ3R5h_7R?%As6V>7*;T8sU|l76KO~g!9ITSte+LIh&2w1R#3z;>u3} z-BaC+mSF0cYQtdym;L>!68~CiYc2fgVvULqq^Gj+>~t`YY$<5Fu4k%&`IVj0A5Byc2GH0@xnP@&}fmOCy5pQ{N5ZbvK!^PIc87%hLj#U!w zaLWrS_;n6pFH_z<0k7s#kR!c9vV!B7jk@F?bv-4>Qu{gS-7qg%PIHj1MAOzq3A6#| zxu*VcZ0Tm!yON3}>yiEvgED_VRcBxjZrFoNMo>df z8J~nq`Gao2IXqR^--83xbo+ss^L`o|#7u8aCtuX$8!}p8c&Qd8eb`Zvfn6Xeq`ogLiR{69lzY$EfER3;u^IgNtgL@oz&{#WpEuiBkLF&yN@w8_N!! z1AKr*2eJItxaks~+sySzww>=MQ|qXl#NyET!N|Eq*w^1V;{Xh!6onKmXx{{?d=@EOwczL1fH2XP4+sTvq=c%Zg>+4qn?<-7O28CIFkQAC;U!h% zNsSoQ0ofD7pTB`oM$%1g?YE$A-jAIu z2oQP-W6TBu@JVCLaV4hNm8yPVh)oHDV)l={g6x-ip(VVt8akxpyX&!3h_2$Qo9QM; zmt!05hfY^rU=XNBP*`D8=0{5RrLd`sptAG_A(mNxDOQ2B8f zVqLvESj?F3tJJHTo8k!?%E=Ix3)lo6nbJ|6=065=9ZL4sZBA9Wbw))8P4h|anIs>7 zi-s63%oBK&%FkcsWB$rS1^qBU6Zi^8;#i;yk(8wY2;k^Q+ALxd%wBXom6D{E)v!O1 z-)E|EA%s&-JqLpx;h%ALW~3MhwqP=@4a;yoFV!#_Rw4mNkh8XNcl~=na&?abxH>j25U{PuGEa7x2@s-fmNY-x8Hx^vG4%Wd6S8ys{`knF5sujn8?wzMr|0b(Ky<%W?E3 z5?~G1&*y8Gv0*ylE>R40QcQj6@q>Z62Ou{RJw#inN!6rP<_<*=#|An0`SJ)LTaJZW z(^d0lfFG;o(&T&9KL`CP@hZa-Cf*$$VK_l4dGm#!AO-c!+ZgX7Qc#(6kNV!7KD@`< z$0M?j{9f#_>H-^$;sJC#=3sQPNtpXq32X$ zl1FWBrNGm~4QsFaXC`R4WL~tC@8%0C^TTvcC&^bO&41fpl##NY`i zCE;O)UHj|DkpFu9V0$l*)q$-+D5oZ=_QE%O5vt{S`UcVSJGPCS{%Te~bSqynqkp)f z+9vGx9tGv=k6;etm8T6^y(X;g=Yw6z#mcBkJc(Ul+)Lqep4}0QK#5+y>un1h_?tf% z<<;&+|GaOD&tGw8C}$*5EUGn1p_eMyXzIy4#N&7@{MeYAt<{p`fsE zR|sM}N$#slNJ_esanFAMy~0e^_%lpqC+t2}r@K4X^rcj^Fh{iuVdvYC@?9U&s8HsBgTSvWo zRvWfH*r^hv`U`UkqLcxz6k2yt5yrb7i{C_;I-M+%&x4GfCF8c|SIQrka28rV@VQJ8 z45&i2Y`)F&QA0+QnRXUh;*v~J^aVd3rwnEOo~I14l;TE>#0VR)j?9TEw_*VMR7{9Y zJ6aOwG36pj3sYTTfeWTB(XYoKIEGWad*hetW|;8# z!itx&rOK`vxzFx;6QlCnFZ{rm`U0|DkLN zA@-cO_hu%!ufZhT=jHoHw|>9%AA2|>=AF9%_8`_G-ZmTgoAn!XzSQ5zL+mc@y+hZY zsIpXh&EKVm^L2o*<$231QpbC_QX^YpXT!|C>ZDsrVZ!{kjcn=}+H7~f-$~ViV@ouD z^5e6`CcDjk`L1F>>t_r;)$YilfaNPi5-&1qFO(F5-!8)Qk<@)q{2l6_Kg|9hme_`-m+g=Y z$__cI1HNLp?NVE(@sr8Ni6s3A3&uUK$YgVKVok7k)!P`^f!t#v-Nd9VA0GaW#ll@E z;G@dq0LSPVUH~*ZU&kD0-}*?7*>dyx#hpiw(tEwqGi+jR2q-4u{yMSOFb&DC&wLja z^jk~^)EqCAO~ggPbqt?VFyC|V|Du}JL;G1-)7sX9(@)sB;5LWB~?!iHDm9V1WU zo{X3`ygk`l1=WF~Crw&fIaQhn=0E>Q@wjVN%*dem_YV%R{*G3R^~ON^bUW?Qk)@A; z9J_C9pg%KyE}7h?rz1oIKjYd$tbnrmjO-0Un>&u@tVnuXj@>#?`;5H zJbrSG9P?($twr1(hSdd#VvI|9wD)OFIVc$N_ptU71yOk3>F zvVWgzL)z>3+cr8k7GzCk$!TfyMeijV`RjcN^WqFf)&F#@P%psVmCpQ26G>ku!A=;x zN?}50vVf;!3o>SapHFxxHiTR>W52gAxOD>(u8BO0y(LL4xj;^6oKNm6T>e^t9Lw@N zgn3J0nk<-ehCU|4K)s)SeAkquTo~fZM56G;Hnothn*7f2%qQ5wd23u_|p(egW85%QDOJF zY5;eK$|o%MZ|7Tb@TALedf^a0NCA8Y&+>e$yuefQl0U3+da8Z6diG_gC7j%9TvQV= z3D0*dpOH29ye;u7UZu$i?%?Ar)({c1Y_ojvUd?g>{H``zu#Mk6oQZ@o2~E{0vT z3_1oZGV$z=!ygS9P+VmYn>-mx)x*3WRhL?W2&;BN%f>rxSpWFsGML|uya-XcK2&a1So0(~!a5`^ma+sVRtgmLpTiFe#J_cml!t7bV|HS=d!vWnhl&dQ>>VD6S zx-UWY7^PB|@YN0F$A5(5fPiaOjQ<%s!|+kbUY_vLw0$%94RXT--(Y+Z%x)qP!ryzX zycr&{oV;}e1}-u<--E&Z&nNqEw+)#@?$L<5QL#)0?=?^dw-fC9Y==K0e^Ibl!FDQS z+FiGjl$$JCA+BTOK!TFr`sR)51; zUjnydm!(e!(B0?-$qO&^617XENPEjz$7R8<@W-Mtvk*!SMam+{=gD0~r{;}6*N-3N z{`OnJ*nghlzu@ytcikdM>TiVq$43wJc6M44AtIIkK<($`)WsV5tr%2Os zqRr<7Z)$`zaPoB;8*(&jN~c2w_CPiq2MY2$?NcQN{?D~)%gZvJ9V&{9t#LFpEpMH;^9ed^oksM`;9*rvFz6d$gBF$ z2sL%7mGjrK*t`wsRExhNi~N1I=41J0L>yKYL~L&ii>1wq_c7Q!4!GP;9kPq?X_RB} zp3268zv6TU>8lcz=Vb?J$s&tprA1s>%kxue&FW&asq!4NvHUo*iQ?42(9m&(fM*Fj z&V_Mo=FyYYw&~yCN~MMp{^-?!TK;W;^p%)Il+B&A{zJq#7|=fA9G9$kGumSmqhxTJ|}T*x0~0R>;eRfLIc9?eX1}WY^pc;qEThl!Tv-n!~qc~e;?G1z2l<|Zi=Mb znmPh}e2jqN#W4^k-F(LIUeJ;)CTL*0pK75de5EOZ;g<6;>$q*P$;AWgfR^NOY9MMRl_+_)$zggXc|EP0x!%^0RLbc56VSE4$2kG0fb}JOG347heQc zKf)oWH+A!*I>p;%H1vGza8o4Xb^zH1D}D(V*%{qUk(S%U0v^0J2Wzj!KfUDdDgv4k zqIOolq5ZEl{PVbV$b<;<$jDld*U#VHhCGehfpHg z8Qubk9Sz8!{F!W!4TsAQ-Tn!~F-_h5)!rl~X`qE6pC*DS$H$i?K&zauDrkJ&b?U4g zBtVDfmOh#OcOI>-cgqTX%$H9U61hm(0SP*t&EB#^9hTf%K>ofS*ZJU;C)%z1HmDME z=nXAE25UaxtQjguk!ABV`TnnO@_Tb2Uyy0w$PeDd-X@z^C^&0c6b%>3;0c(=qBz*GmED%FTh*}@3e7WhXDrtlu#o;4VPJu>mG_0G9h@HZ^?OQW| zOLtvuj9Ez=QQPBWCFKIp#fDK8@{N@j72__*t+FU{J*;? zw#GO}5IKX&?1jcs>6?h?j$v@#m=MDAEdfFo6y`K%*)?0-PMd4bWBB$^aNIW=$R|kn z-%coCFX9XTH z#o1(}>9MYNJ=_}sS|}eiC?|dnjw{IVMOoaO*`BSt@!gD+g`Au`j`0+*v!NS;ut@jI z8G|>dQc8;~BbQ{{NRdcbkw*nMrtv5j7Al5e*H)T(~EtGIaz`_PlB6DJbVz zeR)3qX|tQ~>us^KYVa*GY@Q1K=Oz9t8B&wG)|AL!?jx@95(aZ=&A?d?YT5FC^qm%ohN{-SpTl!6>}ZuT@%|`ZHJ{Uu> zst>1>;&#hN%d7Idy8H@&6DcPMIV4qnL%K~QN10egJkHw=M~LBFa$+8*{A_OHk@b?| zr?beLe#NP&k$9~uie_u^E0E!sv2Q5>*GGc%v+_r}##wxopkFdWfA<`rbF~*~K$W30 zGkkLb4-8@js6k^L+xfPA8UO#|DTTQlm2N-^c2uCh6MjqHho@A2;yTn;0vM_PSlF-| z7={5h9Cpj%#|`8_Qs;WlYH4p08;9utSzX6HYu~)#If7JqOrfM`@|HQn$`=$Pb^%(K zd`p|P0R~ind3!j+4%7&f65)gm5SQ=QJqbI9Qhj+E#r;+&$ZP#dowlZGmYQfAZBPuq z@SiXFe2JT&m@M}tE$4qKwBI#;mJp+9VSNHlW z7?_($cPyRa>_}eqXMQ6fW3k2Kc-|9o?R)kM*uyfQJL6@cjzoZ#6fA=Ks=3iEVBl4v z3bRBM&V0|Stq8T&xaYa&i`)(T9Grt)jZW6|r%6B~eP5gC(;HNT>yJcu4oK^XA9w=K zTg1i@LA^LiO=<8u96Jun$D~y+NAsmkLMDufAqnw+*A3S0uCAVgQ>^zOHErFjSqWxhkWGDz_&3`uY|M7o^$TFLwU7M zBK~4ZaKHdj-a&{c=>lqseOxPBp$xEsJa0iatyKm?Bs4d2IIf+Hvu za-|I*wY14z%___DA6|pR78AwuNm7Z4P3cV(b$KjzCQs(!ph=!uazi%hz{EII<~j~z zDxz5~9Ut^cbAH-lH2=k9x%ooKYYwAWj0Um%`|$t?>_3I z`UQ%4k#0M$N_S!X`*?Ut>BAs(`bKuaX;M@W^Zg9pj;)eutsKAZ$!DfUJ<^LziaPHQ zOC|axdC1p%xo##nOS=u8=zZ%We=oba2mw!M8;tHLTFj3tKh>`y7Yp?d*WmZ%8Y!)~ z(5-$<3Y#oA;_Kb}*J2h&VXgg64OS@8N`z%gKS7DWtMA`JoDa>7I@I%?evH_-ST+Xu zYJ3^EjMz(|A2h3f^s)F0f5OJUcpt78T^41~d2w)lG2ytkgzoHaG%kc!H9%Mc30F*q zZfd$*PXWer!4+$Cg+v^*VDWQAvdCl5^y&vqa?9Nn>?t*A|erDW$!^y%c$xKVbh8 z_xg6BeM7qTzeSQG%qL6@ZD;YOlFV&d*VF*pE4;WPI_!1I`O)3C@S^Z5rSrZx2DuP5 zi1$tCt#bd!`gM(!%D6^;cktiqP?nPB zR<79%yW2+@X!Bjcl9Xkbty50mZZIAO+AtPlKfK^nbBfjD1AJqcbEGufN+P+0Yv|B zg0x!G0e<&qao*m7!VYL|+f7kZWJblEcPFRH4na*Y%~#EPKbkhkcCMdfs&1ZNH8j6>;Bt`+kKku9chui{vCV>40=Y1L`&da8Yp{5{u zFzp5Z^Kvtsa=p|~-g7nj_!kO2Z|*NVTc2&HD;vp4p~C5lw-|iqTb6a-L_VHb^J@}! zjX~SZz2@uK6KXTn8GB*)OsN-`Gq>|1CXA$p!I7B++L|Kmy2S;ifO{f+QX_F=I{xQD z=Y#V8F}RbLhn9-|Su}`(k>V5P04bmx%v%t-V@S|LKKMN zT*k@y^T=>fI$fRVCaSQj^C4$%yR*^5yjXHZEOtm|+wmX+pBP3eU18C)qqoQY@UuSz zBRC%TeXa#?ZQMHA8xR#Vy*}ShpibGSRunlEu^4nQqq^UwYYy%%oD&R6{ptNVu3q9V zL1#nj4$uC0c%_T~9<;`2wVSE1x)q0m{oU1B(-kLh3rCje=}aKe;~Nr818Kx6Uuis> z-d0~F3BP?-UBNvK10`WJ{}xMd!e)b0uq8~$RX%~$ad<+eSi8nv@oom);fl&9aJV`Y z?0pebDifLqA|N4i!TGED2bKGYR}P@Eza{1}Q-LEIPVSl? zaR5_@KBN}jPNg6&*MxZ?+h)IVVAudIzNUe~q8H(Z{)fb)s0nF$9> z&duQJ(G@zCNKu~8SKDTdJ}|;=C0CgG)EsQ2A5Ut61h1~l>=SX`w}{%qmNTj(KOM1q z8FJaOAY#Y8|DtN#Bj_(X!>-UwFhQxiTAIN3QNP!CEl* zYC?8_d)@{*8YY`n!FLKxemdGdN+55X!*|+Njq{Rp1#OOAL}{95WXOmHYB6zGAQXYe zxuHDZ{UsNh03B-8)+hN-0l}iyM|TEZ`M9!;kLkL(FE}cu2@cb}FI^qZ`RKQLUk8Rb zkO{o9g&&GO47FIW?kAKX6@d}uPoUP}b zB&;BqD}o!C?7a`(h?kcoa0F@`caU5j!RkMQd|nISpEvj-a%QPgWvv5J!v?DHK>;x- z7W#D8tDNt)`xp`3F@No3iPB-yXKM+%^v~Isgv%#fVA} z8%?q??u4M;!q*GyzPvV-LgCz=^*XciU*a64B%ggX{5W0HKJm%TAr&(CY$%=!17me2 zcE$UL;coG?qaFb=k)(ECi2V#6oB^*dCiLM{vn*z><0OH#G?~fu$=;O7O)jrCOkT5* zTP1hCw+5mMyCyeZ`T$mqi$N*d=Sa zGQ!!p%R^pHt4Xf9#J`{L-#n48rQ)^LI|00X+D!L#Op+@gooh526t_JoO6_)0`V!nC!}1O12J10afp^>bBQf+*MOdX#!kr3jD1Vd%f zoHEVupl>qI%pA%4Dq>0qFAhq*@H=On5K!(#cXgQ2FVW7wbaWuSnGdCC&@eg10_-{S}=HsJR-Nwul zq6PV)TJ0Zl5Kw(VdCgD9(GI_pGAI+1JYp1A=}uK(9UaCM_NzQlrIc0h$v25+XOgZWY4b%zyv0rjDpV_WdxP4`$W*YN<+IRyZc0C(3rsk zDJ=yJ=NpIw3{Yi$;IqtQp|>)?7k3KPw{;Z;U`pp9{HPgsN-TQ?8Q}}6Vr}j08bYXJMv9EE5 z=+58q-cxi~RY{V1c6<5z>Sqv24JeQMZoDOIHr?FDHirqaD;Mjx7{4eD*88>)CF&Hw zf7@9i>}RX&3BAup&;Nbg(L*Sda99xPHOO4C6Sl-uq#Qi=*v{^ak@+K{Egg1$Z0tTu z$vNBonqJmuJ`}W0Z&21YD>><`kAinHMTJpEI*Ifl*R9$T z=lSS8GODfiXm_%Xhv5TZq70VtAZu$*h0>Du7vw{HdXR5H-Gr%YB{RK4l&0O_(Fvqy>~m!Qtw z>{ZF1cW5nrcc1vMpzShvqUsCJegy!T#unwKgxkq17=hV2dIVj>XCaw=(xs?jNzpwnxr>-fb?~r46N~FCK^nhL+ zJ|YozD|ZE+qW&OElGqAgw0oSV7>8o6JH+vn5rza0i_ zTn`qWL9|$NkPp-2=-B%=U}b4shqiH=_RAm}+!mS-SZP;*)=S^lceyzxux)>QYmqjI zbnUsg6`~61Ze9eF3fKW{xUVYijkwr32zi*aU^t4LEaJ($O64+-Jpl+y_-s!c&2uqE zrVp4Jtc-cma>A9FDIq-=0(&6RF$H*}Uu5J1DWW=Hfl)W0G|w(a)#XZQ%A5y5q0qwJ zcC9C4L`BrYCwueg+Szq`=fKu)}%jKIzHy!Ebu?vwBMWk z20XuOh;bdvZ+HiSdT?<>Wl2Q%%3IhKf1-Ctgin|;8kKC`S(9Wm_LJM0Gty02TyG4c zGDS?X9}{j{tbWcMY4`g^47iC> zLL}20DI45bT{8YF*ED$8C%%Tp9K{O1guig<69|ug=#Snp(%3MomF;bO42Ci~*HNzh z$pT;-iD()jM=~TaQ4LB~OPag{LAJpke_`Y-KOk>VZGL2`hQ9%+WmVao>MFgV)cfjq z8JD0r;lmZn+gIcX{c@Cs$knj6gPd&hfR`b?1M6GF0I{@hg{_xC$TBXp~%tL?)3 zR$9yi;Zjhg-z&U1wDH*PbFgjdu>iR21{-PLvmUP#`^pwhh<8NNffWov4Tc;gff`V; zi|Z;$D)00R(uteo@?HJ0$D3-F$<6rv=kP!82vk}!-*PwbqCS?H(?)-~1=JO{d*AF{ z1!{YOfDWK%_$uMUx%E|oWtWFBLZaB826L#;ry%Ml|9B2!Y-oB7K4_v*bK!PbquA<9 zmTf`QJ2fgcK!R69Lc@>3_{SORVO8u``_UeHzQoQG3aoWpO)8X+L3eOaDY3DbhJ3CvTSt+a^A!K!f+#W$_#OQz$v_ z#JKb(YH0a*KmZXZKro-|_RorsQQ?!7+0YD};7uHvrc3S-s^uBx$R(cNQZDe;n)1z! zY(vM5qo33lE`6ktYm%G6u%w6(XA%c)lyo5kH=pI}b6eirtcU(SM7h$%1*J1WK!}o& zRzl|tq*^b}BPxHS8G|eMz?CQ)nogH67pElcVb@@63f(Hu0Z26Wu}J$PVIme6d&uTJ z2Z7oS+%UjA(rF6SquugWzUEty3OG=I4b+ltcRSMS7xP|IgodC zNWs_atX0-++G=3@04Xi{T_(A8e=@fTppYE6`hP8LN)F`H4qM|2O{S;FE?o=W6aK-h zKf(Z~;l2p$Z}RuFkN5Y;`m{>>Gb($D7_2-NO!;M6x(OeG?PQe%(mbp+ohQin- z&XwM%J6G3xBgh5L7=GU1LUfQB3Nj@}{JNHS@$(?9L(&gfnD5_G$;4zLeTN0DQ-t$| z9^n-+ZdHlWEzs;ZL!CnwE;Y6n_LrC4EWjg|w-M=rPYSbTd8rUeyr0lYQ(BWmo)%n;64wZa~uR#{=~ES=d`9bskGV^TcyT zARz&myB~5c}G9L$2A&HdnqxSL zCKRVtsu~e7uCW91wsh&bD-`Lke)TH4KBL$cg|eYJFCm34^wUj~WMcRcp|CL11-_st zc|{u_Sl%M6N@X|F>G>wi7}7*JnMsb}+4dyT={w$1ttjc*yvDsE#&k+uRZ_>wkSo?m zqjkF>!pPUB0@C|S+t_E*qqv=ouX3xNke+=3v2z}(%2IlKsC=i=o}}0-hDS_tE2NC_ z8gis7jd~sx$4++puTS}8sxN$$WB(}y3Y71T?jP%RI&+)s7@1q^@Q@tw=c+6Krp8kr z*oYF7lxTcOWyCTFSJxDh$4sTbqURBI+fO_In94Ceu2Y!#HxVd->=Ggt&Wbdhnr9Y_ zDq0=W_L#(ac@2;jl@2+yLyCde0S_?KP_C$anJM-4{3|tbc=d>4m)C#T`rjPFIx*9G zCx+wze9_|weJ~k1AuBWj<)yQ6WT{tEea}yhfMf|}Z}&&q`1i3Y)LX4pGehC~usLj(sEY~|wQTdUE*m#;P;Gh?APk9uVICnpz1tBnN# z0Gh49^SXCM(;broPfrgvu2~5H8qwd3KIPu6@6L^6)P<2aR_6dQ@?wzk6;9~mAG=s2g{O@&F|0-_^t$bPaPlXDPWgh8 z)5Z=Vo2u1qe99D_`QpXHm$o*h2_Hns@H$Y3y<(opG<{cDrwPal$+`HDZe{kN(9_Ip z+$i;bo&lGHhvhkH9%2ad~Wm8&4@>oXuJoMy|-hZ7YL!ekMYVf$%4Dy+5*KEBD!^vY2l$ds~P6xEaQ{mi?QYnFm&&axl8&-h|rovOEB znd8scGM`P{^UtJUzMi!(>{XE#dCRIokEN}|z>VB(>-+VEv6A(YCkg+QTyB*8GmWMq ze@}JPMd#ff(Cx5o0o`jS15hpY>qIQ1`#!{q-MRJMfd=zJ=L$^Bo8mY9Lb}l(G0OVm z3jOy^!AEf!j=rsNU-gDOpOm1`13%l~7aXGl)?jVa06~pF?`?@`wB@H60Cg%hds|7A zt5o$qP@c{@Av*yyzoIrUI@D4B?nZVUpr}Rw(fbqdAOya8J$bZFGgnwtwh)~Y=@lK- zusZ_uS&=z~7iKye7Qv^vIi4r$6BjVY4Y%*8g=drI8I|9Hs)ZG=N;cWo?3Cx6HnFkM z4h%A-{&jqciEXRxpJ_JrUL0|m0LL&zx2|(jp=KF9dQi7ytXKz*@{ppWaBMx)a|4uw z3tLc$Gvvv)pW|cvTmS$zeW{keQGk{&QGHzn&DmCmu}uGDcWJ$AO6{Z7&~@M?GImC; zR{1vcSEdME=@<=RPc|gPZ03Odd!FTw7o2O~8K33BAH|D3~_(6Lh6utHPVt8Y73i~F19^uFi_wN;m#=ImAMF{g##p{N>5=OU-41;_oE zE1)U@I=*(=dr`kD5~Vs`Gq#gw;z!PbS3Jbz)|m=7I4|squv;m$&2`*6(kdl$9hJ8- zka9TZA*~l>-)$|08#^jQM@BCpFZG<#mo1XM91b_0V;|VEP8LPVofS($9S_MnJ{X}; zyr6yZr_>^T)n;e*#sxbHh^%x#T*y9B&(0;@`P1k?1# ztLP0X0<>?^CLjkn8+dm>;8L*>&^hmw+z{-Sl&n)&0H--CT^@+?e8M2YiaMd-w$mB|K;VEw5cTm{dxZGg>P&g)z zRwlVADf#Au@EYa77NOBWl*$xrWm`0z>%(8ZAGl&Xco*vK8m2YwUq@2PxG^tCZs$?7 z^7gGorDD3!Njn&HwC@S#Nv#2SVujCTW4t|Bx$ej=W7j3oI$tWFy^vF&ai84u0={IC z8NfM}Jza|CD|B}Z{e2aCDhZmCFUhw4WvlHp%DpScVD?A6c zT?AR~ZoeCkdC2E53}XQy6F6md3(_*GVBTH=|9U~00>Cm4B>O_T?he_i2&{mFiSGTa znW=*3RbRNwzd{v2?CeN^Cg=OcG7GvhnH%ur!?}7Y3RlpX8Co(dTfc9o{fk*hk`a5z zL^{N#N~IKdA3Dm#rs`QozV@9RQZ+SkP_BgeB&ga-D<~LqGGQiTY+QNjrGR#bm$LQI z=S^;@nz<+07cYhXX}4)giZyjoO&bJ}1qk-lI;SJanX#3<=EE3>sO(D1)erlL0&h7Z>iK3UsZFvqL@Hq)ctb{Bt_6gvdcVV`$&E)}6b{Y|P zVrymuCBUuieAf<~j1Ci{r&iMs$XYnkVn zQ<>YrOEAqYRR_#%>?d%|yC?%9GjFPeY5Kz6Ny&+z-_lF4U|V(WwrKX-?!BnHjgQts z$ZM^|I`kumtyQzkq~@(wrR8de3SJDGGL2rUVjK`oeatqGEC`3$fgi^U!gMVr_5?kS zi<5xjc>$qgrKwZ{It$Ab<-qEs62t|g)aV@G1nJpC`{1$MCp0Sgs`v31we1v5$7}G3 ze?ezT-$8J)-PWOTm3uj9rh^%GMBo}Z6HFFWA3VriA&MzEZhY(SV{)Q<*NT#U^1VQC zSQzHVs=o`7b8PG>)O3m!+D};*efK>hPCPmMNKS+6HxuVW0rfN*kMNT zX6Xu=v2sZ1=UxM+tERM)j?lMjMuKCSiXnld2sv#0I1mm+)K+>65ijr%1f|DQj77g1Dw_RzH$wJB8%l#ZI( z%J|%$=~lX#&<=&<5qipgk%Q{<>|Y8|H#QkZmxk zC1mv*WaAs;M|70^2s%CTBc9(V?J4*+sp;@*(`WUm_64>0^`7ne;Wm{^aP~#xZkPji zd8HXcuOc5@dwuM)aHrJW$P1Jb)LAeIU~!AH+I7!_;We>oUWI_;8j*`4PhWAF={W3t zT3SXN@sH{MQ4IZbR@t*4p^psqJ#?g@6X^c{(=@{7G4ek4O`=A{+rNBCY1J4AXAAcnz^mT;VBztR;+^yzx7i=x(|Orw)p)=YiS)xfNs+j5o8s%VSAYIR`Y51Nt`N z%y@a9#OlFcZk_;A9CM1N3jg$B-L%uCklvpz^}l+!5M-Avqk&p)5^*Fts*Swh?)%d5G}a&jf)OB@{DYlIV;*8Y2D1VK_z#^p_iM<6@hfk5$M+%PZD0x< zpW(vk!TRg^(PieM(7ncu(q09?y7*U$FM~TxDh){~L%wp;vBQv%TmCt=p-Kt0c`u5A}c48%-wKqa!ry$dXR0=<4O-N3EM z6sFe2(gO$4;e&_yfG-*@ru~*WIqiKNeMBebR zIO7dks2AzsXQ=sf9AJ!l1ZeR($1T8qBK|vwdoQ(u(_2uPlMhyhJ2A)vM(#rk*!mdP zLHxsR&$BN;gfe}{1VtB^ova07(E=V$A?7gNX_u%IfdkPD2RPw5&ki>o!hqTHLhXI5 z7ntEnYNwL0gfVviaJ{Xs9gHza21$mkToz-@8r;0w;A3)tVW0lD=q5AaK4 zEQe9WLi6{-{`-YF{WYrVhNJ;l_*|5)#B%*zf=%rhCt#Vzf(M+c&2gq=qsG}Y-o|g! z$N1g}*(6f4Ibg>VRytEZra_{5H~u+@@#}o@7~c&+T}H%DjrK*jP${*YJd>MW^Ui+1 z?c8+&#sak6cGfV&g{MzejO$w5#%Ijx4v|26TB!qaq?|b5E+93Dx-ca^3~UDuaFN*M zVgW5i;Bb0PLuz;;@7~=5@<>tuYmyA^1Ie*;CHo$>>j*`O(EWg6u>DfN^oC~r!v%1B z3YVP@&Vj)m=F$;F^REJ62?CsUz40u|phshmJy`2#k;7*Et_W}{)h{CcpZ)OHU#n3l zU*=L3N{~zOGxLXV!Pjsd;b@#ayWL}S{>C@5WR|YEbd{>;9(wA}*1KjHk08Np6mWes zP*sM&AqWTs$BlJT^WS&XH{g&A$CY)$OBMql`=onSrWxYL z@}CV%GVUJ2!6=q=`m)C%NghDVkz*<(YsA+0{$a_}cJnrl4+!sI%hQwxAy(MJ`_V1- zIhh#lw+%&Ge)Y61-19TZpb~v>f6yuI(bpV&`tcDlnllJmuZ@d>c~;s6f0So;$GthP zIMe-2^xt3p-`8HqEdiJyQlgbhD6PlGdnrkQM)RRrdHwV{^Bhgn%G_HP9&!y}#ZSlJ zBdzNrMzKUvY8OmRA{!t!?I}YX$-b5d#K?25yg~JvOmK|ztdA9)aIV4Kxf-s(*7o!* z#7`fR-pY~?O6>X)(D2SC;pPHg2MM+)rC-RL&0Srtz@9_gzZhT(HoXHiQjPOP&mEa_ z=&WNgNLTB{pb!yS#7zL>Id~>`2iPVvH$j{KCAf;B`g>kCC_O2sxn22kv`21_;y;cg zO3;!2f$qXM`|yDB@(Yl)K2=Uf$A~`#={F<2Gqsr1tm>_kEk}uhblZ`t`9TVT zT;hlYy{(8WlEB8J?Kv9)HeJ%d^wP?3M1h(HGg+38?U+}{-D>&}Hwv7$ugZZVmPccY zy}|_D8R5%%nPU3rB1+`6OBf5<4$>Ux@HWdOz}M3Urk9xkZQQD{?*ef4Sc7ZtM>QcT zlMZgPL9p8xtm`&;)T<>glesN00tHhbzkLLMzo?&^%m9T$8^av1EEcfNElmFF%;G5X zwr@h5{9n9C_a}CGwvqUkyJJKt&IWLX15_X%9Wb4C%dVI-n|1WN$s^jky~giF`>0iV zGepl;hZAw`uXn9O$c74R=Uc4>G@xt+u(NBY>s6MpuNg~nl4#|az5i+{{u{p-rig*k z9u7u|uHL}eH0e$n!7H(T8$hU#!n4U5=2UV4v)}*q0cwK94pq49vCl%j+(GUG^BoDF z=+u4v*5cnEiZ`*%cSdxWpTcMNp7Wh@?~BP^<4W`4j*)ydrpFA*h1&dO08!<9W@Sst zj^xP23E-%4b#m|g`j$pI!gjcY+R5{czrj6=q4y7SsTnBF<_zdR91<%?VNM~T|EE2- z6dws^qqb}NeB4-hlom?TKhV7woC8i0cjo7qZ~TAXK#*|JWgs|F8qGZSoOsRN#-3_GA^f}=T|ME&|9riFAU!NW421Q|??@~-IvDw_#}Y1;f*vA(#! z=g!xckgbsid)DA*Cif9g9kMDE0TDb`gHEyVe;|x#F$}!#;DYAtI$J7%Qs>3dyOmpz zyH||nwDP1pd>;F)gJI$M*7&^Vv2(Mh!2} z#A_g|1b~442vVz^-JoDnW50xP*d`wZ=q%GLg!dlY;(7Ddb^&JP1x9Qh5FIE=cD44Y z=;^44_EJCqivLH+QLjtl-hu!2RLWBFfo+fv%=YH_8_%!kr33-A31of^(+3WoB3bk> zrI5pTFudNaNMt zT~V4dR<7~ozSMM2WT|rAo(<`E-Eat^?hKS>J{8vX#3?Gh*C;W}2BX=n2|)xR`%gi7 z3$;e!9SH`L`>nv7fnT5%w6xj>;;OC*(*NtK@%JK#CQVti@xr+Z%}Jy2JGHAwu{=uM zELg9*;B>UX8uM}e=Sd^>@`jhw>RrT)BH>NMm>HL^PRQ@cObQ5AOe%hZD~mJ!+oof=?TA_|Abu3Q_}Rm`!Ve z-?|Kg*9Ob8N!W7Y0U`#&loT|D8e(|YrVoj6h8p(Ew;zTRn7{d(Q1LHJ4>})^Y9J9^ z>t(Lf-1Xa)KTL>L~0w}(Fl6Uq7k3Kd>q zw8>8bXL1Udi9QPR-v)fwemM!dVZeRO(kuqHhENgDQI z_xC}I*#=EQM{Lb(1>)f<8$I4MoERW?Bymj{*mMKe?PZ|SbBb4M-}r}D|DO-s0pSBX z3q3^tDABUax615!xJ`^Hp?a5mJukRBah{WO6LZoYKKcXKqvcS(2)!N1y1x4W26D`k zfX!);GZ8%CnK!IDldAcu-EUZ)kLkw~A3FQHr~>7h(#$?R=#86%5wfKhkUR%@-ibjN zeW~vAfrLWHBLGm6;$WVS;B&Z^PKIT^Gh1`;?IaFp4Z zegIh#H{{t4v6y!I@y17~p)CM#AArkAxJH6GV>(u`OKlR4fVa{E+CDP7NFcw)WbKSu3oS^&Ph26D<0MkcG_pWyWHDpu@ zuzS)8wgOKq29$*x%>~%rM~zh_&Mx9u}PAj%AC z7wh|SPO+%Yy!iqfXx7s^YxnY>5ZwY&!CN4hirfTM1dOXR0V^K_{1y&nTbmtWX0qrt zdRS}bhRk$`KC^NFuH_^^7Q(c_tlEyIm9#N~|GF;HMvEYG^cG5bc%0+Hdbg1FX?qPt zyum!-4vt=Hw^KJ0i$6%zSA!;+VeDoqW+(a%9-|!4xZrB5)@#HXg{jxOT!Mbm`5cgB zf9WoSqsEH^#_J=j*6|Zft--Bs00HkZCnP=zTB6w0bVh``06JS;n(WIaVcGhFIBU=~ ziKGCYb^ZZxqe=uok{EUj)4cuFDmoNrqXMW>O!h!3F^T5QwmVGE`gC|W@q%%7m4Wt| z7ZscQRS!EJUJw8w=-Bk49I2Xa5Xg({Ws#&3mygB+wzp5P{|giN$cyPFvamikz(|OA zzIeoyKFks`H*sC6sO@ReKXL+XSxRqKp_KxGrRLSMq5=Vr;|0SG{CKMX(|9nUb5i(@ zNsYlXZ4;vSJ|NI-SV*#~A0fSNs4gMlw~%L^py^oBL`F)T=2mUgdm`2^`1yO_r>;Lp z=kL}4FS=f!EW)7prG{Ua>?hy0NA+%RQ0=yEIF#=8hiV7;<@`+s)W>;eYueJ)m0Qo^ z=;!X2MkNG-B=$gQswD=N3Emb^!I>W5#-SLGeX}Fx`%h|>mJ-GIV@7GIa5oq_cNRoT z=>Ymc{Q4ac#7wtB{$On%#BTk{Z43A!e~no7s^o%R>te2DFjcP~6mf*SRp$<<{@frQ zu_?ubb_Fs~WkbPwI_*Bck{T6jD)Fbx{lR~N=+uyi9BUUab-zvHL{^&y~^Rhhd z71QmRGW5R-j8qJqYOh44+%?4pc75Cd5mIZ^i4~WaNHjy!A!{>Hwb?;pqrjuZjL8pd z>?**VYW$)v#1k0=vG7xxmIxmX1epGP&wsrzXFx<$GE$Esj1x?6 z`>h!XLDI}THB$es#rzj&|MT7Yl0K%43UR6m)92N$Puwu7Qg3Zi0(XgiQqM6V{=c7_ z3uJU9n7pDQnS4(MPoAZ=d9rzz&sJZeE1tjl9k}K#M+$B|C|xbhQ_l25gvFQ-y${42 zXvsx0CybWZ<4T33XYd3gs$CMWs7n1XKqHrm4QIl%52b{-T@uKL3W9=>RNb?IQ;4W7Lb@;uAY}N9 z!v4PCPZk4qyW8MvmQu9$b>TcotDgtNtd22Tofnt`3YVYeKOJ7_{@tGydv$9u+}gRO zlD7a84d;#!I<2er(~u?E`AFGXATZhl?j`dH&`>elU4CmkEK7NamaDviZTC1_|7aW> z#-io`z_4{E<-H?wXXwWHCy?teA`wGDkt()Osdw37TjI5IK4*=e>tX9o-xdZ3XBDI0 za>2iLC^_Ish4{Xv`XHmCDuujaoTZW?Cv7ELJFfv{w>lqlPPi!Kfu6GQb=6PRNP zz+hKS8xwZaooNr0{+1j{6~xzpW%9gQB~#IKl4?XKXBO<)u_Bud7pz;E7k}tKr-XsDx-)56g&%-VOI&Noa$=~w*FNEFo1OyERIAHcR7#(~*_XE7IVsr&EhF&01ylO7T;1W92!94a8%V0#GJEpWv1A=`oE=Dh~G@C7SW?36sal+!(*Hl zulJuS+kBO$5D?kzwj#eWkYQt-{WK6H9}u!?mFE($)B0M+|xTwgeCt2csjrbw>oBLOC zkr00zj_kM9{$2UBkjQH}u7MxoYB|vCAre$$pp#6%MvcXdwIcz)(XQ4v408tASLc{7 zn(97*6I-v&{b*~416?V9NS4wa;vlFbAgwopVu*lQBrWT5CFJp&`1D+>Y683GNQxddwP$YjsWVvUj%wrm(6 z|LbS^c@~vC7CS6mXEXg_!yJBp-pe)SKpba7T0WJj4|*umJM?=Wi&fHrpp9Az%sV`M z0sQ#eloNUD1%e^YjJtV^Ih;SDGc>@lhw!`Q0c@Yi0R$91Y-K;n)9(yFl@zlsOoJKb znYYz`Aql7s>Lug1LN)$;DT+lQaz2IdDKW5w)@b&CfFOeLB&SZg!m}y4eV0a;?j4YJd=EM$gQIYsEm{SgSso(-u%9T^E~ z(ca!Z%A{A;QUk6|c}Wa5A~0q?rPN#JFUETt$~QO%;?$iAF-gzXNAyr5$wZ2sdQab? zE0PITKFTk^8{hV>duSatH6HGugU_m6ojy?mR4pyS%Iq~)p>atBP7TvghQ*rODL!Be zU{_*yeOyE6M?Jl5DR9T7B^*~q{PDMF&f{Hk zRoF{5Q||DiE@r{=t$){&Y)G-qF~^cm%_3zB6hwG~+}*ORZ;+F3$3i1?Yhf-S;K2EC z>-%*c+5Od5)$$2y}Fp~AH;>vjb5nM^7 z_!KJoc;T>VWvS-LGpXAQ=16}a;I>E`7AR3@&%Yrb|5<;RFog32`XX(^EQ;#zEH1%D zkzCY7F`du)@SLWAStw+y{rIYZ)&X~XV9rke2@)x)GM z#O-F(5^x`e5)u+Ez!rVC=E_cc%het|C1e2aD1dr|So6_Lm%Ppd-boh#th@-Z^y`h# z^D*F0bGUG9Z&Jt6Q#57V-5w<-$T8}^)ZMAwr3s@<>sikSi2^Kuk`F$?+3)+=3;MM) zAZp;Kd>=MEOl)1yLg+_oGHS4#CLkpQy=YBinU!M?6cl^+;x3X#^D&y}6(9rEPIzd6#>WzeUK+-F#_#s$z!uYG=w*)JtS5p#t4h zl!3ZW)%=Z$=^$!D;o{};SCE)CI|r;i9iU~y_^eT=h-n~jL7uj*Ft(6E&>K*bCV(e6 z1Lww3FMvJA)8M$ZpFupBMs$Y2EYFK~j90p@l_P`O5?}Pvo9VXnf4Ccs}$MOAp6`X0?tzMu@acyV9p5zyWuz5HIid{m=II9RYdlnfvY8rEgzyyKI~@ zx#`Xnx;K^GCS;)t5ydi!J+X`?4e))<20;nOBFi2#Q8*Kefe$GFxwl{-0O?MLu#>6njc->g?V0^ zjj@gBYkeLh^k*9dP<w*Yx1H7Bm=kD_!Bi z1vWIH@AUL|x}cbi39c9Tul>c^votGCu-kOb!0X1@g>)T!Nsp`hlK-+}umg;o<_)TyqI+iB3OP_*yS=#?rzrbg9>3 z_ob`!YZ}volune$Uhf=a2JDLi^>0-=IZpkP&MzqrJvCS(EdqXS9do*OM~|x6{1(LN zp54pUF0<<$8oye|_?*wnryg6r53!|qnV z#I^A>T?c^_Mbi;;tv~_Hw#d%__PDf8>ec6*Pq2peaNxm1P$)v1b}o+Q2E^iB$}z4) z^UUkzMv>KQSV`0?tvR0#(u)3V2(;^?m#BFZ^SzD1rf6KjnS#qACyJ_q%DkdH zHSR`vY0&@ibd_OIh1;4Akyd)>ZloDHq+7Z{N*ZJaq`RcMI}~XYaA=T{25F>27`ppz z&bjyg;|DN2?ES58t@Re*=@FUEb#j0B{m(TEV7&*;0lv**_Z@!lIi3ceE9V9P;phb7P@6^pQqUP7k}hL6Yrh8xS6DK83FEvkj8zG4RggXOrtci%n8!7m!V(RybH({yuFH{GwDuzk z7?Y-ATH@)eIM?zu5IyF7w&g+j_*)_gTUGH1gCR_f^GWe7^<0nA;8cVSfk)QrQB%*W zqUaYK*ZTB+dz*@9+sREF`49iVb3H^{OTEEiZPmAz2Y1gA4+LinCVHR5ZhG{ECccpr zn@R}%0BpM4Qi95VnstI%X8!=Ofaw6YRiNJ(C>~+zVxYeNJdXY?!g_n)aY)lfNh7vf zSu|s6kC_$8MpXmt*3TOCeDPT8t=uA@J@sG(xW&{#Im(hpxo+WPvv}V9@l=9|Jkk+9Ar>75ZfKBk6SaEg^3m4bvxRwal@0j5+ zF4PL3$F;xorct39dXVs$Mbww8ljA~>R%I@{Vx`6irk4AntMRV)Ruewwa|5Y^YIc~s zg6Ve!o5-RfvIJ94`p6sNX$1w0Z7z?rjvP98W3pWBar;58;P3y!b6TLG-$Kl)33SND z3#3Dc$7(!Gb4x<$^S@?Mup#L$z2>oqTd*IHxsP8CfZsUZHBnU$8VUv{(=u3=+F!(Q zB8`9UBuyin!)|gu^T5Hi5lma2*ZrjVlCoPc?8FiqR%PK_E50*p-4tVDflriv@}2Uf z@saDLa{b&!v)#PTCOVqEUa8dY5P8%4&dPE=`zd_0U}35dxC|Uuny{&lQM(}Ji7qq; zJ0yYiUM5?n7R8q(rfzZd#kb5>UK?IyFRpySkiS2VhBPaQM%%ia8zRGHE-P_BBA%m6qB^yjnmY8$vfmNioh>fG8?wBXPulJi8!1p_{>^|Il z9&2^o-Q7n;>3%}KH_{ehyy8V!I(wgaw#GxR?pxvqV5OO3qG!YBK*sGQQJk7gRy6QU zp>Gku**dQfy!gbsTRBVQ6w!2Y&R5g&bha~So0IWZ{-xg*|PP-z*w&t zFy7rNK~7Agn-E@m?F&itP*zBHS7_W|9ya zE|k{oU@xRhbR}_L@aH!G(y0OmYUvShv$YEYDS1ZoIom=dmCQ)dff7j9bvl|Tz|&j} zr0oP(H@%6vVu#6iORuCZ$G~gt{Dhw7I)+RBqRou@G`Ilqq{7%#FQpgeshV>a^z0>) z6k%x82TQ^6f)88*C`_>;?e)svICp6(tvUrDD-<5ByzEUm2)LKNE+~KbhwJ$ss?`8yL41w;9f7HMUWcxDaROyzm%oN%tN!Ut zme2JEbKWMG;DSUNJpsfh#S1aFp zt^IB9Fk+J30d-n2ps%nSx?sIUBzQ;EIa%kh=63a#HJ>I1jw?|=(|+LS2cbfNB9+2L_q=Ez&*G^ z?3glm&*XMdERTS(@EtyCN#uqw%NQb#=;wdF(qC6!-+$`IBaGOTl}a04r4Ov)4~m8? z)(=XQU`la-0bc8-ivb&=sQv>8I^?|(ACEarYKhh4@HJ4Bz({X82v614ojU-@#rZS; z=zaGSL*F+C`+n7YGklD&Kuv309m%u*t7gYHknweI;%eYt9La2i1qK|C+0I52hrB(t zn1$-pm_DQfFu_t{JKe(^T~gd`nwI!4nhdxP+P@Nl02B(9U#a zgMMUH3TFQOMd!b!gzTaBM`uflm0!^|Kd$+cf2xNMS+}XU&_$!?DaqgU_}# z1S1q93-R?{vrD)BI@Ev&^>a!P{04U+DTqPH=fmF2|MN5>ncJ)F@jE(U)d-z}%Td!D znlow}k7Rk0%_ypU*=L_wH+XN%(mkfb?u;$8j=Ydi)JlYYVBa7V2itki`-5(=SD1)9 z)1bkW?yD9@>1(Fq8scB##6QgV0Vg<|qeIWeU&oOPHG$HPa<{L26CnDHb#?rHoUWvy z45LS(A7;K8e7j4H)4Ms6@aItIV2ypgXgW+2pUw%EYEE(0T(M4J(LmTqyV$UC6@cgH zEdd6m)+z1 z3Z@3PObj=+=L085WX~6lY{C2L9lAKRNPAG-#+r$_gd+%d>X6-Fam%cb=IVU!&$z<4 z2Z-@`@OG;RThrf%W{~{4o=k-wvLOt6ey=J%@A!(~#m&TAI z5GuvCed?UWnHrOLpXGoeQ*LPU4M*w;WxL2P$veq^`^ax*J^FNC;;_Hj@o-q9sR8I1 zA4m9S&)+*3rc+lxmqBm*&iLBrmVy~cjui36F3>D(tS5W;ad>lc6RlMS2Ff;v`+H5U zjj*NiuV9D2>t;fW1gBn8ubK}g1ZVKN&Gg|(Wkqf(lMT`ODs0IlmFZ+aPLe*CQh6BY z9%*1w2co*#6-2FCk>DU9$&@gnOPYR94h_WZ#yJ#)aNl5@vTA)9MeVSj{i{p&xKH{!5DyLZr{-P!$S3UxUZl3MS@quuw?(EYF*a;vB>v|HhM2v9w{To(L^WNB@9)z0V0i}0I5Dtfi z`%iMM-qr^o!5krS23~wTHtF2eR-CM%Am83f9px_(sI+v4xRQ!`5&;+pdcG>zJRlc( zymVbv%5tgqj_PV(w8M)3s8~+iF*$aud+-Af=h(;NR;S=d!I+bLx@P>TnZihudFHv7 z@P?u8C`+)a0*ACRcwc?e9TTp1YIk$NBhll+Dxhy+8on|b4L|NO*-DVXX*mXtl}k5~ z+b{_Z#TxZ`cBH)VH$LetAm2eksxA7Lr#L0m96)FB?Rpe^lmmEs z7D0vl+H%XE59ojapC-kBN-FanMl&&#t*orf<{89r3pvG(b1|v=P9w5e+~qrso`+%a zz$K1>pkX9ujFu}%)%5mGhd=RF3WJa&{r@D}7D|B{Q+Agi3 z=m1`HA~f2XTHI4Ef+*mCenS$*n8Z0cWrS4CV;){t3Mc2Lo3CxsLA-4L!6og25l|sl zwILM0)sA^E<&LRdlB^%A?3(SOW5g(bOGz<~>p;uJl^$igsJA&0{poZ}6KB+b5g>g- zc-DoTbD}gi79uP@j7+t2%%-b`)DnGCToHB7a^3zAE%=a<+#;>%6Nv070hp8&1K^eL z;h4n6R1J#lV!G`|E;v2I>!bgkTVu2SN@I3WY5Z8dg_={^@&{HfM$`=2m6eR*HRwtL z1m5%y2(Q@zJYCT2XDq(;2e*E{DRN_QyYSd@_xrul3To={(v2LxTaDSy z9b65lMKlMpJr1s0H>Ma={hpt*90Ju3R-l*zJttt()fH&wGk?HDg`>~ck1jj1={~4h znO>Q9mS`UB&KhG7I*hs3;U+D~{dxooxk+x6#-tZVxy|NvmY{V*rU~R=IrT|Lszu}E zDy$aS8?CEF^9y6joq7O{bT*DV2u7^Jypu0DXEb<-4xx4{=(=Qvxdsc|p}crVe_*P5 zG3Mrl3gZGwaTmO3LUv<4ZfT6>etGo0U@*q1q0cgsbO!Yt9!a9r zwuCPy!bKWFW^xti^_2`xri5H^U|--L8hm0w3f->25f;uP7v~!nQh?c3VIf|D;D3Ki z!dDUE0y9E#`VVCYhi-PnF9UfaSCr?MyPb()Ma(2qYuwep=Q!K(I5qzKx~9@SM!X(B zeryyZHwmk-hspeFh2`TKjhU>r?2nZ=k*Yh=sEF< zzG>dZbu`~2DnS)*KRKlRBzP$BLWokcW8F4#_wPxoZ#$>8_11YMK$n>UIPxSHbUBa( z`$mY9S_c`Q9o{qPRFC3om zPTkEbzEWJbLBa<58rEa%O_2K=k>-FN`%xV(xx_C|pZ_Lie+A&8L6~ZhPcGiTG-!q7 z@6=q!5>nxglQ{MX&^eReY76pNktH14DA|q4_vOL}KQIFlfXH@u^w(9&wfDaAW?e{{ z&LVxDkTn!4Kjp{k5tbjme_wu>IC0(c8}e2&qz|$VcvM>ytm2h(;CCcaOeN6n^EKbF zCs9Td>!+c>Gp87YC7hyK%W?=~bTU9ig6#`X;wz>1o64xxKI|$p__a>@mp*(Q#~bjV86G<(k9Khp z^7SE~1>Ae{zB#1_cF55j32_yz=%#1`Lq$+hMBwlZoT=wJP`Cik3qxi#Mux(ZO|M!7 zU=29%0XAd0IVrI0m#FVv1!=`0_$T~#=4bwjVUDjRPH}ufy$SMdUho{6RU@ib^HtMh zF-Jjh*x2BxQ!rHf%w!$(L~qJp$SQuErEL5ViI3N9V;V`8#AGe=ir+wbDTw5Z(5}Z~ z>r=5=5cfZH4{^wjbhYi(j|gOYbVN7eT}4LDr{ToF-z%>ottcWRM7v{EF!@ku8$2`K zqS|&XS2k}d8l>Q_AF9{92;%F2(jHLsZnf5;xG~XMGnh{MwmJxruB<6j_;Wh+CfaQ+ zM^u+{IOxe5RZXWi^}F66q{qW8k<@}ND*?NzaNTd?{;!?P652yQ5zP3f#uUZrwdl(d zPM)yx87|aFZ)~I+A>F2~6w77B-H=cSXj{Z#qj%$w->hl@$tuQ`Kq*#Q;30V~gm|A3 zw!QsFIazvhuLt%4`e8D_HCzSjJr5)7D96R$p4*;iNln>K)4-nokhXnXD*P>0&C_GX= zh`wb{Pk^$3_JRXfQ~xx}EmJ{6KO1A=rCUp6z2s6}4B&zNDZX11=xzses0Es|nqEmJ97+=&r9= z`Ik$Xug?b9sPS-h7T~;jzW&n~gQLMwg^?nF;&`gohh@NpL?mFrbk+&*H&a~zedaYdKB&6Hf%!Su${{#3R5RbJe( z_Ny!2UQ@^?0YATay~f-0KWhGN*nt(v@%lhj!C6t05#q@|s%&0Q8M`wbx~RMRrWgsK zci~7M#L!f$chw+TaOA{9U0wXjLhNx@DOw%W;h+Jd9{u67No+$fz?bpGH`fFTf!3ZS z>#tyPV_tD)-x|O2lxkiFnXPK0AzLznxxV>*^$Ewtal;mj&PHsCqau!O%at6x{Q5I1|CbqK zzJNK$F2K`{ZNaX}2nl3gn8Om5!y|{SywV3IZ`L>$(|$H|l>j6^-Vh)8^#lr~rBoC;fVj z@p4U+(|81b-~TTn1GG`_dqM)PW2w5DZ>l(QXLve$I)!mP;K?rArt)Z&nH$Bug?EO0 z&iD@}r;bAl$y<7#|2zdpXyXO{_((bND*>V)W7>};l8UjemN%igI==}T!U(br)>3>> zz|R;&MX%e(D+=B$bfgb<51Ah%{b=f0fQVl2OAbzuns}iiZqv|1g}iVbWTKhQ;~Uax zjR8x9-yh3QIA+@E~hEnKLf^|P)w-F^(=wZ1V)rvr2)O`&|n7T0jiSG4JsDVIKYI#8qQZ#r>Pmd0GPZIoEv4{86%Sl^{4l6SpoVBDDKW(xtp>VIF+1?hMSP<{EdI8`vDuXanVh zHR0Ex9Mwgqu!d!T{ zDDbT;*GNzspb;>a`V)Sb{6Xls>-kyujv?y@?wPmny*PeXNbtIV`zA| z1kl}203j{D#LSkc4!|mn572%ZwteCUawnrQB+CJ*5lPZ9AjOIOX=b6h1&DhK0p`#o zK+K9HHvR&zPK4y?&0-<|WFAX3Uwk>|2C$SC_%K&X13;~x(Xh#6IgCDZL~lLpd&!k- zq)vI9Lp1`>b4N8I5&uCmr&o=r@lldP8Gp`Re7^!+@e5rUM&|hP<;(V!-XQ=>g9zT4J-?`q z%Rf_}a~sdDVp~d$enJkE%5x!P&mzne;JqzL-Df-st(76iKwl@ZeFsdJozjv~V62ze ztU%#Ol$LDAkQ{Hx_0^a4>xnR#PyMvC+r)XeVyqQHFJf^Xb-v8MGFe}fenWgdn0Ymm zKfV8hPKA?^;cO-MNV|TQaaOF6@`7}Jwxx>&SG7>MI4oj7k!VC@qS@m`h!UVjqW{n- zZ4Qd4tOEy`1VEDKF$XUQBc9%)tA5j)LsZ0L{0G0S^GP&8g&57X^>Y$r_J&FRU4yr1 z!d){A>q7x1S}8*Hzg*~N1MxY*LB+^0^*NJnoCAy32=-4_ zv+1C}TfFd720#_Q05(T3HHtg#PrJd|KEXmUAb(iBL2bFE!&fz5)I+e!G}IZ+@tusXREl_61$mTW&7@Q5 z*HKsKEM<9(SwIq`+_}l!J;WqlICZr3U*b|IhV)svuR4{OA5+!s9q!oVk(fI&XyHhf z46>*lddIPrbmZ>|B#Ds~y(oJ_Gba}S7~4(&*ycLYGN7fBgD$zDj{w1`O=18s;^8RZ z8QVJJpd;PY1BAiMVvpAwD3&A2wY9Vb%l+!lo3>$n)OLj3lxb zBWDqhx;fMnG2Lo)1l0tKv|UzG3%S$Yf_D*>y66j^x>Btpju5^Pcc*{M#BopRc-AG} z+bCh?X-@)@b0$r*Cm;(3^Q$0W(-j#B1K1j(6{diHm0wLE>2rrijs(P15$ArKRBZGW zR!cul1AG_=mH?|9Wk5hK0)kSAyLh1HC%a~pORx6OaNvz$W@J1T18tuqg2cf84$J%~ zIq}HtJea?|!kR&G*MApt7G{zgjqVl=vs?#JK2?ropMNOVvs=o{pWH-enhG}{4_@HR z)<}!<+ZOt|QsuAli9S)n^dY1V=Tm-r+b#L0@~6c{cx7qgyQw#&=s`!w@e7dvK=5YX z6SL&u_oF1FavtFC3xE?o!sdg{gJvkYnQD!?I0+Otq=A(OR_*{!=maB#I;h=Z_A@B5 z%V91EBU9WT!wYkJs%uToH9mR| zfz1uVmWC`!WGd@kz{cjiG51!p9OiYbuf$;ib8DuO@Jnm-mb7Iu!l4z3d(i9auk{Ow zud^jhiQ@vg$-K_y9wK5a+;#$$=0j1)PL4^w>XYJa4M1I<;vciZB`WU+(`&?))Xlt= z6ee->u5BM!KkYwO+MVa7Ye0w|Q`)RuAOdZD*5Wq7Bk_$gs&?yQUd|E=H)C&)!i~QW z_P493n~Kk+H_EuRE=c&_1|Le?DHyhNW{0}*8+|SQX0HS|Xr;}Iu}cQ#+@8oE|GPOA;~6Ve?$G05Uk=X7(&+)V1g)C` z?1XgCV&7?{J=bP91OFXPwj3?KT~JJ5qqg=;aQJPsq%U5euESWKm`oDnGIgfLO#L}~ zG^mjn-yJ|URyG=@Ok_8rCBMi@5_KqV_3X>A0Tr=MQc|4SQDTq0M!%RAV8W zOuQ4ImY>U;r~B9(45?a{%Y?nLfB1aCclBL3yfgcxgS0WgY&4Gg9j8(Cw+Rt8ZGm+c z;+-rbV&~FQ!ZfYDy?0Di^r*6n)%0T8G~&Nae5>T=gG34I6ca9Tyk3Is$~Y{TI#v_{ z;+Lr*U1qQxyte`#;y=3l`kbl2jRtw3#>2AO9;)GTxTBPCS4#VJvcHFBIa5!O%F1NPc7s0E|~L88Ld^@m%;otIVuT{^ahz%2k)x#q~-J6DX}Tt z*e<{imffCCnWB%X+Wl$`J%n#1-rCKkm5_a}xJo$t*Q@;Ku+Ya(4Hl>oq|Y~*VG}*O zb330w);l}Wb-q$$){9&Cc%bU1))q+CXh;0cX=g^2Z|^kOUY%9QCshN3&*kqDoI34| zzPS)E>|@5n)(vI5v@15P_`_&gOJV_!r-|oqXuf04wsWlyfG+1fX#kMT!N|ot5&i7Z zatQj6gx8#K6*S9Nqf>$L<{A|Ku^^^6xXJ_KYu?_dotdQC}zv+()ZvuF+Ki!~|J6A!z zS6r;lG+s9>#iKB3BcmZEczF+7vkDaAb*fzJLYf+sU%Uz>O5@mdO-CgltIr>BmZPSu zZ2HWW*NwBMs5)m|XEe$Q^-QBnGw!tzYI^C$)V%(*Aw+R_Lk-dYXFA52{(2{ppq@}V~1g__S>q?ARRWnl~MdhFv|?|O%(aRUYtFc=bQyK!Q`bp4tj%HjyYyz2Y!$c zAK!$TZe7nqpV2rjE#q27tHZA?vki~N!+*f8%?jg8P1(+w=;yhOw9{w_Cf|O<$@aqY z?kIF-E53v9q76P*AN1GfysY38H9UOfhMhro_`zhpzobvkDvyusqt^AJV1Ai%KTc^H z2sfR6!^Hp=-0pReSkOqPPN~*&bZe%OO=|D6t2*=d?%)i{?1!7e_Fz3$oqK*6GK=Nfp1q9F!*Hg2v-2)V+@1W01dAo zg7OnHGxpVeHx?Jl_ZHhR%HlS34rw16#tKX7dXF8opL<^`gX{4k%FO?KZON(vn4^VO zal+w597FR*2&Xt@Vs#0{1Z0Sy8u#Og6V{IsQ385*0KCC`_xa3kb*4{t`_KM7pDKKU6b&p?uKc zN_=C?cPD6@LfyyN7=B6sf)-eBfrmE6TrN1YAVszYQy+*jSoys7E)8i^8nvslmdL^S`$%?s*Sz695ul`~(wuJ9k`dGOvC zmqOE!2w~L?nR9Z+qovcSh#C#ux18nktvwbW^W?K%v+6L4qM!B*j!|_Gy;p1a9hiLm z5~e9t0$Nb`vXbHKb>dOX!|bF+`9u_St&JJ~90o6au^-|wmI(YcTI{_bB0eW4v5a(- zLF;5lGt%(Kzn}K2J|4F0pj8j-HGk8@JRRkz$WKe#riTJH77ZgnbTVNc^3B>N5FLd* z>D5{7kb(JK5!^V@>}J(%;8!#f!DsjQpN|TR1G>zxBZi4|V5jxK8f{9qGUG3vmDE~0 zVu`qt=4uG_x8Zrf5VK!YU23Ue+nRoc(suC!Z!UmVapB8cp$t0jQ==l7DH*wz!0l{= z+c25UV`p7kQ2u(6JO1srBuH@FZFHa@Qp>9rXnPqj=mgTb7U0)P-~lS3t3S=q4Qj0q zv$rOpygNnsc7OT*VTYAa*iU0cb0#W>9Aa1O>$G6Kcknln*>93sW3CCb1D~(UeDN(k zmCEEdoTk>WI#6?vfBL%+!?-LQrK@U8`tB{Q>MbAU&%a4rMB1>aMIO>Qx#-Pn4*jii zhf$=>LSkrHzH_#M17nTb(Da9Fg6X*AuAom5GrVe(1jz*U2YkmE&mFFYG~O|PD(T?a zNxy*~M7^Y>WN8TH@6@;Xvn60~g~)3OQd*naoED**mX>pp!}+de9`vT9OyWwiE@-KG zY<=&dv^(6j_(wlM^t8)wo_`&}eZZoy>ogUc<~Q#u2%jEp~!cZupB; zv25ft>hDm2LO=fz#bnszV_2AhOXi8mp`Whu7v*&ETOyvIJ%eVo4Wf@Y*@S-o($*LQ zTp&bi;yyX9X|~>~!*YRlylp`J;&WY_K93^x=$U@zr2z-qGES49co_N7uVq|}e=JIS z0cZxF_vy1=0cA*6O4_Wu4vr1N&vtyeACuhTY&1pmk}Ro ze)#8ylhKN@KOoE<-R5kK2TUk#(;k2-{uveHRp$!MC>KPT-4D%a)vsGA1cO~BG#OBS zqP$M**eZ~XeO^iSVlcZA^Sm?E0G3+OY)Bk&){vnj1~c};w#*5ZB*(0VC~@*f($vZX zb3;pp=w{{l-ujQj+WlIUQdQkQOun1y2&@Q4`nT`f8ggIO)NX-IyqLdBRQe6LD`F?x z9g{OnpfklW}^Yxf)SQE$VwV^Y?zn4jpJW5vs z5(6~S=`xIeFJF8}zlQef8j8DwCx;3YRuL;1Lg17sZFV#784iQ-PU1UXjsuaM_QQrn zuSVTNSh(1^-<}GFpfcwy(;+3N7ZQuF6W-$->41h|mIS+%49 z3z8YCE>=U3Km+n4Mpr%;yT0?}2 zep@7CsqBnoB=QQ@VG2Nzf9QfDivZKVtWlwjm)eei$oU+g-jSU4n;Zc)7Ci5hOYSlU zLj|ru&?PBAc9vM=?TaVGedG&xAa4!IAT`Fh9)829#^#*{7FkX7q*UZymNbX1P8n3M z5Dm_z)Fl+Bua}xWXF3{%uv8$(ACrH^7N~HEnSJQ-{l`CREaQJv26WPW_dlrCSP!hK z1BEAm#Tm7tw{Pn#;F30*^F5nsDiz^Cxs@v+*&iwR1re^~dKh6Ny2l2fguZ0|1lUPV z0RCUG{D`6r`RvBcdlu?Z7$B|IShhtB{wD86_a>!au- zs=Ci8VTh#CqM;OS!-vVQ1zshI-_sZkOFWBMpx57*&k`Vamxsk89)g;C3nnOM>3_0y zpjqs|rsDSR?Cj>#8FZ9>eDEWxSG1m!NT?VFUayc!=~9XF;ajj*-v-|Ptlws z0VVG^#An^5m4YCexEmW2bv;VLcPQxNI-d+DjcWICTO@P%aDtAsf}3bXocvFj_ll;g zWQ@zzV^<-3(4SFvn-+&*JFc^JQ!-m?2Ep4dKkP-E!3y_$US2_iDZkgQgTKnQh442+ z2J}(eud)psc3l6a`f?Itht=15ZmY&m2LF?o0MPp3k%xyn`rE?;+kee#T{~Ug%Qu%J z`@u=%-(R#LO1}_v1Q|P{=hrdSTb-d{l6AQ4<4C!6D9~~4$S*l9H#1f&bZ2&qH@S!r zrfkQ#QoU4KxQYL~BQ;2NB89Ls`T*G2goaTJ0HM(%mc@5VA~k#Z>`JZ|=eIgXNpMxlC7R(qw=CT}(atQ#*N9rPb|J`H%lrtP1-> z^w_bQx%;*W9zNc1`Fpjn5j@u=XMr*3oB6Txvldni@s0Z-g`aw0(WjA3;SY?yo62(s zXRSiXNBqe+<-_Fzzb@RST>2xf`!?~^kqDy)I0OrJFTaLV$@)T|;&+D;w zfTyV^om>D%u&sAd;NJLCbFN~jdI z-5QgM!3c1g5akN=X*F7O4*}Y-1-UIMR~AuNgpx&aiqO<(+OK>ro7kU5Z~HHN&1B zaxGv0#(2M4?FsvQqw;>;xP#DCtc&I9NMR@aMe( zNnRki6LVfOnEsX3mYoC*JN>J49?#W~$RziqQMU-M^?2+}^i(ObCLNr1GY}Ybpqzb= z2ybMvPof&&wN8vUmjpAidnWc-jEO>XCBg8m7Y;UCZ;ff37Y(T0tTQW!@fo&A2rsYsNHn8$iF|-}O<~XVW~0TJp_-g{ z?Q+IFzco=pL1G7O!a22>kD?6UuG8dpAp7G}EuB3V7louanI}4cv+(i1lyQPgPZzhY&HZQWk27EUzHpdTv5U1+e>RLpyU>8DW^!|fj&U^Kn0Ln z-`DtRwKL9{o#NC*QsLtgma((+XiYsVXBnsQ+R-ij`x%^jnExhfS$t?!MV#GKj_y#P z=!+5AZvdm4H<~?ReUnp?a>*t|+el^5!P=0&v(3g`zAmgqdl$voA==NQ*C#sX8(Qze z3BNpmx$Vrmfj^57qzkjIzj^Y!ljiPDuf;7CH=XZKAwis z#fqf$ijnKUHcixSyhTtVh#ol+i-7QqLUaf4308TXanbAH{zRX_4*mUnS2Ka=!L`q6 z>O6igkhD?85Qim0*C^!*G%=7gP&^6&!0B%e4ZD5fHFalbbBKXv!Y=@?{jL3H##M&# zFZdBnGT|p`fc6iJ!EoD1XntMI1~J;&{bsVd$o#HR;dZ8)6!tSwi3>?-=|;NvdCL#5 zlPgk!FV-jq=Mtsv>OGj+lKcxE0C1X&K<&_K`DlN)KRckn&!Fq-r_~Kkmg`CS$Gr%d zT#vgwh-H;91g`jh4`CsYVk zznCjgf&l#lWja}79kV^--*1^02HsMkc#}7m!I5-Pbny~z-KyM=89@F8d1gOhVi9xe zKN%_P#&QowUb->BDJ}@-zwD=SQLUPKDlym+__0o9kANL(}95WOd-@`G#n%L@E@;j`AskPs8-kt|sWeC(ay!lu9lqY}n`GI58+mqDCotRSt z-?m?lc95kDJahngGvz+U6;-YJChblzHAYyIN~I?&xQ!k4AmC%gjt*_GKipjY<&@*9`_#@ zecXTRExn6qxBvHeRZg6Z1?{sR3CZ0Z*jt(J6y7TnL>*@iD{Ok3(T@Ei@HWjKIQuD^ zN4I_AT}_q}E;N=O94S@92Yb09Rn+WiEljn-Q|8ys#2dfEw{i9${GK{5x`w910Uvj7 zZcYpEK{AU3XN2_~ffnk2F>(}qvCVe3X@GkxQ0I)HcK;G5 zC)y-b5J}LcRTsNRX;IIpyCj_Mc`K6vM)|iTpK$X>USt~=SM)eg8zsw>+D?*UA-KWn z#9#I;;Nqk5aot3!fCWPsh&uY5sS+wB&;~fDY(g0`GaN&2l{$P3} zQHMExmH=Gcs3*?=r)j|%^re+nFUEvYYJ1@NyZy?OZ9kS=APo7VqnGJ>vjafjg0?Rf zk3jSw1-_+t^V42JbrQnz-3Homm5)skLKreIF*^il-ghdoA*~_vS~+a&NC$!~P{oj_ zb34@QwcIrq%;1g)8_M2XClTH1AU>}3QN|jnB+$oksWK(0bJ7;#rg-QnaDoXA@%_j` zSy6UqHceYbK?kTg*pZ?Xqf!zgJ=ZA|7xNgqXg7L9tv&WB z+TrJpx3`lurwI`Eo+tL}h%zzZ*iS2ejFKw0I(2EHV5t({YKIL){^WGtLwC^ep?@-( z*)Z0|diF#ua?JxMnrp1oe-;nQ7~BNb?JH(oL-Nc(EE{@EDq;z2;pS_KUkSH=kT&YcfmF|*qT>{=B$@lyj<{I2kGFchTi4Ck{1uAuY+z+_PH!v;cTm{rUywZ}P{ zmMo)z=+o}up3rkS+6829Hl%|F+dOZ^vG7^IAA@jI66I0aX8os-iw1k=>XP)i7)?@8 zo8R9;W4fGpY@PApP$F8C09+xec_O^1BGkwZDw>xk&|u-zySBvuZJ_{zl7icp3lTr4 z2h{MF8k`mg|LuZ1@v~x|H*yIQC@Iaqsr(I%p4TAcNW$bf(bFd7wFW~<{2t$tEQ;P( z*}fg{Svv3UE4W_(xf34+zd=e=^;I)vRy1kjWnL8EB1=ge8+h~!^&n|~fv1LZ(lcVI zxso6@u22sl{&L5dLyN=O%TPy;Lq9v zbhS2vqVdx?C40Je3if}#fI*&@*6mx%q@vXp;5M4$C2um^SOlVn+6~GW9(UcBkfTK` zg+rpf*&3>p_gu9o*3Gns?sFsT>oc`L_EJZO0_eJMX#Sv>ok+ZGSd(T0g;165>1qkJ$CAphTb`!w`g^XlQ)W2QBY1o+~0Vf8g z?Y!fI7re#XAhMW1W!9*GB&w~+cP&+MzNl_W8)SG<`6T#@2*vti-i zeCRSayU+_?H0&LEe=&L61H7ep!axTm7nhp$4%j3;@o79S5*WPP&8D{q@w&-4c9 z8$qXqX}D~}AZ;Y<+j1NzO->W6+TE<4;ZD>buhOfWb{iH5pbTLM?kugLmz7%AlenvN zi=kxO#zIGMpn(;DqMSuKLZ(~5+08Pvkt8><^_b$D^EPh^f5mzHk(G3x>j)Yz>n61? zL_q;n$2W9QQ!`1Tt$h>YI#Ir2btn&NB{*A9kOz-%)~jkPY~hCwYI>XE>2$s-G5Vd> z;-L)9b#==j-zf#z&h~f~CZff%4#fNV8+=>;TT6dN)fjQba760X$~9amjj58nP?Bmw zl;y*1z!|J6UwAAhoKlDCwb%>ddocgW+kxb+ilJf$k}k%l{~qbr4!Wf~B;Pin8310F zrQ&(lPVbCLY!dFylpYDx3EP~$9+ZNrNRLBTQ<_Glhk+yR5xXD2ER6yzrqp1;z*i|= zD3d?RH2-{>W6zLTN>wypT=$8I+cUAoovd26KPdXdm8vC82>l0^t|`1bSc=(WY^;6$ zF+LRcjM8KEu-y6hhN#S^lL+V<90?;py`AAU=M&<{%?(@D!-j3y@MzT|+2CtdAgrk^ zt_&bVISp|Fiv4YsEXjz=vz36_xbLTBeh)z&4~8n!{7&hx|6%K`qoRJJwqF@WQt4D0 zL^=fNk_G{h5L6l|Y3c4x=?*E8liJ`l@``q(;p7%ZHkF(Y+m#(GAF!y)gJFfk? z06|k0r*a<_Kp^+n8}pgngLW|Oir0?VDoVMVRHP~4%65}7oD1_n$a6!!cc!+<_J9*Y zIenEO)({K`I(x%6x&pX?Gm2HKWu(8gPE*R+T^UXMSs{d4j+rx=VVWygDbXvh)tP(} zDMv+HAh`ApvoPpEUEx1@prEsh(p4JJ`rV=6l0XyfR|#+pMCGbwB0DM-kZHjPKN;b7 z$?7UGe%ZhhG^uk$30Qd_s?NKligV@bzO~A z!~hEY8H(ojY;Wm15D6~LR}h%JYp|jaUkCy3PAW4cuW4Ec7>{gG;Chd2l;r@NwO+XL33Vek{o#dmvp4>U=M+$k&2@UO z*gJjmO=^Ft8v4fD!M`?E-MFO@^oNDC4yn_nt_kWY*%H|zb4o7kIn}-9NT`${-Yt>m^Ojp zi)cyM1YT-dPOnxt=J(21N_A|9csC={uXVV(&Ezm-`}i{4tPx}0-uhQTPzN5*U2Xc(IrN^tg|;!CSN7wdSR zxa_YYlS|Kd6S6yUseJz?qHf>-RkM~&Rf-!~W_{DkP4(gDG4}KOpcyzw(t(>1CX$I#?t#Uq)Bl^4{%yeD!73vy+pyD9&N1%OSbi*n>G z8renKEurd!iA=ug7foPxb3yhnBFAhIk*1PGpn29(fIV+Nj60OOL0ZRW@NOuqZUrx^ zG9Ddu6ljAt&!SCmam}eHkU!LH5d`tMr)Lr>rgqiRhG!27d)#-?`&4BT2bxj}r<}cT zRfsfP3?JOxK7NlhmK`CFQzJ)|G_D-7hfDe1%!p+=}b-Dc%=ZbOBY?&ntrN ztbTw&x)K5hV!SC@pPkXKFMFh1SiS1p-&t0q__wi zq*#aHK#xE2h3{UXCx1Np`VIH<#r3xz$Q0dXqN061Ev3Em$wcCsST0rK&XaNVDfOiF zgO0v`y^xxJURXA1*8{m7`*Jl&^E`#ZTvS?`#n7>sP!Go}G{2r{<-q8iVO4dg3!0<~ z9*P{^ml!YZ*qn$%bd)2R6W*^&jWnMywv=ad9E@{a({^wcqsz;paDIHZ@XoW^%)jk! z(7k)4)aHG|ZD_q)36EULia-kxMf1$?pkuG)!*w1V{vr$5!d3Paao`VN=11VQzQ~+c zC1xZ4p^9uS$=$<{Z@+(z@K`Xupko6qO=yyr(eg5P{%qVFm%5nz&r%w5E&tX>i}4hB z&d1d2%sC}%turGHXgrS@9o0CjlgwQ^HD z?a38tL!`~rw^|#!pK|Z-(q$j8 zh_iA8=Cgntw> zA&plnA(D&=1aC)a2|WLND%wdfDkEGd_58+4AoF!a-wW#-%>Y|a7rDKPwzJ6)?@x7| z_Ismm-=y(Zv&e>)yb|-I310ayn9^W^RD50J*Q#n-@y`ipavwz6fYOG5vDb%QqvS_+CtpAN`zuKVF3bAR* zRHo?jbc^^6%lkmC|CB<5gQ7iS9VrN!Y_)&AK1=iRsB;m4>s!$09~0%bj1OGj5D7L+ z+1~PldHU(4Z`tqNyd0uoAsdwCn!Ky?2THn>O*xAgxl|UOi6AK)p0i(K`JL>{zfFXN3Ia9vF^-da@`}+G@ITeN z-NC}wX7reWzM$}JCH%f z;@cJulcT`dJ-xJLT66wUQpw+|(T|7WMQgPCA~;$PSNxR2Q~l$Sw7wEc$KB)+ zTN>41yI?wnJz)O0%9CQYR9B2_3$>1(-H@YB?0`_t9P zN4k927xCE8k_h2?vcbt_c-M37$u_bV@w*gM)AW4<(^DBli!pdrD(8C`oRAU)QbKy^>qq8n^E+WSIwL=ub$sn!z9Kr8`FY?2So6uyIru}KsS_H@Vsl|+=kNSzoXY0MNd&| z&KauAUlN<9x4IR9_5=_2yS*lVQio*qdtisKZ}uFn%fy4LD4wc+QGvG)UPETKXMo&% z=`^rT>tF1S?LeZVzssM#N(SXk>{`QLite44{q3qDJmk6T)lAbXF(s1~FDiK}i*PT| zRHTbV<}J4lY9t#^SaO~Ig&2M5jFCMyQiq7byR2s?)lPuyNX(Yc_$kTq*A%lF_X9bUZK%al*3J$sY?ugu`RzS3^vlJFb4qHEPMES9Y7M}47NPH-Dk~CWOkgc4}Mf_o0Oc4<_dzIfWHm`womz@ct#Kp zFxBb4tH_qx5o_NEWOMfB(F;o+6b0e1OX(i=iOOENB?g;dP%S{`$tvyv!<8(?P|ECA zuYwfOej818pgBZotl|z1 zE1Qe#R-R(_+;LAgeDSfUt?EZ#afaoBI7Xrly;|Lykcp0aRYPGz85{@PET92EvDTu8~oOx<5>=+9p&4?2e>>h!;Ir822_ z*Kg0cvdqqTSNu4-@vq-_>cS(TgHh@^uORO$2OpXK@PP4`r#{`f%@@}8qcZXH3a`U! zVh?SY&F4$~4{fU1TR&HuFv=Du-ny(#Nt!4~HS4wUX=B*}V}%qc92x5ASC5O25AhbY z>jd6G0CxQ;QGza^sRN<{IyXK9nN%jY{I`qFyUhA!Ho(!f!l0j^dnE0RPwJ08`VHR9N*2gzSd9nvT=Xp7 z?=`T*HetNhen$(1DFKiVb@jIN+B?PrV9<=G^^oB*t+kn`&bw!9g7R4GjQkQRPvbqE;>AmQ|I)-s=Vq&t9=!H6z zX@g#j@MDxfhXyhpQswFD&D9()@^XaBaz_;@x8AQtOi0elJ9}^J)$|VbiIvcj$HOUk zz&FHCgc?=|WhAY)xGNahL_KVg&F-g+k$o4ZqWoiEh$p#dGc0JtMBb8waG+L!nHnOA zy)|21!fp~?9(w%nE*L(4i{7+oU2Fl}3As0N8ECpe@_Gjj&%zoU&xwssQ7MB%^rMNn zzLVNUKC&YMs^T+w-~;CbO<)n+D{W7yK?2$k3O?wBilMEei3$7=b)nqdw?dMRHA+7t z3t(_gDY)-`tJAshv%Own+}g8z5bGAQ(I6YM(7q75*fX z%4t*N1ueIUWAd>73(b1s6#Q<(#d}1%-zy>F6mFL+zcbfA;1@kPWR)AAX_(HsXLu%! zOqDFJfe(j9{qx#-yIk^luA338EiWE>(S01a{paO5pn(gsreK_d4D!qtX7ZC;WAPl= zj6@z7(Bf`mVewZ5$s1F?(|?vEBKqPN31@X=6WAsZ5A!tkoZ0jyF!q1fkG__J(!-d7 z`5K~^3)Fu&IrPf8BZ1h|B>RlL>do$rqrP8itTJi*I*|P`!YoZWsGj>`$+@N5O@6L$ zuso=PmXfhiI3QNIefW*MFK8Uetes|)f&%lBT)y5+{{X1V>OI+!JhaRPnt zFq!uW5gl&pQaVbF0GW`BdM3qw9>jTuUVXMb9w3g3+mRC(pE%%nkqFE_a8po@AjPZ= zN8W!iD7Qa<{=DJZj?UT;SzLcmm(gT1F3-Kr>+pH!zv!}zt+>J>Ao)cUJ_f~tGz-5? zrCs3qDbglE%@c4i&LNxw)&Z%LzJH9>)Hem9NvsxxZgt{HXa_ z*w4c-YUIN=qYUk)T#(j*jyolUxZYt;I-<0YfKeQ3pLfuKInnIr*z2gppnq%ssX&eX z2g;EPJ)9*~KsA*!@ErdVPK>!I_!T6w=U`xpAj9}GVZNO^H~w0rSSY^WFqFeW zg(&+_Y>R|)0-M&9ei*R!yE&iI&UukZ)@aSgP;{C( zf!)w&AD?<-f5U@$A~k3t8x6sr8Jgs}l)dh^``kM-<$0};HM&xS?Mk?}DcV?ummxam zZ=G-@=oj-TEtjIvI-lMlK>EW*$p)&IalBS|<9itMG@-^4J7En;*MC>6AKi%}4B+LE z>Gh;P7@yTXg;~Lke@9PZ1R|5-wVdqW})Eb7E zE;wGIugrFRE#Ci!bh0vI0>OVkt%xU_hYQ3Vl$W*U2YKyI_UOWz(&Q;@aa{L*M^pDZ z$}hh0>O#xZeTL!+=MV0dYGO1WZ=IbzmeiBZ+vK#F`Q|v}Oq=EpnZK}cO_BIlAS^aL zJhWPPu7UZsCpj4S`#ih>Bn7Kc;IkRRR=kRUY>GGX0mai6sW0}IBaX^3Nt~oR`G~@e-G2HbjEH^zB zfojY={Efz9`R=V>qck_G_m`b|8Nmb|zkF5JAft4Xg$7=@AT!NyDzmpvr`20PeI?CG zav81-99llDlaf~kB}@<22*&|cyUcrfP&T28=}f^iIj_BXIq$c(H|A>ABzt2zFp?Zm z*ZarPJjMnR&3AK4a@-P^D^DjCw8Vz19ft^WBvuKIKU;;kp6-XjGun#RxudpVyZC$K zpO^`@UiR(3Hk}Gh)q?ic!$)&d8(13IQN5GHTHkN=#5=i%VAAQBj{`A9ks4loG%=ZF zK?Ww=i`DSP6&>Uw1qFJHJ$`nM#pn7rZ$bO{kILIA*GNt2cE1Rbohs%En|}Xu*Y#9C zkMrNfTeBs=PqtNRCL1X+C-{#6Gl>Hf_`iC0w*7Ywf?cY69DEELU;|aFf!>(LLk|7c zmMso|K!op5csj4czN2>SKIv`zMEnJ(gH9C|-huflmy#rWSD*c$qy8-x!V1z*|xN1q$o5RVzydPclmJc zjnKmCL9=P_+{~P@HZ6u&*u8<0RCfh8$)t){$#{Mq1Q|G?kf=PxUQ?JU<1_Mp)qes5 z;aM8moh-5i`k@<_B`@T*q0Nk^NI1B|*8rD?TX8i9krzrJZ#?dLnGe7$2WI*k$A7!Y zIBG<3u?-nn>-Wk8)_=*jEQ*1YQ)b~6ifHzAQ1V3kqWEuj(6m7;Qb-Vb8b0zzZ2CQe z%5M=08eJCLdsX-j(ZrtorJm(o*S z-wJ<5SD?El{FqcX^pfZj_dAy{-&t_?B5%Fw>%-J+4aryGC<%T9WX+fRn_&z@-A>ij zOiemcuAr-Ea=b@>yFlbL6PEmm)q^h_sH>E*&lwl&gkv0()1Qy4Gdzv(OvKI5etHvq zRrBTspFsHahqQ(W!U2o5`LoU{fFM&na_empHG!igXDl0++Fxa8uM^T-lXf~(xw@#W zv!x6S^PT*FsCbD^@@_)( zJuWOQmO=H+rXsKy8;zFhtdHTPaODZS?m`&=t1Ri{S6rqt7?^hEgUV3Zmyj##|7G~9 zQ$^OYaxmL8SIczkU5n%_<&IzGx$HBsJpE7{@f$p#2v9S8!%$=kqhRx|&EGHf$c2T2 zgX;N$HI}J#2w3;upCG^W_&lB>qg|{s1|TK{D1OvCY7JK$$qoVJogJwc1)}aw2kx&z z-hx$zYc$&Z{{XswBKf23cpaMC)K!(bKwdT(!m_3VLbadQm^*DiFd+E5JLqtY_S@>| zdJ+zsiX=b(Vssqkxa94!Gh1!hV9^`NH*#@0eid>dO2PGR;>JYP-xL$(-WglbUfx+g z+>zB>bKG^i+&rqs-&B(>1vb$euuw|Al9e!;@PiZsbj*ppp7l-FzD~x>RHi3D$v%9a z_vW@AN=~cDbf96W1Wx)&=o6tnk%|_FfOVw;Lb+S(&bUfOnqqfKswJ(8xl%oB=0rg| zq{X&2(S|T%*7bw;_T+cK zis({!`te}%hgOijEa%2QFSlP?c;DD;ddSlEt1u^r4CN_Ykj54lf>^s^FJk=I_q6== z9p6D%QQ&Vp0f^qobQ6w!o|rr7<>b!#sgZ#GCVp!v;5Z%#`SE-cPPcor43eQK_gEh( zug6{5U8{H7YL-pc8#};(IX)1i(W+h!IT`5FtFdS7+iJ=`x(bYZ81NdU#o-jsHaBbV ztl)F%Y=cC2kZvo)_!d5Rr4#pnZ(7wdA%`NPBgX5n?SJ4bj{IYdU{U&wFmV~Yhpa<{WlIiauex2g!u zGKuM}@ueC+#Z%$C@C}>Vcf7{3dlCE zN3hjciSHR!y2CPbbo6v&WgRzx?ee9#qFTuRL_mDFDI=^qcYISqRR$d(2A?3jI*@IB zKPjaGPl``|GLmBw=esyCFGe-yKn3-B9r0Yc{|Jn(oV{Wkr`yL2Du5U2IRpry%UmYN zCkiM6*zv%f zm#`Xb<}VdntNe!z(SrMG*FJO2_P^aLfi1c-AQtD{fsggo9}wNv7s*#*asOz?PJnD# zwZS>Uga6fU?Q7w04>m_H>!Z65CxG+Wdh=WJciWDag}! zyfwgQGCI;;bV-J|T)M)ltE5E0%A&c#uAd-Awgv4ZGqqFpZUCv`InDD;^rpvY=2OC< zyK}=P36>k%<*uxuChWbGI0SLt(T! zfrJVM4McPyNZVf<`ysi?^k#DVQK8vqJpTB8hTIeR#Qq!xzK)d+R?9RqhRsLfUF@#u zaGsQyYi>qCb6AzU=`5$|Vvgj$9r~0U9BwFNE%-VCBhsOO@Lpzk(dyy`|J3N&`mRp< zQ)yMo?iKrO9&VcZr|#nS4Aj*6FknUuh!3zzNm-zUbZo*rdp(=EUu&~D26zlj51?<# zItmX|ju$j)Uq3ZR4I&Yhq}4AMK4;t*W}3NY5c5FRY@~D_jT$dG2?-i#KiUsFv~bm+ zerD9v=&(2a^AnIVh#@~CQuG^F2}Xs0)l*>4&A$q(GT%*k`&G_D+p-)u+4-gpps!nt zz*<3)mD~kzul|Du?yOG#krpr@SGxauEzwbV`f)SO>80STC1gYD78cIl{GPSKDRN{Xq9$Oki-Ym2^K&xV@@f za~NOwenY#fNWc7Pm$|WIox`$?($A>hxA?22$%6(gI#l^^2#+#YZ3Qg{@)8w*CKq}i zS+9cO6ySaijD2z6Obj1tp67R?wb}bu_vpZq5p`wJ9V`f(8Ua0 ztNSwVib1b=P=oKbI#e~}vG8Njr^f7^%H4Q+WdaFRDv6eBPo#Wr%EbFU;84!z0BbsC zooil+kLYgrt9!kOY3*V@Y@U^qL({#F7U z@L)7#HQn3UuT8a`-44|QqkW~d1N)#My+6_H@hDr&#rX%s+F;MukT(ax5uy|aS7!BY ztRT%tE;PTyc)k7Q=WFLA^(5>_iRiIVFQIz)H6;%%)$_-MCTz7xAq-g4{4IEaLM<;u zrC9Z8HrTv`UvRy`XNPuj{kNI@Wx0n1)9>$wZsl(R6 zOb-N;Mca1pb*n8Z3Vnet>%FZwYchSebrB7F=d5Q?yKe_yN)%S(?swl>&ezV4=1rRX zILeGQoirDhdHX%l`{sidGzD|L1>(3fIS7m(UVN!EK4>m)el%;3d)x0ZUtzeNr-8fq zeor^Pi^V53<(>LO5Py^ExD=a1V6Ao~KvxRa4*@+?(KvAX>;w9kUtcOKv=KB|f4V;< zw*dTRc;`I{v^|?aQ7~Hy2nyQqEdStOOtr(~57F+gd{e{K4CN4Y0JUn-ylo>U${n8o zIUVwbwS5l#l&$8Q{mH)O&vuFvIr48dA%qdc92;9?g3kYgRsZj&F6{dXY4!yo+Tl!j zfMh5U`z8Rv@kVEro*h)}BZ(|{F z>O2tYYx`#qofxq=wzLsk*|$sea`oeqp1ts?>N6)o^=EMv>Gls5E<430CFZ>gkaA@8 zCtc!&JWO{d=J2OOgp$IulMIWAsQ<5`?_aXj1}ulbex8U$MB-h8pD5OMWSk{a4f3~r zGXIikX+QneYEsJa?Lfmfd2G7x5nfWtHA}olzR_}YG(5Xdlg)VZmgCJhw?SEILE%8C z`D3nycAe9|sF{@8*8nlKZ)g;4KW#<1Icr_2E1} z2llp_L5((krpq#m#9rC;T#Gr=`*@~N_f8LHBA?(<$4cQKhi3h-{YAv8&RF5l9(ccfDNGl*g5hQ+n;~+y0UJj*@ z6gZ#Y*c@+0r8Z>WO~f_CHk-(R%MYmmO~^@n+Co6m1?)oW_Diu?_l*johhZlX-q%)0d__a-aDiX)D6BR4dN7b zD5d&?zFap&S%Z9LpOyWsXU&XB-WwHnzBiguPk2w@0)SqRnnPQE3DlEwxg{f+=RhCq z!?e+_RQWKIDf93sLo&H^Ie&vs^|0IIpef5%SahvcJ;byM7k6*k-gqW=qWFjQcxY}G}xB9@4R+h9G3paU9slN5{zN+CNSSi(D*x#q zXEf#NXwZgr`nxMbtg94NN;y0IxG&ykGgPFKw`4Hj2z$pTW2$fd)Rg5U z(6GzX<1IqV#OX3){nlZzmHqoYuw}0Og|)bSjV6PKs_TNOJuN}Z2M5JTnNp3Hewey@ zD2AJq(80-A3 z>O12vFW1WB9~+a&2VwQqkBzfwoeV&~vz5>U$Yvy5X6pCt82p8JY3yh}TywC&ldlKi zY8%oBwtpYbVh6V|bDqw3>{=?6`JuN{6IgCWD$#Kkxz1uubVK`A78_d_HEyUanfhLaxU@TCxNk%mUwiW&B|qE zSzD-jWz*NUQ$vEEKYLhQoA!@Yx=ap9_7b$P1gxXfGncxepx-;oYAYfHhoWS2cmc}H z>6iIZ+wHNiW1J}>?Qh)P=Anf38z`2@9#5}4t~Qb_!`rHYm>TTIKTO>%7oSh3dQQp3 z{rUEcpV6dne9V^zjjo~|?jE+vp!$asGB?9}ISL}~5EzG1mMk}D<&C{f9)=qJD}IuN zMLTTo*D!<7&LufIbT^qhR;8(nR_nagF{BP2at5nRncqeYz-3&`JL8eGcrZCnAr&^u zsC#^`To3Dq6|35wHfX(au7`as!a2^@#DB&=OSTv>^B~u->cTBGf83ZHw|pUoWVtk- z{BXnkwQ4WS&TjVk3(nsbzyz3)%WQh&U$JKL?KgNNiF_i#$8lykQ`QMr90BI1(eT!2 zmKxYf#6)LLdaMM+ahOdSm@mv2e)w!$!H~KMvt;9Gy%OonKTfFCs5vrwjfSM&IU# zn;_DTUGaxI${W4LDS5oGM$+8qbmYW79b8C_d)%)ZG-bPg*yYxay43kzps-cyftZ(r zv_~VC%B$1ni&~%@NiZ8!Nw^!SQqM3QG*qrh`L&9*B&@aaB0hK+f<24RSNu|p=rDY7 z>hG2MGMm;T9C?#fN-&|vCR4ME&-bq&Ye3nGbeaut`NQe-tzT9aG)1zk@R zqxq+qM_x+b8pj>Z;2m5o3_4id8c#lI_%CG`y$N<3WRDiz1?Zi(Ljwba;ih^>#X%Uu z&ZrL0zbzZAeDI98uX*egUiIEg@H&|gI}!d^CI56-lD-}L$!s$P%)lIw*k>>HXXBaG zOLbrXGjyeO$yoSEZFzw|gREsnk$)8$_`Nq!WEu6ykO{;%3P_Mbi^bZWZT8;)H~;ZB zDRHUdu3n|_C?4G`^6@|SEj zY>ceCe=)RRY0$dMKzzgYJnYfk=-FW9Zq24bn!xoeNXySVIGxD7JP2!6DO<=c0Y`{M zn%$zX1g5&I@O@w(Er8OpT!7&|-x#w02b4q4k8ijAOm$d8zhuo0@G)*vnY|I`A5zkp zq7xp*G4jc`>0iDX`f=pkza1RXf5kg(*Qaq(xhZ4wulNUFYjmb2z8W=}4M}AIo8k#^ z$0A7yvTOCKUC5!c;a&4bEgoa1h)PqcRcn>OXL24x^vA|~u=vKDM=C`K6!;3b$^pLj zP_2PQT1@I{nN`_Dh2}7+-O)tTRnBk08F-TpKi57vqcDW72a5z_(S5Bl<4{>wMiE$e%h?@NYahp=0kIV+?j}r%-`G3V zKHT0sc9hRF6-}+x`uyXf_2)5msVT4Kk2CFIL|r0XZbCgxwY^88&EJS~sBVD&6hZ&~ zb$hz3+=DgX9eEhF{&{vV!~ioW{!ni5Zj}!U9&3GwfsJy*+*?k@JUeXv>1`53szQt0F#i}=*e=3XzKO$$*b_m zu~tTSCcprt*b?wk*wU3yOz&?3^f4*wwSLnY;(E;K$~lc$R&H*D$Mckj5pz}va0D22 z`3kD7+;88RO(VJRNiKx-rE2#1{?^1lQ1P$I8rg4KPt(Z

`IUwc)&aIf^ckHOo{& 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

i%gZP5|Ntj8RZGq_gSymN2d;(cxFu^-{z; zyW+XX09i06oJ1P7ZM?$j9_hNA!vv)1-`eoy1t4h%leXzsQuqpmgg+g*E2pp?Zb}{m zT0KudqnY%d%%{17*FDZ6`E5xC#CPKAEQRCEq~i)FRJ?HC!~<=EZEyeaj)lKXISr;~ z`M1>+0aR(hQ##|>c3|))6s+phR_PSDH@;^yDB~-hT^jI?Fp5<~LRl=3Ka;SW!gjqn z_-PztwFuLwOzoGTOh*5q93UQ*(;aXVvk1g5M!}da;J{HS38IbPPi$whl}excyPVJB z!#X-vOeK+?Kp`s-etdHs2o9<=qk$&D%bshswqRxZyBYd`I*B?V@n>GO4OBk+2=^14Ts8ZuWtvmoeC?*JR9Na(W zj86}SHD>71LtdKvX7!C6(^Nhf3<;j~bkLS@mae$kYCPa(F^qHbKx`YHVa$ ztz&10%2d~RyH2gU3(_P7-T?pDW6$x-cGs{&_6R)V?>QTA#4DU`Kz{gZ=d}XXNR#oT z?cd}j7`oD099;3^{-j~6&t*4DC$#{$m$>I_tod65*?tPaQ8(iw2zP!vn|K90m+h(y z@>538Vq!2ny^qc=cP6~Z>2?4BhJI=@6A#L+ur8sYe4ID)g#AAq;Ok`wwUJ8%f@Wy- zuAA+Xhbj_$Q~cE)J1et4KJgP?gs!})0uraoF86^mxi#QWvm(S}l^rdwm>fssO3#l+ zM4*`Y6SDh5gP9Is3v;iC_vZrD_|j{OTQpHOJiPc_Ie@)^StM) zsZ|zbW?Zw*&O9`VZjgit7_?Eb&B&9-5OiG*P-C?*)HC{YLU!YZSA=yE zPLhYnVtu_VT35vdA`Air7Ins64c{is_rpp$2Km;8K#ZVm;(UKzYT55zZ86hYX192X z6*!8Y2{YRaYM@M71-^ObOv(&yP?Z|&sgsW|@2U63z3r*qB%ulPKZU+wb22}2Zt#r zgPqbTGF2ct(ez4ri2UJbC=bZ}4CLsKFDUQ+=6LW?g{~TtQ~jd~Pn0edsrQNC&kiU*Mq-rCq4FVM~3Eil=n_vp6&|<<&07Y7f=8jS%u0czc-vbQ!e|j zs$8x`rLP~gp$bEAI5#rcD&%{kg#_Y23p2{*r3czK#kPI2h`f1~J&K2q-HV}p6-{TFE@9pXiHAY;n_W!MD%(3=kQ9k>E1fW z(lc+ex)Hix&w9$}=OML3uK(D^-Me|(u|hn@)3@RFO$l!b!oCdNQ%;#QE=FZzP>0tz z4e_M_VXpQ&wL##3z+X+Joti9d-x_?+N;!2`dh^@RhT99}WeAXQ)-jg_(F%9-^Ko^$ zzLnKWx>gKpm5!4QFCx~BvQ=O>u-cr}peGbg`$ffEQ+t%NB!zA@ghKh%gHz}bj~}Bd ztI*HF$82$v)^V?6F42Rlvw5*b0n<~J8jG7_1{qtzdG-EAX2;Nn&P;`idx`;VIk_bx zw4pQV9befAJZR=sswBS8se9Wx`A0wvB;(fqKjm8}8Qu(KCzT9Io}uG*bdH<3Od8Y2 zRMiLhvKSFYkrW$NR*fG^!}2&=?=LCu(3y=;)aEn0`rcCM`wDb5d^o{8Xyk(uan@Ik z4Y{9I5hp4&ncOw}L6AbWd`9YIi9%spl}kjvIY9fG=kX7=0svlLZbh*r*jeysC6hE_ zCek@2Yk-eJx@@wJcND*gmmb=dT&DDhz$ZGq#(eZAJ1tlFQowo_ysH* z@AKzM^k-Z>|3|O+2hDtK88x2!V{v#z$v8IQMKiTQqoEF0utKfy=)2TpW{0*LTp=e5*r+Q>fXBA z-ZNKY*piUDp%S4-_XEe}()4e4xPJyK>_{>(A%PM z?)DRA-7%42uPTw%+M-KSQ|JKKI#R6^1z6e)YQc^py>8r!G^WWSyIV7S# z{tp3hjil>->}7INMU|}77L!I1)Y&PAmo7;Nu4wmu>p&93ax#!ZKzRk6_ zRas9^!gIqb26c-?3h|;=1BM;>adz^ZgkgUfb#?w=}5dxzllm|_S zsy**38hy`x0o3l>)0^+8AZJT{sm)?fG3%niO4Wf4zuTHYy{3=fZ7r>LndbszP|pd; z#gDesp2c1^0~plrom*5jR1?|M&VJH}o8n&kg|Pw_ujhY!ifqkJKsMvYztX2>swOx( zKC_Y8zs7NxyLCJvckzI6^mjmztJE(s#zL{S9HaW2+a7&C<4#%&Nm0_S(DJ9{aW1oT zay9xtgabqo?cuzOkMN5gT+QyI$i;522O7yP@#N4qlGRb|5Q9aM$k=lV+PB`!J8{1F zEv{!rS3`e&_4XN05)U$Ru$CP>%6fLfEAz!`;Pr#&5sTDwZ$$H>vNNZ%iqYe#@n*5J z9uu?kZBS~7@q6ytv}{~@XN zB;scQ*Bu=taFvCn(G!+Tc2xRq`7!egO3pJHK~jW3CkT>(kT8bvo-j$5)0I* zcnba}W?~o2doRc|)_l5y!n^Y_{+Y?EikcPFai!Vk`T2waq4FO7&EU?6&)M+bzKgZ8 zYi0H+VuV4;b`euHCRIg_H%iUa;R}MR;XcOOvzc$Q&aN*jW^s`CN^qS&J1wm@ZPrV` z*;DDU^H!FhC+UO8jw z%zS=+{_gj1V{lWCc2oeqWP)3I+4O7qc&|~(qy66rg_Qc9_AM)rOwDwrwsWAff`Q;H z4*?T1oop8l4?8zAoj3!5fmp>$r(xe3%9`f8j24Vsvn?0s`EdS#-BjQQ{@;!J&WwA1 ze)sA7!E5{cLnVwbF=HG=ndy3G2o1z0UOM&Zd06{$YT|~H5t*2nVDmVaSrhSxALs#Y zqbTIV8Z#C@2=S8kg?EcMia0WKC%hcU$2fQwR=imA?J(eHnRP|9bSGZ)=t_p<0w#G> z=(=H|S#O2>y<-@A=LCFrh%>+lvO~_+*7NWa&1(x4Q|jfq;Ogg#-zJy2e{!=({Q618 zSDjzrxl-XoOSSAzM=IZz<}7#>XSdUD91#$t6QjqMYda?H_=va~G$4%g+>oY6Yr)3z z^qH|nbl=5x2WDyP0R9!%4D)`DVZ(%qmjYhd5_A&C0Ez&K=>g;zL+qKFK@U!bf-S+#Wr_@bXH*SFNJ@*mlUW7%tZW#p8* zhI%Fy$f|&A{K8WqISGSN$3sy$G%pEGuO1%m#Mq?qeewhWOpHy6LhYb?U)NyJKlbd` zO;T<-XCB3$sRk6`eugphy4V_@B znv?>og|7Qd)Wt;n_|joa8bO;v=&Ki*h1VM{e1UC7cxVDM2D3*5vMb|tV^+G~!PZ+4 z_DS#yB&i7xQPrB?0R7qkG3B_o0^MbsXu1;T#fRt+Sd{l{?AxZ;1|~$hefj!SbGaY| z3NBpTLfMJVnfSyl4V@^z3j2&==*No}yn0)in35;5EoChs3G@EH16|$p#2W#rx z;_9v)bz1Qh3hrF4uoyWQo&YX-xu8_t%3+x?D5QEJ4*c@#t2yUvFT1BHZy!BI?pb2M zmnoC`pLD6o$`10ZkZu`=6szgnDgMU|9y5qFL``W{fNjE{`X4Su&LQUKx zR5QI=udb9VV)X7);JVE4Sv(uvwUi&#x$4jGgy=0te@p6FzC|o#@*;^0lC9vOQ;4f$_@?%B#3zw z{|0irrn1D5Q$rm(A;4q|NFHCRkCts2?I&)=pq2rxoUuyJ$UqpL-H6NZ(5-k$lsob5 zLQjb%98r0Kn&HbJ{nUnNxT1`KEZ_))9|gC>!JDQe`xq%4OD+IG*sQz~MVZ2mvrEqn zT(R!$Nq7?N^NcE1*Obq1#HVc|;fhg<6B#a11x~>yx@*!qXj4FxbDWPTeRl|7V#SnpN{fiv)OQ7lP?d}xxM`N0xx2|W9Qe*c6$1gA97}8MIX@D z5O)7|Q+=%cW&zT4-RaWX7W_o}je9XI$wTY=UY`qjoe_SjFIwaF16)muyQ(Ga1@YIV zC9B1h5U%__e|~$LYM{wIeEj(Hg{U~US?1d{aoGSDpjH4}(sY}(E3ULipi4X>>*i)e zY@HwfOUUI4h&U$vDcHOrtg*3Eh`KY`W#%|De5c!|u6TT;kkZVFdigWK38|ME^#jU0h`2{3R8!vQ^!=Kf8PovC(TxT$fGN0xC$R_A|wDIb}avNj~1BpsorB3^4^oDT9xWgm8 zX-}Oo4HngMkvn=jnHois3CPnEK@d}h4B^nOaUh*sia(CwL1J|s)id!SQt4~)$2qCF z-Xw&}(ystf)>{>p{)bJBW2qtUa>7sd$Te2KlF=va8%o^NpFXS6CHc}upph-R# zN85SfA=HCX{*vLpHbv5T!`=1Zd?ejOQ&cOH<{cbYHi5F*tQTUttR>DB;HoV4`;9Xg zFb-Kx=mJ;kVmj&hB$d3qtH~~=ny_WsaP295beUOofup_b>_J3n@(rGM`%#|y{Zy?zdFscA|NqHuaNdSq1rvj0(D-r zXfYR9vYT$dv8qLRL!a(+*fB1F30-yJb0X^3IZt&VN9SVVjrck30*yCny2>F#Bm>fsv9$LlKpuyU*j7|afa7fBw&fA<7cH@GSOrVE$icI)lh&KU& zQh^*{zGE+5n5L%YML8hfmBS=Gp%5ZFbN>l3;ZPBRMf?~UU@2Tip9ag=CBU_Z#*V^u zim59kFTS3-OkdS!Nxgi-r+h?E-lq|*KpQ=;+{%9FEO{kV6OJ^6IidVsZ0XOxQxN|U9{!E z&DjubN-!9&@k8ZZr4P+@WX-E5DaVWn=abV?NTdD;Xht%+$k|T&@Btg7#fePZ7uI2s z;_`?vHQPgNm%sVJwoUpNEZWZxT!sX&*0yyYm|UM}pB6{?f4@t^22|y6FV1=(k5Dkk zXR*-sL$A)j#Kh$LslnPf#(sDQ)J(Rwy%P+)9UmiWA11p{{7nCZ4ijBH8?e)){cQDR zCQ0_2c0cq@D-$@s{GaVN2I(Ca%uDvsOV=4N79x~B#HoZGkSMfW*md)R56t|+kWs}a{#t+`}Wk4SvaPJmefn?)z)Qsknm4ED;>C6qq z3^KjF9;dvw0lrQzz$Le@FcNE_MN7_bq&;u2Onn5=%RjP%d!N?PfB9#@@3bJ3<&J_g z`$(8p6N!8%jwmzHKcA_-m`Zk9jFJn&gsD+cV~-NyhkxO!9oOetH(quezh!6t)b8wc z^`k#aOG|{W#>{isTmHy-inYEv<8TVwiZiLeASFW@qB)d=B>V~s1r}2y#PfL;I__S+ z76HXv?>k6p{tb^$TMwg?Ha;=YW(1OodgFy1v!Dcy`xPV}AU4}-!8KyagG+1QK-vpu zs-YK%NZyv{j9NQ(K%B5G2rk+Iol?=jh?p0;aW!L22S~-vwK$Y9QZwavA9c$N7(TjC zw58^1KSsWvCd<+_eecpIS<&8bFcr4DyV=k+cySE&Znhz8OXT~go>NhzWZea}sI?mp za3Y^dLe16l`N1bXdTzNehT@S>ZaV!iqegkhOG?wgyfUC}sa}Oz|NM3F3yl}9ahz0o zWS9?H9#I%j7)Cv7vS{pvbQdkd&mHACUl-VI4!S4GY9=qCJzDaG_=we1w7^if5U#_Z z4531J-)*+QYJkrYlnli|&8psjlylv=DBL|c(5MBB+#-l$3rWS|9m#NpgdITBe-hkn z-=~`56|m`<5Du~=7<&GDThXvVZ1&i9fot^TU5>}rFMIrk@|=`cWgCByW_bq5yq3=J znJK3?6X_o~t;&S0Su(zcrl*d}-g>*qG@KpOEc27B0w9 z1-M2PsGYP4Bz}*``k@;x4t-?yu_8t4OrI&Y!yrY%@I6t=qD(Tm#Oe#5b!Q?X?^A?I zat=|LJ)#cluEOF7oR1uPuHR)i{Z*f0$st-}L8Ca6Ec1iXiTL0Oi5h$+qhNj3h@0s5 znKLkVTVN9~_&&x_87~oy&Bc{cKxZr~iR*4?66pD=f=5FBJ$ z+j#Y21-a)--p&lQeO^{M>iP-ON`()IR#8Oe327$yqFC!T)Jg*c+;;>in?LbxtXL>` zIdE)nMm zVMOO*mQD*`GG>htZg<#;zxiRlHUs0`2liW-Jn zcx$PYQ~h`6Z+MJ>zTVs$h)KM0lO5zd)Lyn3EQyYsgP@5-8U*JidA3O}w`EW^k;^Z) zy?jZph`HXC&9h*`&{lR~=X=%ecmel9&VjlB%`M((iu22#sXl+FdhL&u`<7kA+d@MUPWDP1w_3IRRJh%J>@5A-KXG z%@Na(WXgrpzM$qu##ryM+^HF|D6^ZmZ3&|K9##JU7LCxSiT9R2CpX*^svK0u$5QU@ z*<1DwP<6Fr931)9j&U3XK=6ZGDm$4%mPy{0Nc-o?ji)UG+Zj0OaD3A~tjnBvI>_!s zB#OG9@!>;7FHFeu8`ZJmE!nD4MCtcd>qGh_7)C2!A&CM@0f_aC@}jVYVUPZv$T zGTbC`7O;rlM-cQ^eD!v~)O%t*)d16`f-qgQCfesW$cgs+UMB&HbUr5>zHr~eBySzb zR_J%Eelno^vjH(xt%|zK)-wV5%|2(VR*>FbC2R9ziW?L!Y&({3Fy5xPds=7{U^8<{ zW2EFmzXS9gv54;L&x_E;lFN;n-5~Mvj*E(NzKP1iJ-^*&G}y62s&ozI$Zq@J2^D0F znO^_eNcSoy$0M`pgypIFm;eCRj+`O$ahMw!imial;&tI>M*X8#SWL2^y97i03h~Nb zuo>!7^}PL&?df7TSs}D%g{{X2I2z znG{8a64bri5@6|%u7h1pHP+k+&gP8jE~a@X6_a6LQthE+Oh__#)vbRC}8|lPgN`k!o+>ky?M} zyj{4#XALldmUPI=2n(sL5ks?2F6!91cfCKBUoWdq*Bt>iXH~WS#8h{N$|z25DwNgV zJS=Z{mVNB!!0D*v*~avLtPc_dxIycAv(?8-AdS_`vYas(>^Mh)?^Cn4KN*na3~)+0z4z{fescXQ%S$c)6%La5V2Z-Ilr`=6 zNxRUvKgiWYogm?HIpuXN0s7nb>71O>A@NESr#f$#6^Vx_d*^*b5jNcfvE{ZF$g&li zm?A{^Y*vQL1Q{&7*Ba*7#1VV$4RpbCc$HREkCDvx>y(z;&SoE|zZuf?9{dS}jmcT> zfb(K~RP!7bZu!S2o#wPBsZFOmg4Q z1v6}s9*g;vER#!-;eSwI-*zNoKiS|#Ri;Lly1-F?&+tpK(8qkA;9u}7F)rgZ80bxT zfMn?*dQP^Are)rJ<90oio{oAVTJ0#!KOZ+!w|^Y{M_B_#jg#}MBO=Nf;K z3zhw|w-<-aJT<9$y4I;ShgIfU;9*j`h^F7k^s2*W{qQqKe+;&u^aPdT8RqfjkHdu) zk?7iev1H792!@0DU-V%VzY9J|#PzQT4LuAz3=a(AuSPPh9C8i64^nC_8KkgyKGxEJ|?ANwa5G6<-Grfn& z+cNoeGZRJ$UNg@ZH9%Z(FXQ$>Ds4K+O4}r%bSBW@)-@!eg#GZx8wayTfMmz%;xaiS z;yB&%sy#2U$+JI`nwom*rEe=2)VODWWJKJuwwpU@{LAhs9asz&P%Si;q<#u3H_>uG%%nBxU$ERIA zhgHu_?x+J&d_)|(=$iY>Kk~?1-RWuD37tWj!3_JyCsO{cyUs>E_C}rrF4w?A)C+JD z=T17;VSA#pg9DOl`9=q_BzM{Gea^DQi*b2GFl`fYRbhug=awhqg6 zuJwy+)(u*&%e>aStF^EvD|zshG3%D|QAD4@VZLv8=IHK$um6@}DM5U1+nzFIk5QGri!oIF3+(9-UkdY)uIb z0{r{o>*PC=rn(678(C`Q1R*T!uO1_DS7pBH%cKbl$1+=%vpWL@5mATh&sr-^3s7bl zL(15#3{7(3ms!i7TgKn~Rfk&vM>^!(j)@Ql;_j~wBx*@#O5S<^)CODCFwR~<0~b$X z`9g_TLnX<)bBtt?ck$fhhTS_&2Gg|*9Rn5jaY2Ka1M`QidGHsKFrJ+Lw`!new(Gvj z8ddwoU>-KiOb~k9M{sahUg+K4qw^T)BqpB^&BKZ`JaEUXA~osY4{Sh4;!i1ro`^C{gA zo@QpVV^?eZUhJLwkTZ6x`1RBB4>m}g<}{|$_w7&;%6t0k+mlg#bdRjGx55m}tL+R{ z{;c%rx{f$PT%UI>x z^4w9uOe=R@Y>IGrJXNJhxFgdn=Ve*@n1=q~F{9$S-R%OlI_lhq#!aD`!2+J%JWX?W zOuez@1mcoB3dR{_?oiMaH8p2{a9zs!z@P9|HYk_*_(M52IaK1l9W zl$oM?A6Dz(k8I*Rw#Rpt39upg)BSu!##&sd6;x~1>lh5H-}mV_Js0nnvx_25<21f4>ZZ$#fiVK-0JgNnZpluw0RHqfuu{n_|H4)i49>s zZ;qwEL10hh(TK32VeDKo7D-nY$Z(;k_hc8L6lJo6#-F1e(0$#QwSIkWBZyWDU8gK>>%beW2k71Nu=02{I2G;Dgq`dyWF#nd*ij`M9xC!Y6#?$^2CfGPm}4>Ks? zeJ~hF(6^VcjFfX9;C(nw?>MX$4VFHsGcX|*`dqUdN+mZIqn*odFUnd!I~>3sc+h}P z=Hh73kLPN}oIpAsYCEZUBu&2DVQQ-Vo_{&8YA`B{S^xNxZb0+tz*px3_zix)taeAa zcGdW1dWWbz==U9ShlUHBsvYv$m_E8h;W8|rj-uDNF}y1|Suwv~F%S{&&)&UJLAu;c zV&e%%dihBq|J2{VkKU}-Au}kQ%m%+g~m@x+!}Z`Gg_pRI%#dj z;)izouTwG`2=%ba?XTp%Fm@1hv|&wg?0aQcObxh~zC}@{iW$I}q6Fn>E;tUL_f7~u zZ>CqLykZ^Wy?Howf%^DVUxO-%6Ln`gR&)>yNt1jOiBF0jGW(T`0S1h-b>lO}AjsJpSHxk`P&{da*sSG3J zfc{VGiGEWNwulWI%ImIlr?3$NrYdu?QaD?k!ba9ES*-=4)UH_U##PT?mtwmdurW6^)dWgR>^9o}EY!(E zL2K`y(}Np3w^ui8W2T+EPV9@>o3;vji1)mHRUsz#%gep8fb!dBuigaarFJ_#yvpJ$ z%KilwQZioo&+IF{__CS0Gbq;XQP?PBzN8C|UXwk8x~vfEz69!+hi;Oe0BW6f3+d_OwzD#V@Vy54Klz+84T7I?6Rtofu%ktG&e4MXVRVT$a66ouf7U1H58m77*Z9YX zBBht_Ewd{WsQk^cx`L*1;vv7&I~!1`^y&ds?_Ktr%A>jBSiw0&!LuE0GIzPV{uG~_Mbv!o~gl_Tq^#6-fp*kLj%3?#K~-qjK|` zSnfuP8xO4h`;6#H)ejTh*q+h2cj=Wc>1wFm+_oBYMDNw@$`EB0cpXU+V~@efz!+#* zZHpw!JNSNgj{3+~Lw%9&Z{rYJj->55cZ>qGGXXJ}1hq8H&&_15{ zzY)FP2bPxrE@81(6_%Wlc>h5wm@gjThT;sWVt`80{t*<#4 z`n;mQ+ix(C=&>T42|HIfsEFA0WxPRT8E@<+S~cF;XOGS_Zn2j*YB*12yb68>hSzmU zyYhf$%CQ}AAK!tc#)S^$_7aY|@{T_qEjQGb|FLbQzwm3g%+cEO%_jSb*EXKf@YqH? zU!=vau64H~I8BImOP{cxWT)(g*P!3Sar3QP7m2E?j{!4smKmeCk8?WVgIgJN2_u-( z;fe}5COPAy;ybuWlL?xu;oeo!!O%Bp!uc$-@r<~~PXnnJyE8aM@3p1cVz2LWRP+;^ zy&@H*eRkI-7@L27PD{hQQr8`hNtzrK82xyT>N&L>WoT#Ln17_-KMQ=E9y1MHb%?E) zDo<8DJoiVB+?uZL+XnlU?zPdKReqesksJi}@m{yZd$kEz^uNOLOExb# z&t0}|#u}bG7x(^(3@gJ@U5gl*-IiNiy!JvzRWa;8Wq6^cC;bEK4 z8YtAXCa2!z3^z(0s`cxwg!3EHw$J&JlI)8M+HaiJW(3Lxz=?>MC~>{!@JJWT^U4X5 zBE$dlGX7_zw8jD8ws$4px*Au>o#y*-*4~D;AufAg+CF6@UV1H%qw4w!`|F*jiLJvN z(0y2Tw^I7mk*o!~1hh&uqoG?#PJDxfFK?+#1<$681hUClI}wL}>*%n;T3^maQ3@{iG`n_6J@Q~>EO=S%+l zKeoOCD9Y}Q7nbfukS;;GYbjAuLIeRtN?JOX?iLnNQb3SY8bP|GRYJNOq?Zs0se9J? z{`bzEapq(E9AMA;o^zh(_x$2ynaIZnpcO~!`O1)J17aW<$U}N#vNJfEd``GI))7Uv z<7z)sJ6H%xa_Kzw@s4F>=_D~RKo`7@$EWM@A|Zji2#C@tFTgxTA4ny`fH{&XxTy?v zfdhCk5U5Mm6t^MHF3)yLW*dlpyveQ{Ed&TEyh>^Sgi}2=eIiPU5PdX6JZl&q^xp8P{s2IuF zL2@{Xkuz~l<7Qv@&lO4m_e~vJfW(i1L_=<)N*|lXgVKUW0f6RH#;Tg>eVWhjYDAfQ+0h$_4hq@*4V2`z zX~LI()(o!u*SJyYwx7D|ziWta1+j<6Xbqu(=fE~`R?o8D<4kQyy!)1Q1M`AS|4&Bv| zjJy*B#Kkj{-}Z^$w$V3$2>^WRym5X5F|GIi8))763Hd+@YNSJnr46P$^3*K zK}Tgn4Ui(k($iZD1adL143wh1jf}L4y?F9gC=d&j;J2<)=xZp7x#)~fF!}w|O@Ikw zwS~F)#B_|6H+CfL$KRu?X68U5nx7y5z?Fg<<%_)4!d7IF?sIt$@UKzfvlP87fl~gr z=})g-p|II*Bc7agY=+U**(FC_NjNgzTd|>={*z;%El^HkNW&!u>8gr6KM*>Ss zEt=J9ul$#AX41}hFa(u!2F-RZw2q7(SopofbjJo#W}vV{tA%UW4kQIl2_gA{@8 z2EVesAE4>7mXQ%c$SoJDRNTS$g7F9oi`=NslHPT+4vD|NsbUOdGovq$PG-DYw#7pZOT2X$w z{?Anuuy+?~j+ieIZS&#tyOLfEZ~jIP{w)uqG{B0`9%OphNQRHR~HS%E#N4 z15;-}8AH3K-&iHSF&kvyP}iyQ^3|*Bt->!6v#7YXwbo4GHti7dHK60UYz4m~!$Oa! z<0~klmW7p}a*?8WW{GrQ9$3%HfoF!6B0ns{D$9bP_gn~i{@qOSQ!AKV_1?5+(AM}y z%m9TUXgvWP145o(+Ia98MX9T9D;_Xf;)q?XR(NC|rkL?J74(HLGv^Bv=zw^UNvolI z96u!Cs^0~<^-9yF4|cz$ITD^PGh8jat)lgsbsdnNE8gA#(2uav)aGc>w0kZwQc;4% zUt$3lE^9h^1V2X_lhJN)9BR>8%A`CJ>GqF?fr%BKa&7yAWHL|y&_FBs<{tRv3x}0a;N^aQ{YxRp}P`2F}PG9f3HpXuK@r3h4Zg< zR*Up>uW%#!3HbcI&fH@fuJGFhPkQ`|?EEqc(#fAV}8ei?SnC}nxd_0?yfl#{sAo4c2in7C?wJZ>C-Q7&Vm zY-ATO=MgG$3ZY$GmstZ&eC&&aa!AB`DFj8vVIz#NSz&tx&*RW9Fb?`usp#-z;>|@A z`)hr+N2fO#{?^_>J?P>OMCRH(&;y1CV$<*Y{BK_XzJ3uTHxdRd@Wip_Py z2{7WI**N2=LJr_TX-F+#HD4SVRDL;<;QTC&kq_fk+MRNJeAdTBUMI8-6wp1Vy!GqM z#}3eKie_*hP@Dif)FoGlqz(s0{zae9Y@Pgrl6R`vtjrkHX$ag!eMYdpruk6djDrN2 z?=HN}fxqwLO7Q+N;Oc5vuZ)yp9C`cY#~jXs6Z`oJt(H}Px5x%KUm4!dH!Wt6x_u1B zK+FaKwZzOj_JOb;uJW3D6c2bu+UcZeSV`uzm znb2(&d}80bD92~*gB8cAB-;yDgj24nd4qPuw2ER(t)K7uTZzeS> zzBE{-t62HMJ8VIVJ+VfK$@q=n;PUyOK(}1;#>ejyl`60H~rgd%IdgJ>*|24 z2@@{yjmqHhN_}v;>aaNOn;U!@0UH^Lqq#!Ou$2Yi4aTuZi1KR}yOY->c7RmpskjWF|Y|fiCaid|5{dED56{i6Agf5}Dc zZg*6G6J*UU0uSiLKe}Wx$j4y}az4J#jzAKD+fE5g%(KGWchXmSmw^pQ=7TyH3-*4R zb)exR`vAOvpQ$lDm2eItFrmAJ2eBxP_&x)yft-fG&g9dm%|cZ(q;}~6onnT)Q6;Da z#ituB^`Qemb;4oX+z1j+r0{Zt?0uG^*tgAzBT9hUR3gHpDlai9=^OZk_+zK*rjwyY zrpR3s$%d+CFp0xFiytE%K*G?z7qQG0L+Bs?+8wKX)GtB$N$P9=U^>v9l|FOg*+$v+ zK0ks*J?j6g{69BkRtHYP&3&QGQ0C`YL4b8QMd{B``c;cq;&>)R`v;gB4^VbWN#a_a zWPc@0yW+^zTF*9U^sE4^1I{9?9L1v}Kif($E-+cJA zxMT4e{q@;v4rdz$nbk88R>j7LDRYC_D2ZuXvkf(2HcWK7#Xg|R<}Nlhe-=wsVP$Gx zC-r^*H5$uIqn2sSr_RT+xDn(cu4Lz)85});qMS)N_Nvko{{qMfL;Eu0sYs~b^R-yu zNpRGDeE-U$ljFb}+^cPD?n^Tp)ODuqm{ys;+DBqrNNsn48SBAtt)5}*JoIvbN9n{8 z+e5aa-cGXRUtM>IvmkZ}JVi7Yt*)3>E=4V`+l2gBAo4qc z&Udn?DU|nji!1xz1Ddj91~Bf^O%?Q-C*;qte!XT1$wzvKzD#0aOB1$PvJ=_^3;qac z`dR`;bo#W?diT~W{AQfHDTMLZo5c94Gm*v-lbM-;VsYoRIl{=pY~^JXH2;d zrdL7EcL!q+k0X=N%+_vV>P{yai@zqS%nM|Y6UKd&UVj%7|0{!$Qo}QUY#7m5uJvz7 zt;&n2=zED5&Qwt{B-yi24kgKLtMj7|z^^{*PJ_M?b%w#8O`;m!3a*6XMZcRv0jdvI`(QDx}lJq<=q~$=JSqz+ar)>M{ltk*r zlU=Ru-r5Cf>B^ki=Ie2=xGCiE`v5p+9Wy2A9WDx|0l%|(`JvEh6UiFTBPIC=ahht_ zs_eS}0kMZa7$4+G)2)M&HreWuT)>8QP!wS3X=vm4hj6E5^33(U4+X2hS=O(^_o4A_ zz3U>WPhD<7<<&_R(qmnEwqP=UV9+JybLZ=^1eP1%`&-7PKdy7ha|h{J^)Dq&`L;mR zZ$z>4I%}^|tD(#dP|G#AgH>MhVl6lXA>xG@rGPBtm9Gau?g9=%2w)?H@)sV@_eJ_YW(Rp8Hb;_PmMuQQnNi=P zwE*!~sKMn&(?&MWT>0x_XhDY5*>Xc4p-H zY86V;O`yl>ugW&>6#2n{ujm4-=PSV3Nn%q&23(ULv&ZmB^Qa9!as2M>5-YO|t^}*h z-3~HMVJ)Uh;QD*JdX{F+w(C8`m{*wr=$uNxRZGbMg$L(;1=A3)jo4IBhMyS4%r^!s zrbo2DH{k<kpdVX5`6#smN zkT0q+y6^+bX8xO6+}O1kqS1PmQ+JctW`tAoW5_SS6vafe1T@%Fl)Gx7uvcnbVnE5L zs;(}g!dB9Ay|EoYxn4PG!V!5F3Jd6lK1=^AaxU!g@8h zizfiktE=499ZO@SsdD{h*?FWHdHGE6jmcNjiFG6T9gXZSuQ z(&jjBMQC5`45=ML6z#h<`Wc;;|jCXyu}T1VOJxq_{P6`yAuu2xiOIPquLo>l@mL7d-1O&p zw(AP`4!>>4Of=OK#5$C?Ol}EC6FoXcxwyeuYI+#(=u^`x(LFx zJ?ae|ST@C(pR=I2x+WWW+w?eSU`c|#_*}g5qX>krVOx+(NN5<&Jb;RZnKGMn_S5t! zwE;PhHKc^cux*<ij#CH#Rv7Z>C*k*;%cKag3C?mU< z@@@p)6uk`x79w6KEW4220r8kiJh!be9w4As#xu!%rLuJ5*nDcKEE!|%dK3o!Mo-y0 z@|MI4Q*%9(B@5+x@TN(z8iFv|+egQ{^RlhWGe`&?m2V!<#Rf817kFCgD2e6JxQm>L zG0_0&&J@LVGA#XC4wX9T^cCpTZyy!pCc41SsMR&osSSt8hM#l55=CG{a8GDFbB{B>nqZ;WY28jp!-VGqg|`=yO>V3AyI@)5#LgxR=y9BsaG zYmk&hPX6GD|4J>=Ea$v7=;lq=Tnq1Fp=(*s*LcbDKj2Ru;lg_3vKhW0WZ8ds0bM-VjA3jS&xB6FU?$NC>ppUEv{=3(A#>slguK;s zsY%4N$4rFdN1XfF9;6okgb2VcKy}gU33n#j!di2+T=vU*7k>`QKQI>#HhF`S5)%O} zU5@?-{|oO;5wCh+yEh~O_j1xn@FCaD#V@aZh_W>rM!Uru0!8wLQN~Q4B|IQp#ejN?xKGl~Y^Ef4E9*lX3U+|_obxYkOz8{01947|PS#QiCN{w))&FI$hnOknNH+wqR z3e4DcZcF^sY@_2q0$X>&P$b8^vqE-`vrM|-)dT!bY8-R~^=|yzeJTnlEIaTvAqC7# zF8-%iE4hV@oPVbef8Gs6@NPgknL+pAdWSq(@zfnC1c?%O48|lVZBtf{@A*u9K_b)H zfl4HMfL@8uLXLpVRmzk68zR*vmzn%U~&ndE!2D=jW#-Er0W5BDvII=U9853fmjo)0Fotd3JEid zzt|r0^AYqtlCOTg&4DfG1C*;TuZMH~V~a6DWZ2~v6*SOs?*{G^;B!*|-+vrkwsqH8 zXjkIL>o7FnQ!`u%Bz94KCoaSm+{GAHroykn;-66Ft5)&n%WGtz>*8IZeD?49Z9Ykg`?7m@*k|Bb0G*ic|hL zB^&&EE7RN@`;;yr*@$rUcH1Lxw&_#RSsM$f5j~}I?G*^CHzHo<9&-kda}0R-@x+)^ z^$@V&x~8YCS$HoCsbQH8iN3cq08Ym9N?^pio6Lfy06Gm;5mdKak+zFvrWv79^hom1 zW9IV}z;Mp&T>pNdLZFoR4=dmhMAi5h8^>zJn4?>6RsQ;?v_iiQ^`B-1S%zYhCBM`q$GG^?hb~x-YRdWcJUx4;sG2BnuY-)ehnyQm1)0EohFaG4k@YDqjLG z1cuEPP6^3?vkXPOq%$#JM3}E-{RAUWeb6MogTpJKjAOG9dfL3#u^Lrz0(ASgQ*L=PdBVh=}z4Pus@|NG7EWJMwqF-Wq2POcT)dt$DV zcO;O~&U5GlAZFG7`jK*iWUYsT6auSzvaJieZm%A&;JKyyq3!zn{+D5n8aCh0|r@PIunb|J~d`fm_Gt^q^MCUFU!lJ8M%|a@l|!Q9hTV zNKLnref2U02`UqssLQ$cvOCk<8-1B9hn&-abSEzKxW7=-xT(RjAn%FU@=V)Xr6Bth5tKx z{ci+*sB!G{ZYWqbTy>MGJfr~)?~4Q#c1kKu$BqMfW$bZ}QYf+>MLvRP+uzA@ONY(1SH z0Ji_tc`q^Z`=6imdCJsfoVH(G%kLf0tM0@wIQ^$NiIhXdd3&unur)y{G|{4tT)g4Jg_+(tQL z<&^QH97cbU1n}%NWm+pgD@)akgf{8_zmJ|7>EWCjd$bZ4XprUl%;Scg%Op|+6Az}X z3gJ=D`5;k>D11$(MTad{7kp+QS|anwT75YVIY4EI$I%(bK7_@}v2|q+7vUi=&Vw&9 z%UK4Qjwr{x`eL?l3-<<@o!A6l5u}lp;3xi$sRhSfBAB^il`7Hu?BTmgytdNdd+Pt6 zlONcxS?kE1pJDob%_J1cGU9w)&g@3NB_Jv0@0T_I+}s62!@C}qg#i${o=?1eiSbc`p@>z} z_1@nIQ!p%5OW`+_z`b-T-$SxZM3k!kv;0AxsYvHfsy59mZJHo>Fr5+wE%whD(K8PH z?M%?^cLT-1*(R9Tm}+Z)_*Im!dZ(^ez}OV2eExdB*!Bmb=vB=?Oh3i4&d*N1r2d)yOyhjv=5-&Ek zK7L+#Q3ktKfNJq@{aV*(+pSSNClk}pJ=?bJBZCi7z9PvLV21#{il?y`YF=s-QT+9S zf&!ug%}+X{e_Ibg-PM3#>^P}ts1~{#p>urJ%FWTLb){L3@r=m&fOKB+%?r4?GyqAU zz4qX=1(KLXp+pb|ALl-8Ut@o=SyV=^FylsbrSWV2l7fQXAW6J!bknf@kXa{3_7YKP z>S#jug@tiqR>bSTyaoF(Im;K%1Zt9=4HMg@qf+25vyF?4EhpfQcHYHmFIsHDac#h> z*k`ii*(RExoX{&zGr3u=4DVJC*$Lg($4zT#Lxcm*MU(CUeqQ*O_uJvI!5>0aS2+mZ zH%rampep;~GGSGoU+z@OZ($c2(;OO)L4DwR*L0x$b9+p~byP%v6qOcRZJ+o-!r#5C zPuA1+y;p?e4rNC}z(Oj!Y)efAZ)N}g)Gau7v-pio*tzi^6n{mZc-YOisBLP)@9%6R zvF_SHTZ|vGVOJq8BJyJbbVH7<2XhOGpy&k0MU7)Rg+r5s_5LfyBJw5PBn89Tj z36sqQi9Vi0{#PawTk3LmG^uu#y4-IE9^b5IFhzW@zkFsN9q4w256DHDe&Pw%dkPLxp|~Lc69Z%5M5?` z{q@3yqBM(^p*Juvq!kBR!-%5Q2N6YRScFw=*bOabcdEV*ZO26_l;1TWZ|2}{!pPae z_2~qw6PHb=^ju5%Def72Oes;!5+xl*nUD#2564eG?3@Nvj~3ujJwJ$je=p~Za*STl zc}r))iOMiQ|1M@QJf)Ko#Vn zZR=b}Uh6upwjw?3T1@t=sy>v;%PGww*Npn-mz3y6)(|p zq*T60cvhk=@Fq0M0P|h{b^@^xSztYOw;-=#KtlulTqO`fmqD_#Zf3}33*1A4z^I@Y zc#sXnf%9{j+}_qSeJPgN1GHVGBuU^A?g<0|vRY!h<1hj77UH&mE!MN2LLH}Pd#e&k z!KZy9@AZr+697gtd;WwLO@xo=pH7hn2VtJ`>964jWc*C@E zm(~TBPts%{9@k@nbtqC!uNMflt+k$z3=Ee`^Dkd#EGO++#%h}j9Q+ZcC6(KNar7Sf z7$~r>S2#(uDbY?@{7_7>c$2em+=%@9RBZoiGpu;Hp!zBJ->t8ui6ER;~Kh2o966%{ix--9Bc~n<^OzXSU}aou*_x3kV41XLeNJ3ZXF(W z;Sh0f>3ID`RfJaWPb>aY#uK7e-8TJiP7adHHyORd8n8%?sq92z)Y>Fo$GN}3u48%( z(R^GhD2OxAchDEEWhZP$60;5h_3q@*{eB<)nZJUn`zlN7!ZZhvzB|{@{uv-5_dO|V zDMA4)HJ{hPaM6UjtAareAwz0(j>Pq`7_KF<0ea70MQZ0wm6kS z>nDCeIvg6T;U)%R)AWV9;no2n-OLHJ@CbJuem#FuFU_L(TL zcKL&cAcqD`|0=F6?y(&XRnW_w#QqbzHR0j@oBb7iL!Pr7GZj0-wQ=34qH#cK8W}i` zZIU2Xpy>4)@GBJcgV z#M)6{_~)er8{lBS1jx{G<;z4UzM`Qf@DNCa_$@4 zz`eeWlJnN|j<`0B2d0Z`EDPdu+r;IGyG-nc7xysrAo|A+7?Mi|NZhIg0EMac0(pph zkAXbvhJSf?GgwEkAqniyj6fv7{S>D$`H1&h%qDhuzHi^pqd+ zSP7)Gv+6rijYG^QuLzc1`)K(8?xsj6#FPFfK7+VnkX4?LDPIocTBjkK&i|}UB+?~h z>9Fmzc7j}_kv9;#OS_{d)cG@y#{u;|oX$Y8@iu>5C{&kfn5Q{L?0P_@guSKsBK!&U@vv5k5Mst8Q2vP$ z8TBEHUvB&R3_{ygmEAdWx^{`zXLkB&PS6nNjlG9svZm)J+58qArtszOed}4$_?gq+nI>tZZ?GAR(CJ^ES6?Mz<>kWJ< z>=R8v&a+M>I_>Z=B*RizXcN9f7fa`b=$C>~HINP?r+5zD-ap6nm9ln$OSBAe+pK!j z!{OSu6w^h{`4{ia#!|p#S^08)N2u2W;czv9EEyw3fg4@ z8RAq3!WM@VPi;~9X`-Vy>;z#FW!Tm)Qhsb*XMV!C?Ow^U?&`KGP9;lOPo)@C$#1ce zke=M;S;oGl&cDn-PZ%+aQe?&)(tPUoF4%s|ugR)K^bg0lRSzAhdZ`EMd|K%%!Tdho zCU8fY-Oxy{9o7reG5rry0S-LC_pbHXW(a30{HBMsfZE3p)9XsiS~DioIly2a>v}9R zT_?0Ymw1~{_s7B&)y}>=7n`~m4AJ?wWQP#y{`@ncBUEPKT^-~|DK^u-C3<*%_xGb=Exe>)E3 z9IKv5k^-RKw;?}JG^W%E`%i@sgw9O-ZkZ*h@NHp73JO$ZvVXm@vN-08)kH^{o0{!Z z9yDgX0|uC}+g^!Wdh{_Qj0Q|IKLBvqawuZ<$=IW2-Lma)vO?8tO#qr7>)1A9HN=GE z2ov9hP?_poW7(#cqb^lYBEv|!&~HcBPb^cJ)-WYmaD}^y$-m|bIrYbx$F^zY+dm$y zoy2w;p$_9-=CF>%bGUan{nW$r{H)@lma9Ka4C5ADe%bG9tqis=>hi8EUBK|?c28+f z0&)2=oNeRsfVLRhpjI9IRTK2@!ULG(%Zjc2K|B%2c42BF1zl*Ln|*g&iro-1Mt+*# zp_QUKhHrTyZHJcdvWC=%b9*l&tyU)!?ZW}@8mS}}{0PV9A&&ritbu3o&mlzCTV16!QRn(X%YRBlk!p0B7*jBKx7 z$Q1yaK3cDp_&5{Eg1Eto5o*g@#b?AGsOSxvx`02xm9)y0@2YpJxd!8VGwr3XK2{1z zmF^nPxZakwJ9ev z-QcHhw;B*Ae`Y#s5^F+cD%&1oM$O)?5lDv#E{8353_HGUH!6iLJrjo-8k%CR@6x!6 zi!at@(99n4hQrDv49W9q$KlZXkg5O6JQ(VPCE9D5{I1xC1kyGI*cL!H<-~ z$62_$r&?=2hq+81|A291f7eZ4S9dG~_*ohdw`X^5j+|%MOZd&xUY{cFqD$g}2BwY5wX>lS^&5AOJl|Asx!pn{E zf`NYquZ7ZCu5ww@<{0Frcw_!AYAlByt&s&>9cF=#68GymLUT*YYL%h5oM^)P68{1C za`fmio-dRHcPN9Nuq`-5Ae<^*j|`+D4Z>W2qE3QsVv&l+Wfo~T{lnFZtkF*Kt(nN| zB6?YMA@Dc2f%35wUlNWfm+OO_q4eY7qfmB6U)eE3>b2dWBDepmLPUy+0+zkDyrp33 z9ZL6S@8LDEbglvZ7ubLclR2&~XI%x*|>rFoa$voM1yuqZHn3!)b*)@54MxJ~k z(EI@KXk~Lqo;Q#+;UHq;-VDEeSe{*3et{wx=dZjHUG$&H3^U-bo%E2;r_i7GkPjG( zEuC)t&`(xb`#0Pm4!Xkvlw^aYZ#Q4w14!nwvYT3F@U?8QeR;Rr@((l;k%F`!2x;q~ znicSCl7hEC`hIVQ=GsRe?Bnh?Gv?`kUuxuX%I%Z33Qc->-k=Gu+@1gt#&>2F+fBzv31!x?4-Sa}lC#0am%VC=73(7P3+zU?;boG8Rrt<-LKm)$-R|z*k zMMl{uAJcA=!cpQK`Y@AYnd(YrhIi8cEh{6W^A?H_RO^l?0J$<4Q9oVvxH=rZQTF>4_s0JMVD zhlUVaQ541sHvqGQVd;DAkovI~{osC~e&T)3(`G?`-i07w$D=pKYrA|IJIQra zqTgh`JCwI|ToPcF{9y~4-~-EjZ&s2}RvwqLeY9+J{0&g{U#9?*iZ101ihrmjaWZgMZ0BLi-(XadLhrK>-v(j0q?|1>~7~tM}_%kyfN&WdDAv zok%u4cI|3SV7RL$vL9dK9`X?(|2)5gE*~KM^6gS#A$A81YB9}MeB*+?+g2W&^iB;x zQ>wDBifMZVz{aIl(*w%zAII2Xyg4*qBM2_bfp zjmyiD@r`~PyNXI5Y63$9qM|+3eRn@>*qmMN!2@KDD5+D6RciS(ECLYfO)u7DuO<&Z z0)Gw>K<&YDI=P|shx-v^&bqMq^lsN?Q9m=WG5X%>hMDJEKVrW3l>(ckJgS*%Bd+@o zZYS!ZXREgIObGP$Gd(Jxqum8`klrb85sE6G*K#7*by`a~7avZLFdh_onhcq!zlAG+u=RzYK6%<>j z+DGezFMIcV+`vU#0}2|Kr}DZwiKIc0lRm!$ClUg)HgSZGmLHT^z7dzo$$8qm5?`RpC`=YOd{tqkbBuGD7? zy{-=>LTR2n?Ikku+4*n;SeS#Zsj{Vss!Whh)mH4iuH)A+5PG_wP*pR(FD!s0TyPiZ zz=@iD_%Tym@#=TI$BGBq`cE!-4+cURfs|>g7x;Sf0GV|1Qc$%}+pb*WFq)nGW5Wl` zs_1R*_<78`F7I?VH%d6^&z2}Yl)inL^nx;P_$G~5kX-*H9}(gR+tXcpD@mOz!=ty* zuL!zBuzdlEs`3w`$!+`l4vV9o*eROLqsiuGo9NN&B4dgN+J9IB6ve4n<_U5_{32YA zb-y3QdMGz)6gYQ(2Cb9yQ*Z#V5Vs6$zx=zyqBP`)dZPTpmTUa;?XS1sP;$xsfBdY< zPDoSSc)7TWP&*nj-d-82=y_%8ibJ7Lq{eQUhD@?@>=05lFJri;sQOt@GAMA|}7?2N06hmjWRrg_`?m zu}M1A?w3_f2Rmww+!|qnZF`H?p|iNQ<aAwH)t3PBF zM3s>PgE3?56b)EXcjJnC+u=>LxDY_8hiPNOEQp}n{7Ba-|C_GdU*hS%yxf|31Lgs3 z;KV8h28bEL0hi~OEK2alm!Jdck7#-oA1^?tM2ju0YS1Yc7@XmQd&7U!utcj)@i_D0 z4!%;A5ah-Fvp2LVFWIQ;I~$x#q3R1+yjGXGDWwL94mQ)39*;#1%Ho#=OL~o8V7}t~ zJpp5cL}>=@y6P;&SX(Jh?c)`iRd^hGt);udd8Secy_p3DaJsCJb%&fU85r02UD^-B zx>KhX&Gda_K{E*5Dz4;QnEi>14Krr)ulC9O_bRr z299LM$2-%Bb*ZNcu97gmN)Am? z&HmgNP^$p-z^w@3AkY>x8Y)oCgiZq|>v#z1Rb2*bx=C~KoCKSFtZq5wnW^KMr>U3& zxFxol)OTZz5-LvYI(9_&{}UN6B?X=}^~bzluA~mDj|QrJ?``k|r3-);oCmvfX2$Hm z0OC=aCoCQLX@EIfq-%bapDg=o;+cPb82NBDSaj?zH8RSUd*RcBz zJ1`mqZ~ld$?00!9Uyu5N+=7DRjnSfUY2awpEkI>$H6CbeG`P{(*OhezZi7bal;KM0 zSK?$x)q|iWdQ>yxa+fEDN;&{ITk!<^a<<}KbXs|1gG$hoebyN9)stv(>gG08wlh=1 zqaT+oP2W+zW=oD=nQkyPsZyX)XhmaS;909ZY3aJ|6Qg+1O=b3^<&~|Yz-AL@(6Ps; z?%w0BU?O2=WX{68M|`Iq7aN;d+Ozy9SNUW% zv5wp4U8Q(B8BtKNP{lMDcPM?__5TOuK^*hsHKFspMr#re8RXJN_Z|77AoE zF7Se`;2<^8YTnDuYO30$y5!;rI#}aq3~NQj!ryN*WM~P<|7=1Wh=R_D67)(9WwG>z z>};Wrult0PB`gkTn zGm_){TJ*u!Yq6JQx62P0i3J`g(A#j5@R4uZwXAOFm+VfO4_K1XXh=C8g-h_Ayo&aD zn$X>;g#~`uUu!U(Cbz!)?sj5WkB(V!{O*Wohth)$bB5)T+2RrI$2C&-vE{OUlJaNj z!-z|_U~%2gRLsSGzraTs{E`j{9StbwVoAw29Lq3asgbk;32 zWFUdt-l8$zJfMP<;cr>`rD3nb6tXk?HMhkSa(1q}7tU|&&Bc1#N;AE^5qe{Fzr}@x zSYYmcn&ykmQ5m7DYxf(k$9%PYTvM?%TdT*{Vn)QJg!}uU2g-e5dtPWysc;8a;Y^#z zPL-C*%x&fP-9Rmynv=c;yo3`NvszA_aIP-f=TYf6t|sprj8|Xn^n9O>C5-bpAo{&@ zS>a@IwMC0Wwb8Rij|Thku16rbyDd#XMKV27RIs~iz+*RKLl+`vLDF-?`${gE_rq2* z&=NfI-bcXbB;8KpSL2hwz*&rKd$eS3wj^!bcCN{yDXCKk8>mKlD}UpSa+eI*^b|xdB|j9(XL0 z24^6M88pfHu}A13M`C5dz{F{uGD=6r9n_5(yC~CxyAH-a*3JdL-rRczIILW(9}*$Q zP14vCSRQ%ayDN=0<}pKbz&Xo%6__c`*7^ILdKhGg*?v_>z13|0Qr^Ue6x-))qtRQI zXW;&7+DlV-#C-`?L7##$5b*~`dNh3psw>$!s{ycTnh&wDizzQWCmf@_-L`7-4C2D2 zAKe)+eP+$hgj1vsdBUy5D;U?%|E;8#$JpkO+qa_6J$f8Evip3z)S)3{jJ$+et3pqC z=l4EC#nxd&{qTjmKKjD|H>Z39Xlleh5dEmfw58Eo_;F7Jo$($9&bp(qzw1ho{eCh< z-3^B~jwOB#+wbqY-tbRI|oGwnbbEqYa8xPnL1ICvKSj%({ zIDMlO(|G68#qTCYAa`r7_cir$p@_C>_l(8*^yl{)hiT!-AZa2HNb??x0L4#w4do_X9jo)_iu3ih-B$T+sT!CN`nch-rCctXB4 zJ%WF%CF^ny9&BzDluw9KByFotRo&JeciyF&ZQIA22_K~?w*H{5{`Og@#8vL$rGe3q z`i5r5uH*CZ)}nH+zL!T8sDJmPJCljMP~OjelZ-+;Lz{Nct&P0Qv}!hTudDl9+YdVS zS~3lBCg)8c#P1@Y73Z($!$p4z3EHcX+ln73XB)k+f%bv{Qff21&F?b^1tRh)@cIB1 zI_HbPW}Poyl|);VZ5r8zFM)dDYGuZCV5ih;r#Gll=Ng6R67m%0BvD?~4;^R6x@~FW zJCboQAh}g+1o~ZBcaU=&fFEZe&4qdboMilf6VgK%)FS`Su=#mo_^9KolgAl9=N88ffel707Ct<4oNQJx25Q!4m0be zKnH&enD6Wkw*p)v=sfh7(w?6V=*mq$sy>S*f5cw`?{%V+BvH5 zSFzz*o(a|)gE`$yATY#u)~U*Hy(etz!8eCmV(eVT^3EvbL+q-P^KWjss_roi&YNJ^ zcc9HTGm9uPOwNA#eu%7`=*|&M&OG%6%>J2B%}qY?h#@^~7tLX~x?xM@&$?O7ovrI$ z6}%ZGMzvFZmvIBLBN4J57+bhQ6yMvgUR=3y-QuqbTHB?F^7uU5?7wzcM$(osYxcMm zuM?UmfXesiLx?id-zvZXeaT)(#1ky4@BDcm_tZu>u(w|a=t1si4>F(>nNB&amL>z# znkwA@x#nbFL?r4NsYv!mEZy}X^Z*C31v?|t&k=1+ccA&cTWCI*t#IRW9>wkpLKC!K zhcx3oJnXM`^C+cOR2vnVj1PKOifzziLZN=BHCixfAfy=g0L!Z^+}|6o{dB8Y3NwiF zZtL8r=k@48kNq)iel?IjvxP}S-9(Rid^we9N6?{YA|qU!Selg$-l1>Si@-4}ESV zQA$5jyp~41`aYnbs9vl)5fnYY4%z2;sATJHM=1wY8LoF3Wu|tFuYT%{xdQpo5D18p zcsBzCrioP<4lJ~%EQ1cfWN{<<1rfMB<68mCd+7*NFr?Xr`i3V2J!2(k#h=5r7M+W4 z|Nh=jnS%raCW@o@YYT9hvS7V3Z__-j1U^B1K4#lL-Yds^6!w@PQ9cWT(=os5wv@rk z5Blb>&*^19=^6KdU}JaL%#W(;T_i1wP& z{r==AL7Pf)JA5C$6!TD454t#{?bL6TmKxNQ&}F-jAm;pz4fS+%67FmdYs$LcP2yJh z{LIn%3P?@jd#auMqVRP=9ca&7{-iV7y0i-^H7YSkHV91BnGcn}@=D{44EgiojfMtZ z9|C7oaG->(=m98ha6Ghndg)*@6+XZng(m!olEPzb_g~lS{F>8j=`BJf-wmLKFajRx zM^%Ek4{;`T@1{%z3?y7DwJPbw1ww5Hhb!4_$28#XgM~M1+v43P?YyUatbEl{ACP|V zf+paLXN|e)K}1$#y%&Q0>P)+4x%s->L&>zCJvzrVESc_(M(3KxBTSn0e8!pD^|%}a ze~xGrbk5|!DHBUmn!d3dzh*kxq{;)fA*Yd9;0Tr>zhUtn#C8j4TaF~zjd8CCK72Z$sF`pp!-JMQ*- zK#wY6?%h%q%7S2Buz5EQ47tZlQ`9oS-K$894cOGP4!j6EK!NH2%;L8tJM26ZsAp8< z`FVwMVKg6}t`OFK3(Q>>wwhfxZd@{wF-thxob__(YZGx;eiiGmztZXO(`=X-)$zdZ z*Ygq4e>?v8mGm!jv#5DBR6o7 zzyM!PfjN%iZ_#$2?0Hu3t*mV99s#^CDA``ZnEltRpm9#&S-PayR)A7x?~&>vQmtM) zBgt{HRrSG$NWoFmXpS z5ck$~KD(wg4Z_qkvimYPh>Jsm7Z44*2ua}CvaN2EA?}=gcHDABmthOUWTl{_(o)v! ziz99gUG2(FYIuhOFX~k7NbS+Q{@4Sr<0)Id8ZhjAo0Bsu zud$J<@{&2;8YBd03={$9a}pwFYEe7=4%vd}YVslITS?$CwMKrawfk8Ey0Ze8I#6a0 zr?5vNveV_Jx81Hwm(6-i^X%M2+U*s1F<5xh`#%k@vXDJl2nnstZ^=L-k}AJ5`My!8 zko7QRKztehIJxD#6YxBG{m%Eh+4^r`{%z7ep}ROLurYK~qPs9LuBpvEYqJQ?&Nw(| zG$E02G$744uep>%j?=>|z@q`Nx> zrBgaLqBH^`9h>g%4ke^>(*qUnzgFJ;YLV@xnfCKrYvLdD;@`EL z*hwvNDBE6MbDIW=2LaI)UT=d@iP z_o^5#ItBEY&j#pz)3d6Y*J%aq-vu(fT26~Q4f)gc^aY(&NNy+1eGS)ccpLP*s|zfL zo3bottKU05qhBku>lHNEH4zYGqr)1X$n;XFS^LXFR15qBl3jcL@a>I~g4YM!*DWY_ zP2U)kYFYB068r%VqnhxBIt;pl8~!uEiw_)Cwp@VKlWa=$r^~ zu1;rc=Rl+g`2jU#)@8v94@op8g-Lc5%)fSTuUhVAz*ww5I7X84r!EM}+XcSo3}p&` z*)|^dH~jpFf_dd=6ZG60*bZU2X#=}FnRs7b=oOHZ$&ON%t%AS|87oWUfh6u-AQN{4 z3tWXBG_F$+VsZvpv=T-NaVeXZIL*g3EjL9nXI9HK3+`J*=8rTAyg^jL?!Bb;@B|j0 zy!DK0_GfY3YJ#H}<^{f2-Wuh5R;X0!<;=qED#em9+n5XRHOj$Bgq$@j)V&Sk{&BN+ zcWe8@7Uyt{QkvN7cvz{k-)imIS+4=#eIDXG!4#-F8~13oRMLoFP;fztT@RQIL{Se9 z+=gNLeZ(1~htqN%Rg$O!!EYR8E;{pn6^IH0cT}W50OSFD9Y7$xP#j7b388`b7|G0| zymnqjs^yjC#``G5&9T++5ZR`S|JTL}5UZWd2fw2tM)?DUKew)lHd+T?N(yYjmq;Oj z6x|+W0_#zEkt2U@LBWD3hG&R%{}PA1!$9;8_CqG@-TJ2+$_Z_HIX66YUXTjgrMO4G zaBDm0ajC1@qF=}Eu02lT`Zi|Uc#=@nGB!|@bQyTYT^4?|nE%3NyXx)S^-BYD4Wun+ zyPC|Se&QRyEeB;1u6+#;%GkN7I-)b$-8}xEjXt|g1)ZX;sW=u5I*FO?3<~0%#)I*V&Lf z@T+iYfIl~7)u0NJ3kc;+Yyh%*Eb_Z0r^#w@ikp_Zv%6U!wOiBK>Pjl(F)M_JR$Q2GH^#s)amqWJKH4>CHofF|hHEphwF+*`)Wvydx^7uwB-YlnF@Mc3H zT2!5Q?=ynv&5cz!Mv(Jd=09((PlB)z8S-NvpmYjfJLo{*%Q}5OH&VG4&Z`iQpQ|1u zEFAaA)Gdw8)Gbe$!@i$lx*LGO(jo-e)R}SjSItI<+t-oMNV;Tx_iFm-xTz@xo^Q4c ziPe2gp5wi{RR&G?jC=YAHjTibY>lJKL9pr0?!JGHLEl-5-Wz)VgrxaMvgT^=>W53# z?CxDJ9Hm%;X1576vF@!n;nuWk8R~B&1h4>(9O*8Ke{%xV1I&_@a>Zw2;xY$PFpc|0 zEPKB2P5!!LKBTfgn+e&{SAg^$>-td8Gy?@7b&aR(je1 zzu6F}3&{U9eQF+;KLBvvfRrw~ilo$>#`8m!*3ZHY{^@f--8_45uNLa7+&RgTAKLzE zE>YYjs{)L!1zt>_tPhjZl_;5GF0zMvnf2Ut3f^Lvh;3m`uus!t?;LNlZz>qYiyw7J zv`b+}qeu0yf4omWl*62&__}dRVH4~|LrPniv#XNvU3p4|xoH`4vt>X%rU7W{>-w}1*;fRCcXCtQJzLP> z1(7bkjn>IOy#^^cocOG9d)t6LDe3!xb{?IYox-t#nm1qvj_H8wJD^toX{>=|zD@oG zE$|=w%KD>aEaL{m|2oO%gT*kzOR}DxFT$zp2um5;(hdy9H6%Wq4U}gBP(f!%ynORe zKyINVfnztMcp{?dt9)6WS>%<;WjT##cB?`4Y@V@^xBeU-*YS_E;oChE+<-Bu?axKW z&k&mT)`}fv1oSsK@vNPR9y{AvJ5SM#j&&FOLao1j!?n5AL1Lf|sT=8cwGmg<*_toX z?QE(Z$S5~DMXnj0P_atX6FUl;!%X~h=BPT{=Te4=lJ?(FMKb3qhNqbi)Y<8CAyKnt z1!t4N2=(*HnP~jd{lquT^1Y&4*=Wk z!E)(Lo?@_(RH_w0ipqkv$l+MD&x0zHl&eGDL@`9uXJ&SH=6x5-Zlw4TD(tAeCLD?# zMH`jY`mTq9^WK6YLh>);_S>>Ap~wcbY05_@@#eQ&{5dSDIjvIC*vNm~TL8I$DPC0@ z*?~O>ryVxy7R@#o=BLHZ64jmp7!T90|EUCr#qy*rNOoBiQg=N50@K}mSCzxbs%pjbMHLR@Y(D*QLn-HM+blJ zU{tWdy20k2VGh)p?!Dx5o^fY7<}t#E#WNhF();P1qoi0Pn=|9@ngEBmEG`5Oib60M zX&Td5`Vp<*B7+$})Q_KWtW<;HMm8otE&p)CJzh&M?N@f=extfSHhdQ48`QEel!>Y) zV;kNgqiD%gshJ`Ya|&NZXn7`P3Drp+N00T-lE+j#*GtjNvu{24H}&}r#LiCl=Is?o zE~7jyOLJz?WI_aO^6d1R__(&mMZSi~)RY;XJU*L!Ie5X7?ELmR@U>sZ^$f;feUOWs zBv0m1>Oiq|icy(Ee>#T}cQo|y%^}560+zys_K$Pp`G%vfn})yV1YjodjtrgT5nk|9 zXWjKHsrFJOMG0cJOq?Ku6hOSEs;(kiq)6Ig+`ZxlO|Lw8Mjtz}Y(^6(|5p^6v`;fz z6%(Y;zq-bDW6z15Wt1ixW9+cwHQ=%Z`EGv0$J?pAyY~??DhBOr#1QV8jH=Mb7zEiM zE^Tb1myuBW9@>Zp&x4)m?lX4HntA#sOtUeJ?t5`-@w`_1X^_#H*eXZYZa#Z#b-N!R zNKsBn=l?OqS+Y;1Sqs#I*+2~PucDkgFkLtNeitzL(NO}~B8_IKt)l)0N?w?@{p?0c z4wHWA&{IHP8#703XL*`6KoDkw{iNU0lrWzkesZ!}CyoFA&V$cdM{ zNIBM}4t`E%?KxUvO>F9dw0~OTDJ0TaV^h(fatxO517X$Ha)((bNpk#?!YBl>hRsDe zSnI+$13mucy0sxGTnxQD2+}g7(GI%PJTwL`5%6oKrY+su&Io3+Tjg*Z(yeo=7PM~2 zFb489j~B_#c31XlLoI_nh;?qy99mpTplA8?ni_AkibcEyEtq6ndq18$+8r|pb6&Vv zn`wVP9DLW2d>Bh>v*_z5b;0OiMKl##`RUmo$VaLeh9LY}&gi4KgCFiCBu$gLOmrz! zYZi}C5aMan9^K=+Hhc<>)e;KUAU#Z|@Qe0St$Yl1;G198Z&qAkmt58!#z^0C-odO3 zT=IWSaa%9g_cBTy{i01;29{hzLH2och!S6=8p|R~4$8s$+hHkv_^mboV$H4*?bQhP zz=i=p?+BJW45u`yI21YIIH-(@p5yl9%UNh8F5d36_byL& zwvLm{hI9Ldgd>zDvLXUsbe0yjW?A?q#EVauqqYs{dmeNIb{5sl z>!?y)5NkctJwiF%&scv4HV!hF`O!HoS&FF@Eb-HL^OkN+Z25S9&P-Al_!E0(X(`00 zAfY_bdR5b*YXpl|37=m0Fsj8pAu8~1aX*T`_dzvZGN0nzYs5NXm7z}ceP(bn`gHA4 z=_P~Oo{*pMaNQ`PdWo>#@o18({bj3vF%eewCK8vS@Bt0(iJ~f+Kvpm%Gxh6(O`HL- zXFH`_Z!{&co#cXv{69xzRQw9l5t*DS;~4}Fn9JF+@t9DRgSC~P7U@=gv~n&a8^s1E z`CJ8?Dhh)A1f&w;C+0>LCw6^f?doB^tx5Sz-1-2gf2O(`pM93K6QAGRx)Nyd)sdf# z`==2ZCz!g+An$$M!9PLG7dRP+YRhmDUCe5?3VMSRj+B_nIuG_G6Y3z|{(uvNULNHk264{C6mB-{K;?MI_dwi+pF$emVpU^w5 zRVTD_B*GLkQ1O^_pCg5<+rDan&P(Xk^ylPwUFsWVTUIK$CN%5~iJ(>K6_d@!i>?&@ zUW<`_!Sb1HNncYX@+1B7^+P)tvM|VLv8ZLYdTj&XCEv9FF6|pGGs!87%CB=UtaPGI z@w8Xn25*uRu$~X~O*wxbJZ$@dvXa>my{#PiQ`+;}v`^1>C+ZOhJE}C2t{qPShNkfA z9J=73tzN}Plxw<^rk~xI1!kD0>G<4?i`VeeFHHxM%;tSq_+*zpFw)x#j8?Ej!y-2? z$F`KP0&_eB683fZpTdHQogv)PczTXX*!mrZOL@sft13ejzoum6>yzUVdK{^IG$P|W zJ?ww<7BI;bCo@@nUX>oYM%m=Ym)|EY2>`hfLT^77#8n*_(h}6n^Ux_Cjah3}z4)c} z+UsfiA1?r=L<@&r96ZY!iH5$F8$P#qtCNX>ug(FBVv)|Jf5ux65PBlM*);6LP~1vd zDFz6|i^s?BlP)q~6IU9v`UMdt^y(6w7lsmFG91+P|H`NvHD|#Ki4B>+8plBPGlXP2 zUHGLv$WyY#pNcLL$q+a17GzhiKLcc`I-pz>{k;4T7+BF*@-J5TW&G84eO@0IXGcA( z_|b&wyTYJkb2X0yT~c0{5-a#}!!nNvbuO;2aES{y`A;-NJ)K zCNBA?{CDscjtO@6E00_kV<7gE(N$aT$~`dWhqFG8_vM$HCgM7oeM&Hs=gYe_XZ9O~ zm6?CEJsE-VA;t^mPrkITWv@H0PVg;Mtc2xq17BL=bTD2CTYZE0Yd43dL>wnyVjlG0 zyI>UPt15=+hRHvcF-92J9`;5|J}wD4G!uo$KUI=~MS8M#b-jx=;yHEB=5M^!`ZwNU zzwn@f<;tF@tLcGZ8>A;a;pp`XHP*2#7m*il*zpxIh*g>_Y3ZpeMi6O6-zmM0D9~wf z9o5TF52f4D9)Z@H2Q1eK@Stw5qc$B$Yy?YOoFtAaOfOXGMP}}t|KLjT#2S>I)^BjM z9^w3VFe3EE1LF(xE6?-&b;q=y;FJnZ#Ypj%&+rF6Khbdo^3AE!m%&z;&g~&rkoN`@ zOkG(Gd#-h^{EP1o+!kBCHpbho)c(3CISeU}pS(UP@ebYPKX&6F8xj_kzJ~ExAur}B z>0v@xZep+vl#R+Z9b1U!zpJHaD> zs34xv0Q#yIYQW?=Nm6KJdE_9%9J7&vX~k$Q*~3Qo)#%08FQukgb}5%)=Q*bdO82PE z=iFiSC5n|Q>$rX^PsMmSHi5_9rCIv|_=3=NzkHt2(utYUSbh4a=sNYqy9g?SkE& z6N?-2JI&!=Jg)&ZXx%rL6D{)jGV+o0SYsJO`Dxusuq)FrmD=!=e2F(VheeAoDQCy> zbT)my4&|wZ%?D3MHJ}k}jQ+{B3f!)euDNOE39DtfmsL$_x z9+UZ(-C={k>XOk@gev_ywG}D12Dr#ut{3>Y1A|;DsD{J9R2R<<%bHx?(rxc@W?oor z95%8KejeVBQiJk$QWOuwaI7vQK0=K;0-AUz`M@~BI~akU-UB#EOHAawx{8k?sA5jR?GcYn5(y?8=JHAU8nNkhQ9K zD0=fzo`<|C@(HcVxpYvpA)2xu_izPrn-g7TCSll@3My=kBnVqjtUbe0nyRA#vV{2& zKHp&%`LJ7CMCfMsMdHHskNjZee%jRJ`o)Uaw&DoKdg5R9AOTLyP~I9M$jxz3Us;3^ zXQ+pNL!D!A#Lz=APivLeE>>uPrFl2UQzOzzlpfMokhsRPJ2uQ1->v$tRbDOf+ z=za{#V?~QX@%d+;E0n5=z~0)m$``vp(V_swYEzPhBsORb8xHoIuPcaT9FL2_V(!Pj1F_`{Y;Nm38s z#7mAE%!h^7hrOg?6n#D)SsVJljs~*mo||G`=4@W)b+M(HSLy;&(u{%3O~*X9 z?r6IF{*_n*VqCexPHr9cI8h(vV&92e5nw0y#svgfyekreWO%x3FzSH20q3UDQnGNr#jeYdx06-9Qt-;fKxQ3L`JXMYq{y3@mdDw7aaM>? z)Nllka`r!!4^5RH`SSOT$_$CW_LENv?NLz>W(y5QUUNtBaLn%mB;N$Yl633P2QoPD zDn`-5LOn{Zpp#N#w|FYkc<&~ezTOkB!y^IH_QAh#Ya7~d0+DJ_YKT&2(?$C04G zPgYX96r6hw{u^264_|{Z%7bxPa$Jr8D3V7^Nzeqxll1e%FL@640mwmi6zVaR78shh z7xoi2;YDdK4QMlY(nu~_8dTx_Iu)*=SLbQ8S}!=w#)qtRRrv7cFLAn^-tou%?lte% zN5SY{zGQPRX3Nsr+;!nS?Z64LJ=hO%-oWIRJ&s(+*?8J2pZI=3p|e9Qb||3ST_x+l z9#gaj=9p6N62!9EW!4%(R$>snwr;;RXz*9(|H!lI(s3@T|)i7mF)VYr=LzN5dC zR(t8SH5B!H+)#DTx83^c3YfQtpsZcMJYJ+|m@Mg^^NTI%=G($A-o$kBVvTq5{14U~ zdL+n8F0G0IpQb{YBS~H-Trou5g943lZR5Z`SLNKc&feMQn^w0KWdjf{sdZSddT`|4 zAhkP>HFB~Rax<>z8AFntAd||F{x=JQoh#Y>jr0Rq%IOWyw8_jbbgSEb6X{MJO?Y_R z4}U|qOlu^Ya4;%X)~|hGs7$^otnw;qXqjgEf<47>))2Y(hlt4!Y6#k3mePCV{=dR@ zQMN|v3I96T9&JePSalx~;J0PdgMn2ww0-!LR;^Yw)Pg-yk&q`dMnSs@vCqqLOHx5k zYwxXlL*wJDtHHNTKTc*b&`37Ih#|6-g!VIOE6$Ny&3cKd6XoW;v!)7OF-{lf-)Vzb z4Vne0^H4eea4A0sIgz1Xnqd+NFAVa`X4R7fL4ubJv+KT7%*S`My`yUZ4k}W&`ZNMl z@AF1n>p)YOMW?>XC*~RPjm$tJLcVVqX3liL?a(tmA4!sl$OM{jxq4fqq>Wa)RkE;+ zNyR>F4HTI9(ZmdY?k_W*VR>WWwC=Dn)f4{+yH@ls>Qrj^fwah6bR?r>SGh@>uu4P| zs=z5{HA%|q_@K~cxHwyquxgdRE^;w1-)&~*d!V}cJBj((pXV^_$)b)aBmBWhAA~lV zWgkB&NA}r~BXt~KMxZGSh!-aJe|8L@Qv6Un3Y2dL#Y0!yR(_=yo0G!v+p?^$!w;NT z>01qX5m$)c-c!HS<&h4Z%@-QlD!D)GAY5Z<#EzU(j`^3^_M@_CeC@}*%q^9LtT_OW zTLpcIIp}e;36*A3$xSHNZq&%~>sR%1H%TMUGsR8u1`x@QDq4khO)dmMDPDZXFRaO3 z#{~0%)8`&v+-3tqfi1OwZy(>I=zhQa*!~DGkY-N=O@)kjW252LQ0;i{TumpuG|$Y; zLD#Q~+MCz6dC`4jRk@`DaawbPGiF$Bp*Q?9pA<^fMf7b>C3Es^7G3wq;?Y`1p<`&i zK;8VzZ8cen89=isL9^aX(l!2tZ_Qjwo2|aEsN6ZtNw<1`75RbrMorj!Cqsmzs*`PC>nK2t7qs}b!rRc?x8-D;U87}O*E7ocU|N(<@W0F%e0%!9T8cjY z1O6x`grcyEB9KO)K{hP#L_=;GU7AextNo+3!SJP3OWtj3=Sjl=O244M z@a1WcAmk{i{x{|+Nv)KJw#N9vXA+`vM^3H&PSIs0UqUD;ec@+u`(I15HZlEPtte6apgwSYYLx{3MUEbIQW@FI+jz_2!G^#KMWm zOlvHz{p+T$J^dcMZrBDR1Ijiu?5j0w%}zVc%(&ayj<$=={1C3Q3z-c;M0TtBp=Ymb z3(jlJmdI-MKlx?x9aZDlj%FWJt9={(_n~-$0(U`wNMI%JVCg{Le(UpuI4&a6^R-`O z)Ko4-kq%TlJTV)t)EwWDsUPkZSQLvjt2Ax{FIDKOWrZc|Hc~-=Tfb1ZhT~BFD5D^9 z2{5=b>M4nnK>)N;J35M2?yy=KOUA`4$C>M^bcRNjF~jUGN<&HIUXA=f)_T*IQaC$e zYhP-gC-fxJ9vhh*RshB0UnZ2}9QZ6crd=uu{2Z7A+oXPQ(cwiK_)FOra0^T<$ z#zr_PxWa>jtbg&x#eFz5+Yf{f^nBpwRb$k9(dpFXNlY*#MnW>NLGd7aV3o>vurBB>HCx@Dd1m8O!hQ@f6dx2#GP-O<~z z6-1J8aUlJ6@n-WCcjWl_fNJ>Ph)E8UT1}rEp2J-M^}{QuX=$Y!KjZFO)sasu@ zX17g4OHRI5nk7640<|{}V_WD1-@ksAz-f1Pvi!an27zL^<*C;b&Gti&d%BCiqdvoI z;3}S5H@Wr_H)`NhUv1>!%}Wrm;ca)~x7$J-x65huBz8UvpA%@A;wpRB5Wat2UTk}9 z_;w@>db$%;+5bQAfrcI~@rV!7c-`hm78M=+0V{km%`45;+?X-C6w7~7^@(uk^T;^U z^t2v}<(R`U?onCD=T(6cGgudk91`ilh-^l)P1A|`j3cJWDbPc-p91$OTNltoN-Vy> ziia2(ZQVH;=N9eF8D1D5HzwXRQcJH|Xnc4-a$1EYZJNtG z@?zJn*y=Bt^@2!H%nN`CjdF^4*`qIxQtk+yUXAnZEjkh!*6B{*Jo=FxK6GV9&KMm_ z2R{hZYugm)n+q0~zQDJ0 zs6-*qH@v599#nNzG85A#b(txjuiK0COHaHCby|vfufLPemg=Ad6=!L!nG~NCThH21 zpT5s&@$#S97=mK1G0rNk3wSL>wmXXz6(-zyDUT67adu4DDuJcSgDpvQu|E0Z3u-u{ zKN}GrWeh~G*Ssvzd!|0|6Xj1qWAkzkw~>l0=EENz>>^hxmd(T z<&j^KpEMucEDTh2h?SzLz;Nd2B)L16I0M!V)bi9y43!w<;-bRQ2;vtl`65U4ex2*{ z!f-5n`dd_AyHnJuwW4ER(Q#rnwW22Uzz{2k?&*w~c2S3mOm)j&%|@ZZ&eg!O9+OBS zjPNg_80*c4b1kiJH+lN7gTy|@4=gme?3tys*MeDKXRv5`j`(qh9OQ6+u`M#aO>(pm z5JS@I*#jEHKjgRB(6ny6(vx`A^l?7%JPCr#QD21X-&+D3Ezun&rCLGkz}$RpgpGQfU|HPlP8SrXO%c{T zjQ)f>>3x|jKMET6@)ybHp=ZHQ71FQd#zHt4GEQE_vK$Ay3;b9M8#3dT^JH9;8z^Zl zrBTa`0G$n=zE>rYDL?Rg&g4c_A&#PAYZSmyQV{d&?>5a3V2>Xj9?q-fYn4Mk_tMV<%FD4U7H+ z>G>=4ApB=zSa%MykRmg$BH#Dr5s{i=v%&10O0#ev9ddAcqa)RULdGmRoqTo|MV@SP zys-Qx&rAeJ^oUfvfj)SF27oZ@$8{GFEnh?iecC*T1Yo zKWaf&5a>cR=GOIZlfw_^+i+|(sqzR(Qc@)s%K@F@mETfudkyeL$ZWLdu#!85@tG&Q z*DZvrUSY0*Y9l7YWKodar41-_(=Rsd>*PbDRIa@lS^X}D z%y|7W*wUzTttjFC$mP_qR$lt(XBRya12rAe-YP#g9$T4MHS2^yGo1L57*k zjg3tu$aUQgpqB4WwA?yni%4%r+8GVzw9B*R6Z)X-C1VFEwAtX}h8Fjpz;mF@CE~Sg z#K`F-Y`w^0%5mG&=RPua@g*^z$W=SN_!Z2FEhaqG*D}a(QrF}Dt))@YwUQ()uBSEm zXWnV#lYjZDtQcRuPziotMTC)~wvzqk zHI3lpDp9!er8m2~EDT?Aw(fGXKtS&9NH5;U%~=DyI=0s%4i;fv!w<@g@S zSCQ+v=qdQ|xZ+uUL&2xxXKW;CP6xyMCzT8P#?e3!gXy}Pt>{|*OBvvGU;!tsR@mo; z%#j}c)4p)Df;GuXuZe!8Flr?Ue=b{0ex9je`)e)0aQ5jVUao^;gs_9ORN89Xd^Td9 zWpFsk(oZ94zfxvJRXVW<6OoH!>ZKgb>FIV;)07FavS&Kc&nq7KUUMeDQkEbiM81c6 z>*2F?7jj!X*xf+gZ-Ccg*>B2q)?a6HE%$5IUyWd?0c+;bRwew2a0)k$wm z@bBm&>Z_3FvPyA)CGo4H4SW6{J+w=mKrFCP48|{#>`8*&5cV8RFmfjMe!V26`>x_G zV>DuV7JIhc*R(Iu8=4p9C5?yD*ImlS9a$ zfo0;&<`HkA`zZ+Lz!G>k-SX>R_OK72p_1#Lclc6X;m0I3ap{^j$`D)XPFNRa{paalo}b_WWQvrqttpR;l8ZiFu|>6e*+c>1dSw z_BhGG*z6=%#nG*qO}(TYc3#z9Q^fFl|DIZXFP|z>XvSZt@oku~X<)2>7-W|g7sVu- zJy-0@kF|SGOl_B>33Axt#t|V|w=-ZTM?mPAc7)WSb+^uzVBJm?b!FRF9q}fnnSRY7kYl#yU>oo}&B( zVA*4EzugQi*k@u5J&vR)n9tpiMGc?|s;O61C!&BB%nEL1LpFa*8?C!9Gw*%X{`|+=4_{sHlPt#jaNFb6zG{ zwSahpZ!8pfD?0hf3H7!?{(1MCSO?7KA}VfdCy=L%us|L^A0&|^|IVpPAb7VZP|8nE zMJ9^uVIOR}r=6oux3g_OBM=bm4<}V(QBg1m>Pgspy%!n-GTkzH7~N)kKE%Q}$KE?t zZ^%9p#I6;?!mW!R9CRdcPutG8qH2&5NLmG+iA%VjuXw13Z4#MC+rB4;a7q~Rb0FJx zFH;VG4Ka&N>#<*`0%V@p7S)XAfVWH?J;2&b-Y!cQKfXh?$PAJRM_C z1dX4)j7UG06`)J3FEdF9`4Sn(IITxuku6IYYa`aG_3ESY3l~8KXxHx0u0QVual%?Fg6>?RuY8banhvV z6p?gREhnd=iVD$eRCAWK-`3QaBY{qUwgNQ4gJDX#H}}Cd$cbidm<<&3QY>45na%#< z1nf=s0I|VTEZy}Q(}kV7^|VyaWsU>0+cDfEVf{WGx29hbUB>D>`o8NY16_ZRQocx< zVC!4iZ=0GSm^Qg~X38ki+C z7=oyJsSG|!3w^uqkCgZmU_#_VI5`HbsqtYjj(agnxnvES&ojt)os%B~lc!J{y=x(A zZ_}!{qFh^-0T1a<0pNr;G$qtAI02&Yci|h*B9l`vhp3AA5Kd9qvv6Imjq-sY;BOha zwg`}+kMnINIAt7QyxBmgQ*|N2J7=e(BUFmseBNJ2rDQ=ZXrO0?98~wN8?(mV_^`$V zNiG3J;i%nO0d_$#9#2cSe$Nl~>sCz`We-xsED?qgI$)MIlJB%eyhnbKuVlW6Sh@d7 zC-wo&LBA|t43pj_`v@a@kS-Wzg(8%&7;FI{gndulLypC2Up_W|dcVj0CYWdJm&^h! zNt+{*b?0m_ehX9~B;WU23oYIhJr=wEwdS`i&j`NqqS9Bc1DYB^=wV2#X~QhiWB{oe zVX@`aRhD1x@}%L|hi?h#9L3*o`tlyf_E7%8M2hb47CA+SN<8ZHs{HpgGqn<7x%W!G z^76bqv$9i}_@DPzaqj*ZtD*jJB91_j=@%(0(>I)3rn_C*hAM-cmlvU*V8)YZE*iZm z)c{zQGKj9ug`(d8u9@`$o1I=SsTx7mc@_GJx3U7 z(Qe425~rqjC&x7xJ@Jb z3kGI*87?M9&1%D`s}(hplRtDBMOuWVo$}Jq>M57q#FxNxN4kzhUfUUd^EyB8d1aUB zjJ}d_52S-o3kzwzrcFWbklEW7!sGDt_g$?(vI@=9sMw2am}y2h)*m|x8O{0unflJ0 z;3;RW(n3?B5BPxt2N_?(Vh}avWQ9Mh@+1m+Hy>dYHh@G9arju@(%LTdu{}A5Q!r>V zwE+ja`~-6uJ=jU1ry}E+XDgXvGWKJ~*aIKzdykHYH&nxg06GD3fa(MXum&|t<_mQk z{VMmfPqi#Les%9&J(O-bo+k2S0$>wXe&*13qx=@^sp5Lt=W?MmpA!cP4r46zs0@`u zG`I8)GhU<|rl}8pCK+^=hQADrnATfhz65p)!8bqFUBum{HEJAg2Jn(+7Gni13|cZ% zx6j_r1btg&tk1tiJ#LZH@wC3v_;S7=Lx20yV|%jjzjm-ibdhN6Px zg}22qy?W!c3Gz{zb3!%2rI@kvse`Zq1Q^Z7TD`i0g3_qSizEsW?sHL3mIaDb(}f`S zv~U*3#=!)_ou4HCunC2pQIWoX^hqcSeLu8R?m$T`+Wv*{tGr?zLo5|Pc?J=uX!k(z zaw5BLa4YW5R=$f*a5%0H**7rpvxSa4m9U7Zvgby{4~pwGYu6oIEGeOxD36jMd(%|^ z%Usn=>3cd-#rPopvR}jUd8w3IX&FCaoNMf+ZUKr$!y^LR+^d^y)vNA5a5GgKUC~3f zX|$EyDr19cwdGjJ<7CJru zz;QBAS0YXQbL~;@1P$|3_$ug!TU2Bel!dDyTu4fw_16W@4ER+Isk4p#xit?Es-M7F2GFX_$P&S#rk6D4OayPf6@U%e zB(nA2HXX_sfgvs+`1^PyT;OtD-(dW@Sv4TyH+0N>d5?$sG72GF<(?OZwm7I5pMBgi+oY`B19HM&y)Q=LC_4bUP=fmkkJzC=nWFmEex?`X4c%z zE=uG{>%zI|O;H|v`tRLx$3&bl^}EGwt&ZQB*%s*>^aQg^!9$04M{v#V0;a|I@GJ5{ z!g{rnNZ8z+D9(xcu8V%&mjerL8B>1G4i#a5@;Y`*t;?}Hn8dCZI!+#Gx3)&`_W5VH z7e!SFpb~5UTtm24D0-xLN>H;9LJ7DI$I_1#W(835xN5wfBMYKVJxNce^pm+gJ?zmiVVshK!C}m=5fuO+=MB47!4~k=0Y@cOurY|M`eFIWs zFu;Wyqj~bq7bm@ByLxWv7E`>>&)9MUDtIBGImZjTdkGW(ShU)}1ZxE_q}1d>Te&lAfA;aM}N2}_9gL0zE(>6kmJP~ePg43TX=QbxV(G4hkV{2 zlxr2>EgT`gwRq}L@T2%(yLari{4`D+ zW{EUoHtv5kWpe=H;!2u)nw}4HHZeHDo-wR$gF!b~Ey(xBf8w2C(Gv*xr2m;B&7% z*h>#k)++?3%)J@aa5;c{+(?R=Qt+#uW(u{%)-PeKAXg{d6v!%@ap=`OQ#}hu5w_37 zTV9+dW-L6n)=BO$)6yudd$Z~CsMo{Ud?;%S)yZXFOH&MkC4%YeRwEk|tiJVam;pZ| z3vVacK&f)6#j9FiJ>D*WjY;v3zlA$yDW}Gy00<_}s#b8(?5v6KJ_nK0rk+42!|M#f zljJ}M?becx>R`a2ng~#aER*#1DX4F*wS2}EY_<7SHKEUI4vYcuaBy)Un<3o>EQ_e# zuYa=q)21FEjJp0HRm4CzhSS@NMRLL1v3j{!9G0T%x&qwVFYl8Li?$|Wn}H_T1d}D8 zeOI1@0*IsT6*z(pv$gLrnbl_mMv{=;UB1G0Lfz-k%9mDyjB)ub^qpI@U2{G=#IN4b zY!PW{|D?PjL!=yFatAjAc<%_1{D6a+3ZPr`V2C;OP44iC*TkkB9dB#4tT zO-q$V+FpYPpial8&iFor?Xd5717>@C#y`l+X@TdRmfeIn`;V{9JBkl>u^4E)H9j8* zS=_WSH!39ozmYf3AtvYM5o8p)HImg!3?$te0*`(0B}g4*w^u1U(cS4B3He2| z6F<{*{F}4e&G!pU6`OPK*(L@X=fgccdnZ!%@V59Z`OSLWwZr}`q88|-&hf=R#>a3~ zX8Nib)tZC6`~W{Kc|~yCdvope!hL8T`>}(N#O+%!q|yc3WKSh<4|4|)mLLZp>u2lw zn06OX*0o(gJ0shZS`HtMX14ZYYLVut}Aqkv7!hA{HqR#LloU0#8$SiB3uj-YdOVNasjuqRkL6l zE8aA-J8QN!&bV3CWeHrY3Jh{K!{ASS)NeG3^D|DStJdoqY1TNc=6{Z37r#g0V$#04 zxEyS`sNPT1g4{eB^hh^=?Dx92NY=svdHuRaKmPG`P-H{Q^~$b}=dsn+k-bfwHnAw6 zRk#|REI*AmkaMh7){%q!8o1ZmEP!MrN6Vqq*S)}0Z`^t`H$r@MpGc(ABKHC{Y~Fgh zkFEIoAQ#Yw0-ws}A&EDa^Ac|octPhFe!W9Sk$t^X1<_i=yFfX_p_pA{c18T>?Hy%; zXZN?5IGRs(u_S?=eAgX_CoD06M6_i)aa*qDZt!p0ZJ^!u1kS)fS#= zg!Y*q@7&nE92^wc9&TWcB{*m+zb}(!fWFJ;%Nhp}+&=pufVy$k!Gq*W5^xOH4gT>; z=i1`fV>GMGa<)mWNu8IY%W`NuL^0G8DZ0ddm07n!c>S^<=3-*5rH3Gd(^kL988-c> zHzdY;mDw)L0QW)+wU+Qb_oDW)?@Yts)t=!Wn=gD}A#1iOmE}Eir5RakRz|~4x~jQR z0a*ImcQLrO);BeqRUXy)1eWL@gjjuC(L`=;=mw{+rB|MqMJSN^$U`renazglas5Uo zFp0OoH~hN^7wV@L`c<``uJl2{8Iu5kTmYxkX(G%zcu^{c`Lqn|`ln=S-Z$ z3p6dtPv993!bMIEyKZ&NP>omSq)xaUx}?J@xz|padXXj?7^!tbe8IkjvqwI!N7Jn( z_}>TT6JjpZxH-9MuuvtZo!2taJSE75u7uN{5rP9=O^XL`M|e@w>)co?vbB*dq{9QE zd`FQ7coyGKzA^su1vij#Uw{7F_2A$ud3N}U{eaadG}Jeuf{I#hB^_;D0HKFi@M>ys zpbh|_dgs0QIn%f3xex@|%8>AC5UL#C0FE-edv!b>lXm60m$#|CGFVSlvMsii7#WkI zLkLG~!ypF0bcqM~{uyX^NAmS};WZKc?KRi12+gM;=)56g8o7;pttb!o64i9~UtGfj zVJBL@&lO-B^jT<9K;^1;+7#c%bZeD4-DJkbq2zWLm6RcA&0C8O65F21ia z$7$wRUpx(TCWilKLRXde;MwBz%{BeyZod*t!jan)a}S#iw#vm4(C^C|9qS|r{CEZ% zL58>sFK{}*UtZC2K9Ill6H=o>2g}g8{E0Jba4+0qY0y> zCG|lZjuSBlxa=!6wv|A=d{gu4BL~Tu(Jkm>ZRgivKYLMwQ2r1GGxF)@*47S zMaT5pzU!FJ@87+}Gq@{BElZm?g1KUZWxEZc3jHX|UG~F{)jC*xJ1@5lon?rDlN@mI zv7ZFo7u4qcR+NTqP-)%d64vdW!mCAoK;Y4$nY}=&nFBTroL6<*tKJ}L-4;ZN$A({$ zq1>boOV}$?yy<#B{Lz{h?y5F&^Vb(7x67vCdI7?`q?_HzvTK)Nk?!xo2IoahraS!O zyw<2S+>6uW^&>+_YfWcD{~X>QgcwQR>@a3g&1-qc<}r@@t8iN;BBAZDDSabVY(n(C z{Wv~$%4HxV@CnIjRnTnaG|_euyhi^#o;wsm4{MzT&UV};?CY05bpI78NnJM3YC>0~ z-FN`!%5tcv9kWuUGm2=O>Z1PW}ThqS{YZz?(L%qwTMCj{O z$>)r#>h9A}wYch|#>Q{f3WR?Q`h{=TkFa(bIJ`9>@Xf*}Ud_L!ld9-&_}_65Vl1!_ z`d&wZcX2h%)s%dnK{WWZAZfnW?jUiv3<}C`l?rfMZnzqDDcTw<$oZv8Jo6Y{zxHSv zW&%Nx9%Naro^qI77BO1AQgWuhzIJi|^F4uHjf*-1^$W2`E161_Ng)&>8jOcH3=w7D<_52G#0THuP_P9eoTw8=c@2(JP(pFxuKFv_hLj!HDSmV# zDeb+++F);$Z-rWSihMZN(|m+6v==BlD7OVlrt%fE>_%OkHYWZ<)0Onetq%SzqwiAX z&Y&*6$G_M7452VITXl!UGQ@ADCsIqhv7-f?15SV)`Ba-L<=gd>nQ?wvv_H)ZU|0oF z02d%&SoX%g^%z4VEEeG@n`Lg^XYtv1*BrDh?YE8Uk5$w7S(@5&;lpn?N5k;Ofk+T9Q%TEunRZ+*9@Q8^ve zp)FbJ$-i+Vz(I1PYBRv-NJli_RC5O+QG1mj_RaIj5uTNfOZ?_ps>vL`%kS|8%|kHE zW)=GNg%v|`?0E%RBj(HF7kU}{R;tPKa$cHT2LmNbjY#n7l!$Q|X{viAj8wD0nUi@s z=SVUoyD8zGEUf7Wk z+AEB2YPp5HR)BOMxixu8{${X$z^sKlMq9vq195V}KP!tjVdP)7 zTWXw8W6@*0KZAt(XK7R#IGa@+mT64O%}D4Gyr=ly*8VgRPj)X87s>U&E?yu+$>rk* zH99zt@RM5$e$Rj%{uo8mt?Gq&58h^F+mDeF2-{kk$hiOOXZ~GneiQ8u6X2W28PU8riIB8~7CE zKfvJkjOpLc{QDbyiFvU2eI(tm)0vc1Nk03G1$(miJ;lL694MnABrksd5NaZyty^kF@ z#~V>p)IPk4#UX$6r!f8BKSSsnq9povQbjdHUwA#wrhHxU2{<2+&B+rV_xIX(gPbcWN++Q&x2*XoN(=zL#^_Ko#Y5za`)c^Ss|Hse& zKV0{JpYYE+_}_i_?>_wZ4*l(req: Request, options?: NodeAskOptions) => Promise + + /** + * Send a modification request to a specific workspace + * @param workspaceId - Target workspace identifier + * @param req - The modification request + * @returns Promise resolving to the response value + */ + modify: (workspaceId: WorkspaceUuid, req: Request) => Promise> + + /** + * Health check for workspaces + * @param workspaces - Array of workspace identifiers to ping + * @param processChildren - Whether to include child workspaces + */ + ping: (workspaces: WorkspaceUuid[], processChildren: boolean) => Promise + + /** + * Inform clients about some request/Response + * @param req - Array of responses to broadcast + */ + broadcast: (req: Array>) => Promise + + /** + * Gracefully close the node and cleanup resources + */ + close: () => Promise +} +``` + +### Workspace Interface + +Interface for individual workspace instances within nodes. + +```typescript +interface Workspace { + /** + * Unique identifier for the workspace + */ + _id: WorkspaceUuid + + /** + * Execute a query on the workspace + * @param req - The query request + * @returns Promise resolving to the query result + */ + ask: (req: Request) => Promise> + + /** + * Execute a modification on the workspace + * @param req - The modification request + * @returns Promise resolving to the modification result + */ + modify: (req: Request) => Promise> + + /** + * Suspend any system resources, be ready for a resume before any new requests. + */ + suspend: () => Promise + + /** + * A restore state and be able to respond for user actions. + */ + resume: () => Promise + + /** + * Permanently close the workspace and cleanup all resources + */ + close: () => Promise +} +``` + +## πŸ—οΈ Node Management + +### NodeManager Interface + +Central manager for node discovery and access. + +```typescript +interface NodeManager extends NodeDiscovery { + /** + * Get a node instance by its identifier + * @param node - Node identifier + * @returns Promise resolving to the node instance + */ + node: (node: NodeUuid) => Promise +} +``` + +### NodeFactory Type + +Factory function for creating node instances. + +```typescript +type NodeFactory = (node: NodeUuid) => Promise +``` + +### NodeAskOptions + +Extended options for node query operations. + +```typescript +interface NodeAskOptions extends AskOptions { + /** + * Specific workspaces to target for the request + * If not specified, all accessible workspaces are queried + */ + target?: WorkspaceUuid[] +} +``` + +## 🏒 Workspace Operations + +### WorkspaceFactory Type + +Factory function for creating workspace instances. + +```typescript +type WorkspaceFactory = (workspaceId: WorkspaceUuid) => Promise +``` + +### Workspace Lifecycle + +Workspaces follow a specific lifecycle pattern: + +```mermaid +stateDiagram-v2 + [*] --> Created: WorkspaceFactory() + Created --> Active: First Request + Active --> Suspended: suspend() + Suspended --> Active: resume() + Active --> Closed: close() + Suspended --> Closed: close() + Closed --> [*] +``` + +### Example Workspace Implementation + +```typescript +class WorkspaceImpl implements Workspace { + constructor(public readonly _id: WorkspaceUuid, private pipeline: Pipeline, private config: WorkspaceConfig) {} + + async ask(req: Request): Promise> { + try { + // Validate request + await this.validateRequest(req) + + // Execute query through pipeline + const result = await this.pipeline.ask(req) + + // Transform and return result + return this.transformResponse(result) + } catch (error) { + throw new NetworkError('Query failed', { cause: error }) + } + } + + async modify(req: Request): Promise> { + try { + // Start transaction + const transaction = await this.pipeline.startTransaction() + + // Execute modification + const result = await transaction.modify(req) + + // Commit transaction + await transaction.commit() + + return this.transformResponse(result) + } catch (error) { + // Rollback on error + await transaction?.rollback() + throw new NetworkError('Modification failed', { cause: error }) + } + } + + async suspend(): Promise { + await this.pipeline.suspend() + this.config.suspended = true + } + + async resume(): Promise { + await this.pipeline.resume() + this.config.suspended = false + } + + async close(): Promise { + await this.pipeline.close() + } +} +``` + +## πŸ” Discovery Services + +### NodeDiscovery Interface + +Service for discovering and managing node topology. + +```typescript +interface NodeDiscovery { + /** + * Get the node responsible for a specific workspace + * @param workspace - Workspace identifier + * @returns Node identifier + */ + byWorkspace: (workspace: WorkspaceUuid) => Promise + + /** + * Get the node responsible for a specific account + * @param account - Account identifier + * @returns Node identifier + */ + byAccount: (account: AccountUuid) => Promise + + /** + * Get all available nodes + * @returns Iterable of node identifiers + */ + list: () => Iterable + + /** + * Get statistics/metadata for a specific node + * @param node - Node identifier + * @returns Node metadata + */ + stats: (node: NodeUuid) => Promise +} + +/** + * Node metadata type + */ +type NodeData = Record +``` + +### WorkspaceDiscovery Interface + +Service for discovering workspace locations and relationships. + +````typescript +interface WorkspaceDiscovery { + /** + * Get all workspaces accessible by an account + * @param account - Account identifier + * @returns Array of workspace identifiers + */ + byAccount: (account: AccountUuid) => Promise + + /** + * Get child workspaces of a parent workspace + * @param workspace - Parent workspace identifier + * @returns Array of child workspace identifiers + */ + byWorkspace: (workspace: WorkspaceUuid) => Promise +} + +### AccountDiscovery Interface + +Service for discovering accounts associated with workspaces. + +```typescript +interface AccountDiscovery { + /** + * Get all accounts that have access to a specific workspace + * @param workspace - Workspace identifier + * @returns Array of account identifiers + */ + byWorkspace: (workspace: WorkspaceUuid) => Promise +} +```` + +## πŸš€ Transport Layer + +### ClientTransport Interface + +Interface for client-side transport communication. + +```typescript +interface ClientTransport { + /** + * Send a request to a specific client + * @param clientId - Account identifier + * @param reqId - Request identifier + * @param body - Request body + * @returns Promise resolving to response + */ + request: (clientId: AccountUuid, reqId: RequestId, body: any) => Promise + + /** + * Subscribe to messages for an account + * @param account - Account identifier + */ + subscribe: (account: AccountUuid) => void + + /** + * Unsubscribe from messages for an account + * @param account - Account identifier + */ + unsubscribe: (account: AccountUuid) => void + + /** + * Close the transport connection + */ + close: () => Promise +} +``` + +### ServerTransport Interface + +Interface for server-side transport communication. + +```typescript +interface ServerTransport { + /** + * Node identifier for this transport + */ + nodeId: NodeUuid + + /** + * Send a request to a target node + * @param target - Target node identifier + * @param body - Request body + * @returns Promise resolving to response + */ + request: (target: NodeUuid, body: any) => Promise + + /** + * Send a message to a target node + * @param target - Target node identifier + * @param reqId - Request identifier (optional) + * @param body - Message body + */ + send: (target: NodeUuid, reqId: RequestId | undefined, body: any) => Promise + + /** + * Close the transport connection + */ + close: () => Promise +} +``` + +### Static Discovery Implementation + +```typescript +class StaticNodeDiscovery implements NodeDiscovery { + private nodes: Map + private accountHashRing: ConsistentHashRing + + constructor(nodes: Array<[NodeUuid, NodeMetadata]>) { + this.nodes = new Map(nodes) + this.accountHashRing = new ConsistentHashRing(Array.from(this.nodes.keys())) + } + + async getAccountNode(account: AccountUuid): Promise { + return this.accountHashRing.getNode(account) + } + + async getNodes(): Promise { + return Array.from(this.nodes.keys()) + } + + async registerNode(node: NodeUuid, metadata: NodeMetadata): Promise { + this.nodes.set(node, metadata) + this.accountHashRing.addNode(node) + } + + async unregisterNode(node: NodeUuid): Promise { + this.nodes.delete(node) + this.accountHashRing.removeNode(node) + } +} +``` + +## πŸ‘₯ Session Management + +### SessionManager Interface + +Central coordinator for client sessions and workspace access. + +```typescript +interface SessionManager { + /** + * Register a new client session + * @param account - Account identifier + * @param sessionId - Session identifier + * @returns Client interface for the session + */ + register: (account: AccountUuid, sessionId: string) => Promise + + /** + * Unregister and close a client session + * @param sessionId - Session identifier + */ + unregister: (sessionId: string) => Promise + + /** + * Close the session manager and all active sessions + */ + close: () => void +} +``` + +### Client Interface + +Interface for client sessions to interact with the network. + +```typescript +interface Client { + /** + * Account associated with this client + */ + account: AccountUuid + + /** + * Unique session identifier + */ + sessionId: string + + /** + * Send a query request + * @param req - The request data + * @param options - Optional request configuration + * @returns Promise resolving to the response + */ + ask: (req: T, options?: AskOptions) => Promise> + + /** + * Send a modification request to a specific workspace + * @param workspaceId - Target workspace identifier + * @param req - The modification request data + * @returns Promise resolving to the response + */ + modify: (workspaceId: WorkspaceUuid, req: T) => Promise> + + /** + * Callback for handling broadcast messages + */ + onBroadcast?: (response: Response) => void + + /** + * Callback for handling session close + */ + onClose?: () => void +} +``` + +## πŸ“¨ Request/Response Types + +### Core Types + +```typescript +/** + * Unique identifier types + */ +type WorkspaceUuid = string & { __workspaceUuid: true } +type AccountUuid = string & { __accountUuid: true } +type NodeUuid = string & { __nodeUuid: true } + +/** + * Request identifier type + */ +type RequestId = string & { __requestId: true } + +/** + * Request structure + */ +interface Request { + _id: RequestId + account: AccountUuid + + // Workspace filter + workspace?: WorkspaceUuid | WorkspaceUuid[] + + workspaces: Record // A list of already processed workspaces. + data: T +} + +/** + * Response structure + */ +interface Response { + _id: RequestId | undefined + account: AccountUuid + + nodeId: NodeUuid + workspaceId: WorkspaceUuid + data: ResponseValue +} + +/** + * Response value wrapper + */ +interface ResponseValue { + value: T[] + total: number +} + +/** + * Request acknowledgment + */ +interface RequestAkn { + // A list of nodes we need to retrieve data from, or retry to ask again if required. + workspaces: Record +} +``` + +### Request Options + +```typescript +interface AskOptions { + /** + * Specific workspaces to target for the request + */ + workspace?: WorkspaceUuid[] +} +``` + +### Node Metadata + +```typescript +interface NodeMetadata { + /** + * Geographic region where the node is located + */ + region: string + + /** + * Processing capacity of the node + */ + capacity: number + + /** + * Network endpoints for the node + */ + endpoints?: { + internal: string + external: string + } + + /** + * Node status information + */ + status?: { + healthy: boolean + lastSeen: number + version: string + } +} +``` + +## ❌ Error Handling + +### NetworkError Class + +```typescript +class NetworkError extends Error { + constructor(message: string, public code?: string, public details?: any) { + super(message) + this.name = 'NetworkError' + } +} +``` + +### Error Types + +```typescript +enum NetworkErrorCode { + // Connection errors + CONNECTION_FAILED = 'CONNECTION_FAILED', + CONNECTION_TIMEOUT = 'CONNECTION_TIMEOUT', + CONNECTION_REFUSED = 'CONNECTION_REFUSED', + + // Authentication errors + AUTHENTICATION_FAILED = 'AUTHENTICATION_FAILED', + AUTHORIZATION_FAILED = 'AUTHORIZATION_FAILED', + SESSION_EXPIRED = 'SESSION_EXPIRED', + + // Request errors + INVALID_REQUEST = 'INVALID_REQUEST', + REQUEST_TIMEOUT = 'REQUEST_TIMEOUT', + RATE_LIMITED = 'RATE_LIMITED', + + // Workspace errors + WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND', + WORKSPACE_UNAVAILABLE = 'WORKSPACE_UNAVAILABLE', + WORKSPACE_SUSPENDED = 'WORKSPACE_SUSPENDED', + + // Node errors + NODE_NOT_FOUND = 'NODE_NOT_FOUND', + NODE_UNAVAILABLE = 'NODE_UNAVAILABLE', + NODE_OVERLOADED = 'NODE_OVERLOADED', + + // System errors + INTERNAL_ERROR = 'INTERNAL_ERROR', + SERVICE_UNAVAILABLE = 'SERVICE_UNAVAILABLE', + MAINTENANCE_MODE = 'MAINTENANCE_MODE' +} +``` + +### Error Handling Patterns + +```typescript +// Retry with exponential backoff +async function retryWithBackoff( + operation: () => Promise, + maxRetries: number = 3, + baseDelay: number = 1000 +): Promise { + let lastError: Error + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await operation() + } catch (error) { + lastError = error + + if (attempt === maxRetries) { + throw new NetworkError('Max retries exceeded', 'MAX_RETRIES', { + attempts: attempt + 1, + lastError + }) + } + + // Exponential backoff with jitter + const delay = baseDelay * Math.pow(2, attempt) + Math.random() * 1000 + await new Promise((resolve) => setTimeout(resolve, delay)) + } + } + + throw lastError +} + +// Circuit breaker pattern +class CircuitBreaker { + private failures: number = 0 + private lastFailTime: number = 0 + private state: 'closed' | 'open' | 'half-open' = 'closed' + + constructor(private failureThreshold: number = 5, private timeout: number = 60000) {} + + async execute(operation: () => Promise): Promise { + if (this.state === 'open') { + if (Date.now() - this.lastFailTime > this.timeout) { + this.state = 'half-open' + } else { + throw new NetworkError('Circuit breaker is open', 'CIRCUIT_OPEN') + } + } + + try { + const result = await operation() + this.onSuccess() + return result + } catch (error) { + this.onFailure() + throw error + } + } + + private onSuccess(): void { + this.failures = 0 + this.state = 'closed' + } + + private onFailure(): void { + this.failures++ + this.lastFailTime = Date.now() + + if (this.failures >= this.failureThreshold) { + this.state = 'open' + } + } +} +``` + +## πŸ’‘ Usage Examples + +### Basic Client Usage + +```typescript +import { SessionManagerImpl, StaticNodeDiscovery, StaticWorkspaceDiscovery } from '@hcengineering/network' + +// Setup discovery services +const nodeDiscovery = new StaticNodeDiscovery([ + ['node1', { region: 'us-east', capacity: 100 }], + ['node2', { region: 'us-west', capacity: 150 }] +]) + +const workspaceDiscovery = new StaticWorkspaceDiscovery({ + user1: ['workspace1', 'workspace2'], + user2: ['workspace3'] +}) + +// Create session manager +const sessionManager = new SessionManagerImpl(nodeFactory, operationHandler, workspaceDiscovery, nodeDiscovery) + +// Register client and perform operations +async function example() { + // Register a new client session + const client = await sessionManager.register('user1' as AccountUuid, 'session1') + + // Set up broadcast handler + client.onBroadcast = (response) => { + console.log('Received broadcast:', response) + } + + // Perform a query + const queryResult = await client.ask( + { + method: 'findDocuments', + collection: 'tasks', + filter: { status: 'active' } + }, + { + timeout: 5000, + useCache: true + } + ) + + // Perform a modification + const modifyResult = await client.modify('workspace1' as WorkspaceUuid, { + method: 'updateDocument', + collection: 'tasks', + id: 'task123', + updates: { status: 'completed' } + }) + + console.log('Query result:', queryResult) + console.log('Modify result:', modifyResult) +} +``` + +### Advanced Node Implementation + +```typescript +class AdvancedNode implements Node { + private workspaces: Map = new Map() + private circuitBreaker = new CircuitBreaker() + + constructor(public readonly _id: NodeUuid, private workspaceFactory: WorkspaceFactory, private config: NodeConfig) {} + + async ask(req: Request, options?: NodeAskOptions): Promise { + const requestId = generateId() + + try { + // Process request with circuit breaker + await this.circuitBreaker.execute(async () => { + const workspaces = options?.target || (await this.getAvailableWorkspaces()) + + // Distribute query across target workspaces + const promises = workspaces.map(async (workspaceId) => { + const workspace = await this.getOrCreateWorkspace(workspaceId) + return workspace.ask(req) + }) + + // Wait for all responses with timeout + const results = await Promise.allSettled(promises) + + // Aggregate results + const aggregatedResult = this.aggregateResults(results) + + // Store result for later retrieval + await this.storeResult(requestId, aggregatedResult) + }) + + return { + id: requestId, + acknowledged: true, + timestamp: Date.now() + } + } catch (error) { + throw new NetworkError('Ask operation failed', 'ASK_FAILED', { + requestId, + error: error.message + }) + } + } + + async modify(workspaceId: WorkspaceUuid, req: Request): Promise> { + try { + const workspace = await this.getOrCreateWorkspace(workspaceId) + return await workspace.modify(req) + } catch (error) { + throw new NetworkError('Modify operation failed', 'MODIFY_FAILED', { + workspaceId, + error: error.message + }) + } + } + + async ping(workspaces: WorkspaceUuid[], processChildren: boolean): Promise { + const promises = workspaces.map(async (workspaceId) => { + try { + const workspace = this.workspaces.get(workspaceId) + if (workspace) { + // Perform health check + await this.checkWorkspaceHealth(workspace) + + if (processChildren) { + const children = await this.getChildWorkspaces(workspaceId) + await this.ping(children, false) + } + } + } catch (error) { + console.warn(`Ping failed for workspace ${workspaceId}:`, error) + } + }) + + await Promise.allSettled(promises) + } + + async broadcast(responses: Array>): Promise { + // Implement broadcast logic based on your transport layer + // This could use WebSockets, message queues, etc. + for (const response of responses) { + await this.sendToClients(response) + } + } + + async close(): Promise { + // Close all workspaces + const closePromises = Array.from(this.workspaces.values()).map((ws) => ws.close()) + await Promise.allSettled(closePromises) + + this.workspaces.clear() + } + + private async getOrCreateWorkspace(workspaceId: WorkspaceUuid): Promise { + let workspace = this.workspaces.get(workspaceId) + + if (!workspace) { + workspace = await this.workspaceFactory(workspaceId) + this.workspaces.set(workspaceId, workspace) + } + + return workspace + } + + private aggregateResults(results: PromiseSettledResult[]): any { + const successful = results + .filter((result): result is PromiseFulfilledResult => result.status === 'fulfilled') + .map((result) => result.value) + + // Implement your aggregation logic here + return successful.reduce((acc, result) => { + // Merge results based on your data structure + return { ...acc, ...result } + }, {}) + } +} +``` + +### Custom Discovery Service + +```typescript +class DatabaseNodeDiscovery implements NodeDiscovery { + constructor(private database: Database) {} + + async getAccountNode(account: AccountUuid): Promise { + const result = await this.database.query('SELECT node_id FROM account_node_mapping WHERE account_id = ?', [account]) + + if (result.length === 0) { + // Assign node using consistent hashing + const availableNodes = await this.getNodes() + const nodeId = this.hashToNode(account, availableNodes) + + // Store mapping in database + await this.database.execute('INSERT INTO account_node_mapping (account_id, node_id) VALUES (?, ?)', [ + account, + nodeId + ]) + + return nodeId + } + + return result[0].node_id as NodeUuid + } + + async getNodes(): Promise { + const result = await this.database.query('SELECT node_id FROM nodes WHERE status = "active"') + + return result.map((row) => row.node_id as NodeUuid) + } + + async registerNode(node: NodeUuid, metadata: NodeMetadata): Promise { + await this.database.execute( + 'INSERT OR REPLACE INTO nodes (node_id, metadata, status, last_seen) VALUES (?, ?, "active", ?)', + [node, JSON.stringify(metadata), Date.now()] + ) + } + + async unregisterNode(node: NodeUuid): Promise { + await this.database.execute('UPDATE nodes SET status = "inactive" WHERE node_id = ?', [node]) + } + + private hashToNode(account: AccountUuid, nodes: NodeUuid[]): NodeUuid { + // Simple hash-based selection + const hash = this.simpleHash(account) + const index = hash % nodes.length + return nodes[index] + } + + private simpleHash(str: string): number { + let hash = 0 + for (let i = 0; i < str.length; i++) { + const char = str.charCodeAt(i) + hash = (hash << 5) - hash + char + hash = hash & hash // Convert to 32-bit integer + } + return Math.abs(hash) + } +} +``` + +--- + +For more detailed examples and advanced usage patterns, refer to the [Huly Examples Repository](https://github.com/hcengineering/huly-examples). diff --git a/network/docs/readme.md b/network/docs/readme.md new file mode 100644 index 00000000000..181b7d1a5b8 --- /dev/null +++ b/network/docs/readme.md @@ -0,0 +1,208 @@ +# Huly Virtual Network + +A distributed network architecture for the Huly platform that enables scalable, fault-tolerant communication between accounts, workspaces, and nodes. + +![Schema](./Schema.png) + +## Overview + +The Huly virtual network implements a distributed system with the following key components: + +- **Nodes**: Computational units that handle requests and manage workspaces +- **Workspaces**: Isolated environments that contain application data and logic +- **Accounts**: User identities that can access multiple workspaces +- **Sessions**: Client connections to the network + +## Architecture Components + +### Account β†’ Workspace Mapping + +The `AccountDB` is responsible for mapping `AccountUuid` to `WorkspaceUuid[]` representing all workspaces accessible by a given account. + +**Key Features:** + +- Multi-tenant workspace access +- Role-based permissions (Owner, Member, Guest) +- Workspace discovery and enumeration + +### Account β†’ Node Mapping + +Each account is mapped to a specific node using a consistent hashing algorithm: + +```text +AccountUuid β†’ hash β†’ DHT β†’ NodeId +``` + +**Implementation:** + +```typescript +hash(AccountUuid) % nodes.length +``` + +**Benefits:** + +- Load balancing across nodes +- Consistent routing for user operations +- Fault tolerance through re-hashing + +### Workspace β†’ Workspace Mapping + +Workspaces can aggregate content from sub-workspaces, enabling hierarchical organization and unified access patterns. + +**Features:** + +- Parent-child workspace relationships +- Cascading operations across workspace hierarchies +- Unified query interface for related workspaces + +### Workspace Lifecycle Management + +Workspaces have complex startup/shutdown cycles managed by the network: + +- **Lazy Loading**: Workspaces are activated on-demand +- **Resource Management**: Automatic cleanup of unused workspaces +- **Health Monitoring**: Continuous workspace health checks + +## Core Operations + +### Query Operations (Map/Reduce) + +Distributed query processing across multiple workspaces: + +```text +1. Request with RequestId +2. AccountUuid β†’ PersonalId β†’ NodeId (routing) +3. Post request to Personal NodeId + 3.1 Personal Node: Resolve workspace β†’ NodeIds mapping + 3.2 Personal Node: Distribute query to required nodes + 4.1 Target Nodes: Check workspace status, activate if needed + 4.2 Target Nodes: Execute query on workspace + 4.3 Target Nodes: Process child workspaces if applicable + 4.4 Target Nodes: Subscribe to workspace changes + 4.5 Target Nodes: Perform map/reduce on results + 4.6 Target Node: Pass result to personal Node. + 3.3 Personal Node: Pass result to client. + 3.4. Collect and aggregate responses + 3.5. Handle retries for failed workspaces + 3.6. Cancel requests when needed + 3.7. Return final response to client +``` + +### Modify Operations + +Transactional modifications across the distributed system: + +```text +1. Request with RequestId +2. AccountUuid + PersonalId β†’ NodeId (routing) +3. Post to Personal NodeId + 3.1 Personal Node: WorkspaceId β†’ NodeId resolution + 4.1 Target Node: Execute operation on workspace + 4.2 Target Node: Return response to personal node + 3.2 Personal Node: Forward response to client +``` + +### Broadcast Operations + +Efficient message distribution to multiple clients: + +**Account Broadcast:** + +```text +1. AccountUuid β†’ PersonalId β†’ NodeId (targeting) +2. Post message to client's personal node +3. Node broadcasts to all connected clients +``` + +**Workspace Broadcast:** + +```text +1. WorkspaceId β†’ AccountUuid[] β†’ NodeId[] (fan-out) +2. Broadcast to all relevant nodes +3. Each node broadcasts to its connected clients +``` + +## Implementation Details + +### Core Interfaces + +**Node Interface:** + +```typescript +interface Node { + _id: NodeUuid + ask: (req: Request, options?: AskOptions) => Promise + modify: (workspaceId: WorkspaceUuid, req: Request) => Promise> + ping: (accounts: AccountUuid[]) => Promise + broadcast: (req: Array>) => Promise + close: () => Promise +} +``` + +**Workspace Interface:** + +```typescript +interface Workspace { + _id: WorkspaceUuid + lastUse: number + ask: (req: Request) => Promise> + modify: (req: Request) => Promise> + ping: () => void + close: () => Promise +} +``` + +### Discovery Services + +**Node Discovery:** + +- Hash-based node selection +- Health monitoring and failover +- Dynamic node registration/deregistration + +**Workspace Discovery:** + +- Account-to-workspace mapping +- Workspace hierarchy resolution +- Real-time workspace availability + +### Session Management + +**Client Session:** + +```typescript +interface Client { + account: AccountUuid + sessionId: string + ask: (req: T, options?: AskOptions) => Promise> + modify: (workspaceId: WorkspaceUuid, req: T) => Promise> + onBroadcast?: (response: Response) => void + onClose?: () => void +} +``` + +## Usage Examples + +```typescript +// Initialize node discovery +const nodeDiscovery = new StaticNodeDiscovery([ + ['node1', { region: 'us-east', capacity: 100 }], + ['node2', { region: 'us-west', capacity: 150 }] +]); + +// Create workspace discovery +const workspaceDiscovery = new StaticWorkspaceDiscovery({ + 'user1': ['workspace1', 'workspace2'], + 'user2': ['workspace3'] +}); + +// Initialize session manager +const sessionManager = new SessionManagerImpl(nodeFactory, operationHandler, workspaceDiscovery, nodeDiscovery); + +// Register client +const client = await sessionManager.register('user1', 'session1'); + +// Perform operations +const result = await client.ask('query-data', { workspace: ['workspace1'] }); +await client.modify('workspace1', { action: 'update', data: {...} }); +``` diff --git a/network/todo.md b/network/todo.md new file mode 100644 index 00000000000..4e6a84f3f2d --- /dev/null +++ b/network/todo.md @@ -0,0 +1,22 @@ +# Problems not solve + +- [x] Basic functional implementaion and zeromq transport based implementation. + + - [x] basic tests + +- Add opentelementry for monitoring/logging + +- Add docker + real network benchmark test + +- Retry logic - need more carefully track request/response retry in case of node/workspace mises. + +- Rate limit logic not implemented, unclear how to manage it now. + +- Memory overhelming issues, if ask request too many data per node or final reduce node. + + 1. Possible solution implement streaming of responses. + 2. Add limits per workspace request? + +- Work on more real life examples. Integrate into platform. + +- Not sure if warmup/ping is really needed. diff --git a/network/zeromq/.eslintrc.js b/network/zeromq/.eslintrc.js new file mode 100644 index 00000000000..ce90fb9646f --- /dev/null +++ b/network/zeromq/.eslintrc.js @@ -0,0 +1,7 @@ +module.exports = { + extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'], + parserOptions: { + tsconfigRootDir: __dirname, + project: './tsconfig.json' + } +} diff --git a/network/zeromq/.npmignore b/network/zeromq/.npmignore new file mode 100644 index 00000000000..e3ec093c383 --- /dev/null +++ b/network/zeromq/.npmignore @@ -0,0 +1,4 @@ +* +!/lib/** +!CHANGELOG.md +/lib/**/__tests__/ diff --git a/network/zeromq/config/rig.json b/network/zeromq/config/rig.json new file mode 100644 index 00000000000..78cc5a17334 --- /dev/null +++ b/network/zeromq/config/rig.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json", + "rigPackageName": "@hcengineering/platform-rig", + "rigProfile": "node" +} diff --git a/network/zeromq/jest.config.js b/network/zeromq/jest.config.js new file mode 100644 index 00000000000..2cfd408b679 --- /dev/null +++ b/network/zeromq/jest.config.js @@ -0,0 +1,7 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'], + roots: ["./src"], + coverageReporters: ["text-summary", "html"] +} diff --git a/network/zeromq/package.json b/network/zeromq/package.json new file mode 100644 index 00000000000..fb8cc290012 --- /dev/null +++ b/network/zeromq/package.json @@ -0,0 +1,42 @@ +{ + "name": "@hcengineering/network-zeromq", + "version": "0.6.0", + "main": "lib/index.js", + "svelte": "src/index.ts", + "types": "types/index.d.ts", + "author": "Anticrm Platform Contributors", + "template": "@hcengineering/node-package", + "license": "EPL-2.0", + "scripts": { + "build": "compile", + "build:watch": "compile", + "test": "jest --passWithNoTests --silent --forceExit", + "format": "format src", + "_phase:build": "compile transpile src", + "_phase:test": "jest --passWithNoTests --silent --forceExit", + "_phase:format": "format src", + "_phase:validate": "compile validate" + }, + "devDependencies": { + "@hcengineering/platform-rig": "^0.6.0", + "@typescript-eslint/eslint-plugin": "^6.11.0", + "eslint-plugin-import": "^2.26.0", + "eslint-plugin-promise": "^6.1.1", + "eslint-plugin-n": "^15.4.0", + "eslint": "^8.54.0", + "@typescript-eslint/parser": "^6.11.0", + "eslint-config-standard-with-typescript": "^40.0.0", + "prettier": "^3.1.0", + "typescript": "^5.8.3", + "@types/node": "^22.15.29", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "@types/jest": "^29.5.5", + "@types/uuid": "^8.3.1" + }, + "dependencies": { + "@hcengineering/network": "^0.6.0", + "zeromq": "^6.5.0", + "uuid": "^8.3.2" + } +} diff --git a/network/zeromq/src/__test__/dummy.ts b/network/zeromq/src/__test__/dummy.ts new file mode 100644 index 00000000000..ff0a2fd0d2a --- /dev/null +++ b/network/zeromq/src/__test__/dummy.ts @@ -0,0 +1,36 @@ +import type { Request, ResponseValue, Workspace, WorkspaceUuid } from '@hcengineering/network' + +export class DummyWorkspace implements Workspace { + _id: WorkspaceUuid + + data: any[] + + constructor (id: WorkspaceUuid) { + this._id = id + this.data = [`hello from ${this._id}`] + } + + async ask(req: Request): Promise> { + return { value: this.data, total: 1 } + } + + async modify (req: Request): Promise> { + if (req.data.action === 'broadcast') { + // Simulate broadcasting by adding to the workspace data + this.data.push(`broadcasted: ${req.data.data}`) + return { value: this.data, total: 1 } + } + if (req.data.action === 'add') { + // Simulate adding data to the workspace + this.data.push(req.data.data) + return { value: this.data, total: 1 } + } + return { value: ['done', this._id], total: 0 } + } + + async suspend (): Promise {} + + async resume (): Promise {} + + async close (): Promise {} +} diff --git a/network/zeromq/src/__test__/node.spec.ts b/network/zeromq/src/__test__/node.spec.ts new file mode 100644 index 00000000000..42054e943f6 --- /dev/null +++ b/network/zeromq/src/__test__/node.spec.ts @@ -0,0 +1,121 @@ +import { + SessionManagerImpl, + StaticNodeDiscovery, + type Node, + type NodeImpl, + type WorkspaceFactory +} from '@hcengineering/network' +import type { ZMQNodeData } from '../types' + +import { TickManagerImpl } from '@hcengineering/network' +import type { SessionManager } from '@hcengineering/network/types/api/client' +import { createZMQNode } from '../node' +import { DummyWorkspace } from './dummy' +import { nodes, users, workspaces, wsDiscovery } from './samples' + +describe('test zeromq nodes', () => { + const host = 'localhost' + const port = [3561, 3562, 3563, 3564, 3565] + + const zmqSimpleDiscovery = new StaticNodeDiscovery([ + [nodes.node1, { host: '0.0.0.0', port: port[0] }], + [nodes.node2, { host: '0.0.0.0', port: port[1] }], + [nodes.node3, { host: '0.0.0.0', port: port[2] }], + [nodes.node4, { host: '0.0.0.0', port: port[3] }], + [nodes.node5, { host: '0.0.0.0', port: port[4] }] + ]) + + const _nodes: Node[] = [] + const tickManager = new TickManagerImpl(20) + + const wsFactory: WorkspaceFactory = async (workspaceId) => new DummyWorkspace(workspaceId) + let sessionManager: SessionManager + + beforeAll(async () => { + _nodes.push( + await createZMQNode(nodes.node1, host, port[0], zmqSimpleDiscovery, wsDiscovery, wsFactory, tickManager) + ) + _nodes.push( + await createZMQNode(nodes.node2, host, port[1], zmqSimpleDiscovery, wsDiscovery, wsFactory, tickManager) + ) + _nodes.push( + await createZMQNode(nodes.node3, host, port[2], zmqSimpleDiscovery, wsDiscovery, wsFactory, tickManager) + ) + _nodes.push( + await createZMQNode(nodes.node4, host, port[3], zmqSimpleDiscovery, wsDiscovery, wsFactory, tickManager) + ) + _nodes.push( + await createZMQNode(nodes.node5, host, port[4], zmqSimpleDiscovery, wsDiscovery, wsFactory, tickManager) + ) + sessionManager = new SessionManagerImpl(_nodes[1] as NodeImpl, tickManager, wsDiscovery, zmqSimpleDiscovery) + }) + + afterAll(async () => { + for (const node of _nodes) { + await node.close() + } + sessionManager?.close() + }) + + it('test node client', async () => { + const client1 = await sessionManager.register(users.user1, 's1') + const helloResp = await client1.ask('hello') + + expect(helloResp.value.map((it) => it)).toEqual([ + 'hello from ws1', + 'hello from ws10', + 'hello from ws2', + 'hello from ws3', + 'hello from ws7', + 'hello from ws8', + 'hello from ws9' + ]) + }) + it('check ask for selected ws and all childs', async () => { + const client1 = await sessionManager.register(users.user1, 's1') + + const helloResp = await client1.ask('hello', { workspace: [workspaces.ws1] }) + + expect(helloResp.value.map((it) => it)).toEqual([ + 'hello from ws1', + 'hello from ws10', + 'hello from ws7', + 'hello from ws8', + 'hello from ws9' + ]) + }) + it('check ask for single child ws of ws', async () => { + const client1 = await sessionManager.register(users.user1, 's1') + + const helloResp = await client1.ask('hello', { workspace: [workspaces.ws7] }) + + expect(helloResp.value.map((it) => it)).toEqual(['hello from ws7']) + }) + it('check ask for childs of child workspaces', async () => { + const client1 = await sessionManager.register(users.user1, 's1') + + const helloResp = await client1.ask('hello', { workspace: [workspaces.ws8] }) + + expect(helloResp.value.map((it) => it)).toEqual(['hello from ws10', 'hello from ws8', 'hello from ws9']) + }) + + it('check broadcast', async () => { + const client1 = await sessionManager.register(users.user1, 's1') + + client1.onBroadcast = (response) => { + expect(response.data.value[0]).toBe('broadcasted1') + } + + const node5 = _nodes[4] + + await node5.broadcast([ + { + account: users.user1, + data: { value: ['broadcasted1'], total: 1 }, + _id: undefined, + nodeId: nodes.node5, + workspaceId: workspaces.ws1 + } + ]) + }) +}) diff --git a/network/zeromq/src/__test__/samples.ts b/network/zeromq/src/__test__/samples.ts new file mode 100644 index 00000000000..4dd2a062583 --- /dev/null +++ b/network/zeromq/src/__test__/samples.ts @@ -0,0 +1,43 @@ +import type { AccountUuid, NodeUuid, WorkspaceUuid } from '@hcengineering/network' +import { StaticNodeDiscovery, StaticWorkspaceDiscovery } from '@hcengineering/network' + +export const workspaces = { + ws1: 'ws1' as WorkspaceUuid, + ws2: 'ws2' as WorkspaceUuid, + ws3: 'ws3' as WorkspaceUuid, + ws4: 'ws4' as WorkspaceUuid, + ws5: 'ws5' as WorkspaceUuid, + ws6: 'ws6' as WorkspaceUuid, + ws7: 'ws7' as WorkspaceUuid, + ws8: 'ws8' as WorkspaceUuid, + ws9: 'ws9' as WorkspaceUuid, + ws10: 'ws10' as WorkspaceUuid +} + +export const users = { + user1: 'user1' as AccountUuid, + user2: 'user2' as AccountUuid +} + +export const nodes = { + node1: 'node1' as NodeUuid, + node2: 'node2' as NodeUuid, + node3: 'node3' as NodeUuid, + node4: 'node4' as NodeUuid, + node5: 'node5' as NodeUuid +} + +export const simpleDiscovery = new StaticNodeDiscovery([ + [nodes.node1, {}], + [nodes.node2, {}], + [nodes.node3, {}], + [nodes.node4, {}], + [nodes.node5, {}] +]) + +export const wsDiscovery = new StaticWorkspaceDiscovery({ + [users.user1]: [workspaces.ws1, workspaces.ws2, workspaces.ws3], + [users.user2]: [workspaces.ws4, workspaces.ws5, workspaces.ws6], + [workspaces.ws1]: [workspaces.ws7, workspaces.ws8], + [workspaces.ws8]: [workspaces.ws9, workspaces.ws10] +}) diff --git a/network/zeromq/src/__test__/transport.spec.ts b/network/zeromq/src/__test__/transport.spec.ts new file mode 100644 index 00000000000..e6214d24edd --- /dev/null +++ b/network/zeromq/src/__test__/transport.spec.ts @@ -0,0 +1,91 @@ +import { StaticNodeDiscovery, type AccountUuid, type NodeUuid } from '@hcengineering/network' +import { ZMQClientPublisher, ZMQClientTransport, ZMQServerTransport } from '../transport' +import type { ZMQNodeData } from '../types' + +import { nodes } from './samples' + +describe('test zeromq transport', () => { + const host = 'localhost' + const port = [3555, 3556] + const publisherPort = [3557] + + const node1 = nodes.node1 + const node2 = nodes.node2 + + let node1Transport: ZMQServerTransport + let node1Publisher: ZMQClientPublisher + let node2Transport: ZMQServerTransport + let clientTransport: ZMQClientTransport + + const discovery = new StaticNodeDiscovery([ + [node1, { host, port: port[0] }], + [node2, { host, port: port[1] }] + ]) + + beforeAll(async () => { + node1Publisher = new ZMQClientPublisher(host, publisherPort[0]) + node1Transport = new ZMQServerTransport( + 'node1' as NodeUuid, + discovery, + async (sendResponse, clientId, reqId, body) => { + await sendResponse(body) + }, + (client, req, body) => node1Publisher.send(client, req, body), + { host, port: port[0] } + ) + node2Transport = new ZMQServerTransport( + 'node2' as NodeUuid, + discovery, + async (sendResponse, clientId, reqId, body) => { + await sendResponse(body) + }, + undefined, + { host, port: port[1] } + ) + + await node1Transport.start() + await node1Publisher.start() + await node2Transport.start() + + clientTransport = new ZMQClientTransport(host, port[0], publisherPort[0], async (clientId, reqId, body) => { + // Handle incoming responses + console.log(`Received response for ${clientId} with reqId ${reqId}:`, body) + }) + }) + + afterAll(async () => { + await node1Transport?.close() + await node1Publisher?.close() + await node2Transport?.close() + await clientTransport?.close() + }) + + it('should send and receive messages', async () => { + const clientId = 'test-client' as AccountUuid + const body = { message: 'Hello, ZeroMQ!' } + + const s = performance.now() + const count = 1000 + for (let i = 0; i < count; i++) { + // Send a request from the client + const response = await clientTransport.request(clientId, body) + expect(JSON.stringify(response)).toBe(JSON.stringify(body)) + } + const e = performance.now() + console.log(`Time taken for ${count} requests: ${Math.round(((e - s) * 100) / count) / 100} ms`) + }) + + it('should send and receive node messages', async () => { + const body = { message: 'Hello, ZeroMQ!' } + + const s = performance.now() + const count = 1000 + for (let i = 0; i < count; i++) { + // Send a request from the client + const response = await node1Transport.request(node2, body) + expect(JSON.stringify(response)).toBe(JSON.stringify(body)) + } + const e = performance.now() + console.log(`Time taken for ${count} requests: ${Math.round(((e - s) * 100) / count) / 100} ms`) + }) +}) diff --git a/network/zeromq/src/index.ts b/network/zeromq/src/index.ts new file mode 100644 index 00000000000..e4a350c1bfe --- /dev/null +++ b/network/zeromq/src/index.ts @@ -0,0 +1,2 @@ +export * from './transport' +export * from './types' diff --git a/network/zeromq/src/node.ts b/network/zeromq/src/node.ts new file mode 100644 index 00000000000..2ded420f283 --- /dev/null +++ b/network/zeromq/src/node.ts @@ -0,0 +1,109 @@ +import { + NodeImpl, + NodeManagerImpl, + type Node, + type NodeAskOptions, + type NodeDiscovery, + type NodeUuid, + type Request, + type RequestAkn, + type Response, + type ResponseValue, + type ServerTransport, + type WorkspaceDiscovery, + type WorkspaceFactory +} from '@hcengineering/network' +import type { TickManager } from '@hcengineering/network/types/api/utils' +import { ZMQServerTransport } from './transport' +import type { ZMQNodeData } from './types' + +const nodeTransportOps = { + ask: 'a', + modify: 'm', + ping: 'p', + broadcast: 'b' +} + +class NodeProxyImpl implements Node { + constructor ( + readonly _id: NodeUuid, + readonly transport: ServerTransport + ) {} + + async ask(req: Request, options?: NodeAskOptions): Promise { + return (await this.transport.request(this._id, { op: nodeTransportOps.ask, req, options })) as RequestAkn + } + + async modify(workspaceId: string, req: Request): Promise> { + return (await this.transport.request(this._id, { + op: nodeTransportOps.modify, + workspaceId, + req + })) as ResponseValue + } + + async ping (workspaces: string[], processChildren: boolean): Promise { + await this.transport.send(this._id, undefined, { op: nodeTransportOps.ping, workspaces, processChildren }) + } + + async close (): Promise { + // No operation required + } + + async broadcast(req: Array>): Promise { + await this.transport.send(this._id, undefined, { op: nodeTransportOps.broadcast, req }) + } +} + +export async function createZMQNode ( + nodeId: NodeUuid, + host: string, + port: number, + discovery: NodeDiscovery, + workspaceDiscovery: WorkspaceDiscovery, + workspaceFactory: WorkspaceFactory, + tickManager: TickManager +): Promise { + let transport: ZMQServerTransport | undefined // eslint-disable-line prefer-const + + // A node connection manager + const nodeManager = new NodeManagerImpl(async (node) => { + return new NodeProxyImpl(node, transport as ServerTransport) + }, discovery) + + const node = new NodeImpl(nodeId, workspaceFactory, workspaceDiscovery, nodeManager, tickManager, async () => { + await transport?.close() + }) + + transport = new ZMQServerTransport( + nodeId, + discovery, + async (sendResponse, clientId, reqId, body) => { + // Handle incoming requests, + switch (body.op) { + case nodeTransportOps.ask: { + const result = await node?.ask(body.req, body.options) + await sendResponse(result) + break + } + case nodeTransportOps.modify: { + const result = await node?.modify(body.workspaceId, body.req) + await sendResponse(result) + + break + } + case nodeTransportOps.ping: + await node?.ping(body.workspaces, body.processChildren) + break + case nodeTransportOps.broadcast: + await node?.broadcast(body.req) + break + } + }, + undefined, + // Handle incoming requests, + { host, port } + ) + await transport.start() + return node +} diff --git a/network/zeromq/src/transport/client.ts b/network/zeromq/src/transport/client.ts new file mode 100644 index 00000000000..c5f14f5d0d7 --- /dev/null +++ b/network/zeromq/src/transport/client.ts @@ -0,0 +1,134 @@ +// +// Copyright Β© 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { AccountUuid, ClientTransport, RequestId } from '@hcengineering/network' + +import * as zmq from 'zeromq' +import { transportOperations, type ClientHandler } from '../types' + +export class ZMQClientTransport implements ClientTransport { + subscriber: zmq.Subscriber + pusher: zmq.Push + + reqCounter: number = 0 + + subscribed = new Set() + + requests = new Map void>() + + constructor ( + host: string, + port: number, + publisherPort: number, + readonly handler: ClientHandler, + options?: zmq.SocketOptions + ) { + this.subscriber = new zmq.Subscriber(options) + this.pusher = new zmq.Push(options) + + this.subscriber.connect(`tcp://${host}:${publisherPort}`) + this.pusher.connect(`tcp://${host}:${port}`) + + void this.handleMessages(this.subscriber, this.handler) + } + + private async send (clientId: AccountUuid, reqId: RequestId, body: any): Promise { + if (!this.subscribed.has(clientId)) { + this.subscriber.subscribe(clientId) + } + await this.pusher.send(JSON.stringify([transportOperations.clientSend, clientId, reqId, body])) + } + + async request (clientId: AccountUuid, body: any): Promise { + return await new Promise((resolve, reject) => { + const reqId = (clientId + '-' + this.reqCounter++) as RequestId + this.requests.set(reqId, resolve) + void this.send(clientId, reqId, body).catch((err) => { + reject(err) + }) + }) + } + + async handleMessages (socket: zmq.Pull | zmq.Subscriber, handler: ClientHandler): Promise { + for await (const msg of socket) { + try { + const [clientId, reqId, body] = + msg.length === 1 ? JSON.parse(msg.toString()) : [msg[0].toString(), ...JSON.parse(msg[1].toString())] + const res = this.requests.get(reqId) + if (res !== undefined) { + this.requests.delete(reqId) + res(body) + } else { + // Incoming request + await handler(clientId, reqId, body) + } + } catch (err: any) { + console.error('Error handling message:', err) + } + } + } + + subscribe (account: AccountUuid): void { + this.subscribed.add(account) + this.subscriber.subscribe(account) + } + + unsubscribe (account: AccountUuid): void { + this.subscriber.unsubscribe(account) + this.subscribed.delete(account) + } + + async close (): Promise { + this.subscriber.close() + this.pusher.close() + + while (!this.subscriber.closed && !this.pusher.closed) { + await new Promise((resolve) => setTimeout(resolve)) + } + } +} + +/** + * A server component to manage clients via ZMQ Subscriber pattern + * + * Should be passed to ZMQServerTransport to handle client responses + */ +export class ZMQClientPublisher { + publisher: zmq.Publisher // Client responses + + constructor ( + readonly host: string, + readonly port: number, + options?: zmq.SocketOptions + ) { + this.publisher = new zmq.Publisher(options) + } + + start (): Promise { + return this.publisher.bind(`tcp://${this.host}:${this.port}`) + } + + async send (clientId: AccountUuid, reqId: RequestId | undefined, body: any): Promise { + // Client is event target + await this.publisher.send([clientId, JSON.stringify([reqId, body])]) + } + + async close (): Promise { + this.publisher.close() + while (!this.publisher.closed) { + await new Promise((resolve) => setTimeout(resolve, 1)) + } + } +} diff --git a/network/zeromq/src/transport/index.ts b/network/zeromq/src/transport/index.ts new file mode 100644 index 00000000000..7636a1faf5d --- /dev/null +++ b/network/zeromq/src/transport/index.ts @@ -0,0 +1,2 @@ +export * from './client' +export * from './server' diff --git a/network/zeromq/src/transport/server.ts b/network/zeromq/src/transport/server.ts new file mode 100644 index 00000000000..6334802c29e --- /dev/null +++ b/network/zeromq/src/transport/server.ts @@ -0,0 +1,169 @@ +// +// Copyright Β© 2025 Hardcore Engineering Inc. +// +// Licensed under the Eclipse Public License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. You may +// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import type { AccountUuid, NodeDiscovery, NodeUuid, RequestId, ServerTransport } from '@hcengineering/network' + +import * as zmq from 'zeromq' +import { transportOperations, type ServerHandler, type ZMQNodeData } from '../types' + +export class ZMQServerTransport implements ServerTransport { + nodes = new Map() + pullSocket: zmq.Pull + + reqCounter: number = 0 + + requests = new Map void>() + + constructor ( + readonly nodeId: NodeUuid, + readonly discovery: NodeDiscovery, + readonly handler: ServerHandler, + readonly handleClientSend: + | ((clientId: AccountUuid, reqId: RequestId | undefined, body: any) => Promise) + | undefined, + readonly options: { + host: string + port: number + zmq?: zmq.SocketOptions + } = { host: '0.0.0.0', port: 3700 } + ) { + this.pullSocket = new zmq.Pull(this.options.zmq) + } + + async send (target: NodeUuid, reqId: RequestId | undefined, body: any): Promise { + // Our node is event target + await this.node(target).send(JSON.stringify([transportOperations.nodeSend, this.nodeId, reqId, body])) + } + + async sendClient (clientId: AccountUuid, reqId: RequestId | undefined, body: any): Promise { + const clientNodeId = await this.discovery.byAccount(clientId) + if (this.nodeId === clientNodeId) { + await this.handleClientSend?.(clientId, reqId, body) + } else { + // Wrong node, pass to proper one + await this.node(clientNodeId).send(JSON.stringify([transportOperations.clientRedirect, clientId, reqId, body])) + } + } + + async request (targetNode: NodeUuid, body: any): Promise { + return await new Promise((resolve, reject) => { + const reqId = (this.nodeId + '-' + this.reqCounter++) as RequestId + this.requests.set(reqId, (value) => { + if (this.nodeId === 'node4') { + console.log('recieve response', targetNode, 'reqId', reqId, 'body', body, value) + } + resolve(value) + }) + setTimeout(() => { + if (this.requests.has(reqId)) { + console.error('Request timeout', targetNode, 'from', this.nodeId, reqId, body) + } + }, 10000) + + if (this.nodeId === 'node4') { + console.log('send request', targetNode, 'reqId', reqId, 'body', body) + } + void this.send(targetNode, reqId, body).catch((err) => { + reject(err) + }) + }) + } + + async handleMessages (socket: zmq.Pull | zmq.Subscriber, handler: ServerHandler): Promise { + for await (const msg of socket) { + try { + const [target, clientId, reqId, body] = + msg.length === 1 ? JSON.parse(msg.toString()) : [msg[0].toString(), ...JSON.parse(msg[1].toString())] + + if (this.nodeId === 'node5') { + console.log('message to node5', target, clientId, reqId, body) + } + if (target === transportOperations.clientRedirect) { + // Forward to client + void this.sendClient(clientId, reqId, body).catch((err) => { + console.error('failed to send client redirect', err) + }) + continue + } + + const res = this.requests.get(reqId) + if (res !== undefined) { + this.requests.delete(reqId) + res(body) + } else { + if (target === transportOperations.nodeSend) { + // Node <-> Node request + void handler((resp) => this.send(clientId, reqId, resp), { node: clientId }, reqId, body).catch((err) => { + console.error('failed to handle message', err) + }) + } else if (target === transportOperations.clientSend) { + // Client request + await handler((resp) => this.sendClient(clientId, reqId, resp), { client: clientId }, reqId, body).catch( + (err) => { + console.error('failed to handle message', err) + } + ) + } + } + } catch (err: any) { + console.error('Error handling message:', err) + } + } + } + + async start (): Promise { + await this.pullSocket.bind(`tcp://${this.options.host}:${this.options.port}`) + + // Initialize connections to all node's + for (const nde of this.discovery.list()) { + if (nde !== this.nodeId) { + await this.connect(nde) + } + } + + void this.handleMessages(this.pullSocket, this.handler) + } + + async close (): Promise { + this.pullSocket.close() + + for (const nde of this.nodes.values()) { + nde.close() + } + + while (!this.pullSocket.closed && this.nodes.values().some((it) => !it.closed)) { + await new Promise((resolve) => setTimeout(resolve)) + } + } + + private async connect (node: NodeUuid): Promise { + let push = this.nodes.get(node) + if (push === undefined) { + const stats = await this.discovery.stats(node) + push = new zmq.Push(this.options.zmq) + push.connect(`tcp://${stats.host}:${stats.port}`) + this.nodes.set(node, push) + } + return push + } + + private node (node: NodeUuid): zmq.Push { + const nodeRef = this.nodes.get(node) + if (nodeRef === undefined) { + throw new Error('Invalid node') + } + return nodeRef + } +} diff --git a/network/zeromq/src/types.ts b/network/zeromq/src/types.ts new file mode 100644 index 00000000000..94d16deeaa2 --- /dev/null +++ b/network/zeromq/src/types.ts @@ -0,0 +1,21 @@ +import type { AccountUuid, NodeData, NodeUuid, RequestId } from '@hcengineering/network' + +export type ZMQNodeData = NodeData & { + host: string + port: number +} + +export type ClientHandler = (clientId: AccountUuid, reqId: RequestId | undefined, body: any) => Promise + +export type ServerHandler = ( + sendResponse: (body: any) => Promise, + clientId: { node: NodeUuid } | { client: AccountUuid }, + reqId: RequestId | undefined, + body: any +) => Promise + +export const transportOperations = { + nodeSend: '@', + clientSend: '%', + clientRedirect: '$' +} diff --git a/network/zeromq/tsconfig.json b/network/zeromq/tsconfig.json new file mode 100644 index 00000000000..c6a877cf6c3 --- /dev/null +++ b/network/zeromq/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json", + + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "declarationDir": "./types", + "tsBuildInfoFile": ".build/build.tsbuildinfo" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "lib", "dist", "types", "bundle"] +} \ No newline at end of file diff --git a/rush.json b/rush.json index 051cfb01406..7550d625e76 100644 --- a/rush.json +++ b/rush.json @@ -2652,6 +2652,16 @@ "packageName": "@hcengineering/model-billing", "projectFolder": "models/billing", "shouldPublish": false + }, + { + "packageName": "@hcengineering/network", + "projectFolder": "network/core", + "shouldPublish": false + }, + { + "packageName": "@hcengineering/network-zeromq", + "projectFolder": "network/zeromq", + "shouldPublish": false } ] }