@crowdedkingdoms/crowdy-dsh 0.4.12 → 0.4.13-test.1
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/dsh-web/BUILD.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"package": "@crowdedkingdoms/crowdy-dsh",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"builtAt": "2026-10-
|
|
3
|
+
"version": "0.4.13",
|
|
4
|
+
"builtAt": "2026-10-24T23:04:25.889Z",
|
|
5
5
|
"source": {
|
|
6
6
|
"repository": "CrowdedKingdoms/crowdy-dsh",
|
|
7
|
-
"commit": "
|
|
7
|
+
"commit": "e04cd33909e335ec360c653e557139378a7f1dbc",
|
|
8
8
|
"dirty": false
|
|
9
9
|
},
|
|
10
10
|
"upstream": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"crowdyjs": {
|
|
17
17
|
"name": "@crowdedkingdoms/crowdyjs",
|
|
18
|
-
"version": "18.
|
|
18
|
+
"version": "18.20.0-test.1",
|
|
19
19
|
"source": "node_modules"
|
|
20
20
|
},
|
|
21
21
|
"files": {
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"manifest.webmanifest": "sha256:9b54d57eea00f2fdd1affe6e7999a4209a515755f12ac1a3299da2d55e178d4c",
|
|
113
113
|
"preview/bootstrap-CYECZNXt.js": "sha256:9f303a0485c0badfdd19fad0c357ef050c5b9a343744ab5bf0c70015e7d95092",
|
|
114
114
|
"preview/crowdy-boot-Cklh1qNl.js": "sha256:b21e8eb126e1cd8a52159d2d577e610df6c8af36a436f9ed5028cfabd11a85ff",
|
|
115
|
-
"preview/vfs-image.tar.gz": "sha256:
|
|
115
|
+
"preview/vfs-image.tar.gz": "sha256:1b14864b2df102de16cd3f5042abe4c12c80d672a0e0803b4a926a972836af48",
|
|
116
116
|
"preview/worker-C8-_ANaz.js": "sha256:81099e5732dfe0c32bacd5897316b7956c5a0a22da01b4a0c6e21d0a3b9d9aa1"
|
|
117
117
|
}
|
|
118
118
|
}
|
|
Binary file
|
|
@@ -6752,6 +6752,7 @@ var RealtimeClient = class {
|
|
|
6752
6752
|
this.statusListeners = /* @__PURE__ */ new Set();
|
|
6753
6753
|
this.subscribers = /* @__PURE__ */ new Map();
|
|
6754
6754
|
this.pending = /* @__PURE__ */ new Map();
|
|
6755
|
+
this.sessionEpochValue = 0;
|
|
6755
6756
|
this.nextSubscriberId = 1;
|
|
6756
6757
|
this.subscribedAppId = null;
|
|
6757
6758
|
this.opening = null;
|
|
@@ -6961,16 +6962,69 @@ var RealtimeClient = class {
|
|
|
6961
6962
|
});
|
|
6962
6963
|
}
|
|
6963
6964
|
waitForSequence(sequenceNumber, timeoutMs = this.waitTimeoutMs) {
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
6971
|
-
|
|
6972
|
-
|
|
6965
|
+
const wait = this.expectSequence(sequenceNumber, timeoutMs);
|
|
6966
|
+
wait.start();
|
|
6967
|
+
return wait.promise;
|
|
6968
|
+
}
|
|
6969
|
+
/**
|
|
6970
|
+
* Register a wait for `sequenceNumber` now and start its clock later.
|
|
6971
|
+
*
|
|
6972
|
+
* An awaited send registers before it sends, so an echo that arrives before
|
|
6973
|
+
* the send's own response is not missed, and calls `start()` once the send has
|
|
6974
|
+
* been accepted: the time the send leg took (a lazy proxy-session open, the
|
|
6975
|
+
* binary relay's handshake) is then not taken from the echo's window.
|
|
6976
|
+
* {@link waitForSequence} is this with the clock started at once.
|
|
6977
|
+
*
|
|
6978
|
+
* @param sequenceNumber - The uint8 sequence the response will carry.
|
|
6979
|
+
* @param timeoutMs - How long to wait once started; defaults to
|
|
6980
|
+
* {@link RealtimeConfig.waitTimeoutMs}.
|
|
6981
|
+
*/
|
|
6982
|
+
expectSequence(sequenceNumber, timeoutMs = this.waitTimeoutMs) {
|
|
6983
|
+
let settled = false;
|
|
6984
|
+
let wait;
|
|
6985
|
+
const promise = new Promise((resolve, reject) => {
|
|
6986
|
+
wait = {
|
|
6987
|
+
resolve: (notification) => {
|
|
6988
|
+
settled = true;
|
|
6989
|
+
resolve(notification);
|
|
6990
|
+
},
|
|
6991
|
+
reject: (error) => {
|
|
6992
|
+
settled = true;
|
|
6993
|
+
reject(error);
|
|
6994
|
+
}
|
|
6995
|
+
};
|
|
6973
6996
|
});
|
|
6997
|
+
promise.catch(() => void 0);
|
|
6998
|
+
const waits = this.pending.get(sequenceNumber) ?? [];
|
|
6999
|
+
waits.push(wait);
|
|
7000
|
+
this.pending.set(sequenceNumber, waits);
|
|
7001
|
+
return {
|
|
7002
|
+
promise,
|
|
7003
|
+
start: () => {
|
|
7004
|
+
if (settled || wait.timer !== void 0)
|
|
7005
|
+
return;
|
|
7006
|
+
wait.timer = setTimeout(() => {
|
|
7007
|
+
this.removePending(sequenceNumber, wait);
|
|
7008
|
+
wait.reject(new CrowdyRealtimeError(`Timed out waiting for UDP response sequence ${sequenceNumber}`, { code: "UDP_SEQUENCE_TIMEOUT", retryable: true }));
|
|
7009
|
+
}, timeoutMs);
|
|
7010
|
+
},
|
|
7011
|
+
cancel: () => {
|
|
7012
|
+
if (wait.timer !== void 0)
|
|
7013
|
+
clearTimeout(wait.timer);
|
|
7014
|
+
settled = true;
|
|
7015
|
+
this.removePending(sequenceNumber, wait);
|
|
7016
|
+
}
|
|
7017
|
+
};
|
|
7018
|
+
}
|
|
7019
|
+
/**
|
|
7020
|
+
* How many times this client's realtime session has been established: every
|
|
7021
|
+
* transition into `connected` (the first connect, each reconnect) and every
|
|
7022
|
+
* move to another instance counts one. An awaited send compares it with the
|
|
7023
|
+
* epoch of its last confirmed response to know it is the first send of a new
|
|
7024
|
+
* session.
|
|
7025
|
+
*/
|
|
7026
|
+
sessionEpoch() {
|
|
7027
|
+
return this.sessionEpochValue;
|
|
6974
7028
|
}
|
|
6975
7029
|
/**
|
|
6976
7030
|
* Whether the binary relay transport is currently active and able to carry
|
|
@@ -7265,6 +7319,7 @@ var RealtimeClient = class {
|
|
|
7265
7319
|
this.wsUrl = wsUrl;
|
|
7266
7320
|
this.binaryRelayUrl = deriveBinaryRelayUrl(wsUrl);
|
|
7267
7321
|
this.connectFailures = 0;
|
|
7322
|
+
this.sessionEpochValue += 1;
|
|
7268
7323
|
this.restartBinaryRelay();
|
|
7269
7324
|
if (opts.moveNow)
|
|
7270
7325
|
this.restartGraphqlSubscription();
|
|
@@ -7358,6 +7413,7 @@ var RealtimeClient = class {
|
|
|
7358
7413
|
this.wsUrl = target.wsUrl;
|
|
7359
7414
|
this.binaryRelayUrl = deriveBinaryRelayUrl(target.wsUrl);
|
|
7360
7415
|
this.connectFailures = 0;
|
|
7416
|
+
this.sessionEpochValue += 1;
|
|
7361
7417
|
this.onEndpointMove?.({ httpUrl: target.httpUrl, wsUrl: target.wsUrl });
|
|
7362
7418
|
this.restartBinaryRelay();
|
|
7363
7419
|
}
|
|
@@ -7522,6 +7578,8 @@ var RealtimeClient = class {
|
|
|
7522
7578
|
if (status === this.statusValue)
|
|
7523
7579
|
return;
|
|
7524
7580
|
this.statusValue = status;
|
|
7581
|
+
if (status === "connected")
|
|
7582
|
+
this.sessionEpochValue += 1;
|
|
7525
7583
|
for (const listener of [...this.statusListeners]) {
|
|
7526
7584
|
listener(status);
|
|
7527
7585
|
}
|
|
@@ -7644,9 +7702,11 @@ var ActorClient = class {
|
|
|
7644
7702
|
* applied echo — your own `ActorUpdateNotification` (the sender is included in
|
|
7645
7703
|
* the chunk fan-out), correlated by `sequenceNumber`.
|
|
7646
7704
|
*
|
|
7647
|
-
* The
|
|
7648
|
-
*
|
|
7649
|
-
*
|
|
7705
|
+
* The game server holds the first message into a newly entered region until
|
|
7706
|
+
* its permissions load (realtime server v0.42.0 and later). Against an older
|
|
7707
|
+
* server the first awaited send of each session is retried once (see
|
|
7708
|
+
* `UdpAPI.sendActorUpdateAndWait`); if `join` still rejects with a timeout,
|
|
7709
|
+
* call it again.
|
|
7650
7710
|
*
|
|
7651
7711
|
* @param chunk - The chunk to enter, `{ x, y, z }` with each axis a signed
|
|
7652
7712
|
* int64 decimal string. A chunk is a 16x16x16 voxel cube.
|
|
@@ -17184,6 +17244,7 @@ var UdpAPI = class {
|
|
|
17184
17244
|
this.waitForGameplayTokenRefresh = waitForGameplayTokenRefresh;
|
|
17185
17245
|
this.sequences = new SequenceAllocator();
|
|
17186
17246
|
this.inFlightActorUpdates = /* @__PURE__ */ new Set();
|
|
17247
|
+
this.confirmedEpoch = -1;
|
|
17187
17248
|
}
|
|
17188
17249
|
/**
|
|
17189
17250
|
* Resolves when every in-flight {@link sendActorUpdate} has settled.
|
|
@@ -17370,8 +17431,22 @@ var UdpAPI = class {
|
|
|
17370
17431
|
* @param input - {@link ActorUpdateRequestInput} (see {@link sendActorUpdate}
|
|
17371
17432
|
* for field units/encoding). Omit `sequenceNumber` to let the SDK allocate
|
|
17372
17433
|
* one.
|
|
17373
|
-
*
|
|
17374
|
-
*
|
|
17434
|
+
* Every `send*AndWait` behaves as follows:
|
|
17435
|
+
* - `timeoutMs` counts from when the send was accepted, not from the call, so
|
|
17436
|
+
* a slow send leg (the first send opening the proxy session, the binary
|
|
17437
|
+
* relay finishing its handshake) does not shorten the wait for the echo.
|
|
17438
|
+
* - The first awaited send of a new session (the first connect, a reconnect,
|
|
17439
|
+
* a move to another instance) is retried once with a fresh sequence number
|
|
17440
|
+
* if it is refused `UNAUTHORIZED` or, for the actor and voxel updates the
|
|
17441
|
+
* server echoes, if its echo times out (`UDP_SEQUENCE_TIMEOUT`), which is
|
|
17442
|
+
* how a game server older than v0.42.0 answers a session's first messages;
|
|
17443
|
+
* the resolved notification then carries the retry's sequence number. A
|
|
17444
|
+
* send whose `sequenceNumber` you chose is not retried, and neither is the
|
|
17445
|
+
* timeout of an audio, text or event send, which is how those succeed.
|
|
17446
|
+
*
|
|
17447
|
+
* @param options - `timeoutMs`: how long to wait for the echo once the send
|
|
17448
|
+
* is accepted, in milliseconds; defaults to the client's realtime
|
|
17449
|
+
* `waitTimeoutMs` (5000).
|
|
17375
17450
|
* @returns The correlated {@link SpatialNotification} (your own
|
|
17376
17451
|
* `ActorUpdateNotification` self-echo).
|
|
17377
17452
|
* @throws {CrowdyGraphQLError} if the underlying send is rejected.
|
|
@@ -17379,7 +17454,8 @@ var UdpAPI = class {
|
|
|
17379
17454
|
* request timeout.
|
|
17380
17455
|
* @throws {CrowdyRealtimeError} if no echo arrives within `timeoutMs`
|
|
17381
17456
|
* (`code === 'UDP_SEQUENCE_TIMEOUT'`, retryable), or the server returns a
|
|
17382
|
-
* matching `GenericErrorResponse` (its `code` is the server's `errorCode`)
|
|
17457
|
+
* matching `GenericErrorResponse` (its `code` is the server's `errorCode`),
|
|
17458
|
+
* after the one retry described above when it applies.
|
|
17383
17459
|
* @example
|
|
17384
17460
|
* ```ts
|
|
17385
17461
|
* const unsubscribe = client.udp.subscribe({}, appId); // echoes need an open WS
|
|
@@ -17394,11 +17470,7 @@ var UdpAPI = class {
|
|
|
17394
17470
|
* ```
|
|
17395
17471
|
*/
|
|
17396
17472
|
async sendActorUpdateAndWait(input, options = {}) {
|
|
17397
|
-
|
|
17398
|
-
const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
|
|
17399
|
-
await this.sendActorUpdate(request);
|
|
17400
|
-
this.subs.flushSends();
|
|
17401
|
-
return wait;
|
|
17473
|
+
return this.sendAndWait(input, options.timeoutMs, true, (request) => this.sendActorUpdate(request));
|
|
17402
17474
|
}
|
|
17403
17475
|
/**
|
|
17404
17476
|
* Send a single voxel (block) update for spatial replication to nearby
|
|
@@ -17465,11 +17537,7 @@ var UdpAPI = class {
|
|
|
17465
17537
|
* (its `code` is the server's `errorCode`).
|
|
17466
17538
|
*/
|
|
17467
17539
|
async sendVoxelUpdateAndWait(input, options = {}) {
|
|
17468
|
-
|
|
17469
|
-
const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
|
|
17470
|
-
await this.sendVoxelUpdate(request);
|
|
17471
|
-
this.subs.flushSends();
|
|
17472
|
-
return wait;
|
|
17540
|
+
return this.sendAndWait(input, options.timeoutMs, true, (request) => this.sendVoxelUpdate(request));
|
|
17473
17541
|
}
|
|
17474
17542
|
/**
|
|
17475
17543
|
* Send a spatial voice/audio packet, fanned out to nearby actors as a
|
|
@@ -17528,11 +17596,7 @@ var UdpAPI = class {
|
|
|
17528
17596
|
* `errorCode`, e.g. `UNAUTHORIZED` for a missing voice permission).
|
|
17529
17597
|
*/
|
|
17530
17598
|
async sendAudioPacketAndWait(input, options = {}) {
|
|
17531
|
-
|
|
17532
|
-
const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
|
|
17533
|
-
await this.sendAudioPacket(request);
|
|
17534
|
-
this.subs.flushSends();
|
|
17535
|
-
return wait;
|
|
17599
|
+
return this.sendAndWait(input, options.timeoutMs, false, (request) => this.sendAudioPacket(request));
|
|
17536
17600
|
}
|
|
17537
17601
|
/**
|
|
17538
17602
|
* Send ONE webcam video fragment, fanned out to nearby actors as a
|
|
@@ -17643,11 +17707,7 @@ var UdpAPI = class {
|
|
|
17643
17707
|
* `errorCode`).
|
|
17644
17708
|
*/
|
|
17645
17709
|
async sendTextPacketAndWait(input, options = {}) {
|
|
17646
|
-
|
|
17647
|
-
const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
|
|
17648
|
-
await this.sendTextPacket(request);
|
|
17649
|
-
this.subs.flushSends();
|
|
17650
|
-
return wait;
|
|
17710
|
+
return this.sendAndWait(input, options.timeoutMs, false, (request) => this.sendTextPacket(request));
|
|
17651
17711
|
}
|
|
17652
17712
|
/**
|
|
17653
17713
|
* Send a custom, app-defined client event for spatial replication to nearby
|
|
@@ -17704,11 +17764,7 @@ var UdpAPI = class {
|
|
|
17704
17764
|
* `errorCode`).
|
|
17705
17765
|
*/
|
|
17706
17766
|
async sendClientEventAndWait(input, options = {}) {
|
|
17707
|
-
|
|
17708
|
-
const wait = this.subs.waitForSequence(request.sequenceNumber, options.timeoutMs);
|
|
17709
|
-
await this.sendClientEvent(request);
|
|
17710
|
-
this.subs.flushSends();
|
|
17711
|
-
return wait;
|
|
17767
|
+
return this.sendAndWait(input, options.timeoutMs, false, (request) => this.sendClientEvent(request));
|
|
17712
17768
|
}
|
|
17713
17769
|
/**
|
|
17714
17770
|
* Send a direct actor-to-actor message, delivered only to the actor whose
|
|
@@ -17923,6 +17979,53 @@ var UdpAPI = class {
|
|
|
17923
17979
|
subscribe(handlers, appId) {
|
|
17924
17980
|
return this.subs.subscribe(handlers, appId);
|
|
17925
17981
|
}
|
|
17982
|
+
/**
|
|
17983
|
+
* What every `send*AndWait` does.
|
|
17984
|
+
*
|
|
17985
|
+
* The wait is registered before the send, so an echo that beats the send's
|
|
17986
|
+
* own response is not missed, and its clock starts once the send has been
|
|
17987
|
+
* accepted, so a lazy proxy-session open or the relay's handshake on the send
|
|
17988
|
+
* leg is not taken from the echo's window.
|
|
17989
|
+
*
|
|
17990
|
+
* The first awaited send of a new session (the first connect, a reconnect, or
|
|
17991
|
+
* a move to another instance) is retried once, with a fresh sequence number,
|
|
17992
|
+
* when it is refused `UNAUTHORIZED` or, for a send the server echoes, when
|
|
17993
|
+
* its echo times out: a game server from before the one that holds a
|
|
17994
|
+
* session's first messages refuses or drops them while the session's
|
|
17995
|
+
* permissions load. A send the server does not echo (audio, text, a client
|
|
17996
|
+
* event) succeeds by timing out, so its timeout is never retried. Only when
|
|
17997
|
+
* the SDK chose the sequence number, because the retry needs one of its own
|
|
17998
|
+
* and a caller who chose theirs is correlating by it.
|
|
17999
|
+
*/
|
|
18000
|
+
async sendAndWait(input, timeoutMs, echoed, send) {
|
|
18001
|
+
const epoch = this.subs.sessionEpoch();
|
|
18002
|
+
const retryable = epoch !== this.confirmedEpoch && input.sequenceNumber == null;
|
|
18003
|
+
try {
|
|
18004
|
+
const echo = await this.sendOnceAndWait(this.withSequence(input), timeoutMs, send).catch((error) => {
|
|
18005
|
+
if (!retryable || !isFirstSendFailure(error, echoed))
|
|
18006
|
+
throw error;
|
|
18007
|
+
return this.sendOnceAndWait(this.withSequence({ ...input, sequenceNumber: void 0 }), timeoutMs, send);
|
|
18008
|
+
});
|
|
18009
|
+
this.confirmedEpoch = epoch;
|
|
18010
|
+
return echo;
|
|
18011
|
+
} catch (error) {
|
|
18012
|
+
if (!echoed && isTimeout(error))
|
|
18013
|
+
this.confirmedEpoch = epoch;
|
|
18014
|
+
throw error;
|
|
18015
|
+
}
|
|
18016
|
+
}
|
|
18017
|
+
async sendOnceAndWait(request, timeoutMs, send) {
|
|
18018
|
+
const wait = this.subs.expectSequence(request.sequenceNumber, timeoutMs);
|
|
18019
|
+
try {
|
|
18020
|
+
await send(request);
|
|
18021
|
+
} catch (error) {
|
|
18022
|
+
wait.cancel();
|
|
18023
|
+
throw error;
|
|
18024
|
+
}
|
|
18025
|
+
this.subs.flushSends();
|
|
18026
|
+
wait.start();
|
|
18027
|
+
return wait.promise;
|
|
18028
|
+
}
|
|
17926
18029
|
withSequence(input) {
|
|
17927
18030
|
return {
|
|
17928
18031
|
...input,
|
|
@@ -17930,6 +18033,12 @@ var UdpAPI = class {
|
|
|
17930
18033
|
};
|
|
17931
18034
|
}
|
|
17932
18035
|
};
|
|
18036
|
+
function isFirstSendFailure(error, echoed) {
|
|
18037
|
+
return error instanceof CrowdyRealtimeError && (error.code === "UNAUTHORIZED" || echoed && isTimeout(error));
|
|
18038
|
+
}
|
|
18039
|
+
function isTimeout(error) {
|
|
18040
|
+
return error instanceof CrowdyRealtimeError && error.code === "UDP_SEQUENCE_TIMEOUT";
|
|
18041
|
+
}
|
|
17933
18042
|
|
|
17934
18043
|
var STATUS_FIELDS = `
|
|
17935
18044
|
configured connected accountLogin accountType owner repo branch githubSha repositorySelection installUrl
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crowdedkingdoms/crowdy-dsh",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.13-test.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -94,11 +94,11 @@
|
|
|
94
94
|
"crowdyDsh": {
|
|
95
95
|
"build": {
|
|
96
96
|
"package": "@crowdedkingdoms/crowdy-dsh",
|
|
97
|
-
"version": "0.4.
|
|
98
|
-
"builtAt": "2026-10-
|
|
97
|
+
"version": "0.4.13",
|
|
98
|
+
"builtAt": "2026-10-24T23:04:25.889Z",
|
|
99
99
|
"source": {
|
|
100
100
|
"repository": "CrowdedKingdoms/crowdy-dsh",
|
|
101
|
-
"commit": "
|
|
101
|
+
"commit": "e04cd33909e335ec360c653e557139378a7f1dbc",
|
|
102
102
|
"dirty": false
|
|
103
103
|
},
|
|
104
104
|
"upstream": {
|
|
@@ -109,7 +109,7 @@
|
|
|
109
109
|
},
|
|
110
110
|
"crowdyjs": {
|
|
111
111
|
"name": "@crowdedkingdoms/crowdyjs",
|
|
112
|
-
"version": "18.
|
|
112
|
+
"version": "18.20.0-test.1",
|
|
113
113
|
"source": "node_modules"
|
|
114
114
|
},
|
|
115
115
|
"files": {
|
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
"manifest.webmanifest": "sha256:9b54d57eea00f2fdd1affe6e7999a4209a515755f12ac1a3299da2d55e178d4c",
|
|
207
207
|
"preview/bootstrap-CYECZNXt.js": "sha256:9f303a0485c0badfdd19fad0c357ef050c5b9a343744ab5bf0c70015e7d95092",
|
|
208
208
|
"preview/crowdy-boot-Cklh1qNl.js": "sha256:b21e8eb126e1cd8a52159d2d577e610df6c8af36a436f9ed5028cfabd11a85ff",
|
|
209
|
-
"preview/vfs-image.tar.gz": "sha256:
|
|
209
|
+
"preview/vfs-image.tar.gz": "sha256:1b14864b2df102de16cd3f5042abe4c12c80d672a0e0803b4a926a972836af48",
|
|
210
210
|
"preview/worker-C8-_ANaz.js": "sha256:81099e5732dfe0c32bacd5897316b7956c5a0a22da01b4a0c6e21d0a3b9d9aa1"
|
|
211
211
|
}
|
|
212
212
|
}
|