swarpc 0.8.0 → 0.9.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/dist/client.d.ts +11 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +8 -4
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +18 -18
- package/package.json +5 -1
- package/src/client.ts +17 -7
- package/src/server.ts +34 -35
package/dist/client.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @module
|
|
3
3
|
* @mergeModuleWith <project>
|
|
4
4
|
*/
|
|
5
|
-
import { type LogLevel } from "./log.js";
|
|
5
|
+
import { type Logger, type LogLevel } from "./log.js";
|
|
6
6
|
import { ClientMethod, Hooks, zProcedures, type ProceduresMap } from "./types.js";
|
|
7
7
|
/**
|
|
8
8
|
* The sw&rpc client instance, which provides {@link ClientMethod | methods to call procedures}.
|
|
@@ -20,16 +20,25 @@ export type SwarpcClient<Procedures extends ProceduresMap> = {
|
|
|
20
20
|
* @param options various options
|
|
21
21
|
* @param options.worker if provided, the client will use this worker to post messages.
|
|
22
22
|
* @param options.hooks hooks to run on messages received from the server
|
|
23
|
+
* @param options.restartListener if true, will force the listener to restart even if it has already been started
|
|
23
24
|
* @returns a sw&rpc client instance. Each property of the procedures map will be a method, that accepts an input and an optional onProgress callback, see {@link ClientMethod}
|
|
24
25
|
*
|
|
25
26
|
* An example of defining and using a client:
|
|
26
27
|
* {@includeCode ../example/src/routes/+page.svelte}
|
|
27
28
|
*/
|
|
28
|
-
export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel, hooks, }?: {
|
|
29
|
+
export declare function Client<Procedures extends ProceduresMap>(procedures: Procedures, { worker, loglevel, restartListener, hooks, }?: {
|
|
29
30
|
worker?: Worker;
|
|
30
31
|
hooks?: Hooks<Procedures>;
|
|
31
32
|
loglevel?: LogLevel;
|
|
33
|
+
restartListener?: boolean;
|
|
32
34
|
}): SwarpcClient<Procedures>;
|
|
35
|
+
/**
|
|
36
|
+
* Starts the client listener, which listens for messages from the sw&rpc server.
|
|
37
|
+
* @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
|
|
38
|
+
* @param force if true, will force the listener to restart even if it has already been started
|
|
39
|
+
* @returns
|
|
40
|
+
*/
|
|
41
|
+
export declare function startClientListener<Procedures extends ProceduresMap>(l: Logger, worker?: Worker, hooks?: Hooks<Procedures>): Promise<void>;
|
|
33
42
|
/**
|
|
34
43
|
* Generate a random request ID, used to identify requests between client and server.
|
|
35
44
|
* @source
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAgB,KAAK,MAAM,EAAE,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACnE,OAAO,EACL,YAAY,EACZ,KAAK,EAGL,WAAW,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAA;AAGnB;;;;GAIG;AACH,MAAM,MAAM,YAAY,CAAC,UAAU,SAAS,aAAa,IAAI;IAC3D,CAAC,WAAW,CAAC,EAAE,UAAU,CAAA;CAC1B,GAAG;KACD,CAAC,IAAI,MAAM,UAAU,GAAG,YAAY,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;CACrD,CAAA;AAkBD;;;;;;;;;;;GAWG;AACH,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EACE,MAAM,EACN,QAAkB,EAClB,eAAuB,EACvB,KAAU,GACX,GAAE;IACD,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,KAAK,CAAC,EAAE,KAAK,CAAC,UAAU,CAAC,CAAA;IACzB,QAAQ,CAAC,EAAE,QAAQ,CAAA;IACnB,eAAe,CAAC,EAAE,OAAO,CAAA;CACrB,GACL,YAAY,CAAC,UAAU,CAAC,CAiG1B;AA6BD;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,UAAU,SAAS,aAAa,EACxE,CAAC,EAAE,MAAM,EACT,MAAM,CAAC,EAAE,MAAM,EACf,KAAK,GAAE,KAAK,CAAC,UAAU,CAAM,iBA4D9B;AAED;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
|
package/dist/client.js
CHANGED
|
@@ -19,13 +19,16 @@ let _clientListenerStarted = false;
|
|
|
19
19
|
* @param options various options
|
|
20
20
|
* @param options.worker if provided, the client will use this worker to post messages.
|
|
21
21
|
* @param options.hooks hooks to run on messages received from the server
|
|
22
|
+
* @param options.restartListener if true, will force the listener to restart even if it has already been started
|
|
22
23
|
* @returns a sw&rpc client instance. Each property of the procedures map will be a method, that accepts an input and an optional onProgress callback, see {@link ClientMethod}
|
|
23
24
|
*
|
|
24
25
|
* An example of defining and using a client:
|
|
25
26
|
* {@includeCode ../example/src/routes/+page.svelte}
|
|
26
27
|
*/
|
|
27
|
-
export function Client(procedures, { worker, loglevel = "debug", hooks = {}, } = {}) {
|
|
28
|
+
export function Client(procedures, { worker, loglevel = "debug", restartListener = false, hooks = {}, } = {}) {
|
|
28
29
|
const l = createLogger("client", loglevel);
|
|
30
|
+
if (restartListener)
|
|
31
|
+
_clientListenerStarted = false;
|
|
29
32
|
// Store procedures on a symbol key, to avoid conflicts with procedure names
|
|
30
33
|
const instance = { [zProcedures]: procedures };
|
|
31
34
|
for (const functionName of Object.keys(procedures)) {
|
|
@@ -103,9 +106,10 @@ async function postMessage(l, worker, hooks, message, options) {
|
|
|
103
106
|
/**
|
|
104
107
|
* Starts the client listener, which listens for messages from the sw&rpc server.
|
|
105
108
|
* @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
|
|
109
|
+
* @param force if true, will force the listener to restart even if it has already been started
|
|
106
110
|
* @returns
|
|
107
111
|
*/
|
|
108
|
-
async function startClientListener(l, worker, hooks = {}) {
|
|
112
|
+
export async function startClientListener(l, worker, hooks = {}) {
|
|
109
113
|
if (_clientListenerStarted)
|
|
110
114
|
return;
|
|
111
115
|
// Get service worker registration if no worker is provided
|
|
@@ -120,7 +124,7 @@ async function startClientListener(l, worker, hooks = {}) {
|
|
|
120
124
|
}
|
|
121
125
|
const w = worker ?? navigator.serviceWorker;
|
|
122
126
|
// Start listening for messages
|
|
123
|
-
l.debug(
|
|
127
|
+
l.debug(null, "Starting client listener", { worker, w, hooks });
|
|
124
128
|
w.addEventListener("message", (event) => {
|
|
125
129
|
// Get the data from the event
|
|
126
130
|
const eventData = event.data || {};
|
|
@@ -136,7 +140,7 @@ async function startClientListener(l, worker, hooks = {}) {
|
|
|
136
140
|
// Get the associated pending request handlers
|
|
137
141
|
const handlers = pendingRequests.get(requestId);
|
|
138
142
|
if (!handlers) {
|
|
139
|
-
throw new Error(`[SWARPC Client] ${requestId} has no active request handlers`);
|
|
143
|
+
throw new Error(`[SWARPC Client] ${requestId} has no active request handlers, cannot process ${JSON.stringify(data)}`);
|
|
140
144
|
}
|
|
141
145
|
// React to the data received: call hook, call handler,
|
|
142
146
|
// and remove the request from pendingRequests (unless it's a progress update)
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACtD,OAAO,EACL,kBAAkB,EAKlB,uBAAuB,EACvB,gBAAgB,EAChB,WAAW,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAA;AAGnB;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,UAAU,SAAS,aAAa,IAAI;IAC3D,CAAC,WAAW,CAAC,EAAE,UAAU,CAAA;IACzB,CAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAA;IAClD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CACnC,GAAG;KACD,CAAC,IAAI,MAAM,UAAU,GAAG,CACvB,IAAI,EAAE,uBAAuB,CAC3B,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EACtB,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,EACzB,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CACzB,KACE,IAAI;CACV,CAAA;AAKD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EAAE,MAAM,EAAE,QAAkB,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAAO,GAC5E,YAAY,CAAC,UAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAgB,KAAK,QAAQ,EAAE,MAAM,UAAU,CAAA;AACtD,OAAO,EACL,kBAAkB,EAKlB,uBAAuB,EACvB,gBAAgB,EAChB,WAAW,EACX,KAAK,aAAa,EACnB,MAAM,YAAY,CAAA;AAGnB;;;GAGG;AACH,MAAM,MAAM,YAAY,CAAC,UAAU,SAAS,aAAa,IAAI;IAC3D,CAAC,WAAW,CAAC,EAAE,UAAU,CAAA;IACzB,CAAC,gBAAgB,CAAC,EAAE,kBAAkB,CAAC,UAAU,CAAC,CAAA;IAClD,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAA;CACnC,GAAG;KACD,CAAC,IAAI,MAAM,UAAU,GAAG,CACvB,IAAI,EAAE,uBAAuB,CAC3B,UAAU,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EACtB,UAAU,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,EACzB,UAAU,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CACzB,KACE,IAAI;CACV,CAAA;AAKD;;;;;;;;;GASG;AACH,wBAAgB,MAAM,CAAC,UAAU,SAAS,aAAa,EACrD,UAAU,EAAE,UAAU,EACtB,EAAE,MAAM,EAAE,QAAkB,EAAE,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,QAAQ,CAAA;CAAO,GAC5E,YAAY,CAAC,UAAU,CAAC,CA8J1B"}
|
package/dist/server.js
CHANGED
|
@@ -106,16 +106,21 @@ export function Server(procedures, { worker, loglevel = "debug" } = {}) {
|
|
|
106
106
|
await postError("No input provided");
|
|
107
107
|
return;
|
|
108
108
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
try {
|
|
110
|
+
// Call the implementation with the input and a progress callback
|
|
111
|
+
const result = await implementation(payload.input, async (progress) => {
|
|
112
|
+
l.debug(requestId, `Progress for ${functionName}`, progress);
|
|
113
|
+
await postMsg({ progress });
|
|
114
|
+
}, {
|
|
115
|
+
abortSignal: abortControllers.get(requestId)?.signal,
|
|
116
|
+
logger: createLogger("server", loglevel, requestId),
|
|
117
|
+
});
|
|
118
|
+
// Send results
|
|
119
|
+
l.debug(requestId, `Result for ${functionName}`, result);
|
|
120
|
+
await postMsg({ result });
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
117
123
|
// Send errors
|
|
118
|
-
.catch(async (error) => {
|
|
119
124
|
// Handle errors caused by abortions
|
|
120
125
|
if ("aborted" in error) {
|
|
121
126
|
l.debug(requestId, `Received abort error for ${functionName}`, error.aborted);
|
|
@@ -123,17 +128,12 @@ export function Server(procedures, { worker, loglevel = "debug" } = {}) {
|
|
|
123
128
|
abortControllers.delete(requestId);
|
|
124
129
|
return;
|
|
125
130
|
}
|
|
126
|
-
l.
|
|
131
|
+
l.info(requestId, `Error in ${functionName}`, error);
|
|
127
132
|
await postError(error);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
.then(async (result) => {
|
|
131
|
-
l.debug(requestId, `Result for ${functionName}`, result);
|
|
132
|
-
await postMsg({ result });
|
|
133
|
-
})
|
|
134
|
-
.finally(() => {
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
135
|
abortedRequests.delete(requestId);
|
|
136
|
-
}
|
|
136
|
+
}
|
|
137
137
|
});
|
|
138
138
|
};
|
|
139
139
|
return instance;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "swarpc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Full type-safe RPC library for service worker -- move things off of the UI thread with ease!",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"service-workers",
|
|
@@ -58,5 +58,9 @@
|
|
|
58
58
|
"typescript": "^5.9.2",
|
|
59
59
|
"vite": "^7.0.6",
|
|
60
60
|
"vitest": "^3.2.4"
|
|
61
|
+
},
|
|
62
|
+
"volta": {
|
|
63
|
+
"node": "22.18.0",
|
|
64
|
+
"npm": "11.5.2"
|
|
61
65
|
}
|
|
62
66
|
}
|
package/src/client.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @module
|
|
2
|
+
* @module
|
|
3
3
|
* @mergeModuleWith <project>
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -47,8 +47,9 @@ let _clientListenerStarted = false
|
|
|
47
47
|
* @param options various options
|
|
48
48
|
* @param options.worker if provided, the client will use this worker to post messages.
|
|
49
49
|
* @param options.hooks hooks to run on messages received from the server
|
|
50
|
+
* @param options.restartListener if true, will force the listener to restart even if it has already been started
|
|
50
51
|
* @returns a sw&rpc client instance. Each property of the procedures map will be a method, that accepts an input and an optional onProgress callback, see {@link ClientMethod}
|
|
51
|
-
*
|
|
52
|
+
*
|
|
52
53
|
* An example of defining and using a client:
|
|
53
54
|
* {@includeCode ../example/src/routes/+page.svelte}
|
|
54
55
|
*/
|
|
@@ -57,11 +58,19 @@ export function Client<Procedures extends ProceduresMap>(
|
|
|
57
58
|
{
|
|
58
59
|
worker,
|
|
59
60
|
loglevel = "debug",
|
|
61
|
+
restartListener = false,
|
|
60
62
|
hooks = {},
|
|
61
|
-
}: {
|
|
63
|
+
}: {
|
|
64
|
+
worker?: Worker
|
|
65
|
+
hooks?: Hooks<Procedures>
|
|
66
|
+
loglevel?: LogLevel
|
|
67
|
+
restartListener?: boolean
|
|
68
|
+
} = {}
|
|
62
69
|
): SwarpcClient<Procedures> {
|
|
63
70
|
const l = createLogger("client", loglevel)
|
|
64
71
|
|
|
72
|
+
if (restartListener) _clientListenerStarted = false
|
|
73
|
+
|
|
65
74
|
// Store procedures on a symbol key, to avoid conflicts with procedure names
|
|
66
75
|
const instance = { [zProcedures]: procedures } as Partial<
|
|
67
76
|
SwarpcClient<Procedures>
|
|
@@ -186,9 +195,10 @@ async function postMessage<Procedures extends ProceduresMap>(
|
|
|
186
195
|
/**
|
|
187
196
|
* Starts the client listener, which listens for messages from the sw&rpc server.
|
|
188
197
|
* @param worker if provided, the client will use this worker to listen for messages, instead of using the service worker
|
|
198
|
+
* @param force if true, will force the listener to restart even if it has already been started
|
|
189
199
|
* @returns
|
|
190
200
|
*/
|
|
191
|
-
async function startClientListener<Procedures extends ProceduresMap>(
|
|
201
|
+
export async function startClientListener<Procedures extends ProceduresMap>(
|
|
192
202
|
l: Logger,
|
|
193
203
|
worker?: Worker,
|
|
194
204
|
hooks: Hooks<Procedures> = {}
|
|
@@ -210,7 +220,7 @@ async function startClientListener<Procedures extends ProceduresMap>(
|
|
|
210
220
|
const w = worker ?? navigator.serviceWorker
|
|
211
221
|
|
|
212
222
|
// Start listening for messages
|
|
213
|
-
l.debug(
|
|
223
|
+
l.debug(null, "Starting client listener", { worker, w, hooks })
|
|
214
224
|
w.addEventListener("message", (event) => {
|
|
215
225
|
// Get the data from the event
|
|
216
226
|
const eventData = (event as MessageEvent).data || {}
|
|
@@ -230,7 +240,7 @@ async function startClientListener<Procedures extends ProceduresMap>(
|
|
|
230
240
|
const handlers = pendingRequests.get(requestId)
|
|
231
241
|
if (!handlers) {
|
|
232
242
|
throw new Error(
|
|
233
|
-
`[SWARPC Client] ${requestId} has no active request handlers
|
|
243
|
+
`[SWARPC Client] ${requestId} has no active request handlers, cannot process ${JSON.stringify(data)}`,
|
|
234
244
|
)
|
|
235
245
|
}
|
|
236
246
|
|
|
@@ -255,7 +265,7 @@ async function startClientListener<Procedures extends ProceduresMap>(
|
|
|
255
265
|
|
|
256
266
|
/**
|
|
257
267
|
* Generate a random request ID, used to identify requests between client and server.
|
|
258
|
-
* @source
|
|
268
|
+
* @source
|
|
259
269
|
* @returns a 6-character hexadecimal string
|
|
260
270
|
*/
|
|
261
271
|
export function makeRequestId(): string {
|
package/src/server.ts
CHANGED
|
@@ -45,7 +45,7 @@ const abortedRequests = new Set<string>()
|
|
|
45
45
|
* @param options various options
|
|
46
46
|
* @param options.worker if provided, the server will use this worker to post messages, instead of sending it to all clients
|
|
47
47
|
* @returns a SwarpcServer instance. Each property of the procedures map will be a method, that accepts a function implementing the procedure (see {@link ProcedureImplementation}). There is also .start(), to be called after implementing all procedures.
|
|
48
|
-
*
|
|
48
|
+
*
|
|
49
49
|
* An example of defining a server:
|
|
50
50
|
* {@includeCode ../example/src/service-worker.ts}
|
|
51
51
|
*/
|
|
@@ -170,43 +170,42 @@ export function Server<Procedures extends ProceduresMap>(
|
|
|
170
170
|
return
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
)
|
|
185
|
-
// Send errors
|
|
186
|
-
.catch(async (error: any) => {
|
|
187
|
-
// Handle errors caused by abortions
|
|
188
|
-
if ("aborted" in error) {
|
|
189
|
-
l.debug(
|
|
190
|
-
requestId,
|
|
191
|
-
`Received abort error for ${functionName}`,
|
|
192
|
-
error.aborted
|
|
193
|
-
)
|
|
194
|
-
abortedRequests.add(requestId)
|
|
195
|
-
abortControllers.delete(requestId)
|
|
196
|
-
return
|
|
173
|
+
try {
|
|
174
|
+
// Call the implementation with the input and a progress callback
|
|
175
|
+
const result = await implementation(
|
|
176
|
+
payload.input,
|
|
177
|
+
async (progress: any) => {
|
|
178
|
+
l.debug(requestId, `Progress for ${functionName}`, progress)
|
|
179
|
+
await postMsg({ progress })
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
abortSignal: abortControllers.get(requestId)?.signal,
|
|
183
|
+
logger: createLogger("server", loglevel, requestId),
|
|
197
184
|
}
|
|
185
|
+
)
|
|
198
186
|
|
|
199
|
-
l.error(requestId, `Error in ${functionName}`, error)
|
|
200
|
-
await postError(error)
|
|
201
|
-
})
|
|
202
187
|
// Send results
|
|
203
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
188
|
+
l.debug(requestId, `Result for ${functionName}`, result)
|
|
189
|
+
await postMsg({ result })
|
|
190
|
+
} catch (error: any) {
|
|
191
|
+
// Send errors
|
|
192
|
+
// Handle errors caused by abortions
|
|
193
|
+
if ("aborted" in error) {
|
|
194
|
+
l.debug(
|
|
195
|
+
requestId,
|
|
196
|
+
`Received abort error for ${functionName}`,
|
|
197
|
+
error.aborted
|
|
198
|
+
)
|
|
199
|
+
abortedRequests.add(requestId)
|
|
200
|
+
abortControllers.delete(requestId)
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
l.info(requestId, `Error in ${functionName}`, error)
|
|
205
|
+
await postError(error)
|
|
206
|
+
} finally {
|
|
207
|
+
abortedRequests.delete(requestId)
|
|
208
|
+
}
|
|
210
209
|
})
|
|
211
210
|
}
|
|
212
211
|
|