@horizon-republic/nestjs-jetstream 2.3.0 → 2.3.4

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/index.cjs CHANGED
@@ -203,7 +203,11 @@ var JetstreamRecordBuilder = class {
203
203
  constructor(data) {
204
204
  this.data = data;
205
205
  }
206
- /** Set the message payload. */
206
+ /**
207
+ * Set the message payload.
208
+ *
209
+ * @param data - Payload to serialize via the configured {@link Codec}.
210
+ */
207
211
  setData(data) {
208
212
  this.data = data;
209
213
  return this;
@@ -211,6 +215,8 @@ var JetstreamRecordBuilder = class {
211
215
  /**
212
216
  * Set a single custom header.
213
217
  *
218
+ * @param key - Header name (e.g. `'x-tenant'`).
219
+ * @param value - Header value.
214
220
  * @throws Error if the header name is reserved by the transport.
215
221
  */
216
222
  setHeader(key, value) {
@@ -221,6 +227,7 @@ var JetstreamRecordBuilder = class {
221
227
  /**
222
228
  * Set multiple custom headers at once.
223
229
  *
230
+ * @param headers - Key-value pairs to set as headers.
224
231
  * @throws Error if any header name is reserved by the transport.
225
232
  */
226
233
  setHeaders(headers2) {
@@ -229,12 +236,20 @@ var JetstreamRecordBuilder = class {
229
236
  }
230
237
  return this;
231
238
  }
232
- /** Set RPC request timeout in milliseconds. */
239
+ /**
240
+ * Set per-request RPC timeout.
241
+ *
242
+ * @param ms - Timeout in milliseconds. Overrides the global RPC timeout for this request only.
243
+ */
233
244
  setTimeout(ms) {
234
245
  this.timeout = ms;
235
246
  return this;
236
247
  }
237
- /** Build the immutable JetstreamRecord. */
248
+ /**
249
+ * Build the immutable {@link JetstreamRecord}.
250
+ *
251
+ * @returns A frozen record ready to pass to `client.send()` or `client.emit()`.
252
+ */
238
253
  build() {
239
254
  return new JetstreamRecord(this.data, new Map(this.headers), this.timeout);
240
255
  }
@@ -270,7 +285,14 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
270
285
  pendingTimeouts = /* @__PURE__ */ new Map();
271
286
  /** Subscription to connection status events for disconnect handling. */
272
287
  statusSubscription = null;
273
- /** Establish connection. Called automatically by NestJS on first use. */
288
+ /**
289
+ * Establish connection. Called automatically by NestJS on first use.
290
+ *
291
+ * Sets up the JetStream RPC inbox (if in JetStream mode) and subscribes
292
+ * to connection status events for fail-fast disconnect handling.
293
+ *
294
+ * @returns The underlying NATS connection.
295
+ */
274
296
  async connect() {
275
297
  const nc = await this.connection.getConnection();
276
298
  if (this.isJetStreamRpcMode() && !this.inboxSubscription) {
@@ -283,15 +305,23 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
283
305
  });
284
306
  return nc;
285
307
  }
286
- /** Clean up resources. */
308
+ /** Clean up resources: reject pending RPCs, unsubscribe from status events. */
287
309
  async close() {
288
310
  this.statusSubscription?.unsubscribe();
289
311
  this.statusSubscription = null;
290
312
  this.rejectPendingRpcs(new Error("Client closed"));
291
313
  }
292
- /** Direct access to the raw NATS connection. */
314
+ /**
315
+ * Direct access to the raw NATS connection.
316
+ *
317
+ * @throws Error if not connected.
318
+ */
293
319
  unwrap() {
294
- return this.connection.unwrap;
320
+ const nc = this.connection.unwrap;
321
+ if (!nc) {
322
+ throw new Error("Not connected \u2014 call connect() before unwrap()");
323
+ }
324
+ return nc;
295
325
  }
296
326
  /**
297
327
  * Publish a fire-and-forget event to JetStream.
@@ -304,10 +334,13 @@ var JetstreamClient = class extends import_microservices.ClientProxy {
304
334
  const { data, hdrs } = this.extractRecordData(packet.data);
305
335
  const subject = this.buildEventSubject(packet.pattern);
306
336
  const msgHeaders = this.buildHeaders(hdrs, { subject });
307
- await nc.jetstream().publish(subject, this.codec.encode(data), {
337
+ const ack = await nc.jetstream().publish(subject, this.codec.encode(data), {
308
338
  headers: msgHeaders,
309
339
  msgID: crypto.randomUUID()
310
340
  });
341
+ if (ack.duplicate) {
342
+ this.logger.warn(`Duplicate event publish detected: ${subject} (seq: ${ack.seq})`);
343
+ }
311
344
  return void 0;
312
345
  }
313
346
  /**
@@ -571,11 +604,16 @@ var ConnectionProvider = class {
571
604
  if (this.connectionPromise) {
572
605
  return this.connectionPromise;
573
606
  }
574
- this.connectionPromise = this.establish();
607
+ this.connectionPromise = this.establish().catch((err) => {
608
+ this.connectionPromise = null;
609
+ throw err;
610
+ });
575
611
  return this.connectionPromise;
576
612
  }
577
613
  /**
578
- * Get JetStream manager. Cached after first call.
614
+ * Get the JetStream manager. Cached after first call.
615
+ *
616
+ * @returns The JetStreamManager for stream/consumer administration.
579
617
  */
580
618
  async getJetStreamManager() {
581
619
  if (this.jsmInstance) return this.jsmInstance;
@@ -584,7 +622,7 @@ var ConnectionProvider = class {
584
622
  this.logger.log("JetStream manager initialized");
585
623
  return this.jsmInstance;
586
624
  }
587
- /** Direct access to the raw NATS connection (assumes already connected). */
625
+ /** Direct access to the raw NATS connection, or `null` if not yet connected. */
588
626
  get unwrap() {
589
627
  return this.connection;
590
628
  }
@@ -624,8 +662,7 @@ var ConnectionProvider = class {
624
662
  this.monitorStatus(nc);
625
663
  return nc;
626
664
  } catch (err) {
627
- const natsErr = err;
628
- if (natsErr.code === "CONNECTION_REFUSED") {
665
+ if (err instanceof import_nats4.NatsError && err.code === "CONNECTION_REFUSED") {
629
666
  throw new Error(`NATS connection refused: ${this.options.servers.join(", ")}`);
630
667
  }
631
668
  throw err;
@@ -669,53 +706,21 @@ var EventBus = class {
669
706
  this.logger = logger;
670
707
  this.hooks = hooks ?? {};
671
708
  }
672
- /** Emit a lifecycle event. Dispatches to custom hook or Logger fallback. */
709
+ /**
710
+ * Emit a lifecycle event. Dispatches to custom hook if registered, otherwise no-op.
711
+ *
712
+ * @param event - The {@link TransportEvent} to emit.
713
+ * @param args - Arguments matching the hook signature for this event.
714
+ */
673
715
  emit(event, ...args) {
674
716
  const hook = this.hooks[event];
675
- if (hook) {
676
- try {
677
- hook(...args);
678
- } catch (err) {
679
- this.logger.error(
680
- `Hook "${event}" threw an error: ${err instanceof Error ? err.message : err}`
681
- );
682
- }
683
- return;
684
- }
685
- this.defaultHandler(event, args);
686
- }
687
- /** Default Logger-based handlers for each event type. */
688
- defaultHandler(event, args) {
689
- switch (event) {
690
- case "connect" /* Connect */:
691
- this.logger.log(`Connected to NATS: ${args[0]}`);
692
- break;
693
- case "disconnect" /* Disconnect */:
694
- this.logger.warn("NATS connection lost");
695
- break;
696
- case "reconnect" /* Reconnect */:
697
- this.logger.log(`Reconnected to NATS: ${args[0]}`);
698
- break;
699
- case "error" /* Error */:
700
- this.logger.error(`Transport error: ${args[0]}`, args[1] ?? "");
701
- break;
702
- case "rpcTimeout" /* RpcTimeout */:
703
- this.logger.warn(`RPC timeout: ${args[0]} (cid: ${args[1]})`);
704
- break;
705
- case "messageRouted" /* MessageRouted */:
706
- this.logger.debug(`Message routed: ${args[0]} [${args[1]}]`);
707
- break;
708
- case "shutdownStart" /* ShutdownStart */:
709
- this.logger.log("Graceful shutdown initiated");
710
- break;
711
- case "shutdownComplete" /* ShutdownComplete */:
712
- this.logger.log("Graceful shutdown complete");
713
- break;
714
- case "deadLetter" /* DeadLetter */: {
715
- const info = args[0];
716
- this.logger.warn(`Dead letter: ${info?.subject ?? "unknown"}`);
717
- break;
718
- }
717
+ if (!hook) return;
718
+ try {
719
+ hook(...args);
720
+ } catch (err) {
721
+ this.logger.error(
722
+ `Hook "${event}" threw an error: ${err instanceof Error ? err.message : err}`
723
+ );
719
724
  }
720
725
  }
721
726
  };
@@ -732,6 +737,8 @@ var JetstreamHealthIndicator = class {
732
737
  *
733
738
  * Returns the current connection status without throwing.
734
739
  * Use this for custom health endpoints or monitoring integrations.
740
+ *
741
+ * @returns Connection status with server URL and RTT latency.
735
742
  */
736
743
  async check() {
737
744
  const nc = this.connection.unwrap;
@@ -754,7 +761,9 @@ var JetstreamHealthIndicator = class {
754
761
  * Returns `{ [key]: { status: 'up', ... } }` on success.
755
762
  * Throws an error with `{ [key]: { status: 'down', ... } }` on failure.
756
763
  *
757
- * @param key Health indicator key (default: 'jetstream')
764
+ * @param key - Health indicator key (default: `'jetstream'`).
765
+ * @returns Object with status, server, and latency under the given key.
766
+ * @throws Error with `{ [key]: { status: 'down' } }` when disconnected.
758
767
  */
759
768
  async isHealthy(key = "jetstream") {
760
769
  const status = await this.check();
@@ -793,12 +802,18 @@ var JetstreamStrategy = class extends import_microservices2.Server {
793
802
  transportId = /* @__PURE__ */ Symbol("jetstream-transport");
794
803
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
795
804
  listeners = /* @__PURE__ */ new Map();
805
+ started = false;
796
806
  /**
797
807
  * Start the transport: register handlers, create infrastructure, begin consumption.
798
808
  *
799
809
  * Called by NestJS when `connectMicroservice()` is used, or internally by the module.
800
810
  */
801
811
  async listen(callback) {
812
+ if (this.started) {
813
+ this.logger.warn("listen() called more than once \u2014 ignoring");
814
+ return;
815
+ }
816
+ this.started = true;
802
817
  this.patternRegistry.registerHandlers(this.getHandlers());
803
818
  const streamKinds = this.resolveStreamKinds();
804
819
  if (streamKinds.length > 0) {
@@ -817,17 +832,18 @@ var JetstreamStrategy = class extends import_microservices2.Server {
817
832
  }
818
833
  callback();
819
834
  }
820
- /** Gracefully stop the transport. */
835
+ /** Stop all consumers, routers, and subscriptions. Called during shutdown. */
821
836
  close() {
822
837
  this.eventRouter.destroy();
823
838
  this.rpcRouter.destroy();
824
839
  this.coreRpcServer.stop();
825
840
  this.messageProvider.destroy();
841
+ this.started = false;
826
842
  }
827
843
  /**
828
844
  * Register event listener (required by Server base class).
829
845
  *
830
- * Stores callbacks for potential use. Primary lifecycle events
846
+ * Stores callbacks for client use. Primary lifecycle events
831
847
  * are routed through EventBus.
832
848
  */
833
849
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
@@ -836,9 +852,17 @@ var JetstreamStrategy = class extends import_microservices2.Server {
836
852
  existing.push(callback);
837
853
  this.listeners.set(event, existing);
838
854
  }
839
- /** Unwrap the underlying NATS connection. */
855
+ /**
856
+ * Unwrap the underlying NATS connection.
857
+ *
858
+ * @throws Error if the transport has not started.
859
+ */
840
860
  unwrap() {
841
- return this.connection.unwrap;
861
+ const nc = this.connection.unwrap;
862
+ if (!nc) {
863
+ throw new Error("Not connected \u2014 transport has not started");
864
+ }
865
+ return nc;
842
866
  }
843
867
  /** Access the pattern registry (for module-level introspection). */
844
868
  getPatternRegistry() {
@@ -873,23 +897,37 @@ var import_nats5 = require("nats");
873
897
  // src/context/rpc.context.ts
874
898
  var import_microservices3 = require("@nestjs/microservices");
875
899
  var RpcContext = class extends import_microservices3.BaseRpcContext {
876
- /** Get the underlying NATS message (JsMsg for JetStream, Msg for Core). */
900
+ /**
901
+ * Get the underlying NATS message.
902
+ *
903
+ * @returns `JsMsg` for JetStream handlers, `Msg` for Core RPC handlers.
904
+ */
877
905
  getMessage() {
878
906
  return this.args[0];
879
907
  }
880
- /** Get the NATS subject this message was published to. */
908
+ /** @returns The NATS subject this message was published to. */
881
909
  getSubject() {
882
910
  return this.args[0].subject;
883
911
  }
884
- /** Get all NATS message headers, or undefined if none are present. */
912
+ /** @returns All NATS message headers, or `undefined` if none are present. */
885
913
  getHeaders() {
886
914
  return this.args[0].headers;
887
915
  }
888
- /** Get a single header value by key. Returns undefined if the header or headers object is missing. */
916
+ /**
917
+ * Get a single header value by key.
918
+ *
919
+ * @param key - Header name (e.g. `'x-trace-id'`).
920
+ * @returns Header value, or `undefined` if the header is missing.
921
+ */
889
922
  getHeader(key) {
890
923
  return this.args[0].headers?.get(key);
891
924
  }
892
- /** Type guard: narrows getMessage() return type to JsMsg when true. */
925
+ /**
926
+ * Type guard: returns `true` when the message is a JetStream message.
927
+ *
928
+ * Narrows `getMessage()` return type to `JsMsg`, giving access to
929
+ * `ack()`, `nak()`, `term()`, and delivery metadata.
930
+ */
893
931
  isJetStream() {
894
932
  return "ack" in this.args[0];
895
933
  }
@@ -1008,6 +1046,7 @@ var CoreRpcServer = class {
1008
1046
 
1009
1047
  // src/server/infrastructure/stream.provider.ts
1010
1048
  var import_common5 = require("@nestjs/common");
1049
+ var import_nats6 = require("nats");
1011
1050
  var STREAM_NOT_FOUND = 10059;
1012
1051
  var StreamProvider = class {
1013
1052
  constructor(options, connection) {
@@ -1050,8 +1089,7 @@ var StreamProvider = class {
1050
1089
  this.logger.debug(`Stream exists, updating: ${config.name}`);
1051
1090
  return await jsm.streams.update(config.name, config);
1052
1091
  } catch (err) {
1053
- const natsErr = err;
1054
- if (natsErr.api_error?.err_code === STREAM_NOT_FOUND) {
1092
+ if (err instanceof import_nats6.NatsError && err.api_error?.err_code === STREAM_NOT_FOUND) {
1055
1093
  this.logger.log(`Creating stream: ${config.name}`);
1056
1094
  return await jsm.streams.add(config);
1057
1095
  }
@@ -1099,6 +1137,7 @@ var StreamProvider = class {
1099
1137
 
1100
1138
  // src/server/infrastructure/consumer.provider.ts
1101
1139
  var import_common6 = require("@nestjs/common");
1140
+ var import_nats7 = require("nats");
1102
1141
  var CONSUMER_NOT_FOUND = 10014;
1103
1142
  var ConsumerProvider = class {
1104
1143
  constructor(options, connection, streamProvider, patternRegistry) {
@@ -1139,8 +1178,7 @@ var ConsumerProvider = class {
1139
1178
  this.logger.debug(`Consumer exists: ${name}`);
1140
1179
  return info;
1141
1180
  } catch (err) {
1142
- const natsErr = err;
1143
- if (natsErr.api_error?.err_code === CONSUMER_NOT_FOUND) {
1181
+ if (err instanceof import_nats7.NatsError && err.api_error?.err_code === CONSUMER_NOT_FOUND) {
1144
1182
  this.logger.log(`Creating consumer: ${name}`);
1145
1183
  return await jsm.consumers.add(stream, config);
1146
1184
  }
@@ -1216,6 +1254,7 @@ var MessageProvider = class {
1216
1254
  }
1217
1255
  logger = new import_common7.Logger("Jetstream:Message");
1218
1256
  destroy$ = new import_rxjs3.Subject();
1257
+ activeIterators = /* @__PURE__ */ new Set();
1219
1258
  eventMessages$ = new import_rxjs3.Subject();
1220
1259
  commandMessages$ = new import_rxjs3.Subject();
1221
1260
  broadcastMessages$ = new import_rxjs3.Subject();
@@ -1250,6 +1289,10 @@ var MessageProvider = class {
1250
1289
  destroy() {
1251
1290
  this.destroy$.next();
1252
1291
  this.destroy$.complete();
1292
+ for (const messages of this.activeIterators) {
1293
+ messages.stop();
1294
+ }
1295
+ this.activeIterators.clear();
1253
1296
  this.eventMessages$.complete();
1254
1297
  this.commandMessages$.complete();
1255
1298
  this.broadcastMessages$.complete();
@@ -1257,8 +1300,13 @@ var MessageProvider = class {
1257
1300
  /** Create a self-healing consumer flow for a specific kind. */
1258
1301
  createFlow(kind, info) {
1259
1302
  const target$ = this.getTargetSubject(kind);
1303
+ let consecutiveFailures = 0;
1260
1304
  return (0, import_rxjs3.defer)(() => this.consumeOnce(info, target$)).pipe(
1305
+ (0, import_rxjs3.tap)(() => {
1306
+ consecutiveFailures = 0;
1307
+ }),
1261
1308
  (0, import_rxjs3.catchError)((err) => {
1309
+ consecutiveFailures++;
1262
1310
  this.logger.error(`Consumer ${info.name} error, will restart:`, err);
1263
1311
  this.eventBus.emit(
1264
1312
  "error" /* Error */,
@@ -1269,13 +1317,14 @@ var MessageProvider = class {
1269
1317
  }),
1270
1318
  (0, import_rxjs3.repeat)({
1271
1319
  delay: () => {
1272
- this.logger.warn(`Consumer ${info.name} stream ended, restarting...`);
1320
+ const delay = Math.min(100 * Math.pow(2, consecutiveFailures), 3e4);
1321
+ this.logger.warn(`Consumer ${info.name} stream ended, restarting in ${delay}ms...`);
1273
1322
  this.eventBus.emit(
1274
1323
  "error" /* Error */,
1275
1324
  new Error(`Consumer ${info.name} stream ended`),
1276
1325
  "message-provider"
1277
1326
  );
1278
- return (0, import_rxjs3.timer)(100);
1327
+ return (0, import_rxjs3.timer)(delay);
1279
1328
  }
1280
1329
  }),
1281
1330
  (0, import_rxjs3.takeUntil)(this.destroy$)
@@ -1286,8 +1335,13 @@ var MessageProvider = class {
1286
1335
  const js = (await this.connection.getConnection()).jetstream();
1287
1336
  const consumer = await js.consumers.get(info.stream_name, info.name);
1288
1337
  const messages = await consumer.consume();
1289
- for await (const msg of messages) {
1290
- target$.next(msg);
1338
+ this.activeIterators.add(messages);
1339
+ try {
1340
+ for await (const msg of messages) {
1341
+ target$.next(msg);
1342
+ }
1343
+ } finally {
1344
+ this.activeIterators.delete(messages);
1291
1345
  }
1292
1346
  }
1293
1347
  /** Get the target subject for a consumer kind. */
@@ -1427,11 +1481,14 @@ var EventRouter = class {
1427
1481
  /** Subscribe to a message stream and route each message. */
1428
1482
  subscribeToStream(stream$, label) {
1429
1483
  const subscription = stream$.pipe(
1430
- (0, import_rxjs4.mergeMap)((msg) => this.handle(msg)),
1431
- (0, import_rxjs4.catchError)((err, caught) => {
1432
- this.logger.error(`Unexpected error in ${label} event router`, err);
1433
- return caught;
1434
- })
1484
+ (0, import_rxjs4.mergeMap)(
1485
+ (msg) => (0, import_rxjs4.defer)(() => this.handle(msg)).pipe(
1486
+ (0, import_rxjs4.catchError)((err) => {
1487
+ this.logger.error(`Unexpected error in ${label} event router`, err);
1488
+ return import_rxjs4.EMPTY;
1489
+ })
1490
+ )
1491
+ )
1435
1492
  ).subscribe();
1436
1493
  this.subscriptions.push(subscription);
1437
1494
  }
@@ -1505,7 +1562,7 @@ var EventRouter = class {
1505
1562
 
1506
1563
  // src/server/routing/rpc.router.ts
1507
1564
  var import_common10 = require("@nestjs/common");
1508
- var import_nats6 = require("nats");
1565
+ var import_nats8 = require("nats");
1509
1566
  var import_rxjs5 = require("rxjs");
1510
1567
  var RpcRouter = class {
1511
1568
  constructor(messageProvider, patternRegistry, connection, codec, eventBus, timeout) {
@@ -1522,11 +1579,14 @@ var RpcRouter = class {
1522
1579
  /** Start routing command messages to handlers. */
1523
1580
  start() {
1524
1581
  this.subscription = this.messageProvider.commands$.pipe(
1525
- (0, import_rxjs5.mergeMap)((msg) => this.handle(msg)),
1526
- (0, import_rxjs5.catchError)((err, caught) => {
1527
- this.logger.error("Unexpected error in RPC router", err);
1528
- return caught;
1529
- })
1582
+ (0, import_rxjs5.mergeMap)(
1583
+ (msg) => (0, import_rxjs5.defer)(() => this.handle(msg)).pipe(
1584
+ (0, import_rxjs5.catchError)((err) => {
1585
+ this.logger.error("Unexpected error in RPC router", err);
1586
+ return import_rxjs5.EMPTY;
1587
+ })
1588
+ )
1589
+ )
1530
1590
  ).subscribe();
1531
1591
  }
1532
1592
  /** Stop routing and unsubscribe. */
@@ -1564,7 +1624,7 @@ var RpcRouter = class {
1564
1624
  async executeHandler(handler, data, msg, replyTo, correlationId) {
1565
1625
  const nc = await this.connection.getConnection();
1566
1626
  const ctx = new RpcContext([msg]);
1567
- const hdrs = (0, import_nats6.headers)();
1627
+ const hdrs = (0, import_nats8.headers)();
1568
1628
  hdrs.set("x-correlation-id" /* CorrelationId */, correlationId);
1569
1629
  let settled = false;
1570
1630
  const timeoutId = setTimeout(() => {
@@ -1579,8 +1639,12 @@ var RpcRouter = class {
1579
1639
  if (settled) return;
1580
1640
  settled = true;
1581
1641
  clearTimeout(timeoutId);
1582
- nc.publish(replyTo, this.codec.encode(result), { headers: hdrs });
1583
1642
  msg.ack();
1643
+ try {
1644
+ nc.publish(replyTo, this.codec.encode(result), { headers: hdrs });
1645
+ } catch (publishErr) {
1646
+ this.logger.error(`Failed to publish RPC response for ${msg.subject}`, publishErr);
1647
+ }
1584
1648
  } catch (err) {
1585
1649
  if (settled) return;
1586
1650
  settled = true;
@@ -1614,10 +1678,17 @@ var ShutdownManager = class {
1614
1678
  this.eventBus.emit("shutdownStart" /* ShutdownStart */);
1615
1679
  this.logger.log(`Graceful shutdown started (timeout: ${this.timeout}ms)`);
1616
1680
  strategy?.close();
1617
- await Promise.race([
1618
- this.connection.shutdown(),
1619
- new Promise((resolve) => setTimeout(resolve, this.timeout))
1620
- ]);
1681
+ let timeoutId;
1682
+ try {
1683
+ await Promise.race([
1684
+ this.connection.shutdown(),
1685
+ new Promise((resolve) => {
1686
+ timeoutId = setTimeout(resolve, this.timeout);
1687
+ })
1688
+ ]);
1689
+ } finally {
1690
+ clearTimeout(timeoutId);
1691
+ }
1621
1692
  this.eventBus.emit("shutdownComplete" /* ShutdownComplete */);
1622
1693
  this.logger.log("Graceful shutdown complete");
1623
1694
  }