Skip to content

Commit 06d2973

Browse files
committed
build: release 3.4.0
1 parent c6919fe commit 06d2973

18 files changed

+607
-445
lines changed

README.md

Lines changed: 118 additions & 201 deletions
Large diffs are not rendered by default.

dist/client.cjs

Lines changed: 141 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,12 @@ module.exports = __toCommonJS(client_exports);
3535
var import_crypto2 = __toESM(require("crypto"), 1);
3636
var import_fs2 = __toESM(require("fs"), 1);
3737
var import_url = require("url");
38-
var import_avro_js2 = __toESM(require("avro-js"), 1);
38+
var import_avro_js3 = __toESM(require("avro-js"), 1);
3939
var import_certifi = __toESM(require("certifi"), 1);
4040
var import_grpc_js = __toESM(require("@grpc/grpc-js"), 1);
4141
var import_proto_loader = __toESM(require("@grpc/proto-loader"), 1);
4242

43-
// src/eventParseError.js
43+
// src/utils/eventParseError.js
4444
var EventParseError = class extends Error {
4545
/**
4646
* The cause of the error.
@@ -87,12 +87,13 @@ var EventParseError = class extends Error {
8787
}
8888
};
8989

90-
// src/pubSubEventEmitter.js
90+
// src/utils/pubSubEventEmitter.js
9191
var import_events = require("events");
9292
var PubSubEventEmitter = class extends import_events.EventEmitter {
9393
#topicName;
9494
#requestedEventCount;
9595
#receivedEventCount;
96+
#latestReplayId;
9697
/**
9798
* Create a new EventEmitter for Pub/Sub API events
9899
* @param {string} topicName
@@ -104,36 +105,85 @@ var PubSubEventEmitter = class extends import_events.EventEmitter {
104105
this.#topicName = topicName;
105106
this.#requestedEventCount = requestedEventCount;
106107
this.#receivedEventCount = 0;
108+
this.#latestReplayId = null;
107109
}
108110
emit(eventName, args) {
109111
if (eventName === "data") {
110112
this.#receivedEventCount++;
113+
this.#latestReplayId = args.replayId;
111114
}
112115
return super.emit(eventName, args);
113116
}
114117
/**
115-
* Returns the number of events that were requested during the subscription
118+
* Returns the number of events that were requested when subscribing.
116119
* @returns {number} the number of events that were requested
117120
*/
118121
getRequestedEventCount() {
119122
return this.#requestedEventCount;
120123
}
121124
/**
122-
* Returns the number of events that were received since the subscription
125+
* Returns the number of events that were received since subscribing.
123126
* @returns {number} the number of events that were received
124127
*/
125128
getReceivedEventCount() {
126129
return this.#receivedEventCount;
127130
}
128131
/**
129-
* Returns the topic name for this subscription
132+
* Returns the topic name for this subscription.
130133
* @returns {string} the topic name
131134
*/
132135
getTopicName() {
133136
return this.#topicName;
134137
}
138+
/**
139+
* Returns the replay ID of the last processed event or null if no event was processed yet.
140+
* @return {number} replay ID
141+
*/
142+
getLatestReplayId() {
143+
return this.#latestReplayId;
144+
}
145+
/**
146+
* @protected
147+
* Resets the requested/received event counts.
148+
* This method should only be be used internally by the client when it resubscribes.
149+
* @param {number} newRequestedEventCount
150+
*/
151+
_resetEventCount(newRequestedEventCount) {
152+
this.#requestedEventCount = newRequestedEventCount;
153+
this.#receivedEventCount = 0;
154+
}
135155
};
136156

157+
// src/utils/avroHelper.js
158+
var import_avro_js = __toESM(require("avro-js"), 1);
159+
var CustomLongAvroType = import_avro_js.default.types.LongType.using({
160+
fromBuffer: (buf) => {
161+
const big = buf.readBigInt64LE();
162+
if (big < Number.MIN_SAFE_INTEGER || big > Number.MAX_SAFE_INTEGER) {
163+
return big;
164+
}
165+
return Number(BigInt.asIntN(64, big));
166+
},
167+
toBuffer: (n) => {
168+
const buf = Buffer.allocUnsafe(8);
169+
if (n instanceof BigInt) {
170+
buf.writeBigInt64LE(n);
171+
} else {
172+
buf.writeBigInt64LE(BigInt(n));
173+
}
174+
return buf;
175+
},
176+
fromJSON: BigInt,
177+
toJSON: Number,
178+
isValid: (n) => {
179+
const type = typeof n;
180+
return type === "number" && n % 1 === 0 || type === "bigint";
181+
},
182+
compare: (n1, n2) => {
183+
return n1 === n2 ? 0 : n1 < n2 ? -1 : 1;
184+
}
185+
});
186+
137187
// src/utils/configuration.js
138188
var dotenv = __toESM(require("dotenv"), 1);
139189
var import_fs = __toESM(require("fs"), 1);
@@ -230,7 +280,7 @@ var Configuration = class _Configuration {
230280
};
231281

232282
// src/utils/eventParser.js
233-
var import_avro_js = __toESM(require("avro-js"), 1);
283+
var import_avro_js2 = __toESM(require("avro-js"), 1);
234284
function parseEvent(schema, event) {
235285
const allFields = schema.type.getFields();
236286
const replayId = decodeReplayId(event.replayId);
@@ -313,7 +363,7 @@ function getChildFields(parentField) {
313363
const types = parentField._type.getTypes();
314364
let fields = [];
315365
types.forEach((type) => {
316-
if (type instanceof import_avro_js.default.types.RecordType) {
366+
if (type instanceof import_avro_js2.default.types.RecordType) {
317367
fields = fields.concat(type.getFields());
318368
}
319369
});
@@ -477,33 +527,7 @@ function base64url(input) {
477527
}
478528

479529
// src/client.js
480-
var CUSTOM_LONG_AVRO_TYPE = import_avro_js2.default.types.LongType.using({
481-
fromBuffer: (buf) => {
482-
const big = buf.readBigInt64LE();
483-
if (big < Number.MIN_SAFE_INTEGER || big > Number.MAX_SAFE_INTEGER) {
484-
return big;
485-
}
486-
return Number(BigInt.asIntN(64, big));
487-
},
488-
toBuffer: (n) => {
489-
const buf = Buffer.allocUnsafe(8);
490-
if (n instanceof BigInt) {
491-
buf.writeBigInt64LE(n);
492-
} else {
493-
buf.writeBigInt64LE(BigInt(n));
494-
}
495-
return buf;
496-
},
497-
fromJSON: BigInt,
498-
toJSON: Number,
499-
isValid: (n) => {
500-
const type = typeof n;
501-
return type === "number" && n % 1 === 0 || type === "bigint";
502-
},
503-
compare: (n1, n2) => {
504-
return n1 === n2 ? 0 : n1 < n2 ? -1 : 1;
505-
}
506-
});
530+
var MAX_EVENT_BATCH_SIZE = 100;
507531
var PubSubApiClient = class {
508532
/**
509533
* gRPC client
@@ -515,14 +539,20 @@ var PubSubApiClient = class {
515539
* @type {Map<string,Schema>}
516540
*/
517541
#schemaChache;
542+
/**
543+
* Map of subscribitions indexed by topic name
544+
* @type {Map<string,Object>}
545+
*/
546+
#subscriptions;
518547
#logger;
519548
/**
520549
* Builds a new Pub/Sub API client
521-
* @param {Logger} logger an optional custom logger. The client uses the console if no value is supplied.
550+
* @param {Logger} [logger] an optional custom logger. The client uses the console if no value is supplied.
522551
*/
523552
constructor(logger = console) {
524553
this.#logger = logger;
525554
this.#schemaChache = /* @__PURE__ */ new Map();
555+
this.#subscriptions = /* @__PURE__ */ new Map();
526556
try {
527557
Configuration.load();
528558
} catch (error) {
@@ -636,21 +666,21 @@ var PubSubApiClient = class {
636666
/**
637667
* Subscribes to a topic and retrieves all past events in retention window.
638668
* @param {string} topicName name of the topic that we're subscribing to
639-
* @param {number} numRequested number of events requested
669+
* @param {number} [numRequested] optional number of events requested. If not supplied or null, the client keeps the subscription alive forever.
640670
* @returns {Promise<EventEmitter>} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events
641671
* @memberof PubSubApiClient.prototype
642672
*/
643-
async subscribeFromEarliestEvent(topicName, numRequested) {
673+
async subscribeFromEarliestEvent(topicName, numRequested = null) {
644674
return this.#subscribe({
645675
topicName,
646676
numRequested,
647677
replayPreset: 1
648678
});
649679
}
650680
/**
651-
* Subscribes to a topic and retrieve past events starting from a replay ID
681+
* Subscribes to a topic and retrieves past events starting from a replay ID.
652682
* @param {string} topicName name of the topic that we're subscribing to
653-
* @param {number} numRequested number of events requested
683+
* @param {number} numRequested number of events requested. If null, the client keeps the subscription alive forever.
654684
* @param {number} replayId replay ID
655685
* @returns {Promise<EventEmitter>} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events
656686
* @memberof PubSubApiClient.prototype
@@ -664,13 +694,13 @@ var PubSubApiClient = class {
664694
});
665695
}
666696
/**
667-
* Subscribes to a topic
697+
* Subscribes to a topic.
668698
* @param {string} topicName name of the topic that we're subscribing to
669-
* @param {number} numRequested number of events requested
699+
* @param {number} [numRequested] optional number of events requested. If not supplied or null, the client keeps the subscription alive forever.
670700
* @returns {Promise<EventEmitter>} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events
671701
* @memberof PubSubApiClient.prototype
672702
*/
673-
async subscribe(topicName, numRequested) {
703+
async subscribe(topicName, numRequested = null) {
674704
return this.#subscribe({
675705
topicName,
676706
numRequested
@@ -682,28 +712,44 @@ var PubSubApiClient = class {
682712
* @return {PubSubEventEmitter} emitter that allows you to listen to received events and stream lifecycle events
683713
*/
684714
async #subscribe(subscribeRequest) {
715+
let { topicName, numRequested } = subscribeRequest;
685716
try {
686-
if (typeof subscribeRequest.numRequested !== "number") {
687-
throw new Error(
688-
`Expected a number type for number of requested events but got ${typeof subscribeRequest.numRequested}`
689-
);
690-
}
691-
if (!Number.isSafeInteger(subscribeRequest.numRequested) || subscribeRequest.numRequested < 1) {
692-
throw new Error(
693-
`Expected an integer greater than 1 for number of requested events but got ${subscribeRequest.numRequested}`
694-
);
717+
let isInfiniteEventRequest = false;
718+
if (numRequested === null) {
719+
isInfiniteEventRequest = true;
720+
subscribeRequest.numRequested = numRequested = MAX_EVENT_BATCH_SIZE;
721+
} else {
722+
if (typeof numRequested !== "number") {
723+
throw new Error(
724+
`Expected a number type for number of requested events but got ${typeof numRequested}`
725+
);
726+
}
727+
if (!Number.isSafeInteger(numRequested) || numRequested < 1) {
728+
throw new Error(
729+
`Expected an integer greater than 1 for number of requested events but got ${numRequested}`
730+
);
731+
}
732+
if (numRequested > MAX_EVENT_BATCH_SIZE) {
733+
this.#logger.warn(
734+
`The number of requested events for ${topicName} exceeds max event batch size (${MAX_EVENT_BATCH_SIZE}).`
735+
);
736+
}
695737
}
696738
if (!this.#client) {
697739
throw new Error("Pub/Sub API client is not connected.");
698740
}
699-
const subscription = this.#client.Subscribe();
741+
let subscription = this.#subscriptions.get(topicName);
742+
if (!subscription) {
743+
subscription = this.#client.Subscribe();
744+
this.#subscriptions.set(topicName, subscription);
745+
}
700746
subscription.write(subscribeRequest);
701747
this.#logger.info(
702-
`Subscribe request sent for ${subscribeRequest.numRequested} events from ${subscribeRequest.topicName}...`
748+
`Subscribe request sent for ${numRequested} events from ${topicName}...`
703749
);
704750
const eventEmitter = new PubSubEventEmitter(
705-
subscribeRequest.topicName,
706-
subscribeRequest.numRequested
751+
topicName,
752+
numRequested
707753
);
708754
subscription.on("data", (data) => {
709755
const latestReplayId = decodeReplayId(data.latestReplayId);
@@ -713,19 +759,13 @@ var PubSubApiClient = class {
713759
);
714760
data.events.forEach(async (event) => {
715761
try {
716-
let schema = await this.#getEventSchema(
717-
subscribeRequest.topicName
718-
);
719-
if (schema.id !== event.schemaId) {
762+
let schema = await this.#getEventSchema(topicName);
763+
if (schema.id !== event.event.schemaId) {
720764
this.#logger.info(
721-
`Event schema changed, reloading: ${subscribeRequest.topicName}`
722-
);
723-
this.#clearEventSchemaFromCache(
724-
subscribeRequest.topicName
725-
);
726-
schema = await this.#getEventSchema(
727-
subscribeRequest.topicName
765+
`Event schema changed (${schema.id} != ${event.event.schemaId}), reloading: ${topicName}`
728766
);
767+
this.#schemaChache.delete(topicName);
768+
schema = await this.#getEventSchema(topicName);
729769
}
730770
const parsedEvent = parseEvent(schema, event);
731771
this.#logger.debug(parsedEvent);
@@ -748,7 +788,14 @@ var PubSubApiClient = class {
748788
this.#logger.error(parseError);
749789
}
750790
if (eventEmitter.getReceivedEventCount() === eventEmitter.getRequestedEventCount()) {
751-
eventEmitter.emit("lastevent");
791+
if (isInfiniteEventRequest) {
792+
this.requestAdditionalEvents(
793+
eventEmitter,
794+
MAX_EVENT_BATCH_SIZE
795+
);
796+
} else {
797+
eventEmitter.emit("lastevent");
798+
}
752799
}
753800
});
754801
} else {
@@ -760,6 +807,7 @@ var PubSubApiClient = class {
760807
}
761808
});
762809
subscription.on("end", () => {
810+
this.#subscriptions.delete(topicName);
763811
this.#logger.info("gRPC stream ended");
764812
eventEmitter.emit("end");
765813
});
@@ -778,11 +826,33 @@ var PubSubApiClient = class {
778826
return eventEmitter;
779827
} catch (error) {
780828
throw new Error(
781-
`Failed to subscribe to events for topic ${subscribeRequest.topicName}`,
829+
`Failed to subscribe to events for topic ${topicName}`,
782830
{ cause: error }
783831
);
784832
}
785833
}
834+
/**
835+
* Request additional events on an existing subscription.
836+
* @param {PubSubEventEmitter} eventEmitter event emitter that was obtained in the first subscribe call
837+
* @param {number} numRequested number of events requested.
838+
*/
839+
async requestAdditionalEvents(eventEmitter, numRequested) {
840+
const topicName = eventEmitter.getTopicName();
841+
const subscription = this.#subscriptions.get(topicName);
842+
if (!subscription) {
843+
throw new Error(
844+
`Failed to request additional events for topic ${topicName}, no active subscription found.`
845+
);
846+
}
847+
eventEmitter._resetEventCount(numRequested);
848+
subscription.write({
849+
topicName,
850+
numRequested
851+
});
852+
this.#logger.debug(
853+
`Resubscribing to a batch of ${numRequested} events for: ${topicName}`
854+
);
855+
}
786856
/**
787857
* Publishes a payload to a topic using the gRPC client.
788858
* @param {string} topicName name of the topic that we're subscribing to
@@ -874,8 +944,8 @@ var PubSubApiClient = class {
874944
if (schemaError) {
875945
reject(schemaError);
876946
} else {
877-
const schemaType = import_avro_js2.default.parse(res.schemaJson, {
878-
registry: { long: CUSTOM_LONG_AVRO_TYPE }
947+
const schemaType = import_avro_js3.default.parse(res.schemaJson, {
948+
registry: { long: CustomLongAvroType }
879949
});
880950
this.#logger.info(
881951
`Topic schema loaded: ${topicName}`
@@ -890,7 +960,4 @@ var PubSubApiClient = class {
890960
});
891961
});
892962
}
893-
#clearEventSchemaFromCache(topicName) {
894-
this.#schemaChache.delete(topicName);
895-
}
896963
};

0 commit comments

Comments
 (0)