@sharkpush/ts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +350 -0
- package/package.json +90 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 SharkPush
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# @sharkpush/sdk
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@sharkpush/sdk)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
|
|
6
|
+
Pure TypeScript SDK for the [SharkPush](https://sharkpush.dev) REST API —
|
|
7
|
+
identities, devices, events, the objective-based `communicate()` API, and
|
|
8
|
+
real-time SSE/WebSocket subscriptions. Works in **Node.js 18+** and modern
|
|
9
|
+
browsers.
|
|
10
|
+
|
|
11
|
+
This SDK mirrors the iOS / Flutter / React Native / Android SDKs' API
|
|
12
|
+
surface in idiomatic, strict TypeScript (ESM):
|
|
13
|
+
|
|
14
|
+
| Other SDK | TypeScript SDK |
|
|
15
|
+
| ---------------------------------- | ----------------------------------------------------------- |
|
|
16
|
+
| `SharkpushClient` | `SharkpushClient` |
|
|
17
|
+
| `Identity`, `Device`, `DevicePlatform` | `Identity`, `Device`, `DevicePlatform` (interfaces) |
|
|
18
|
+
| `CommunicationIntent`, `CommunicationRequest` | `CommunicationIntent`, `CommunicationRequest` |
|
|
19
|
+
| `Event`, `Message`, `MessageAttempt` | `Event`, `Message`, `MessageAttempt` (interfaces) |
|
|
20
|
+
| `SharkpushError` | `SharkpushError` (abstract) + subclasses |
|
|
21
|
+
| `RetryConfig` | `RetryConfig` |
|
|
22
|
+
| (new) | `subscribeLiveUpdates()` / `subscribeEvents()` (SSE / WS) |
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @sharkpush/sdk
|
|
28
|
+
# or:
|
|
29
|
+
pnpm add @sharkpush/sdk
|
|
30
|
+
# or:
|
|
31
|
+
yarn add @sharkpush/sdk
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
> The package ships as ESM (`"type": "module"`). Node 18+ and modern
|
|
35
|
+
> bundlers (webpack 5+, Vite, esbuild, Rollup) support ESM natively. For
|
|
36
|
+
> older consumers, transpile through your bundler.
|
|
37
|
+
|
|
38
|
+
## Quickstart
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { SharkpushClient } from '@sharkpush/sdk';
|
|
42
|
+
|
|
43
|
+
const client = new SharkpushClient({ apiKey: 'sk_REPLACE_ME' });
|
|
44
|
+
|
|
45
|
+
// 1. Create an identity.
|
|
46
|
+
const identity = await client.createIdentity({
|
|
47
|
+
externalUserId: 'u1',
|
|
48
|
+
email: 'u1@example.com',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// 2. Register a web push token (or 'ios' / 'android' / 'huawei').
|
|
52
|
+
await client.registerDevice({
|
|
53
|
+
identityId: identity.id,
|
|
54
|
+
platform: 'web',
|
|
55
|
+
token: 'REPLACE_ME',
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 3. Publish an event.
|
|
59
|
+
await client.sendEvent({
|
|
60
|
+
name: 'user.signup',
|
|
61
|
+
payload: { plan: 'pro', identity_id: identity.id },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 4. Trigger a communication.
|
|
65
|
+
const req = await client.communicate({
|
|
66
|
+
userId: identity.id,
|
|
67
|
+
objective: 'otp',
|
|
68
|
+
priority: 'normal',
|
|
69
|
+
data: { code: '4242' },
|
|
70
|
+
});
|
|
71
|
+
console.log('req', req.id, req.status);
|
|
72
|
+
|
|
73
|
+
// 5. Subscribe to live updates (SSE by default).
|
|
74
|
+
const unsubscribe = client.subscribeLiveUpdates((snapshot) => {
|
|
75
|
+
console.log('snapshot', snapshot.kind, snapshot.timestamp);
|
|
76
|
+
});
|
|
77
|
+
// … later:
|
|
78
|
+
unsubscribe();
|
|
79
|
+
|
|
80
|
+
// 6. Close the client when done.
|
|
81
|
+
await client.close();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## API reference
|
|
85
|
+
|
|
86
|
+
### `SharkpushClient`
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const client = new SharkpushClient({
|
|
90
|
+
apiKey: 'sk_REPLACE_ME',
|
|
91
|
+
endpoint: 'https://api.sharkpush.dev', // optional
|
|
92
|
+
apiPrefix: '/v1', // optional
|
|
93
|
+
tenantId: 'tenant_abc', // optional
|
|
94
|
+
retryConfig: DEFAULT_RETRY_CONFIG, // optional
|
|
95
|
+
fetchImpl: fetch, // optional (testing)
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The API key **must** start with `sk_`. It is never logged in plaintext;
|
|
100
|
+
`client.toString()` returns a redacted form like
|
|
101
|
+
`SharkpushClient(endpoint: …, apiKey: sk_••••…1234, tenantId: …)`.
|
|
102
|
+
|
|
103
|
+
#### `createIdentity`
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
await client.createIdentity({
|
|
107
|
+
externalUserId?: 'u1',
|
|
108
|
+
anonymous?: false,
|
|
109
|
+
email?: 't@e.com',
|
|
110
|
+
phone?: '+2547…',
|
|
111
|
+
locale?: 'en_KE',
|
|
112
|
+
timezone?: 'Africa/Nairobi',
|
|
113
|
+
idempotencyKey?: 'my-key',
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Creates a new `Identity`. Pass `anonymous: true` for guest identities.
|
|
118
|
+
For non-anonymous identities, `externalUserId` is required.
|
|
119
|
+
|
|
120
|
+
#### `getIdentity`
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
await client.getIdentity('id_42');
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Retrieves an `Identity` by ID.
|
|
127
|
+
|
|
128
|
+
#### `registerDevice`
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
await client.registerDevice({
|
|
132
|
+
identityId: 'id_1',
|
|
133
|
+
platform: 'web', // 'ios' | 'android' | 'web' | 'huawei'
|
|
134
|
+
token: 'REPLACE_ME',
|
|
135
|
+
locale?: 'en_KE',
|
|
136
|
+
timezone?: 'Africa/Nairobi',
|
|
137
|
+
idempotencyKey?: 'my-key',
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Registers / refreshes a push token for an `Identity`.
|
|
142
|
+
|
|
143
|
+
#### `sendEvent`
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
await client.sendEvent({
|
|
147
|
+
name: 'user.signup',
|
|
148
|
+
payload: { plan: 'pro' },
|
|
149
|
+
correlationId?: 'corr-1',
|
|
150
|
+
aggregateId?: 'agg-1',
|
|
151
|
+
idempotencyKey?: 'my-key',
|
|
152
|
+
});
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Publishes an event to the SharkPush event bus.
|
|
156
|
+
|
|
157
|
+
#### `communicate`
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
const req = await client.communicate({
|
|
161
|
+
userId: 'u1',
|
|
162
|
+
objective: 'otp',
|
|
163
|
+
priority: 'normal', // 'low' | 'normal' | 'high' | 'critical'
|
|
164
|
+
constraints?: { … },
|
|
165
|
+
data?: { code: '4242' },
|
|
166
|
+
metadata?: { … },
|
|
167
|
+
channel?: 'push',
|
|
168
|
+
idempotencyKey?: 'my-key',
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Triggers a communication with the given objective. Returns the 202
|
|
173
|
+
Accepted response (`CommunicationRequest`).
|
|
174
|
+
|
|
175
|
+
#### `getMessage` / `listMessages`
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
await client.getMessage('m_42');
|
|
179
|
+
|
|
180
|
+
await client.listMessages({
|
|
181
|
+
limit: 20,
|
|
182
|
+
offset: 0,
|
|
183
|
+
status?: 'DELIVERED',
|
|
184
|
+
channel?: 'push',
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
#### Real-time — `subscribeLiveUpdates`
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const unsubscribe = client.subscribeLiveUpdates(
|
|
192
|
+
(snapshot) => {
|
|
193
|
+
console.log(snapshot.kind, snapshot.timestamp);
|
|
194
|
+
// snapshot.kind ∈ 'message' | 'event' | 'status' | 'heartbeat'
|
|
195
|
+
// snapshot.message? — Message
|
|
196
|
+
// snapshot.event? — Event
|
|
197
|
+
// snapshot.status? — string
|
|
198
|
+
// snapshot.source — 'sse' | 'websocket'
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
streamUrl?: 'https://api.sharkpush.dev/v1/stream', // optional
|
|
202
|
+
transport?: 'sse', // or 'websocket'
|
|
203
|
+
},
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
// … later:
|
|
207
|
+
unsubscribe(); // closes the underlying connection when last handler leaves
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
#### Real-time — `subscribeEvents`
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
const unsubscribe = client.subscribeEvents(
|
|
214
|
+
{ name: 'user.signup' }, // filter (omit `name` for all events)
|
|
215
|
+
(event) => console.log(event.name, event.payload),
|
|
216
|
+
{ transport?: 'sse' },
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
// … later:
|
|
220
|
+
unsubscribe();
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
#### `close`
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
await client.close();
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Closes the HTTP layer and any active stream subscriptions. After
|
|
230
|
+
`close()` is called, the client must not be reused.
|
|
231
|
+
|
|
232
|
+
## Idempotency
|
|
233
|
+
|
|
234
|
+
Every mutating method accepts an optional `idempotencyKey`. If omitted,
|
|
235
|
+
a random UUID v4 is generated. The same key can be safely retried — the
|
|
236
|
+
server deduplicates based on `X-Idempotency-Key`.
|
|
237
|
+
|
|
238
|
+
## Retries
|
|
239
|
+
|
|
240
|
+
HTTP requests retry with exponential backoff (base **500 ms**, max **30 s**,
|
|
241
|
+
max **3 attempts**) on HTTP `5xx` and network errors. `4xx` errors (except
|
|
242
|
+
`429`) are not retried.
|
|
243
|
+
|
|
244
|
+
Customise via `retryConfig`:
|
|
245
|
+
|
|
246
|
+
```ts
|
|
247
|
+
import { DEFAULT_RETRY_CONFIG, SharkpushClient } from '@sharkpush/sdk';
|
|
248
|
+
|
|
249
|
+
const client = new SharkpushClient({
|
|
250
|
+
apiKey: 'sk_REPLACE_ME',
|
|
251
|
+
retryConfig: {
|
|
252
|
+
baseDelayMs: 1000,
|
|
253
|
+
maxDelayMs: 60_000,
|
|
254
|
+
maxAttempts: 5,
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Error handling
|
|
260
|
+
|
|
261
|
+
All errors are typed subclasses of `SharkpushError`:
|
|
262
|
+
|
|
263
|
+
| Class | HTTP status |
|
|
264
|
+
| ---------------------- | ----------------- |
|
|
265
|
+
| `AuthenticationError` | 401, 403 |
|
|
266
|
+
| `ValidationError` | 400, 422 |
|
|
267
|
+
| `NotFoundError` | 404 |
|
|
268
|
+
| `RateLimitError` | 429 |
|
|
269
|
+
| `ServerError` | 5xx |
|
|
270
|
+
| `NetworkError` | (no HTTP) |
|
|
271
|
+
| `DecodeError` | (no HTTP) |
|
|
272
|
+
| `UnknownError` | anything else |
|
|
273
|
+
|
|
274
|
+
```ts
|
|
275
|
+
import {
|
|
276
|
+
RateLimitError,
|
|
277
|
+
AuthenticationError,
|
|
278
|
+
SharkpushError,
|
|
279
|
+
} from '@sharkpush/sdk';
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
await client.communicate({ userId: 'u1', objective: 'otp' });
|
|
283
|
+
} catch (e) {
|
|
284
|
+
if (e instanceof RateLimitError) {
|
|
285
|
+
console.warn(`rate limited, retry after ${e.retryAfterSeconds}s`);
|
|
286
|
+
} else if (e instanceof AuthenticationError) {
|
|
287
|
+
console.warn(`auth failed: ${e.message}`);
|
|
288
|
+
} else if (e instanceof SharkpushError) {
|
|
289
|
+
console.warn(`sharkpush error: ${e.code} ${e.message}`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
## Real-time transports
|
|
295
|
+
|
|
296
|
+
The SDK supports two real-time transports:
|
|
297
|
+
|
|
298
|
+
1. **SSE** (`EventSource`) — preferred. Browser-native; Node 18+ ships
|
|
299
|
+
`EventSource` globally. Auto-reconnects with exponential backoff
|
|
300
|
+
(base 1 s, max 30 s) when the server closes the connection.
|
|
301
|
+
2. **WebSocket** — fallback for environments where SSE is buffered
|
|
302
|
+
(some proxies) or when bidirectional messaging is required. Sends
|
|
303
|
+
the API key as `?api_key=…` and the tenant ID as `?tenant_id=…`
|
|
304
|
+
query parameters.
|
|
305
|
+
|
|
306
|
+
The transport is selected via the `transport` option on
|
|
307
|
+
`subscribeLiveUpdates` / `subscribeEvents`. The default is `sse`.
|
|
308
|
+
|
|
309
|
+
## Browser usage
|
|
310
|
+
|
|
311
|
+
The SDK uses the global `fetch`, `EventSource`, `WebSocket`,
|
|
312
|
+
`AbortController`, `TextDecoder`, `crypto.randomUUID`, and `URL`. All
|
|
313
|
+
modern browsers (Chrome 64+, Firefox 62+, Safari 12.1+, Edge 79+)
|
|
314
|
+
support these natively.
|
|
315
|
+
|
|
316
|
+
```html
|
|
317
|
+
<script type="module">
|
|
318
|
+
import { SharkpushClient } from 'https://esm.sh/@sharkpush/sdk@0.1.0';
|
|
319
|
+
|
|
320
|
+
const client = new SharkpushClient({ apiKey: 'sk_REPLACE_ME' });
|
|
321
|
+
const id = await client.createIdentity({ anonymous: true });
|
|
322
|
+
console.log('identity', id.id);
|
|
323
|
+
</script>
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
See [`examples/browser/`](./examples/browser) for a complete example.
|
|
327
|
+
|
|
328
|
+
## Node.js usage
|
|
329
|
+
|
|
330
|
+
```ts
|
|
331
|
+
import { SharkpushClient } from '@sharkpush/sdk';
|
|
332
|
+
|
|
333
|
+
const client = new SharkpushClient({ apiKey: process.env.SHARKPUSH_API_KEY! });
|
|
334
|
+
// …
|
|
335
|
+
await client.close();
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
See [`examples/node-app/`](./examples/node-app) for a complete example.
|
|
339
|
+
|
|
340
|
+
## Security
|
|
341
|
+
|
|
342
|
+
- The API key is treated as a secret. It is **never** logged in
|
|
343
|
+
plaintext and is redacted in `toString()` output.
|
|
344
|
+
- Use `REPLACE_ME` placeholders in examples and never commit real keys.
|
|
345
|
+
- WebSocket subscriptions send the API key as a query parameter — only
|
|
346
|
+
use `wss://` (TLS) endpoints.
|
|
347
|
+
|
|
348
|
+
## License
|
|
349
|
+
|
|
350
|
+
MIT — see [LICENSE](./LICENSE).
|
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sharkpush/ts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official TypeScript SDK for the SharkPush REST API. Supports identities, devices, events, the objective-based communicate() API, and real-time SSE/WebSocket subscriptions in Node.js and modern browsers.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc",
|
|
23
|
+
"clean": "rimraf dist",
|
|
24
|
+
"dev": "tsc --watch",
|
|
25
|
+
"typecheck": "tsc --noEmit",
|
|
26
|
+
"test": "jest",
|
|
27
|
+
"test:coverage": "jest --coverage",
|
|
28
|
+
"lint": "eslint . --ext .ts"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"sharkpush",
|
|
32
|
+
"sdk",
|
|
33
|
+
"typescript",
|
|
34
|
+
"javascript",
|
|
35
|
+
"push-notifications",
|
|
36
|
+
"notifications",
|
|
37
|
+
"messaging",
|
|
38
|
+
"realtime",
|
|
39
|
+
"sse",
|
|
40
|
+
"websocket",
|
|
41
|
+
"node",
|
|
42
|
+
"browser"
|
|
43
|
+
],
|
|
44
|
+
"author": "SharkPush",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"homepage": "https://docs.sharkpush.dev",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/Roy-Wanyoike/sharkpush.git",
|
|
50
|
+
"directory": "sdks/ts"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/Roy-Wanyoike/sharkpush/issues"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=18"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@babel/core": "^7.24.0",
|
|
63
|
+
"@babel/preset-env": "^7.24.0",
|
|
64
|
+
"@babel/preset-typescript": "^7.24.0",
|
|
65
|
+
"@types/jest": "^29.5.0",
|
|
66
|
+
"@types/node": "^20.0.0",
|
|
67
|
+
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
68
|
+
"@typescript-eslint/parser": "^7.0.0",
|
|
69
|
+
"babel-jest": "^29.7.0",
|
|
70
|
+
"eslint": "^8.50.0",
|
|
71
|
+
"jest": "^29.7.0",
|
|
72
|
+
"rimraf": "^6.0.1",
|
|
73
|
+
"typescript": "^5.4.0"
|
|
74
|
+
},
|
|
75
|
+
"jest": {
|
|
76
|
+
"testEnvironment": "node",
|
|
77
|
+
"transform": {
|
|
78
|
+
"^.+\\.tsx?$": "babel-jest"
|
|
79
|
+
},
|
|
80
|
+
"testMatch": [
|
|
81
|
+
"**/__tests__/**/*.test.ts"
|
|
82
|
+
],
|
|
83
|
+
"collectCoverageFrom": [
|
|
84
|
+
"src/**/*.ts",
|
|
85
|
+
"!src/types.ts",
|
|
86
|
+
"!src/index.ts"
|
|
87
|
+
],
|
|
88
|
+
"forceExit": true
|
|
89
|
+
}
|
|
90
|
+
}
|