From 24c37bb072282b0fcf806d83b2128618d0e8527d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 6 May 2025 22:15:11 +0000 Subject: [PATCH 01/23] feat: Add timeout option to Subscription.close() Implements a new `timeout` option (using `Duration`) for the `Subscription.close()` method. This provides more control over the shutdown process: - If `timeout` is zero, the subscription closes as quickly as possible without nacking buffered messages. - If `timeout` is positive, the subscription attempts to nack any buffered messages (in the lease manager) and waits up to the specified duration for pending acknowledgements and nacks to be sent to the server. - If no timeout is provided, the behavior remains as before (waits indefinitely for pending acks/modacks, no nacking). The core logic is implemented in `Subscriber.close()`. `PubSub.close()` documentation is updated to clarify its scope and recommend using `Subscription.close()` directly for this feature. Includes: - Unit tests for the new timeout behavior in `Subscriber.close()`. - A TypeScript sample (`samples/closeSubscriptionWithTimeout.ts`) demonstrating usage. - Updated JSDoc documentation for relevant methods. --- samples/README.md | 14 ++ samples/closeSubscriptionWithTimeout.ts | 197 ++++++++++++++++++++++++ src/pubsub.ts | 29 +++- src/subscriber.ts | 114 ++++++++++++-- src/subscription.ts | 24 ++- test/subscriber.ts | 144 +++++++++++++++++ 6 files changed, 495 insertions(+), 27 deletions(-) create mode 100644 samples/closeSubscriptionWithTimeout.ts diff --git a/samples/README.md b/samples/README.md index 7635d0758..f7e6c60b9 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1471,6 +1471,20 @@ __Usage:__ `node validateSchema.js ` +### Close Subscription with Timeout + +Demonstrates closing a subscription with a specified timeout for graceful shutdown. + +View the [source code](https://github.com/googleapis/nodejs-pubsub/blob/main/samples/closeSubscriptionWithTimeout.ts). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-pubsub&page=editor&open_in_editor=samples/validateSchema.js,samples/README.md) + +__Usage:__ + + +`node closeSubscriptionWithTimeout.js ` + + diff --git a/samples/closeSubscriptionWithTimeout.ts b/samples/closeSubscriptionWithTimeout.ts new file mode 100644 index 000000000..58be5b55d --- /dev/null +++ b/samples/closeSubscriptionWithTimeout.ts @@ -0,0 +1,197 @@ +// Copyright 2024 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict'; + +/** + * This sample demonstrates how to use the `timeout` option when closing a Pub/Sub + * subscription using the Node.js client library. The timeout allows for graceful + * shutdown, attempting to nack any buffered messages before closing. + */ + +// sample-metadata: +// title: Close Subscription with Timeout +// description: Demonstrates closing a subscription with a specified timeout for graceful shutdown. +// usage: node closeSubscriptionWithTimeout.js + +// [START pubsub_close_subscription_with_timeout] +async function main( + topicName = 'your-topic', // Name of the topic to use + subscriptionName = 'your-subscription' // Name of the subscription to use +) { + // Imports the Google Cloud client library + const {PubSub, Duration} = require('@google-cloud/pubsub'); + + // Creates a client; cache this for further use + const pubsub = new PubSub(); + + // References an existing topic + const topic = pubsub.topic(topicName); + + // References an existing subscription, creating it if necessary + const [subscription] = await topic.subscription(subscriptionName).get({ + autoCreate: true, // Auto-create subscription if it doesn't exist + }); + console.log(`Using subscription: ${subscription.name}`); + + let messageCount = 0; + const messagesToProcess = 10; // Number of messages to receive before demonstrating close + const messagesToAck = 5; // Number of messages to explicitly ack + + // Helper function to simulate processing + const simulateProcessing = (message: any) => { + console.log( + `[${new Date().toISOString()}] Received message #${ + message.id + }, Attributes: ${JSON.stringify(message.attributes)}` + ); + messageCount++; + + // Simulate asynchronous work with a timeout + const processingTime = Math.random() * 500; // 0-500ms delay + return new Promise(resolve => { + setTimeout(() => { + console.log( + `[${new Date().toISOString()}] Finished processing message #${ + message.id + }` + ); + resolve(); + }, processingTime); + }); + }; + + // --- Demonstrate close with zero timeout --- + console.log('\n--- Demonstrating close() with Zero Timeout ---'); + let receivedForZeroTimeout = 0; + const zeroTimeoutClosePromise = new Promise(resolve => { + const messageHandler = async (message: any) => { + receivedForZeroTimeout++; + await simulateProcessing(message); + + // Ack only the first few messages + if (receivedForZeroTimeout <= messagesToAck) { + console.log( + `[${new Date().toISOString()}] Acking message #${message.id}` + ); + message.ack(); + } else { + console.log( + `[${new Date().toISOString()}] Not acking message #${ + message.id + } (will be buffered)` + ); + // Don't ack - these will be buffered by the client library + } + + // Check if we've received enough messages to trigger the close + if (receivedForZeroTimeout >= messagesToProcess) { + // Remove the handler to stop receiving new messages + subscription.removeListener('message', messageHandler); + console.log( + `\n[${new Date().toISOString()}] Received ${messagesToProcess} messages. Closing subscription with zero timeout...` + ); + // Close with a zero timeout. This should be fast but won't nack buffered messages. + await subscription.close({timeout: Duration.from({seconds: 0})}); + console.log( + `[${new Date().toISOString()}] Subscription closed with zero timeout.` + ); + resolve(); // Resolve the promise for this section + } + }; + + // Attach the message handler + subscription.on('message', messageHandler); + console.log( + 'Listening for messages to demonstrate zero-timeout close...' + ); + }); + + await zeroTimeoutClosePromise; + + // --- Demonstrate close with non-zero timeout --- + console.log('\n--- Demonstrating close() with Non-Zero Timeout ---'); + + // Re-open the subscription implicitly by adding a new listener + // Note: In a real application, you might need more robust logic + // if the subscription was deleted or if errors occurred during close. + console.log('Re-attaching listener to receive more messages...'); + messageCount = 0; // Reset message count for the next phase + let receivedForNonZeroTimeout = 0; + + const nonZeroTimeoutClosePromise = new Promise(resolve => { + const messageHandler = async (message: any) => { + receivedForNonZeroTimeout++; + await simulateProcessing(message); + + // Ack only the first few messages again + if (receivedForNonZeroTimeout <= messagesToAck) { + console.log( + `[${new Date().toISOString()}] Acking message #${message.id}` + ); + message.ack(); + } else { + console.log( + `[${new Date().toISOString()}] Not acking message #${ + message.id + } (will be buffered)` + ); + // Don't ack + } + + // Check if we've received enough messages for this part + if (receivedForNonZeroTimeout >= messagesToProcess) { + subscription.removeListener('message', messageHandler); + console.log( + `\n[${new Date().toISOString()}] Received ${messagesToProcess} more messages. Closing subscription with 5s timeout...` + ); + // Close with a non-zero timeout. This attempts to nack buffered messages + // and waits up to 5 seconds for pending acks/nacks. + await subscription.close({timeout: Duration.from({seconds: 5})}); + console.log( + `[${new Date().toISOString()}] Subscription closed with 5s timeout.` + ); + resolve(); + } + }; + + subscription.on('message', messageHandler); + console.log( + 'Listening for messages to demonstrate non-zero-timeout close...' + ); + }); + + await nonZeroTimeoutClosePromise; + + console.log('\nSample finished.'); + + // Optional: Clean up resources + // console.log('Deleting subscription...'); + // await subscription.delete(); + // console.log('Deleting topic...'); + // await topic.delete(); +} +// [END pubsub_close_subscription_with_timeout] + +process.on('unhandledRejection', err => { + console.error(err.message); + process.exitCode = 1; +}); + +// Presumes topic and subscription have been created prior to running the sample. +// If you uncomment the cleanup code above, the sample will delete them afterwards. +main(...process.argv.slice(2)).catch(err => { + console.error(err.message); + process.exitCode = 1; +}); diff --git a/src/pubsub.ts b/src/pubsub.ts index 075d673f5..26f763a13 100644 --- a/src/pubsub.ts +++ b/src/pubsub.ts @@ -352,15 +352,30 @@ export class PubSub { } /** - * Closes out this object, releasing any server connections. Note that once - * you close a PubSub object, it may not be used again. Any pending operations - * (e.g. queued publish messages) will fail. If you have topic or subscription - * objects that may have pending operations, you should call close() on those - * first if you want any pending messages to be delivered correctly. The - * PubSub class doesn't track those. + * Closes the PubSub client, releasing any underlying gRPC connections. + * + * Note that this method primarily closes the gRPC clients (Publisher and Subscriber) + * used for API requests. It does **not** automatically handle the graceful shutdown + * of active subscriptions, including features like message nacking with timeouts. + * + * For graceful shutdown of subscriptions with specific timeout behavior (e.g., + * ensuring buffered messages are nacked before closing), please refer to the + * {@link Subscription#close} method. It is recommended to call + * `Subscription.close({ timeout: ... })` directly on your active `Subscription` + * objects *before* calling `PubSub.close()` if you require that specific + * shutdown behavior. + * + * Calling `PubSub.close()` without first closing active subscriptions might + * result in abrupt termination of message processing for those subscriptions. + * Any pending operations on associated Topic or Subscription objects (e.g., + * queued publish messages or unacked subscriber messages) may fail after + * `PubSub.close()` is called. + * + * Once closed, the PubSub instance cannot be used again. * * @callback EmptyCallback - * @returns {Promise} + * @param {?Error} err Request error, if any. + * @returns {Promise} Resolves when the clients are closed. */ close(): Promise; close(callback: EmptyCallback): void; diff --git a/src/subscriber.ts b/src/subscriber.ts index 156ebfe55..8109f3518 100644 --- a/src/subscriber.ts +++ b/src/subscriber.ts @@ -27,6 +27,7 @@ import {Subscription} from './subscription'; import {defaultOptions} from './default-options'; import {SubscriberClient} from './v1'; import * as tracing from './telemetry-tracing'; +import * as debug from './debug'; import {Duration} from './temporal'; import {EventEmitter} from 'events'; @@ -804,25 +805,60 @@ export class Subscriber extends EventEmitter { } /** - * Closes the subscriber. The returned promise will resolve once any pending - * acks/modAcks are finished. + * Closes the subscriber, stopping the reception of new messages and shutting + * down the underlying stream. The returned promise will resolve once pending + * acknowledgements and modifications have been flushed, or when the timeout + * is reached. * - * @returns {Promise} + * @param {object} [options] Configuration options for closing the subscriber. + * @param {Duration} [options.timeout] The maximum duration to wait for pending + * ack/modAck requests to complete before resolving the promise. + * - If a positive `timeout` is provided (e.g., `Duration.from({seconds: 5})`), + * any messages currently held in the subscriber's buffer (not yet acked/nacked + * by user code) will be immediately nacked. The method will then wait up to + * the specified duration for pending ack/modAck requests to be sent to the + * server. If the timeout is reached, the promise resolves, but some pending + * requests might not have completed. + * - If `timeout` is zero (e.g., `Duration.from({seconds: 0})`), the subscriber + * closes quickly. Buffered messages are *not* nacked, and the method does *not* + * wait for pending ack/modAck requests to complete. + * - If `timeout` is omitted or undefined, the method will wait indefinitely for + * all pending ack/modAck requests to complete. Buffered messages are *not* + * nacked in this case. + * @returns {Promise} A promise that resolves when the subscriber is closed + * and pending operations are flushed or the timeout is reached. * @private */ - async close(): Promise { + async close(options?: {timeout?: Duration}): Promise { if (!this.isOpen) { return; } + const timeout = options?.timeout; + const timeoutMs = timeout?.totalOf('millisecond'); + this.isOpen = false; this._stream.destroy(); const remaining = this._inventory.clear(); - await this._waitForFlush(); + // Requirement #3: Nack remaining messages if a timeout is provided. + if (timeoutMs && timeoutMs > 0) { + debug.subscriber( + `[${this._subscription.name}] Nacking ${remaining.length} messages due to close timeout.`, + ); + remaining.forEach(m => { + this.nack(m); // No need to await this, let it run in the background. + }); + } + await this._waitForFlush(timeout); + + // Clean up any remaining messages that weren't nacked due to timeout. remaining.forEach(m => { - m.subSpans.shutdown(); + // Only shut down spans if they weren't already handled by the nack loop above. + if (!timeout || !timeoutMs || timeoutMs <= 0) { + m.subSpans.shutdown(); + } m.endParentSpan(); }); @@ -1094,30 +1130,80 @@ export class Subscriber extends EventEmitter { * Returns a promise that will resolve once all pending requests have settled. * * @private + * Returns a promise that will resolve once all pending requests have settled, + * or reject if the timeout is reached. * - * @returns {Promise} + * @param {Duration} [timeout] Optional timeout duration. + * @private + * + * @returns {Promise} */ - private async _waitForFlush(): Promise { - const promises: Array> = []; + private async _waitForFlush(timeout?: Duration): Promise { + const flushPromises: Array> = []; + const drainPromises: Array> = []; + const timeoutMs = timeout?.totalOf('millisecond'); + // Flush any batched requests immediately. if (this._acks.numPendingRequests) { - promises.push(this._acks.onFlush()); + flushPromises.push(this._acks.onFlush()); await this._acks.flush(); } if (this._modAcks.numPendingRequests) { - promises.push(this._modAcks.onFlush()); + flushPromises.push(this._modAcks.onFlush()); await this._modAcks.flush(); } + // Wait for the flush promises first. + await Promise.all(flushPromises); + + // Now, prepare the drain promises. if (this._acks.numInFlightRequests) { - promises.push(this._acks.onDrain()); + drainPromises.push(this._acks.onDrain()); } if (this._modAcks.numInFlightRequests) { - promises.push(this._modAcks.onDrain()); + drainPromises.push(this._modAcks.onDrain()); + } + + if (drainPromises.length === 0) { + return; // Nothing to wait for. } - await Promise.all(promises); + // Wait for drain promises, potentially with a timeout. + const drainSettled = Promise.all(drainPromises); + + if (timeoutMs && timeoutMs > 0) { + const timeoutPromise = new Promise((_, reject) => + setTimeout( + () => + reject( + new Error( + `Timed out after ${timeoutMs} ms waiting for subscriber drain.`, + ), + ), + timeoutMs, + ), + ); + + try { + await Promise.race([drainSettled, timeoutPromise]); + } catch (err) { + // Only log the timeout error, don't re-throw. + if ((err as Error).message.startsWith('Timed out')) { + debug.subscriber( + `[${this._subscription.name}] ${ + (err as Error).message + } Some ACKs/modACKs might not have completed.`, + ); + } else { + // Re-throw unexpected errors. + throw err; + } + } + } else { + // Wait indefinitely if no timeout. + await drainSettled; + } } } diff --git a/src/subscription.ts b/src/subscription.ts index 6a5f6023e..6837a7aa9 100644 --- a/src/subscription.ts +++ b/src/subscription.ts @@ -15,7 +15,7 @@ */ import * as extend from 'extend'; -import {CallOptions} from 'google-gax'; +import {CallOptions, Duration} from 'google-gax'; import snakeCase = require('lodash.snakecase'); import {google} from '../protos/protos'; @@ -61,6 +61,7 @@ export type SubscriptionMetadata = { export type SubscriptionOptions = SubscriberOptions & {topic?: Topic}; export type SubscriptionCloseCallback = (err?: Error) => void; +export type SubscriptionCloseOptions = {timeout?: Duration}; type SubscriptionCallback = ResourceCallback< Subscription, @@ -361,26 +362,37 @@ export class Subscription extends EventEmitter { * message events unless you call {Subscription#open} or add new message * listeners. * + * @param {object} [options] Options for the close operation. + * @param {google.protobuf.Duration | number} [options.timeout] Timeout for the close operation. This specifies the maximum amount of time in seconds to wait for the subscriber to drain/close. If not specified, the default is 300 seconds. * @param {function} [callback] The callback function. * @param {?error} callback.err An error returned while closing the * Subscription. * * @example * ``` - * subscription.close(err => { + * subscription.close({timeout: 60}, err => { // timeout in seconds * if (err) { * // Error handling omitted. * } * }); * * // If the callback is omitted a Promise will be returned. - * subscription.close().then(() => {}); + * subscription.close({timeout: 60}).then(() => {}); // timeout in seconds * ``` */ - close(): Promise; + close(options?: SubscriptionCloseOptions): Promise; close(callback: SubscriptionCloseCallback): void; - close(callback?: SubscriptionCloseCallback): void | Promise { - this._subscriber.close().then(() => callback!(), callback); + close( + options: SubscriptionCloseOptions, + callback: SubscriptionCloseCallback, + ): void; + close( + optsOrCallback?: SubscriptionCloseOptions | SubscriptionCloseCallback, + callback?: SubscriptionCloseCallback, + ): void | Promise { + const options = typeof optsOrCallback === 'object' ? optsOrCallback : {}; + callback = typeof optsOrCallback === 'function' ? optsOrCallback : callback; + this._subscriber.close(options).then(() => callback!(), callback); } /** diff --git a/test/subscriber.ts b/test/subscriber.ts index df95cc1b9..26fa3fe9c 100644 --- a/test/subscriber.ts +++ b/test/subscriber.ts @@ -36,6 +36,7 @@ import {Subscription} from '../src/subscription'; import {SpanKind} from '@opentelemetry/api'; import {Duration} from '../src'; import * as tracing from '../src/telemetry-tracing'; +import * as debug from '../src/debug'; type PullResponse = google.pubsub.v1.IStreamingPullResponse; @@ -650,6 +651,149 @@ describe('Subscriber', () => { assert.strictEqual(modAckOnDrain.callCount, 1); }); }); + + describe('close with timeout', () => { + let clock: sinon.SinonFakeTimers; + let inventory: FakeLeaseManager; + let stream: FakeMessageStream; + let ackQueue: FakeAckQueue; + let modAckQueue: FakeModAckQueue; + let debugStub: sinon.SinonStub; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + inventory = stubs.get('inventory'); + stream = stubs.get('messageStream'); + ackQueue = stubs.get('ackQueue'); + modAckQueue = stubs.get('modAckQueue'); + debugStub = sandbox.stub(debug, 'subscriber'); + + // Ensure subscriber is open before each test + if (!subscriber.isOpen) { + subscriber.open(); + } + }); + + afterEach(() => { + clock.restore(); + }); + + it('should call _waitForFlush without timeout if no options', async () => { + const waitForFlushSpy = sandbox.spy(subscriber as any, '_waitForFlush'); + await subscriber.close(); + assert(waitForFlushSpy.calledOnce); + assert.isUndefined(waitForFlushSpy.firstCall.args[0]); + }); + + it('should call _waitForFlush without timeout if timeout is zero', async () => { + const waitForFlushSpy = sandbox.spy(subscriber as any, '_waitForFlush'); + await subscriber.close({timeout: Duration.from({seconds: 0})}); + assert(waitForFlushSpy.calledOnce); + const timeoutArg = waitForFlushSpy.firstCall.args[0]; + assert.strictEqual(timeoutArg.totalOf('second'), 0); + }); + + it('should not nack remaining messages if no timeout', async () => { + const mockMessages = [ + new Message(subscriber, RECEIVED_MESSAGE), + new Message(subscriber, RECEIVED_MESSAGE), + ]; + sandbox.stub(inventory, 'clear').returns(mockMessages); + const nackSpy = sandbox.spy(subscriber, 'nack'); + + await subscriber.close(); + + assert(nackSpy.notCalled); + }); + + it('should not nack remaining messages if timeout is zero', async () => { + const mockMessages = [ + new Message(subscriber, RECEIVED_MESSAGE), + new Message(subscriber, RECEIVED_MESSAGE), + ]; + sandbox.stub(inventory, 'clear').returns(mockMessages); + const nackSpy = sandbox.spy(subscriber, 'nack'); + + await subscriber.close({timeout: Duration.from({seconds: 0})}); + + assert(nackSpy.notCalled); + }); + + it('should nack remaining messages if timeout is non-zero', async () => { + const mockMessages = [ + new Message(subscriber, RECEIVED_MESSAGE), + new Message(subscriber, RECEIVED_MESSAGE), + ]; + sandbox.stub(inventory, 'clear').returns(mockMessages); + const nackSpy = sandbox.spy(subscriber, 'nack'); + const waitForFlushStub = sandbox.stub(subscriber as any, '_waitForFlush').resolves(); + + + await subscriber.close({timeout: Duration.from({seconds: 5})}); + + assert(waitForFlushStub.calledOnce); + assert.strictEqual(nackSpy.callCount, mockMessages.length); + mockMessages.forEach((msg, i) => { + assert.strictEqual(nackSpy.getCall(i).args[0], msg); + }); + }); + + it('should wait for drain promises and respect timeout', async () => { + const ackDrainDeferred = defer(); + const modAckDrainDeferred = defer(); + sandbox.stub(ackQueue, 'onDrain').returns(ackDrainDeferred.promise); + sandbox.stub(modAckQueue, 'onDrain').returns(modAckDrainDeferred.promise); + ackQueue.numInFlightRequests = 1; // Ensure drain is waited for + modAckQueue.numInFlightRequests = 1; + + const closePromise = subscriber.close({ + timeout: Duration.from({milliseconds: 100}), + }); + + // Advance time past the timeout + await clock.tickAsync(101); + + // Promise should resolve even though drains haven't + await closePromise; + + // Check for debug message + assert( + debugStub.calledWithMatch(/Timed out after 100 ms waiting for subscriber drain/) + ); + + // Resolve drains afterwards to prevent hanging tests if logic fails + ackDrainDeferred.resolve(); + modAckDrainDeferred.resolve(); + }); + + it('should resolve early if drain completes before timeout', async () => { + const ackDrainDeferred = defer(); + const modAckDrainDeferred = defer(); + sandbox.stub(ackQueue, 'onDrain').returns(ackDrainDeferred.promise); + sandbox.stub(modAckQueue, 'onDrain').returns(modAckDrainDeferred.promise); + ackQueue.numInFlightRequests = 1; + modAckQueue.numInFlightRequests = 1; + + const closePromise = subscriber.close({ + timeout: Duration.from({seconds: 5}), + }); + + // Resolve drains quickly + ackDrainDeferred.resolve(); + modAckDrainDeferred.resolve(); + + // Advance time slightly, but less than the timeout + await clock.tickAsync(50); + + // Should resolve quickly + await closePromise; + + // Ensure no timeout message was logged + assert( + debugStub.neverCalledWithMatch(/Timed out/) + ); + }); + }); }); describe('getClient', () => { From 687212d36be6a6bd3f5018944099d72104818cdd Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Fri, 9 May 2025 15:18:17 -0400 Subject: [PATCH 02/23] docs: revert README change so release-please can do it --- samples/README.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/samples/README.md b/samples/README.md index f7e6c60b9..7635d0758 100644 --- a/samples/README.md +++ b/samples/README.md @@ -1471,20 +1471,6 @@ __Usage:__ `node validateSchema.js ` -### Close Subscription with Timeout - -Demonstrates closing a subscription with a specified timeout for graceful shutdown. - -View the [source code](https://github.com/googleapis/nodejs-pubsub/blob/main/samples/closeSubscriptionWithTimeout.ts). - -[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-pubsub&page=editor&open_in_editor=samples/validateSchema.js,samples/README.md) - -__Usage:__ - - -`node closeSubscriptionWithTimeout.js ` - - From da726d05a3c5126b5e3c0d84b78ed640c07c523b Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Tue, 13 May 2025 18:49:02 -0400 Subject: [PATCH 03/23] feat: jules' vibin' is too lo-fi, fix some bad assumptions --- samples/closeSubscriptionWithTimeout.ts | 197 ------------------ .../closeSubscriptionWithTimeout.ts | 72 +++++++ src/pubsub.ts | 14 +- src/subscriber.ts | 137 ++++++------ src/subscription.ts | 23 +- test/subscriber.ts | 89 +++----- 6 files changed, 189 insertions(+), 343 deletions(-) delete mode 100644 samples/closeSubscriptionWithTimeout.ts create mode 100644 samples/typescript/closeSubscriptionWithTimeout.ts diff --git a/samples/closeSubscriptionWithTimeout.ts b/samples/closeSubscriptionWithTimeout.ts deleted file mode 100644 index 58be5b55d..000000000 --- a/samples/closeSubscriptionWithTimeout.ts +++ /dev/null @@ -1,197 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -'use strict'; - -/** - * This sample demonstrates how to use the `timeout` option when closing a Pub/Sub - * subscription using the Node.js client library. The timeout allows for graceful - * shutdown, attempting to nack any buffered messages before closing. - */ - -// sample-metadata: -// title: Close Subscription with Timeout -// description: Demonstrates closing a subscription with a specified timeout for graceful shutdown. -// usage: node closeSubscriptionWithTimeout.js - -// [START pubsub_close_subscription_with_timeout] -async function main( - topicName = 'your-topic', // Name of the topic to use - subscriptionName = 'your-subscription' // Name of the subscription to use -) { - // Imports the Google Cloud client library - const {PubSub, Duration} = require('@google-cloud/pubsub'); - - // Creates a client; cache this for further use - const pubsub = new PubSub(); - - // References an existing topic - const topic = pubsub.topic(topicName); - - // References an existing subscription, creating it if necessary - const [subscription] = await topic.subscription(subscriptionName).get({ - autoCreate: true, // Auto-create subscription if it doesn't exist - }); - console.log(`Using subscription: ${subscription.name}`); - - let messageCount = 0; - const messagesToProcess = 10; // Number of messages to receive before demonstrating close - const messagesToAck = 5; // Number of messages to explicitly ack - - // Helper function to simulate processing - const simulateProcessing = (message: any) => { - console.log( - `[${new Date().toISOString()}] Received message #${ - message.id - }, Attributes: ${JSON.stringify(message.attributes)}` - ); - messageCount++; - - // Simulate asynchronous work with a timeout - const processingTime = Math.random() * 500; // 0-500ms delay - return new Promise(resolve => { - setTimeout(() => { - console.log( - `[${new Date().toISOString()}] Finished processing message #${ - message.id - }` - ); - resolve(); - }, processingTime); - }); - }; - - // --- Demonstrate close with zero timeout --- - console.log('\n--- Demonstrating close() with Zero Timeout ---'); - let receivedForZeroTimeout = 0; - const zeroTimeoutClosePromise = new Promise(resolve => { - const messageHandler = async (message: any) => { - receivedForZeroTimeout++; - await simulateProcessing(message); - - // Ack only the first few messages - if (receivedForZeroTimeout <= messagesToAck) { - console.log( - `[${new Date().toISOString()}] Acking message #${message.id}` - ); - message.ack(); - } else { - console.log( - `[${new Date().toISOString()}] Not acking message #${ - message.id - } (will be buffered)` - ); - // Don't ack - these will be buffered by the client library - } - - // Check if we've received enough messages to trigger the close - if (receivedForZeroTimeout >= messagesToProcess) { - // Remove the handler to stop receiving new messages - subscription.removeListener('message', messageHandler); - console.log( - `\n[${new Date().toISOString()}] Received ${messagesToProcess} messages. Closing subscription with zero timeout...` - ); - // Close with a zero timeout. This should be fast but won't nack buffered messages. - await subscription.close({timeout: Duration.from({seconds: 0})}); - console.log( - `[${new Date().toISOString()}] Subscription closed with zero timeout.` - ); - resolve(); // Resolve the promise for this section - } - }; - - // Attach the message handler - subscription.on('message', messageHandler); - console.log( - 'Listening for messages to demonstrate zero-timeout close...' - ); - }); - - await zeroTimeoutClosePromise; - - // --- Demonstrate close with non-zero timeout --- - console.log('\n--- Demonstrating close() with Non-Zero Timeout ---'); - - // Re-open the subscription implicitly by adding a new listener - // Note: In a real application, you might need more robust logic - // if the subscription was deleted or if errors occurred during close. - console.log('Re-attaching listener to receive more messages...'); - messageCount = 0; // Reset message count for the next phase - let receivedForNonZeroTimeout = 0; - - const nonZeroTimeoutClosePromise = new Promise(resolve => { - const messageHandler = async (message: any) => { - receivedForNonZeroTimeout++; - await simulateProcessing(message); - - // Ack only the first few messages again - if (receivedForNonZeroTimeout <= messagesToAck) { - console.log( - `[${new Date().toISOString()}] Acking message #${message.id}` - ); - message.ack(); - } else { - console.log( - `[${new Date().toISOString()}] Not acking message #${ - message.id - } (will be buffered)` - ); - // Don't ack - } - - // Check if we've received enough messages for this part - if (receivedForNonZeroTimeout >= messagesToProcess) { - subscription.removeListener('message', messageHandler); - console.log( - `\n[${new Date().toISOString()}] Received ${messagesToProcess} more messages. Closing subscription with 5s timeout...` - ); - // Close with a non-zero timeout. This attempts to nack buffered messages - // and waits up to 5 seconds for pending acks/nacks. - await subscription.close({timeout: Duration.from({seconds: 5})}); - console.log( - `[${new Date().toISOString()}] Subscription closed with 5s timeout.` - ); - resolve(); - } - }; - - subscription.on('message', messageHandler); - console.log( - 'Listening for messages to demonstrate non-zero-timeout close...' - ); - }); - - await nonZeroTimeoutClosePromise; - - console.log('\nSample finished.'); - - // Optional: Clean up resources - // console.log('Deleting subscription...'); - // await subscription.delete(); - // console.log('Deleting topic...'); - // await topic.delete(); -} -// [END pubsub_close_subscription_with_timeout] - -process.on('unhandledRejection', err => { - console.error(err.message); - process.exitCode = 1; -}); - -// Presumes topic and subscription have been created prior to running the sample. -// If you uncomment the cleanup code above, the sample will delete them afterwards. -main(...process.argv.slice(2)).catch(err => { - console.error(err.message); - process.exitCode = 1; -}); diff --git a/samples/typescript/closeSubscriptionWithTimeout.ts b/samples/typescript/closeSubscriptionWithTimeout.ts new file mode 100644 index 000000000..d521245c5 --- /dev/null +++ b/samples/typescript/closeSubscriptionWithTimeout.ts @@ -0,0 +1,72 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * This sample demonstrates how to use the `timeout` option when closing a Pub/Sub + * subscription using the Node.js client library. The timeout allows for graceful + * shutdown, attempting to nack any buffered messages before closing. + * + * For more information, see the README.md under /pubsub and the documentation + * at https://cloud.google.com/pubsub/docs. + */ + +// sample-metadata: +// title: Close Subscription with Timeout +// description: Demonstrates closing a subscription with a specified timeout for graceful shutdown. +// usage: node closeSubscriptionWithTimeout.js + +// This sample is currently speculative. +// -START pubsub_close_subscription_with_timeout] + +// Imports the Google Cloud client library +import {PubSub, Duration} from '@google-cloud/pubsub'; + +// Creates a client; cache this for further use +const pubsub = new PubSub(); + +async function closeSubscriptionWithTimeout( + topicNameOrId: string, + subscriptionNameOrId: string, +) { + const topic = pubsub.topic(topicNameOrId); + + const timeout = Duration.from({seconds: 10}); + const zeroTimeout = Duration.from({seconds: 0}); + + // Closes the subscription immediately, not waiting for anything. + let subscription = topic.subscription(subscriptionNameOrId); + await subscription.close({timeout: zeroTimeout}); + + // Shuts down the gRPC connection, sends nacks for buffered messages, + // and closes the subscription after a set timeout. + subscription = topic.subscription(subscriptionNameOrId); + await subscription.close({timeout}); +} +// -END pubsub_close_subscription_with_timeout] + +// Presumes topic and subscription have been created prior to running the sample. +// If you uncomment the cleanup code above, the sample will delete them afterwards. +function main( + topicNameOrId = 'YOUR_TOPIC_NAME_OR_ID', + subscriptionNameOrId = 'YOUR_SUBSCRIPTION_NAME_OR_ID', +) { + closeSubscriptionWithTimeout(topicNameOrId, subscriptionNameOrId).catch( + err => { + console.error(err.message); + process.exitCode = 1; + }, + ); +} + +main(...process.argv.slice(2)); diff --git a/src/pubsub.ts b/src/pubsub.ts index 26f763a13..68a905255 100644 --- a/src/pubsub.ts +++ b/src/pubsub.ts @@ -354,14 +354,20 @@ export class PubSub { /** * Closes the PubSub client, releasing any underlying gRPC connections. * + * Note that once you close a PubSub object, it may not be used again. Any pending + * operations (e.g. queued publish messages) will fail. If you have topic or + * subscription objects that may have pending operations, you should call close() + * on those first if you want any pending messages to be delivered correctly. The + * PubSub class doesn't track those. + * Note that this method primarily closes the gRPC clients (Publisher and Subscriber) * used for API requests. It does **not** automatically handle the graceful shutdown - * of active subscriptions, including features like message nacking with timeouts. + * of active subscriptions. * * For graceful shutdown of subscriptions with specific timeout behavior (e.g., * ensuring buffered messages are nacked before closing), please refer to the * {@link Subscription#close} method. It is recommended to call - * `Subscription.close({ timeout: ... })` directly on your active `Subscription` + * `Subscription.close({timeout: ...})` directly on your active `Subscription` * objects *before* calling `PubSub.close()` if you require that specific * shutdown behavior. * @@ -371,10 +377,8 @@ export class PubSub { * queued publish messages or unacked subscriber messages) may fail after * `PubSub.close()` is called. * - * Once closed, the PubSub instance cannot be used again. - * * @callback EmptyCallback - * @param {?Error} err Request error, if any. + * @param {Error} [err] Request error, if any. * @returns {Promise} Resolves when the clients are closed. */ close(): Promise; diff --git a/src/subscriber.ts b/src/subscriber.ts index 8109f3518..7c2629a85 100644 --- a/src/subscriber.ts +++ b/src/subscriber.ts @@ -17,6 +17,7 @@ import {DateStruct, PreciseDate} from '@google-cloud/precise-date'; import {replaceProjectIdToken} from '@google-cloud/projectify'; import {promisify} from '@google-cloud/promisify'; +import {loggingUtils as logging} from 'google-gax'; import {google} from '../protos/protos'; import {Histogram} from './histogram'; @@ -27,15 +28,17 @@ import {Subscription} from './subscription'; import {defaultOptions} from './default-options'; import {SubscriberClient} from './v1'; import * as tracing from './telemetry-tracing'; -import * as debug from './debug'; import {Duration} from './temporal'; import {EventEmitter} from 'events'; export {StatusError} from './message-stream'; +const logDebug = logging.log('pubsub').sublog('debug'); + export type PullResponse = google.pubsub.v1.IStreamingPullResponse; export type SubscriptionProperties = google.pubsub.v1.StreamingPullResponse.ISubscriptionProperties; +export type SubscriptionCloseOptions = {timeout?: Duration}; type ValueOf = T[keyof T]; export const AckResponses = { @@ -806,59 +809,58 @@ export class Subscriber extends EventEmitter { /** * Closes the subscriber, stopping the reception of new messages and shutting - * down the underlying stream. The returned promise will resolve once pending - * acknowledgements and modifications have been flushed, or when the timeout - * is reached. + * down the underlying stream. The behavior of the returned Promise will depend + * on the timeout option. * - * @param {object} [options] Configuration options for closing the subscriber. + * @param {SubscriptionCloseOptions} [options] Configuration options for closing + * the subscriber. * @param {Duration} [options.timeout] The maximum duration to wait for pending * ack/modAck requests to complete before resolving the promise. * - If a positive `timeout` is provided (e.g., `Duration.from({seconds: 5})`), * any messages currently held in the subscriber's buffer (not yet acked/nacked - * by user code) will be immediately nacked. The method will then wait up to - * the specified duration for pending ack/modAck requests to be sent to the + * by user code) will queue nacks immediately. This method will then wait up to + * the specified duration for pending ack/nack requests to be sent to the * server. If the timeout is reached, the promise resolves, but some pending * requests might not have completed. - * - If `timeout` is zero (e.g., `Duration.from({seconds: 0})`), the subscriber - * closes quickly. Buffered messages are *not* nacked, and the method does *not* - * wait for pending ack/modAck requests to complete. - * - If `timeout` is omitted or undefined, the method will wait indefinitely for - * all pending ack/modAck requests to complete. Buffered messages are *not* - * nacked in this case. + * - If `timeout` is zero (e.g., `Duration.from({seconds: 0})`), buffered messages + * are *not* nacked, and the method does *not* wait for pending ack/nack requests + * to complete. The returned Promise resolves immediately. + * - If `timeout` is omitted or undefined, the behavior is the same as receiving a + * positive timeout, but the returned Promise will resolve after waiting + * indefinitely for all pending ack/nack requests to complete, similar to the + * previous, no-parameter behavior. * @returns {Promise} A promise that resolves when the subscriber is closed * and pending operations are flushed or the timeout is reached. * @private */ - async close(options?: {timeout?: Duration}): Promise { + async close(options?: SubscriptionCloseOptions): Promise { if (!this.isOpen) { return; } - const timeout = options?.timeout; - const timeoutMs = timeout?.totalOf('millisecond'); - + // Always close the stream right away so we don't receive more messages. this.isOpen = false; this._stream.destroy(); + + // Grab everything left in inventory so we can nack them if needed. const remaining = this._inventory.clear(); - // Requirement #3: Nack remaining messages if a timeout is provided. - if (timeoutMs && timeoutMs > 0) { - debug.subscriber( - `[${this._subscription.name}] Nacking ${remaining.length} messages due to close timeout.`, - ); - remaining.forEach(m => { - this.nack(m); // No need to await this, let it run in the background. - }); - } + // Nack remaining messages if a timeout was not provided, or if one was + // provided besides zero. + const timeout = options?.timeout; + const timeoutMs = timeout?.totalOf('millisecond'); + if (!timeout || (timeoutMs && timeoutMs > 0)) { + // Call nack() on each message to queue a nack. + remaining.forEach(m => m.nack()); - await this._waitForFlush(timeout); + // Wait until all of those nacks are flushed, or the timeout is reached. + await this._waitForFlush(timeout); + } - // Clean up any remaining messages that weren't nacked due to timeout. + // Clean up OTel spans for any remaining messages that weren't nacked due + // to timeout. remaining.forEach(m => { - // Only shut down spans if they weren't already handled by the nack loop above. - if (!timeout || !timeoutMs || timeoutMs <= 0) { - m.subSpans.shutdown(); - } + m.subSpans.shutdown(); m.endParentSpan(); }); @@ -1139,71 +1141,58 @@ export class Subscriber extends EventEmitter { * @returns {Promise} */ private async _waitForFlush(timeout?: Duration): Promise { - const flushPromises: Array> = []; - const drainPromises: Array> = []; - const timeoutMs = timeout?.totalOf('millisecond'); + const promises: Array> = []; // Flush any batched requests immediately. if (this._acks.numPendingRequests) { - flushPromises.push(this._acks.onFlush()); - await this._acks.flush(); + promises.push(this._acks.onFlush()); + this._acks.flush().catch(() => {}); } if (this._modAcks.numPendingRequests) { - flushPromises.push(this._modAcks.onFlush()); - await this._modAcks.flush(); + promises.push(this._modAcks.onFlush()); + this._modAcks.flush().catch(() => {}); } - // Wait for the flush promises first. - await Promise.all(flushPromises); - // Now, prepare the drain promises. if (this._acks.numInFlightRequests) { - drainPromises.push(this._acks.onDrain()); + promises.push(this._acks.onDrain()); } if (this._modAcks.numInFlightRequests) { - drainPromises.push(this._modAcks.onDrain()); + promises.push(this._modAcks.onDrain()); } - if (drainPromises.length === 0) { - return; // Nothing to wait for. + if (!promises.length) { + // Nothing to wait for. + return; } - // Wait for drain promises, potentially with a timeout. - const drainSettled = Promise.all(drainPromises); + // Wait for promises, potentially with a timeout. + const settled = Promise.all(promises); + const timeoutMs = timeout?.totalOf('millisecond'); if (timeoutMs && timeoutMs > 0) { - const timeoutPromise = new Promise((_, reject) => - setTimeout( - () => - reject( - new Error( - `Timed out after ${timeoutMs} ms waiting for subscriber drain.`, - ), - ), - timeoutMs, - ), - ); + let timedOut = false; + let timeoutId: NodeJS.Timeout | undefined; + const timeoutPromise = new Promise(r => { + timeoutId = setTimeout(r, timeoutMs); + }).then(() => { + timedOut = true; + }); - try { - await Promise.race([drainSettled, timeoutPromise]); - } catch (err) { - // Only log the timeout error, don't re-throw. - if ((err as Error).message.startsWith('Timed out')) { - debug.subscriber( - `[${this._subscription.name}] ${ - (err as Error).message - } Some ACKs/modACKs might not have completed.`, - ); - } else { - // Re-throw unexpected errors. - throw err; - } + await Promise.race([settled, timeoutPromise]); + if (timedOut) { + logDebug.warn( + '[%s] Some acks/nacks might not have finished sending.', + this._subscription.name, + ); + } else { + clearTimeout(timeoutId); } } else { // Wait indefinitely if no timeout. - await drainSettled; + await settled; } } } diff --git a/src/subscription.ts b/src/subscription.ts index 6837a7aa9..3e72e5190 100644 --- a/src/subscription.ts +++ b/src/subscription.ts @@ -15,7 +15,7 @@ */ import * as extend from 'extend'; -import {CallOptions, Duration} from 'google-gax'; +import {CallOptions} from 'google-gax'; import snakeCase = require('lodash.snakecase'); import {google} from '../protos/protos'; @@ -41,7 +41,12 @@ import { SeekResponse, Snapshot, } from './snapshot'; -import {Message, Subscriber, SubscriberOptions} from './subscriber'; +import { + Message, + Subscriber, + SubscriberOptions, + SubscriptionCloseOptions, +} from './subscriber'; import {Topic} from './topic'; import {promisifySome} from './util'; import {StatusError} from './message-stream'; @@ -61,7 +66,6 @@ export type SubscriptionMetadata = { export type SubscriptionOptions = SubscriberOptions & {topic?: Topic}; export type SubscriptionCloseCallback = (err?: Error) => void; -export type SubscriptionCloseOptions = {timeout?: Duration}; type SubscriptionCallback = ResourceCallback< Subscription, @@ -363,21 +367,16 @@ export class Subscription extends EventEmitter { * listeners. * * @param {object} [options] Options for the close operation. - * @param {google.protobuf.Duration | number} [options.timeout] Timeout for the close operation. This specifies the maximum amount of time in seconds to wait for the subscriber to drain/close. If not specified, the default is 300 seconds. + * @param {Duration} [options.timeout] Timeout for the close operation. This + * specifies the maximum amount of time to wait for the subscriber to + * drain/close. If not specified, the default is to wait indefinitely. * @param {function} [callback] The callback function. * @param {?error} callback.err An error returned while closing the * Subscription. * * @example * ``` - * subscription.close({timeout: 60}, err => { // timeout in seconds - * if (err) { - * // Error handling omitted. - * } - * }); - * - * // If the callback is omitted a Promise will be returned. - * subscription.close({timeout: 60}).then(() => {}); // timeout in seconds + * await subscription.close({timeout: Duration.from({seconds: 60})}); * ``` */ close(options?: SubscriptionCloseOptions): Promise; diff --git a/test/subscriber.ts b/test/subscriber.ts index 26fa3fe9c..6ce67efc7 100644 --- a/test/subscriber.ts +++ b/test/subscriber.ts @@ -36,7 +36,6 @@ import {Subscription} from '../src/subscription'; import {SpanKind} from '@opentelemetry/api'; import {Duration} from '../src'; import * as tracing from '../src/telemetry-tracing'; -import * as debug from '../src/debug'; type PullResponse = google.pubsub.v1.IStreamingPullResponse; @@ -179,6 +178,7 @@ interface SubInternals { _inventory: FakeLeaseManager; _onData(response: PullResponse): void; _discardMessage(message: s.Message): void; + _waitForFlush(timeout?: Duration): Promise; } function getSubInternals(sub: s.Subscriber) { @@ -655,23 +655,22 @@ describe('Subscriber', () => { describe('close with timeout', () => { let clock: sinon.SinonFakeTimers; let inventory: FakeLeaseManager; - let stream: FakeMessageStream; let ackQueue: FakeAckQueue; let modAckQueue: FakeModAckQueue; - let debugStub: sinon.SinonStub; + let subInternals: SubInternals; beforeEach(() => { clock = sinon.useFakeTimers(); inventory = stubs.get('inventory'); - stream = stubs.get('messageStream'); ackQueue = stubs.get('ackQueue'); modAckQueue = stubs.get('modAckQueue'); - debugStub = sandbox.stub(debug, 'subscriber'); // Ensure subscriber is open before each test if (!subscriber.isOpen) { subscriber.open(); } + + subInternals = subscriber as unknown as SubInternals; }); afterEach(() => { @@ -679,34 +678,13 @@ describe('Subscriber', () => { }); it('should call _waitForFlush without timeout if no options', async () => { - const waitForFlushSpy = sandbox.spy(subscriber as any, '_waitForFlush'); + const waitForFlushSpy = sandbox.spy(subInternals, '_waitForFlush'); await subscriber.close(); assert(waitForFlushSpy.calledOnce); - assert.isUndefined(waitForFlushSpy.firstCall.args[0]); - }); - - it('should call _waitForFlush without timeout if timeout is zero', async () => { - const waitForFlushSpy = sandbox.spy(subscriber as any, '_waitForFlush'); - await subscriber.close({timeout: Duration.from({seconds: 0})}); - assert(waitForFlushSpy.calledOnce); - const timeoutArg = waitForFlushSpy.firstCall.args[0]; - assert.strictEqual(timeoutArg.totalOf('second'), 0); - }); - - it('should not nack remaining messages if no timeout', async () => { - const mockMessages = [ - new Message(subscriber, RECEIVED_MESSAGE), - new Message(subscriber, RECEIVED_MESSAGE), - ]; - sandbox.stub(inventory, 'clear').returns(mockMessages); - const nackSpy = sandbox.spy(subscriber, 'nack'); - - await subscriber.close(); - - assert(nackSpy.notCalled); + assert.strictEqual(waitForFlushSpy.firstCall.args[0], undefined); }); - it('should not nack remaining messages if timeout is zero', async () => { + it('should not nack remaining messages if zero timeout', async () => { const mockMessages = [ new Message(subscriber, RECEIVED_MESSAGE), new Message(subscriber, RECEIVED_MESSAGE), @@ -726,12 +704,9 @@ describe('Subscriber', () => { ]; sandbox.stub(inventory, 'clear').returns(mockMessages); const nackSpy = sandbox.spy(subscriber, 'nack'); - const waitForFlushStub = sandbox.stub(subscriber as any, '_waitForFlush').resolves(); - await subscriber.close({timeout: Duration.from({seconds: 5})}); - assert(waitForFlushStub.calledOnce); assert.strictEqual(nackSpy.callCount, mockMessages.length); mockMessages.forEach((msg, i) => { assert.strictEqual(nackSpy.getCall(i).args[0], msg); @@ -742,41 +717,50 @@ describe('Subscriber', () => { const ackDrainDeferred = defer(); const modAckDrainDeferred = defer(); sandbox.stub(ackQueue, 'onDrain').returns(ackDrainDeferred.promise); - sandbox.stub(modAckQueue, 'onDrain').returns(modAckDrainDeferred.promise); + sandbox + .stub(modAckQueue, 'onDrain') + .returns(modAckDrainDeferred.promise); ackQueue.numInFlightRequests = 1; // Ensure drain is waited for modAckQueue.numInFlightRequests = 1; - const closePromise = subscriber.close({ - timeout: Duration.from({milliseconds: 100}), - }); + let closed = false; + void subscriber + .close({ + timeout: Duration.from({millis: 100}), + }) + .then(() => { + closed = true; + }); // Advance time past the timeout await clock.tickAsync(101); // Promise should resolve even though drains haven't - await closePromise; - - // Check for debug message - assert( - debugStub.calledWithMatch(/Timed out after 100 ms waiting for subscriber drain/) - ); + assert.strictEqual(closed, true); // Resolve drains afterwards to prevent hanging tests if logic fails ackDrainDeferred.resolve(); modAckDrainDeferred.resolve(); }); - it('should resolve early if drain completes before timeout', async () => { + it('should resolve early if drain completes before timeout', async () => { const ackDrainDeferred = defer(); const modAckDrainDeferred = defer(); sandbox.stub(ackQueue, 'onDrain').returns(ackDrainDeferred.promise); - sandbox.stub(modAckQueue, 'onDrain').returns(modAckDrainDeferred.promise); - ackQueue.numInFlightRequests = 1; + sandbox + .stub(modAckQueue, 'onDrain') + .returns(modAckDrainDeferred.promise); + ackQueue.numInFlightRequests = 1; // Ensure drain is waited for modAckQueue.numInFlightRequests = 1; - const closePromise = subscriber.close({ - timeout: Duration.from({seconds: 5}), - }); + let closed = false; + void subscriber + .close({ + timeout: Duration.from({millis: 100}), + }) + .then(() => { + closed = true; + }); // Resolve drains quickly ackDrainDeferred.resolve(); @@ -785,13 +769,8 @@ describe('Subscriber', () => { // Advance time slightly, but less than the timeout await clock.tickAsync(50); - // Should resolve quickly - await closePromise; - - // Ensure no timeout message was logged - assert( - debugStub.neverCalledWithMatch(/Timed out/) - ); + // Promise should resolve. + assert.strictEqual(closed, true); }); }); }); From 629aa43336cc0aa6aee1d314b6a2a096431ba180 Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Tue, 13 May 2025 18:55:10 -0400 Subject: [PATCH 04/23] samples: typeless a JS sample for close with timeout --- samples/closeSubscriptionWithTimeout.js | 76 +++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 samples/closeSubscriptionWithTimeout.js diff --git a/samples/closeSubscriptionWithTimeout.js b/samples/closeSubscriptionWithTimeout.js new file mode 100644 index 000000000..67584437e --- /dev/null +++ b/samples/closeSubscriptionWithTimeout.js @@ -0,0 +1,76 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This is a generated sample, using the typeless sample bot. Please +// look for the source TypeScript sample (.ts) for modifications. +'use strict'; + +/** + * This sample demonstrates how to use the `timeout` option when closing a Pub/Sub + * subscription using the Node.js client library. The timeout allows for graceful + * shutdown, attempting to nack any buffered messages before closing. + * + * For more information, see the README.md under /pubsub and the documentation + * at https://cloud.google.com/pubsub/docs. + */ + +// sample-metadata: +// title: Close Subscription with Timeout +// description: Demonstrates closing a subscription with a specified timeout for graceful shutdown. +// usage: node closeSubscriptionWithTimeout.js + +// This sample is currently speculative. +// -START pubsub_close_subscription_with_timeout] + +// Imports the Google Cloud client library +const {PubSub, Duration} = require('@google-cloud/pubsub'); + +// Creates a client; cache this for further use +const pubsub = new PubSub(); + +async function closeSubscriptionWithTimeout( + topicNameOrId, + subscriptionNameOrId, +) { + const topic = pubsub.topic(topicNameOrId); + + const timeout = Duration.from({seconds: 10}); + const zeroTimeout = Duration.from({seconds: 0}); + + // Closes the subscription immediately, not waiting for anything. + let subscription = topic.subscription(subscriptionNameOrId); + await subscription.close({timeout: zeroTimeout}); + + // Shuts down the gRPC connection, sends nacks for buffered messages, + // and closes the subscription after a set timeout. + subscription = topic.subscription(subscriptionNameOrId); + await subscription.close({timeout}); +} +// -END pubsub_close_subscription_with_timeout] + +// Presumes topic and subscription have been created prior to running the sample. +// If you uncomment the cleanup code above, the sample will delete them afterwards. +function main( + topicNameOrId = 'YOUR_TOPIC_NAME_OR_ID', + subscriptionNameOrId = 'YOUR_SUBSCRIPTION_NAME_OR_ID', +) { + closeSubscriptionWithTimeout(topicNameOrId, subscriptionNameOrId).catch( + err => { + console.error(err.message); + process.exitCode = 1; + }, + ); +} + +main(...process.argv.slice(2)); From a0ba234c138304dfa1eb00ddaa12ec30f5ece2be Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Mon, 16 Jun 2025 16:51:36 -0400 Subject: [PATCH 05/23] feat: add awaitWithTimeout and test --- src/lease-manager.ts | 2 +- src/util.ts | 34 ++++++++++++++++++++++++ test/util.ts | 61 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/lease-manager.ts b/src/lease-manager.ts index cb55af2fe..e8bfb3719 100644 --- a/src/lease-manager.ts +++ b/src/lease-manager.ts @@ -225,7 +225,7 @@ export class LeaseManager extends EventEmitter { if (this._subscriber.isOpen) { message.subSpans.flowEnd(); process.nextTick(() => { - message.subSpans.processingStart(this._subscriber.name); + message.dispatched(); this._subscriber.emit('message', message); }); } diff --git a/src/util.ts b/src/util.ts index 04bfb270d..6ef9ebb10 100644 --- a/src/util.ts +++ b/src/util.ts @@ -15,6 +15,7 @@ */ import {promisify, PromisifyOptions} from '@google-cloud/promisify'; +import {Duration} from './temporal'; /** * This replaces usage of promisifyAll(), going forward. Instead of opting @@ -83,3 +84,36 @@ export function addToBucket(map: Map, bucket: T, item: U) { items.push(item); map.set(bucket, items); } + +const timeoutToken: unique symbol = Symbol('pubsub promise timeout'); + +/** + * Awaits on Promise with a timeout. Resolves on success, rejects on timeout. + * + * @param promise An existing Promise returning type T. + * @param timeout A timeout value as a Duration; if the timeout is exceeded while + * waiting for the promise, the Promise this function returns will reject. + * @returns In any case, a tuple with the first item being the T value or an + * error value, and the second item being true if the timeout was exceeded. + */ +export async function awaitWithTimeout( + promise: Promise, + timeout: Duration, +): Promise<[T, boolean]> { + let timeoutId: NodeJS.Timeout | undefined; + const timeoutPromise = new Promise((_, rej) => { + timeoutId = setTimeout( + () => rej(timeoutToken), + timeout.totalOf('millisecond'), + ); + }); + try { + const value = await Promise.race([timeoutPromise, promise]); + clearTimeout(timeoutId); + return [value, false]; + } catch (e) { + // The timeout passed. + clearTimeout(timeoutId); + throw [e, e === timeoutToken]; + } +} diff --git a/test/util.ts b/test/util.ts index 6dbdd746c..99a24fe26 100644 --- a/test/util.ts +++ b/test/util.ts @@ -13,8 +13,10 @@ // limitations under the License. import {describe, it} from 'mocha'; -import {addToBucket, Throttler} from '../src/util'; +import {addToBucket, Throttler, awaitWithTimeout} from '../src/util'; import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { Duration } from '../src'; describe('utils', () => { describe('Throttler', () => { @@ -58,4 +60,61 @@ describe('utils', () => { assert.deepStrictEqual(map.get('a'), ['c', 'b']); }); }); + + describe('awaitWithTimeout', () => { + let sandbox: sinon.SinonSandbox; + beforeEach(() => { + sandbox = sinon.createSandbox(); + }); + afterEach(() => { + sandbox.restore(); + }); + + it('handles non-timeout properly', async () => { + const fakeTimers = sandbox.useFakeTimers(0); + let resolve = (...args: unknown[]) => {}; + const testString = 'fooby'; + const testPromise = new Promise(r => { + resolve = () => r(testString); + }); + fakeTimers.setTimeout(resolve, 500); + const awaitPromise = awaitWithTimeout( + testPromise, + Duration.from({seconds: 1}), + ); + fakeTimers.tick(500); + try { + const result = await awaitPromise; + assert.deepStrictEqual(result, [testString, false]); + } catch (e) { + assert.strictEqual(e, null, 'timeout was triggered, improperly'); + } + }); + + it('handles timeout properly', async () => { + const fakeTimers = sandbox.useFakeTimers(0); + let resolve = (...args: unknown[]) => {}; + const testString = 'fooby'; + const testPromise = new Promise(r => { + resolve = () => r(testString); + }); + fakeTimers.setTimeout(resolve, 1500); + const awaitPromise = awaitWithTimeout( + testPromise, + Duration.from({seconds: 1}), + ); + fakeTimers.tick(1500); + try { + const result = await awaitPromise; + assert.strictEqual( + result, + null, + 'timeout was not triggered, improperly', + ); + } catch (e) { + const err: unknown[] = e as unknown[]; + assert.strictEqual(err[1], true); + } + }); + }); }); From 13e58f29da1c8874acb7d995ecfcdbccebb69755 Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Mon, 16 Jun 2025 17:05:52 -0400 Subject: [PATCH 06/23] tests: also test error results without timeout --- test/util.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/test/util.ts b/test/util.ts index 99a24fe26..78ab48a88 100644 --- a/test/util.ts +++ b/test/util.ts @@ -91,6 +91,27 @@ describe('utils', () => { } }); + it('handles non-timeout errors properly', async () => { + const fakeTimers = sandbox.useFakeTimers(0); + let reject = (...args: unknown[]) => {}; + const testString = 'fooby'; + const testPromise = new Promise((res, rej) => { + reject = () => rej(testString); + }); + fakeTimers.setTimeout(reject, 500); + const awaitPromise = awaitWithTimeout( + testPromise, + Duration.from({seconds: 1}), + ); + fakeTimers.tick(500); + try { + const result = await awaitPromise; + assert.strictEqual(result, null, 'non-error was triggered, improperly'); + } catch (e) { + assert.deepStrictEqual(e, [testString, false]); + } + }); + it('handles timeout properly', async () => { const fakeTimers = sandbox.useFakeTimers(0); let resolve = (...args: unknown[]) => {}; From 5db179e0879fec2ea25239b047bb3680eac1174e Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Mon, 16 Jun 2025 18:04:44 -0400 Subject: [PATCH 07/23] feat: update for the current spec, test updates coming --- src/subscriber.ts | 211 +++++++++++++++++++++++++++++------------- src/subscription.ts | 36 +++++-- test/lease-manager.ts | 10 ++ 3 files changed, 185 insertions(+), 72 deletions(-) diff --git a/src/subscriber.ts b/src/subscriber.ts index 7c2629a85..5b3041c92 100644 --- a/src/subscriber.ts +++ b/src/subscriber.ts @@ -18,6 +18,7 @@ import {DateStruct, PreciseDate} from '@google-cloud/precise-date'; import {replaceProjectIdToken} from '@google-cloud/projectify'; import {promisify} from '@google-cloud/promisify'; import {loggingUtils as logging} from 'google-gax'; +import defer = require('p-defer'); import {google} from '../protos/protos'; import {Histogram} from './histogram'; @@ -30,6 +31,7 @@ import {SubscriberClient} from './v1'; import * as tracing from './telemetry-tracing'; import {Duration} from './temporal'; import {EventEmitter} from 'events'; +import {awaitWithTimeout} from './util'; export {StatusError} from './message-stream'; @@ -38,7 +40,6 @@ const logDebug = logging.log('pubsub').sublog('debug'); export type PullResponse = google.pubsub.v1.IStreamingPullResponse; export type SubscriptionProperties = google.pubsub.v1.StreamingPullResponse.ISubscriptionProperties; -export type SubscriptionCloseOptions = {timeout?: Duration}; type ValueOf = T[keyof T]; export const AckResponses = { @@ -50,6 +51,25 @@ export const AckResponses = { }; export type AckResponse = ValueOf; +/** + * Enum values for behaviors of the Subscriber.close() method. + */ +export const SubscriberCloseBehaviors = { + Wait: 0 as const, + Exit: 1 as const, + Timeout: 2 as const, +}; +export type SubscriberCloseBehavior = ValueOf; + +/** + * Options to modify the behavior of the Subscriber.close() method. If + * none is passed, the default is SubscriberCloseBehaviors.Wait. + */ +export interface SubscriberCloseOptions { + behavior?: SubscriberCloseBehavior; + timeout?: Duration; +} + /** * Thrown when an error is detected in an ack/nack/modack call, when * exactly-once delivery is enabled on the subscription. This will @@ -234,10 +254,12 @@ export class Message implements tracing.MessageWithAttributes { orderingKey?: string; publishTime: PreciseDate; received: number; + private _handledPromise: defer.DeferredPromise; private _handled: boolean; private _length: number; private _subscriber: Subscriber; private _ackFailed?: AckError; + private _dispatched: boolean; /** * @private @@ -371,7 +393,9 @@ export class Message implements tracing.MessageWithAttributes { */ this.isExactlyOnceDelivery = sub.isExactlyOnceDelivery; + this._dispatched = false; this._handled = false; + this._handledPromise = defer(); this._length = this.data.length; this._subscriber = sub; } @@ -385,6 +409,38 @@ export class Message implements tracing.MessageWithAttributes { return this._length; } + /** + * Resolves when the message has been handled fully; a handled message may + * not have any further operations performed on it. + * + * @private + */ + get handledPromise(): Promise { + return this._handledPromise.promise; + } + + /** + * When this message is dispensed to user callback code, this should be called. + * The time between the dispatch and the handledPromise resolving is when the + * message is with the user. + * + * @private + */ + dispatched(): void { + if (!this._dispatched) { + this.subSpans.processingStart(this._subscriber.name); + this._dispatched = true; + } + } + + /** + * @private + * @returns True if this message has been dispatched to user callback code. + */ + isDispatched(): boolean { + return this._dispatched; + } + /** * Sets this message's exactly once delivery acks to permanent failure. This is * meant for internal library use only. @@ -411,6 +467,7 @@ export class Message implements tracing.MessageWithAttributes { this.subSpans.ackCall(); this.subSpans.processingEnd(); void this._subscriber.ack(this); + this._handledPromise.resolve(); } } @@ -444,6 +501,8 @@ export class Message implements tracing.MessageWithAttributes { } catch (e) { this.ackFailed(e as AckError); throw e; + } finally { + this._handledPromise.resolve(); } } else { return AckResponses.Invalid; @@ -511,6 +570,7 @@ export class Message implements tracing.MessageWithAttributes { this.subSpans.nackCall(); this.subSpans.processingEnd(); void this._subscriber.nack(this); + this._handledPromise.resolve(); } } @@ -545,6 +605,8 @@ export class Message implements tracing.MessageWithAttributes { } catch (e) { this.ackFailed(e as AckError); throw e; + } finally { + this._handledPromise.resolve(); } } else { return AckResponses.Invalid; @@ -809,31 +871,36 @@ export class Subscriber extends EventEmitter { /** * Closes the subscriber, stopping the reception of new messages and shutting - * down the underlying stream. The behavior of the returned Promise will depend - * on the timeout option. + * down the underlying stream. Any messages being held in buffers in the client + * library will be nacked. The behavior of the returned Promise will depend + * on the behavior option. (See below.) + * + * If no options are passed, it behaves like `ShutdownBehaviors.Wait`. * - * @param {SubscriptionCloseOptions} [options] Configuration options for closing - * the subscriber. - * @param {Duration} [options.timeout] The maximum duration to wait for pending - * ack/modAck requests to complete before resolving the promise. - * - If a positive `timeout` is provided (e.g., `Duration.from({seconds: 5})`), - * any messages currently held in the subscriber's buffer (not yet acked/nacked - * by user code) will queue nacks immediately. This method will then wait up to - * the specified duration for pending ack/nack requests to be sent to the - * server. If the timeout is reached, the promise resolves, but some pending - * requests might not have completed. - * - If `timeout` is zero (e.g., `Duration.from({seconds: 0})`), buffered messages - * are *not* nacked, and the method does *not* wait for pending ack/nack requests - * to complete. The returned Promise resolves immediately. - * - If `timeout` is omitted or undefined, the behavior is the same as receiving a - * positive timeout, but the returned Promise will resolve after waiting - * indefinitely for all pending ack/nack requests to complete, similar to the - * previous, no-parameter behavior. + * @param {SubscriberCloseOptions} [options] Determines the basic behavior of the + * close() function. + * @param {ShutdownBehavior} [options.behavior] The behavior of the close operation. + * - Wait: Works more or less like the original close(), waiting indefinitely. + * - Timeout: Works like Wait, but with a timeout. + * - Exit: Nacks all buffered and leased messages, but otherwise exits immediately + * without waiting for anything. + * Use {@link ShutdownBehaviors} for enum values. + * @param {Duration} [options.timeout] In the case of Timeout, the maximum duration + * to wait for pending ack/nack requests to complete before resolving (or rejecting) + * the promise. * @returns {Promise} A promise that resolves when the subscriber is closed * and pending operations are flushed or the timeout is reached. + * * @private */ - async close(options?: SubscriptionCloseOptions): Promise { + async close(options?: SubscriberCloseOptions): Promise { + const behavior = options?.behavior; + let timeout = options?.timeout; + if (behavior === SubscriberCloseBehaviors.Wait) { + // Convert this case to "timeout === undefined, for infinite wait". + timeout = undefined; + } + if (!this.isOpen) { return; } @@ -845,16 +912,65 @@ export class Subscriber extends EventEmitter { // Grab everything left in inventory so we can nack them if needed. const remaining = this._inventory.clear(); - // Nack remaining messages if a timeout was not provided, or if one was - // provided besides zero. - const timeout = options?.timeout; - const timeoutMs = timeout?.totalOf('millisecond'); - if (!timeout || (timeoutMs && timeoutMs > 0)) { - // Call nack() on each message to queue a nack. - remaining.forEach(m => m.nack()); + // Wait for any dispatched messages to become handled (ack/nack called). + const dispatched = remaining.filter(m => m.isDispatched); - // Wait until all of those nacks are flushed, or the timeout is reached. - await this._waitForFlush(timeout); + // For any that haven't been dispatched, nack them immediately. + const nonDispatched = remaining.filter(m => !m.isDispatched); + nonDispatched.forEach(m => m.nack()); + + // Wait until all of those nacks are flushed, or the timeout is reached. + if (behavior !== SubscriberCloseBehaviors.Exit) { + // Track when we started to make sure we don't go over time with the two + // separate operations. + const waitStart = Date.now(); + let timeoutMs = timeout?.totalOf('millisecond'); + + // Wait for user callbacks to complete. + const dispatchesCompleted = Promise.all( + dispatched.map(m => m.handledPromise), + ); + if (timeoutMs === undefined) { + await dispatchesCompleted; + } else { + try { + await awaitWithTimeout( + dispatchesCompleted, + Duration.from({millis: timeoutMs}), + ); + timeoutMs -= waitStart - Date.now(); + } catch (e) { + const err = e as [unknown, boolean]; + if (err[1] === false) { + // This wasn't a timeout. Pass it on. + throw err[0]; + } else { + // The timeout passed. + timeoutMs = 0; + } + } + } + + // Wait for all acks and nacks to go through. + const flushCompleted = this._waitForFlush(); + if (timeoutMs === undefined) { + await flushCompleted; + } else if (timeoutMs > 0) { + try { + await awaitWithTimeout( + flushCompleted, + Duration.from({millis: timeoutMs}), + ); + } catch (e) { + const err = e as [unknown, boolean]; + if (err[1] === false) { + // This wasn't a timeout. Pass it on. + throw err[0]; + } else { + // The timeout passed. + } + } + } } // Clean up OTel spans for any remaining messages that weren't nacked due @@ -1132,15 +1248,10 @@ export class Subscriber extends EventEmitter { * Returns a promise that will resolve once all pending requests have settled. * * @private - * Returns a promise that will resolve once all pending requests have settled, - * or reject if the timeout is reached. - * - * @param {Duration} [timeout] Optional timeout duration. - * @private * * @returns {Promise} */ - private async _waitForFlush(timeout?: Duration): Promise { + private async _waitForFlush(): Promise { const promises: Array> = []; // Flush any batched requests immediately. @@ -1168,31 +1279,7 @@ export class Subscriber extends EventEmitter { return; } - // Wait for promises, potentially with a timeout. - const settled = Promise.all(promises); - const timeoutMs = timeout?.totalOf('millisecond'); - - if (timeoutMs && timeoutMs > 0) { - let timedOut = false; - let timeoutId: NodeJS.Timeout | undefined; - const timeoutPromise = new Promise(r => { - timeoutId = setTimeout(r, timeoutMs); - }).then(() => { - timedOut = true; - }); - - await Promise.race([settled, timeoutPromise]); - if (timedOut) { - logDebug.warn( - '[%s] Some acks/nacks might not have finished sending.', - this._subscription.name, - ); - } else { - clearTimeout(timeoutId); - } - } else { - // Wait indefinitely if no timeout. - await settled; - } + // Wait for the flush promises. + await Promise.all(promises); } } diff --git a/src/subscription.ts b/src/subscription.ts index 3e72e5190..061cf4561 100644 --- a/src/subscription.ts +++ b/src/subscription.ts @@ -45,7 +45,7 @@ import { Message, Subscriber, SubscriberOptions, - SubscriptionCloseOptions, + SubscriberCloseOptions, } from './subscriber'; import {Topic} from './topic'; import {promisifySome} from './util'; @@ -66,6 +66,7 @@ export type SubscriptionMetadata = { export type SubscriptionOptions = SubscriberOptions & {topic?: Topic}; export type SubscriptionCloseCallback = (err?: Error) => void; +export type SubscriptionCloseOptions = SubscriberCloseOptions; type SubscriptionCallback = ResourceCallback< Subscription, @@ -362,21 +363,36 @@ export class Subscription extends EventEmitter { } /** - * Closes the Subscription, once this is called you will no longer receive + * Closes the Subscription. Once this is called you will no longer receive * message events unless you call {Subscription#open} or add new message * listeners. * - * @param {object} [options] Options for the close operation. - * @param {Duration} [options.timeout] Timeout for the close operation. This - * specifies the maximum amount of time to wait for the subscriber to - * drain/close. If not specified, the default is to wait indefinitely. - * @param {function} [callback] The callback function. - * @param {?error} callback.err An error returned while closing the - * Subscription. + * This stops the reception of new messages and shuts down the underlying stream. + * Any messages being held in buffers in the client library will be nacked. The + * behavior of the returned Promise will depend on the behavior option. (See below.) + * If no options are passed, it behaves like `ShutdownBehaviors.Wait`. + * + * @param {SubscriptionCloseOptions} [options] Determines the basic behavior of the + * close() function. + * @param {SubscriptionCloseBehavior} [options.behavior] The behavior of the close operation. + * - Wait: Works more or less like the original close(), waiting indefinitely. + * - Timeout: Works like Wait, but with a timeout. + * - Exit: Nacks all buffered and leased messages, but otherwise exits immediately + * without waiting for anything. + * Use {@link SubscriptionCloseBehaviors} for enum values. + * @param {Duration} [options.timeout] In the case of Timeout, the maximum duration + * to wait for pending ack/nack requests to complete before resolving (or rejecting) + * the promise. + * @param {function} [callback] The callback function, if not using the Promise-based + * call signature. + * @param {?error} [callback.err] An error returned while closing the Subscription. * * @example * ``` - * await subscription.close({timeout: Duration.from({seconds: 60})}); + * await subscription.close({ + * behavior: SubscriptionCloseBehaviors.Timeout, + * timeout: Duration.from({seconds: 60}) + * }); * ``` */ close(options?: SubscriptionCloseOptions): Promise; diff --git a/test/lease-manager.ts b/test/lease-manager.ts index c95b633f9..0652ee2b7 100644 --- a/test/lease-manager.ts +++ b/test/lease-manager.ts @@ -65,6 +65,7 @@ class FakeMessage { length = 20; received: number; subSpans: FakeSubscriberTelemetry = new FakeSubscriberTelemetry(); + _dispatched = false; constructor() { this.received = Date.now(); @@ -75,6 +76,15 @@ class FakeMessage { } ackFailed() {} endParentSpan() {} + dispatched() { + this._dispatched = true; + } + get isDispatched() { + return this._dispatched; + } + get handledPromise() { + return Promise.resolve(); + } } interface LeaseManagerInternals { From 62afe56c61c982206ca25af9c8f0eff2dfb6397b Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Thu, 19 Jun 2025 16:59:01 -0400 Subject: [PATCH 08/23] tests: misc fixes before further additions --- test/subscriber.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/subscriber.ts b/test/subscriber.ts index 6ce67efc7..2c6dac6b7 100644 --- a/test/subscriber.ts +++ b/test/subscriber.ts @@ -573,6 +573,10 @@ describe('Subscriber', () => { const inventory: FakeLeaseManager = stubs.get('inventory'); const stub = sandbox.stub(inventory, 'clear').returns([message]); + // The leaser would've immediately called dispatched(). Pretend that + // we're user code. + message.ack(); + await subscriber.close(); assert.strictEqual(stub.callCount, 1); assert.strictEqual(shutdownStub.callCount, 1); @@ -622,7 +626,7 @@ describe('Subscriber', () => { assert.strictEqual(modAckFlush.callCount, 1); }); - it('should resolve if no messages are pending', () => { + it('should resolve if no messages are pending', async () => { const ackQueue: FakeAckQueue = stubs.get('ackQueue'); sandbox.stub(ackQueue, 'flush').rejects(); @@ -634,7 +638,7 @@ describe('Subscriber', () => { sandbox.stub(modAckQueue, 'flush').rejects(); sandbox.stub(modAckQueue, 'onFlush').rejects(); - return subscriber.close(); + await subscriber.close(); }); it('should wait for in-flight messages to drain', async () => { From b904884bc6593721de54d77679f53e6ec66bf000 Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Fri, 20 Jun 2025 15:54:36 -0400 Subject: [PATCH 09/23] feat: update Temporal shims to match latest standards --- src/exponential-retry.ts | 6 +- src/message-stream.ts | 2 +- src/temporal.ts | 142 ++++++++++++++++++++++++++++++++++++-- src/util.ts | 5 +- test/exponential-retry.ts | 44 ++++++------ test/temporal.ts | 64 +++++++++++++++-- 6 files changed, 221 insertions(+), 42 deletions(-) diff --git a/src/exponential-retry.ts b/src/exponential-retry.ts index dd12a5b0b..95cc2b5e3 100644 --- a/src/exponential-retry.ts +++ b/src/exponential-retry.ts @@ -77,8 +77,8 @@ export class ExponentialRetry { private _timer?: NodeJS.Timeout; constructor(backoff: Duration, maxBackoff: Duration) { - this._backoffMs = backoff.totalOf('millisecond'); - this._maxBackoffMs = maxBackoff.totalOf('millisecond'); + this._backoffMs = backoff.milliseconds; + this._maxBackoffMs = maxBackoff.milliseconds; } /** @@ -170,7 +170,7 @@ export class ExponentialRetry { next.retryInfo!.callback( next as unknown as T, - Duration.from({millis: now - next.retryInfo!.firstRetry}), + Duration.from({milliseconds: now - next.retryInfo!.firstRetry}), ); } else { break; diff --git a/src/message-stream.ts b/src/message-stream.ts index 0706d3b22..e07fdf159 100644 --- a/src/message-stream.ts +++ b/src/message-stream.ts @@ -63,7 +63,7 @@ const DEFAULT_OPTIONS: MessageStreamOptions = { highWaterMark: 0, maxStreams: defaultOptions.subscription.maxStreams, timeout: 300000, - retryMinBackoff: Duration.from({millis: 100}), + retryMinBackoff: Duration.from({milliseconds: 100}), retryMaxBackoff: Duration.from({seconds: 60}), }; diff --git a/src/temporal.ts b/src/temporal.ts index d20009960..d92aa0c1a 100644 --- a/src/temporal.ts +++ b/src/temporal.ts @@ -29,15 +29,44 @@ export interface DurationLike { hours?: number; minutes?: number; seconds?: number; + milliseconds?: number; + + /** + * tc39 has renamed this to milliseconds. + * + * @deprecated + */ millis?: number; } /** * Simplified list of values to pass to Duration.totalOf(). This * list is taken from the tc39 Temporal.Duration proposal, but - * larger and smaller units have been left off. + * larger and smaller units have been left off. The latest tc39 spec + * allows for both singular and plural forms. + */ +export type TotalOfUnit = + | 'hour' + | 'minute' + | 'second' + | 'millisecond' + | 'hours' + | 'minutes' + | 'seconds' + | 'milliseconds'; + +interface TypeCheck { + total(): number; +} + +/** + * Is it a Duration or a DurationLike? + * + * @private */ -export type TotalOfUnit = 'hour' | 'minute' | 'second' | 'millisecond'; +export function isDurationObject(value: unknown): value is Duration { + return typeof value === 'object' && !!(value as TypeCheck).total; +} /** * Duration class with an interface similar to the tc39 Temporal @@ -46,9 +75,10 @@ export type TotalOfUnit = 'hour' | 'minute' | 'second' | 'millisecond'; * used to set durations in Pub/Sub. * * This class will remain here for at least the next major version, - * eventually to be replaced by the tc39 Temporal built-in. + * eventually to be replaced by the tc39 Temporal.Duration built-in. * * https://tc39.es/proposal-temporal/docs/duration.html + * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration */ export class Duration { private millis: number; @@ -64,33 +94,133 @@ export class Duration { /** * Calculates the total number of units of type 'totalOf' that would * fit inside this duration. + * + * No longer part of the tc39 spec, superseded by total(). + * + * @deprecated */ totalOf(totalOf: TotalOfUnit): number { + return this.total(totalOf); + } + + /** + * Calculates the total number of units of type 'totalOf' that would + * fit inside this duration. The tc39 `options` parameter is not supported. + */ + total(totalOf: TotalOfUnit): number { switch (totalOf) { case 'hour': + case 'hours': return this.millis / Duration.hourInMillis; case 'minute': + case 'minutes': return this.millis / Duration.minuteInMillis; case 'second': + case 'seconds': return this.millis / Duration.secondInMillis; case 'millisecond': + case 'milliseconds': return this.millis; default: - throw new Error(`Invalid unit in call to totalOf(): ${totalOf}`); + throw new Error(`Invalid unit in call to total(): ${totalOf}`); } } + /** + * Equivalent to `total('hour')`. + */ + get hours(): number { + return this.total('hour'); + } + + /** + * Equivalent to `total('minute')`. + */ + get minutes(): number { + return this.total('minute'); + } + + /** + * Equivalent to `total('second')`. + */ + get seconds(): number { + return this.total('second'); + } + + /** + * Equivalent to `total('millisecond')`. + */ + get milliseconds(): number { + return this.total('millisecond'); + } + + /** + * Adds another Duration to this one and returns a new Duration. + * + * @param other A Duration or Duration-like object, like from() takes. + * @returns A new Duration. + */ + add(other: DurationLike | Duration): Duration { + const otherDuration = Duration.from(other); + return Duration.from({ + millis: this.milliseconds + otherDuration.milliseconds, + }); + } + + /** + * Subtracts another Duration from this one and returns a new Duration. + * + * @param other A Duration or Duration-like object, like from() takes. + * @returns A new Duration. + */ + subtract(other: DurationLike | Duration): Duration { + const otherDuration = Duration.from(other); + return Duration.from({ + millis: this.milliseconds - otherDuration.milliseconds, + }); + } + /** * Creates a Duration from a DurationLike, which is an object * containing zero or more of the following: hours, seconds, * minutes, millis. */ - static from(durationLike: DurationLike): Duration { - let millis = durationLike.millis ?? 0; + static from(durationLike: DurationLike | Duration): Duration { + if (isDurationObject(durationLike)) { + const d = durationLike as Duration; + return new Duration(d.milliseconds); + } + + let millis = durationLike.milliseconds ?? durationLike.millis ?? 0; millis += (durationLike.seconds ?? 0) * Duration.secondInMillis; millis += (durationLike.minutes ?? 0) * Duration.minuteInMillis; millis += (durationLike.hours ?? 0) * Duration.hourInMillis; return new Duration(millis); } + + /** + * Compare two Duration objects. Returns -1 if the first is less than the + * second, zero if they are equal, 1 if the first is greater than the second. + * + * Unlike tc39, this version does not accept options for relativeTo. + */ + static compare(first: Duration, second: Duration) { + const diffMs = first.total('millisecond') - second.total('millisecond'); + if (diffMs < 0) { + return -1; + } + if (diffMs > 0) { + return 1; + } + return 0; + } } + +// Simple accessors that can be used independent of the class. These are +// functions and not methods because we don't want to add to what's in +// the tc39 spec. +export const atLeast = (d: Duration, min: Duration) => + Duration.compare(d, min) < 0 ? min : d; +export const atMost = (d: Duration, max: Duration) => + Duration.compare(d, max) > 0 ? max : d; diff --git a/src/util.ts b/src/util.ts index 6ef9ebb10..5320dc4be 100644 --- a/src/util.ts +++ b/src/util.ts @@ -102,10 +102,7 @@ export async function awaitWithTimeout( ): Promise<[T, boolean]> { let timeoutId: NodeJS.Timeout | undefined; const timeoutPromise = new Promise((_, rej) => { - timeoutId = setTimeout( - () => rej(timeoutToken), - timeout.totalOf('millisecond'), - ); + timeoutId = setTimeout(() => rej(timeoutToken), timeout.milliseconds); }); try { const value = await Promise.race([timeoutPromise, promise]); diff --git a/test/exponential-retry.ts b/test/exponential-retry.ts index 50834032a..5a36bd04d 100644 --- a/test/exponential-retry.ts +++ b/test/exponential-retry.ts @@ -55,8 +55,8 @@ describe('exponential retry class', () => { it('makes the first callback', () => { const clock = TestUtils.useFakeTimers(sandbox); const er = new ExponentialRetry( - Duration.from({millis: 100}), - Duration.from({millis: 1000}), + Duration.from({milliseconds: 100}), + Duration.from({milliseconds: 1000}), ); sandbox.stub(global.Math, 'random').returns(0.75); @@ -64,7 +64,7 @@ describe('exponential retry class', () => { let retried = false; er.retryLater(item, (s: typeof item, t: Duration) => { assert.strictEqual(s, item); - assert.strictEqual(t.totalOf('millisecond'), 125); + assert.strictEqual(t.milliseconds, 125); retried = true; }); @@ -78,8 +78,8 @@ describe('exponential retry class', () => { it('closes gracefully', () => { const clock = TestUtils.useFakeTimers(sandbox); const er = new ExponentialRetry( - Duration.from({millis: 100}), - Duration.from({millis: 1000}), + Duration.from({milliseconds: 100}), + Duration.from({milliseconds: 1000}), ); sandbox.stub(global.Math, 'random').returns(0.75); @@ -87,7 +87,7 @@ describe('exponential retry class', () => { const item = makeItem(); er.retryLater(item, (s: typeof item, t: Duration) => { assert.strictEqual(s, item); - assert.strictEqual(t.totalOf('millisecond'), 125); + assert.strictEqual(t.milliseconds, 125); called = true; }); @@ -108,13 +108,13 @@ describe('exponential retry class', () => { it('backs off exponentially', () => { const clock = TestUtils.useFakeTimers(sandbox); const er = new ExponentialRetry( - Duration.from({millis: 100}), - Duration.from({millis: 1000}), + Duration.from({milliseconds: 100}), + Duration.from({milliseconds: 1000}), ); sandbox.stub(global.Math, 'random').returns(0.75); let callbackCount = 0; - let callbackTime: Duration = Duration.from({millis: 0}); + let callbackTime: Duration = Duration.from({milliseconds: 0}); const item = makeItem(); const callback = (s: TestItem, t: Duration) => { @@ -129,11 +129,11 @@ describe('exponential retry class', () => { clock.tick(125); assert.strictEqual(callbackCount, 1); - assert.strictEqual(callbackTime.totalOf('millisecond'), 125); + assert.strictEqual(callbackTime.milliseconds, 125); clock.tick(400); assert.strictEqual(callbackCount, 2); - assert.strictEqual(callbackTime.totalOf('millisecond'), 375); + assert.strictEqual(callbackTime.milliseconds, 375); const leftovers = er.close(); assert.strictEqual(leftovers.length, 0); @@ -143,13 +143,13 @@ describe('exponential retry class', () => { const clock = TestUtils.useFakeTimers(sandbox); const item = makeItem(); const er = new ExponentialRetry( - Duration.from({millis: 100}), - Duration.from({millis: 150}), + Duration.from({milliseconds: 100}), + Duration.from({milliseconds: 150}), ); sandbox.stub(global.Math, 'random').returns(0.75); let callbackCount = 0; - let callbackTime: Duration = Duration.from({millis: 0}); + let callbackTime: Duration = Duration.from({milliseconds: 0}); const callback = (s: TestItem, t: Duration) => { assert.strictEqual(s, item); @@ -163,11 +163,11 @@ describe('exponential retry class', () => { clock.tick(125); assert.strictEqual(callbackCount, 1); - assert.strictEqual(callbackTime.totalOf('millisecond'), 125); + assert.strictEqual(callbackTime.milliseconds, 125); clock.tick(400); assert.strictEqual(callbackCount, 2); - assert.strictEqual(callbackTime.totalOf('millisecond'), 312); + assert.strictEqual(callbackTime.milliseconds, 312); const leftovers = er.close(); assert.strictEqual(leftovers.length, 0); @@ -178,8 +178,8 @@ describe('exponential retry class', () => { const items = [makeItem(), makeItem()]; const er = new ExponentialRetry( - Duration.from({millis: 100}), - Duration.from({millis: 1000}), + Duration.from({milliseconds: 100}), + Duration.from({milliseconds: 1000}), ); // Just disable the fuzz for this test. @@ -187,8 +187,8 @@ describe('exponential retry class', () => { const callbackCounts = [0, 0]; const callbackTimes: Duration[] = [ - Duration.from({millis: 0}), - Duration.from({millis: 0}), + Duration.from({milliseconds: 0}), + Duration.from({milliseconds: 0}), ]; const callback = (s: TestItem, t: Duration) => { @@ -207,7 +207,7 @@ describe('exponential retry class', () => { clock.tick(300); assert.deepStrictEqual(callbackCounts, [2, 0]); assert.deepStrictEqual( - callbackTimes.map(d => d.totalOf('millisecond')), + callbackTimes.map(d => d.milliseconds), [300, 0], ); @@ -220,7 +220,7 @@ describe('exponential retry class', () => { // while the second item should've retried once and quit. assert.deepStrictEqual(callbackCounts, [2, 1]); assert.deepStrictEqual( - callbackTimes.map(d => d.totalOf('millisecond')), + callbackTimes.map(d => d.milliseconds), [300, 100], ); diff --git a/test/temporal.ts b/test/temporal.ts index 030af2122..ee649cfa7 100644 --- a/test/temporal.ts +++ b/test/temporal.ts @@ -13,26 +13,78 @@ // limitations under the License. import {describe, it} from 'mocha'; -import {Duration} from '../src/temporal'; +import {Duration, atLeast as durationAtLeast, atMost as durationAtMost} from '../src/temporal'; import * as assert from 'assert'; describe('temporal', () => { describe('Duration', () => { it('can be created from millis', () => { - const duration = Duration.from({millis: 1234}); - assert.strictEqual(duration.totalOf('second'), 1.234); + const duration = Duration.from({milliseconds: 1234}); + assert.strictEqual(duration.seconds, 1.234); }); + it('can be created from seconds', () => { const duration = Duration.from({seconds: 1.234}); - assert.strictEqual(duration.totalOf('millisecond'), 1234); + assert.strictEqual(duration.milliseconds, 1234); }); + it('can be created from minutes', () => { const duration = Duration.from({minutes: 30}); - assert.strictEqual(duration.totalOf('hour'), 0.5); + assert.strictEqual(duration.total('hour'), 0.5); }); + it('can be created from hours', () => { const duration = Duration.from({hours: 1.5}); - assert.strictEqual(duration.totalOf('minute'), 90); + assert.strictEqual(duration.total('minute'), 90); + }); + + it('can be created from a Duration', () => { + const duration = Duration.from({seconds: 5}); + const second = Duration.from(duration); + assert.strictEqual(duration.milliseconds, second.milliseconds); + }); + + it('adds durations', () => { + const duration = Duration.from({seconds: 10}); + const second = duration.add({milliseconds: 1000}); + assert.strictEqual(second.seconds, 11); + }); + + it('subtracts durations', () => { + const duration = Duration.from({seconds: 10}); + const second = duration.subtract({seconds: 5}); + assert.strictEqual(second.milliseconds, 5000); + }); + + it('compares durations', () => { + const duration = Duration.from({seconds: 10}); + const less = Duration.from({seconds: 5}); + const more = Duration.from({seconds: 15}); + + const minus = Duration.compare(duration, more); + assert.strictEqual(minus, -1); + + const plus = Duration.compare(duration, less); + assert.strictEqual(plus, 1); + + const equal = Duration.compare(duration, duration); + assert.strictEqual(equal, 0); + }); + + it('has working helper functions', () => { + const duration = Duration.from({seconds: 10}); + + const atLeast1 = durationAtLeast(duration, Duration.from({seconds: 5})); + assert.strictEqual(atLeast1.seconds, 10); + + const atLeast2 = durationAtLeast(duration, Duration.from({seconds: 15})); + assert.strictEqual(atLeast2.seconds, 15); + + const atMost1 = durationAtMost(duration, Duration.from({seconds: 5})); + assert.strictEqual(atMost1.seconds, 5); + + const atMost2 = durationAtMost(duration, Duration.from({seconds: 15})); + assert.strictEqual(atMost2.seconds, 10); }); }); }); From 2cf219fdbcb84d1b406d59cca360fbdf5b7933f0 Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Fri, 20 Jun 2025 17:55:46 -0400 Subject: [PATCH 10/23] chore: linter fix --- test/temporal.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/temporal.ts b/test/temporal.ts index ee649cfa7..2e906ce3b 100644 --- a/test/temporal.ts +++ b/test/temporal.ts @@ -13,7 +13,11 @@ // limitations under the License. import {describe, it} from 'mocha'; -import {Duration, atLeast as durationAtLeast, atMost as durationAtMost} from '../src/temporal'; +import { + Duration, + atLeast as durationAtLeast, + atMost as durationAtMost, +} from '../src/temporal'; import * as assert from 'assert'; describe('temporal', () => { From 6e41e4ddde6aceb55abf2cfcdb8a5d3cbb72ce1d Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Fri, 20 Jun 2025 17:57:07 -0400 Subject: [PATCH 11/23] feat: update to latest spec doc info, finish unit tests --- src/lease-manager.ts | 21 ++++++ src/subscriber.ts | 156 +++++++++++++++++++++++-------------------- src/subscription.ts | 18 ++--- test/subscriber.ts | 83 +++++++++++++++++------ 4 files changed, 176 insertions(+), 102 deletions(-) diff --git a/src/lease-manager.ts b/src/lease-manager.ts index e8bfb3719..e5462b078 100644 --- a/src/lease-manager.ts +++ b/src/lease-manager.ts @@ -124,6 +124,7 @@ export class LeaseManager extends EventEmitter { */ clear(): Message[] { const wasFull = this.isFull(); + const wasEmpty = this.isEmpty(); this._pending = []; const remaining = Array.from(this._messages); @@ -133,11 +134,15 @@ export class LeaseManager extends EventEmitter { if (wasFull) { process.nextTick(() => this.emit('free')); } + if (!wasEmpty && this.isEmpty()) { + process.nextTick(() => this.emit('empty')); + } this._cancelExtension(); return remaining; } + /** * Indicates if we're at or over capacity. * @@ -148,6 +153,17 @@ export class LeaseManager extends EventEmitter { const {maxBytes, maxMessages} = this._options; return this.size >= maxMessages! || this.bytes >= maxBytes!; } + + /** + * True if we have no messages in leasing. + * + * @returns {boolean} + * @private + */ + isEmpty(): boolean { + return this._messages.size === 0; + } + /** * Removes a message from the inventory. Stopping the deadline extender if no * messages are left over. @@ -163,6 +179,7 @@ export class LeaseManager extends EventEmitter { } const wasFull = this.isFull(); + const wasEmpty = this.isEmpty(); this._messages.delete(message); this.bytes -= message.length; @@ -176,6 +193,10 @@ export class LeaseManager extends EventEmitter { this._dispense(this._pending.shift()!); } + if (!wasEmpty && this.isEmpty()) { + this.emit('empty'); + } + if (this.size === 0 && this._isLeasing) { this._cancelExtension(); } diff --git a/src/subscriber.ts b/src/subscriber.ts index 5b3041c92..6cd9cdc99 100644 --- a/src/subscriber.ts +++ b/src/subscriber.ts @@ -29,7 +29,7 @@ import {Subscription} from './subscription'; import {defaultOptions} from './default-options'; import {SubscriberClient} from './v1'; import * as tracing from './telemetry-tracing'; -import {Duration} from './temporal'; +import {Duration, atMost as durationAtMost} from './temporal'; import {EventEmitter} from 'events'; import {awaitWithTimeout} from './util'; @@ -55,9 +55,8 @@ export type AckResponse = ValueOf; * Enum values for behaviors of the Subscriber.close() method. */ export const SubscriberCloseBehaviors = { - Wait: 0 as const, - Exit: 1 as const, - Timeout: 2 as const, + NackImmediately: 'NACK' as const, + WaitForProcessing: 'WAIT' as const, }; export type SubscriberCloseBehavior = ValueOf; @@ -70,6 +69,12 @@ export interface SubscriberCloseOptions { timeout?: Duration; } +/** + * Specifies how long before the final close timeout, in WaitForProcessing mode, + * that we should give up and start shutting down cleanly. + */ +const finalNackTimeout = Duration.from({seconds: 1}); + /** * Thrown when an error is detected in an ack/nack/modack call, when * exactly-once delivery is enabled on the subscription. This will @@ -879,12 +884,12 @@ export class Subscriber extends EventEmitter { * * @param {SubscriberCloseOptions} [options] Determines the basic behavior of the * close() function. - * @param {ShutdownBehavior} [options.behavior] The behavior of the close operation. - * - Wait: Works more or less like the original close(), waiting indefinitely. - * - Timeout: Works like Wait, but with a timeout. - * - Exit: Nacks all buffered and leased messages, but otherwise exits immediately - * without waiting for anything. - * Use {@link ShutdownBehaviors} for enum values. + * @param {SubscriberCloseBehavior} [options.behavior] The behavior of the close operation. + * - NackImmediately: Sends nacks for all messages held by the client library, and + * wait for them to send. + * - WaitForProcessing: Continues normal ack/nack and leasing processes until close + * to the timeout, then switches to NackImmediately behavior to close down. + * Use {@link SubscriberCloseBehaviors} for enum values. * @param {Duration} [options.timeout] In the case of Timeout, the maximum duration * to wait for pending ack/nack requests to complete before resolving (or rejecting) * the promise. @@ -894,13 +899,6 @@ export class Subscriber extends EventEmitter { * @private */ async close(options?: SubscriberCloseOptions): Promise { - const behavior = options?.behavior; - let timeout = options?.timeout; - if (behavior === SubscriberCloseBehaviors.Wait) { - // Convert this case to "timeout === undefined, for infinite wait". - timeout = undefined; - } - if (!this.isOpen) { return; } @@ -909,72 +907,84 @@ export class Subscriber extends EventEmitter { this.isOpen = false; this._stream.destroy(); - // Grab everything left in inventory so we can nack them if needed. - const remaining = this._inventory.clear(); - - // Wait for any dispatched messages to become handled (ack/nack called). - const dispatched = remaining.filter(m => m.isDispatched); + // If no behavior is specified, default to Wait. + const behavior = + options?.behavior ?? SubscriberCloseBehaviors.WaitForProcessing; - // For any that haven't been dispatched, nack them immediately. - const nonDispatched = remaining.filter(m => !m.isDispatched); - nonDispatched.forEach(m => m.nack()); + // The timeout can't realistically be longer than the longest time we're willing + // to lease messages. + let timeout = durationAtMost( + options?.timeout ?? this.maxExtensionTime, + this.maxExtensionTime, + ); - // Wait until all of those nacks are flushed, or the timeout is reached. - if (behavior !== SubscriberCloseBehaviors.Exit) { - // Track when we started to make sure we don't go over time with the two - // separate operations. - const waitStart = Date.now(); - let timeoutMs = timeout?.totalOf('millisecond'); + // If the user specified a zero timeout, just bail immediately. + if (Math.floor(timeout.milliseconds) === 0) { + this._inventory.clear(); + return; + } - // Wait for user callbacks to complete. - const dispatchesCompleted = Promise.all( - dispatched.map(m => m.handledPromise), + // Warn the user if the timeout is too short for NackImmediately. + if (Duration.compare(timeout, finalNackTimeout) < 0) { + logDebug.warn( + 'Subscriber.close() timeout is less than the final shutdown time (%i ms). This may result in lost nacks.', + timeout.milliseconds, ); - if (timeoutMs === undefined) { - await dispatchesCompleted; - } else { - try { - await awaitWithTimeout( - dispatchesCompleted, - Duration.from({millis: timeoutMs}), - ); - timeoutMs -= waitStart - Date.now(); - } catch (e) { - const err = e as [unknown, boolean]; - if (err[1] === false) { - // This wasn't a timeout. Pass it on. - throw err[0]; - } else { - // The timeout passed. - timeoutMs = 0; - } + } + + // If we're in WaitForProcessing mode, then we first need to derive a NackImmediately + // timeout point. If everything finishes before then, we also want to go ahead and bail cleanly. + const shutdownStart = Date.now(); + if ( + behavior === SubscriberCloseBehaviors.WaitForProcessing && + !this._inventory.isEmpty + ) { + const waitTimeout = timeout.subtract(finalNackTimeout); + + const emptyPromise = new Promise(r => { + this._inventory.on('empty', r); + }); + + try { + await awaitWithTimeout(emptyPromise, waitTimeout); + } catch (e) { + // Don't try to deal with errors at this point, just warn-log. + const err = e as [unknown, boolean]; + if (err[1] === false) { + // This wasn't a timeout. + logDebug.warn('Error during Subscriber.close(): %j', err[0]); } } + } - // Wait for all acks and nacks to go through. - const flushCompleted = this._waitForFlush(); - if (timeoutMs === undefined) { - await flushCompleted; - } else if (timeoutMs > 0) { - try { - await awaitWithTimeout( - flushCompleted, - Duration.from({millis: timeoutMs}), - ); - } catch (e) { - const err = e as [unknown, boolean]; - if (err[1] === false) { - // This wasn't a timeout. Pass it on. - throw err[0]; - } else { - // The timeout passed. - } - } + // Now we head into immediate shutdown mode with what time is left. + timeout = timeout.subtract({ + milliseconds: Date.now() - shutdownStart, + }); + if (timeout.milliseconds <= 0) { + // This probably won't work out, but go through the motions. + timeout = Duration.from({milliseconds: 0}); + } + + // Grab everything left in inventory. This includes messages that have already + // been dispatched to user callbacks. + const remaining = this._inventory.clear(); + remaining.forEach(m => m.nack()); + + // Wait for user callbacks to complete. + const flushCompleted = this._waitForFlush(); + try { + await awaitWithTimeout(flushCompleted, timeout); + } catch (e) { + // Don't try to deal with errors at this point, just warn-log. + const err = e as [unknown, boolean]; + if (err[1] === false) { + // This wasn't a timeout. + logDebug.warn('Error during Subscriber.close(): %j', err[0]); } } - // Clean up OTel spans for any remaining messages that weren't nacked due - // to timeout. + // Clean up OTel spans for any remaining messages. remaining.forEach(m => { m.subSpans.shutdown(); m.endParentSpan(); diff --git a/src/subscription.ts b/src/subscription.ts index 061cf4561..6e0b6ea0d 100644 --- a/src/subscription.ts +++ b/src/subscription.ts @@ -46,6 +46,7 @@ import { Subscriber, SubscriberOptions, SubscriberCloseOptions, + SubscriberCloseBehaviors, } from './subscriber'; import {Topic} from './topic'; import {promisifySome} from './util'; @@ -67,6 +68,7 @@ export type SubscriptionMetadata = { export type SubscriptionOptions = SubscriberOptions & {topic?: Topic}; export type SubscriptionCloseCallback = (err?: Error) => void; export type SubscriptionCloseOptions = SubscriberCloseOptions; +export const SubscriptionCloseBehaviors = SubscriberCloseBehaviors; type SubscriptionCallback = ResourceCallback< Subscription, @@ -372,14 +374,14 @@ export class Subscription extends EventEmitter { * behavior of the returned Promise will depend on the behavior option. (See below.) * If no options are passed, it behaves like `ShutdownBehaviors.Wait`. * - * @param {SubscriptionCloseOptions} [options] Determines the basic behavior of the + * @param {SubscriberCloseOptions} [options] Determines the basic behavior of the * close() function. - * @param {SubscriptionCloseBehavior} [options.behavior] The behavior of the close operation. - * - Wait: Works more or less like the original close(), waiting indefinitely. - * - Timeout: Works like Wait, but with a timeout. - * - Exit: Nacks all buffered and leased messages, but otherwise exits immediately - * without waiting for anything. - * Use {@link SubscriptionCloseBehaviors} for enum values. + * @param {SubscriberCloseBehavior} [options.behavior] The behavior of the close operation. + * - NackImmediately: Sends nacks for all messages held by the client library, and + * wait for them to send. + * - WaitForProcessing: Continues normal ack/nack and leasing processes until close + * to the timeout, then switches to NackImmediately behavior to close down. + * Use {@link SubscriberCloseBehaviors} for enum values. * @param {Duration} [options.timeout] In the case of Timeout, the maximum duration * to wait for pending ack/nack requests to complete before resolving (or rejecting) * the promise. @@ -390,7 +392,7 @@ export class Subscription extends EventEmitter { * @example * ``` * await subscription.close({ - * behavior: SubscriptionCloseBehaviors.Timeout, + * behavior: SubscriptionCloseBehaviors.WaitForProcessing, * timeout: Duration.from({seconds: 60}) * }); * ``` diff --git a/test/subscriber.ts b/test/subscriber.ts index 2c6dac6b7..758549bac 100644 --- a/test/subscriber.ts +++ b/test/subscriber.ts @@ -102,6 +102,11 @@ class FakeLeaseManager extends EventEmitter { } // eslint-disable-next-line @typescript-eslint/no-unused-vars remove(message: s.Message): void {} + + _isEmpty = true; + get isEmpty() { + return this._isEmpty; + } } class FakeQueue { @@ -664,7 +669,7 @@ describe('Subscriber', () => { let subInternals: SubInternals; beforeEach(() => { - clock = sinon.useFakeTimers(); + clock = sandbox.useFakeTimers(); inventory = stubs.get('inventory'); ackQueue = stubs.get('ackQueue'); modAckQueue = stubs.get('modAckQueue'); @@ -681,24 +686,51 @@ describe('Subscriber', () => { clock.restore(); }); - it('should call _waitForFlush without timeout if no options', async () => { - const waitForFlushSpy = sandbox.spy(subInternals, '_waitForFlush'); + it('should do nothing if isOpen = false', async () => { + const destroySpy = sandbox.spy(subInternals._stream, 'destroy'); + subscriber.isOpen = false; await subscriber.close(); - assert(waitForFlushSpy.calledOnce); - assert.strictEqual(waitForFlushSpy.firstCall.args[0], undefined); + assert.strictEqual(destroySpy.callCount, 0); }); - it('should not nack remaining messages if zero timeout', async () => { - const mockMessages = [ - new Message(subscriber, RECEIVED_MESSAGE), - new Message(subscriber, RECEIVED_MESSAGE), - ]; - sandbox.stub(inventory, 'clear').returns(mockMessages); - const nackSpy = sandbox.spy(subscriber, 'nack'); + it('should clear inventory and bail for timeout = 0', async () => { + const clearSpy = sandbox.spy(inventory, 'clear'); + const onSpy = sandbox.spy(inventory, 'on'); + await subscriber.close({ + timeout: Duration.from({milliseconds: 0}), + }); + assert.strictEqual(clearSpy.callCount, 1); + assert.strictEqual(onSpy.callCount, 0); + }); + + it('should not wait for an empty inventory in NackImmediately', async () => { + const onSpy = sandbox.spy(inventory, 'on'); + await subscriber.close({ + behavior: s.SubscriberCloseBehaviors.NackImmediately, + timeout: Duration.from({milliseconds: 100}), + }); + assert.strictEqual(onSpy.callCount, 0); + }); - await subscriber.close({timeout: Duration.from({seconds: 0})}); + it('should not wait for an empty inventory in WaitForProcessing if empty', async () => { + const onSpy = sandbox.spy(inventory, 'on'); + await subscriber.close({ + behavior: s.SubscriberCloseBehaviors.WaitForProcessing, + timeout: Duration.from({milliseconds: 100}), + }); + assert.strictEqual(onSpy.callCount, 0); + }); - assert(nackSpy.notCalled); + it('should wait for an empty inventory in WaitForProcessing if not empty', async () => { + inventory._isEmpty = false; + const onSpy = sandbox.spy(inventory, 'on'); + const prom = subscriber.close({ + behavior: s.SubscriberCloseBehaviors.WaitForProcessing, + timeout: Duration.from({seconds: 2}), + }); + assert.strictEqual(onSpy.callCount, 1); + clock.tick(3000); + await prom; }); it('should nack remaining messages if timeout is non-zero', async () => { @@ -709,7 +741,9 @@ describe('Subscriber', () => { sandbox.stub(inventory, 'clear').returns(mockMessages); const nackSpy = sandbox.spy(subscriber, 'nack'); - await subscriber.close({timeout: Duration.from({seconds: 5})}); + const prom = subscriber.close({timeout: Duration.from({seconds: 5})}); + clock.tick(6000); + await prom; assert.strictEqual(nackSpy.callCount, mockMessages.length); mockMessages.forEach((msg, i) => { @@ -728,16 +762,18 @@ describe('Subscriber', () => { modAckQueue.numInFlightRequests = 1; let closed = false; - void subscriber + const prom = subscriber .close({ - timeout: Duration.from({millis: 100}), + behavior: s.SubscriberCloseBehaviors.NackImmediately, + timeout: Duration.from({milliseconds: 100}), }) .then(() => { closed = true; }); // Advance time past the timeout - await clock.tickAsync(101); + clock.tick(200); + await prom; // Promise should resolve even though drains haven't assert.strictEqual(closed, true); @@ -758,9 +794,9 @@ describe('Subscriber', () => { modAckQueue.numInFlightRequests = 1; let closed = false; - void subscriber + const prom = subscriber .close({ - timeout: Duration.from({millis: 100}), + timeout: Duration.from({milliseconds: 100}), }) .then(() => { closed = true; @@ -771,7 +807,8 @@ describe('Subscriber', () => { modAckDrainDeferred.resolve(); // Advance time slightly, but less than the timeout - await clock.tickAsync(50); + clock.tick(50); + await prom; // Promise should resolve. assert.strictEqual(closed, true); @@ -932,6 +969,10 @@ describe('Subscriber', () => { delete msgAny.parentSpan; delete msgAny.subSpans; + // Delete baggage for discovering unprocessed messages. + delete addMsgAny._handledPromise; + delete msgAny._handledPromise; + assert.deepStrictEqual(addMsg, message); // test for receipt From ca50c3c063dcb491df5c2b0c0606bada6093c846 Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Fri, 20 Jun 2025 18:10:16 -0400 Subject: [PATCH 12/23] feat: also move the options from close() parameters to subscriber options --- src/subscriber.ts | 35 +++++++++++------------ src/subscription.ts | 36 +++--------------------- test/subscriber.ts | 67 +++++++++++++++++++++++++++++---------------- 3 files changed, 65 insertions(+), 73 deletions(-) diff --git a/src/subscriber.ts b/src/subscriber.ts index 6cd9cdc99..9453924e2 100644 --- a/src/subscriber.ts +++ b/src/subscriber.ts @@ -640,6 +640,18 @@ export class Message implements tracing.MessageWithAttributes { * settings at the Cloud PubSub server and uses the less accurate method * of only enforcing flow control at the client side. * @property {MessageStreamOptions} [streamingOptions] Streaming options. + * If no options are passed, it behaves like `SubscriberCloseBehaviors.Wait`. + * @property {SubscriberCloseOptions} [options] Determines the basic behavior of the + * close() function. + * @property {SubscriberCloseBehavior} [options.behavior] The behavior of the close operation. + * - NackImmediately: Sends nacks for all messages held by the client library, and + * wait for them to send. + * - WaitForProcessing: Continues normal ack/nack and leasing processes until close + * to the timeout, then switches to NackImmediately behavior to close down. + * Use {@link SubscriberCloseBehaviors} for enum values. + * @property {Duration} [options.timeout] In the case of Timeout, the maximum duration + * to wait for pending ack/nack requests to complete before resolving (or rejecting) + * the promise. */ export interface SubscriberOptions { minAckDeadline?: Duration; @@ -649,6 +661,7 @@ export interface SubscriberOptions { flowControl?: FlowControlOptions; useLegacyFlowControl?: boolean; streamingOptions?: MessageStreamOptions; + closeOptions?: SubscriberCloseOptions; } const minAckDeadlineForExactlyOnceDelivery = Duration.from({seconds: 60}); @@ -876,29 +889,15 @@ export class Subscriber extends EventEmitter { /** * Closes the subscriber, stopping the reception of new messages and shutting - * down the underlying stream. Any messages being held in buffers in the client - * library will be nacked. The behavior of the returned Promise will depend - * on the behavior option. (See below.) + * down the underlying stream. The behavior of the returned Promise will depend + * on the closeOptions in the subscriber options. * - * If no options are passed, it behaves like `ShutdownBehaviors.Wait`. - * - * @param {SubscriberCloseOptions} [options] Determines the basic behavior of the - * close() function. - * @param {SubscriberCloseBehavior} [options.behavior] The behavior of the close operation. - * - NackImmediately: Sends nacks for all messages held by the client library, and - * wait for them to send. - * - WaitForProcessing: Continues normal ack/nack and leasing processes until close - * to the timeout, then switches to NackImmediately behavior to close down. - * Use {@link SubscriberCloseBehaviors} for enum values. - * @param {Duration} [options.timeout] In the case of Timeout, the maximum duration - * to wait for pending ack/nack requests to complete before resolving (or rejecting) - * the promise. * @returns {Promise} A promise that resolves when the subscriber is closed * and pending operations are flushed or the timeout is reached. * * @private */ - async close(options?: SubscriberCloseOptions): Promise { + async close(): Promise { if (!this.isOpen) { return; } @@ -907,6 +906,8 @@ export class Subscriber extends EventEmitter { this.isOpen = false; this._stream.destroy(); + const options = this._options.closeOptions; + // If no behavior is specified, default to Wait. const behavior = options?.behavior ?? SubscriberCloseBehaviors.WaitForProcessing; diff --git a/src/subscription.ts b/src/subscription.ts index 6e0b6ea0d..06e4ad602 100644 --- a/src/subscription.ts +++ b/src/subscription.ts @@ -369,47 +369,19 @@ export class Subscription extends EventEmitter { * message events unless you call {Subscription#open} or add new message * listeners. * - * This stops the reception of new messages and shuts down the underlying stream. - * Any messages being held in buffers in the client library will be nacked. The - * behavior of the returned Promise will depend on the behavior option. (See below.) - * If no options are passed, it behaves like `ShutdownBehaviors.Wait`. - * - * @param {SubscriberCloseOptions} [options] Determines the basic behavior of the - * close() function. - * @param {SubscriberCloseBehavior} [options.behavior] The behavior of the close operation. - * - NackImmediately: Sends nacks for all messages held by the client library, and - * wait for them to send. - * - WaitForProcessing: Continues normal ack/nack and leasing processes until close - * to the timeout, then switches to NackImmediately behavior to close down. - * Use {@link SubscriberCloseBehaviors} for enum values. - * @param {Duration} [options.timeout] In the case of Timeout, the maximum duration - * to wait for pending ack/nack requests to complete before resolving (or rejecting) - * the promise. * @param {function} [callback] The callback function, if not using the Promise-based * call signature. * @param {?error} [callback.err] An error returned while closing the Subscription. * * @example * ``` - * await subscription.close({ - * behavior: SubscriptionCloseBehaviors.WaitForProcessing, - * timeout: Duration.from({seconds: 60}) - * }); + * await subscription.close(); * ``` */ - close(options?: SubscriptionCloseOptions): Promise; + close(): Promise; close(callback: SubscriptionCloseCallback): void; - close( - options: SubscriptionCloseOptions, - callback: SubscriptionCloseCallback, - ): void; - close( - optsOrCallback?: SubscriptionCloseOptions | SubscriptionCloseCallback, - callback?: SubscriptionCloseCallback, - ): void | Promise { - const options = typeof optsOrCallback === 'object' ? optsOrCallback : {}; - callback = typeof optsOrCallback === 'function' ? optsOrCallback : callback; - this._subscriber.close(options).then(() => callback!(), callback); + close(callback?: SubscriptionCloseCallback): void | Promise { + this._subscriber.close().then(() => callback!(), callback); } /** diff --git a/test/subscriber.ts b/test/subscriber.ts index 758549bac..40d7730a5 100644 --- a/test/subscriber.ts +++ b/test/subscriber.ts @@ -696,38 +696,50 @@ describe('Subscriber', () => { it('should clear inventory and bail for timeout = 0', async () => { const clearSpy = sandbox.spy(inventory, 'clear'); const onSpy = sandbox.spy(inventory, 'on'); - await subscriber.close({ - timeout: Duration.from({milliseconds: 0}), + subscriber.setOptions({ + closeOptions: { + timeout: Duration.from({milliseconds: 0}), + }, }); + await subscriber.close(); assert.strictEqual(clearSpy.callCount, 1); assert.strictEqual(onSpy.callCount, 0); }); it('should not wait for an empty inventory in NackImmediately', async () => { const onSpy = sandbox.spy(inventory, 'on'); - await subscriber.close({ - behavior: s.SubscriberCloseBehaviors.NackImmediately, - timeout: Duration.from({milliseconds: 100}), + subscriber.setOptions({ + closeOptions: { + behavior: s.SubscriberCloseBehaviors.NackImmediately, + timeout: Duration.from({milliseconds: 100}), + }, }); + await subscriber.close(); assert.strictEqual(onSpy.callCount, 0); }); it('should not wait for an empty inventory in WaitForProcessing if empty', async () => { const onSpy = sandbox.spy(inventory, 'on'); - await subscriber.close({ - behavior: s.SubscriberCloseBehaviors.WaitForProcessing, - timeout: Duration.from({milliseconds: 100}), + subscriber.setOptions({ + closeOptions: { + behavior: s.SubscriberCloseBehaviors.WaitForProcessing, + timeout: Duration.from({milliseconds: 100}), + }, }); + await subscriber.close(); assert.strictEqual(onSpy.callCount, 0); }); it('should wait for an empty inventory in WaitForProcessing if not empty', async () => { inventory._isEmpty = false; const onSpy = sandbox.spy(inventory, 'on'); - const prom = subscriber.close({ - behavior: s.SubscriberCloseBehaviors.WaitForProcessing, - timeout: Duration.from({seconds: 2}), + subscriber.setOptions({ + closeOptions: { + behavior: s.SubscriberCloseBehaviors.WaitForProcessing, + timeout: Duration.from({seconds: 2}), + }, }); + const prom = subscriber.close(); assert.strictEqual(onSpy.callCount, 1); clock.tick(3000); await prom; @@ -741,7 +753,12 @@ describe('Subscriber', () => { sandbox.stub(inventory, 'clear').returns(mockMessages); const nackSpy = sandbox.spy(subscriber, 'nack'); - const prom = subscriber.close({timeout: Duration.from({seconds: 5})}); + subscriber.setOptions({ + closeOptions: { + timeout: Duration.from({seconds: 5}), + }, + }); + const prom = subscriber.close(); clock.tick(6000); await prom; @@ -762,14 +779,15 @@ describe('Subscriber', () => { modAckQueue.numInFlightRequests = 1; let closed = false; - const prom = subscriber - .close({ + subscriber.setOptions({ + closeOptions: { behavior: s.SubscriberCloseBehaviors.NackImmediately, timeout: Duration.from({milliseconds: 100}), - }) - .then(() => { - closed = true; - }); + }, + }); + const prom = subscriber.close().then(() => { + closed = true; + }); // Advance time past the timeout clock.tick(200); @@ -794,13 +812,14 @@ describe('Subscriber', () => { modAckQueue.numInFlightRequests = 1; let closed = false; - const prom = subscriber - .close({ + subscriber.setOptions({ + closeOptions: { timeout: Duration.from({milliseconds: 100}), - }) - .then(() => { - closed = true; - }); + }, + }); + const prom = subscriber.close().then(() => { + closed = true; + }); // Resolve drains quickly ackDrainDeferred.resolve(); From ce5e917cf57ded890b40d90991af36e71fccde03 Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Fri, 4 Jul 2025 16:13:16 -0400 Subject: [PATCH 13/23] chore: fix linter errors --- test/util.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/util.ts b/test/util.ts index 78ab48a88..5355cefc1 100644 --- a/test/util.ts +++ b/test/util.ts @@ -16,7 +16,7 @@ import {describe, it} from 'mocha'; import {addToBucket, Throttler, awaitWithTimeout} from '../src/util'; import * as assert from 'assert'; import * as sinon from 'sinon'; -import { Duration } from '../src'; +import {Duration} from '../src'; describe('utils', () => { describe('Throttler', () => { @@ -72,7 +72,7 @@ describe('utils', () => { it('handles non-timeout properly', async () => { const fakeTimers = sandbox.useFakeTimers(0); - let resolve = (...args: unknown[]) => {}; + let resolve = () => {}; const testString = 'fooby'; const testPromise = new Promise(r => { resolve = () => r(testString); @@ -93,7 +93,7 @@ describe('utils', () => { it('handles non-timeout errors properly', async () => { const fakeTimers = sandbox.useFakeTimers(0); - let reject = (...args: unknown[]) => {}; + let reject = () => {}; const testString = 'fooby'; const testPromise = new Promise((res, rej) => { reject = () => rej(testString); @@ -114,7 +114,7 @@ describe('utils', () => { it('handles timeout properly', async () => { const fakeTimers = sandbox.useFakeTimers(0); - let resolve = (...args: unknown[]) => {}; + let resolve = () => {}; const testString = 'fooby'; const testPromise = new Promise(r => { resolve = () => r(testString); From f2898a3d79b574a8dad671ca77783324496b6b81 Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Tue, 8 Jul 2025 16:30:58 -0400 Subject: [PATCH 14/23] samples: update to latest API changes --- samples/closeSubscriptionWithTimeout.js | 41 ++++++++++++++----- .../closeSubscriptionWithTimeout.ts | 41 ++++++++++++++----- src/index.ts | 2 + 3 files changed, 64 insertions(+), 20 deletions(-) diff --git a/samples/closeSubscriptionWithTimeout.js b/samples/closeSubscriptionWithTimeout.js index 67584437e..76c8940ee 100644 --- a/samples/closeSubscriptionWithTimeout.js +++ b/samples/closeSubscriptionWithTimeout.js @@ -34,7 +34,11 @@ // -START pubsub_close_subscription_with_timeout] // Imports the Google Cloud client library -const {PubSub, Duration} = require('@google-cloud/pubsub'); +const { + PubSub, + Duration, + SubscriptionCloseBehaviors, +} = require('@google-cloud/pubsub'); // Creates a client; cache this for further use const pubsub = new PubSub(); @@ -45,17 +49,34 @@ async function closeSubscriptionWithTimeout( ) { const topic = pubsub.topic(topicNameOrId); - const timeout = Duration.from({seconds: 10}); - const zeroTimeout = Duration.from({seconds: 0}); - // Closes the subscription immediately, not waiting for anything. - let subscription = topic.subscription(subscriptionNameOrId); - await subscription.close({timeout: zeroTimeout}); + let subscription = topic.subscription(subscriptionNameOrId, { + closeOptions: { + timeout: Duration.from({seconds: 0}), + }, + }); + await subscription.close(); - // Shuts down the gRPC connection, sends nacks for buffered messages, - // and closes the subscription after a set timeout. - subscription = topic.subscription(subscriptionNameOrId); - await subscription.close({timeout}); + // Shuts down the gRPC connection, and waits for just before the timeout + // to send nacks for buffered messages. If `timeout` were missing, this + // would wait for the maximum leasing timeout. + subscription = topic.subscription(subscriptionNameOrId, { + closeOptions: { + behavior: SubscriptionCloseBehaviors.WaitForProcessing, + timeout: Duration.from({seconds: 10}), + }, + }); + await subscription.close(); + + // Shuts down the gRPC connection, sends nacks for buffered messages, and waits + // through the timeout for nacks to send. + subscription = topic.subscription(subscriptionNameOrId, { + closeOptions: { + behavior: SubscriptionCloseBehaviors.NackImmediately, + timeout: Duration.from({seconds: 10}), + }, + }); + await subscription.close(); } // -END pubsub_close_subscription_with_timeout] diff --git a/samples/typescript/closeSubscriptionWithTimeout.ts b/samples/typescript/closeSubscriptionWithTimeout.ts index d521245c5..a13965b54 100644 --- a/samples/typescript/closeSubscriptionWithTimeout.ts +++ b/samples/typescript/closeSubscriptionWithTimeout.ts @@ -30,7 +30,11 @@ // -START pubsub_close_subscription_with_timeout] // Imports the Google Cloud client library -import {PubSub, Duration} from '@google-cloud/pubsub'; +import { + PubSub, + Duration, + SubscriptionCloseBehaviors, +} from '@google-cloud/pubsub'; // Creates a client; cache this for further use const pubsub = new PubSub(); @@ -41,17 +45,34 @@ async function closeSubscriptionWithTimeout( ) { const topic = pubsub.topic(topicNameOrId); - const timeout = Duration.from({seconds: 10}); - const zeroTimeout = Duration.from({seconds: 0}); - // Closes the subscription immediately, not waiting for anything. - let subscription = topic.subscription(subscriptionNameOrId); - await subscription.close({timeout: zeroTimeout}); + let subscription = topic.subscription(subscriptionNameOrId, { + closeOptions: { + timeout: Duration.from({seconds: 0}), + }, + }); + await subscription.close(); - // Shuts down the gRPC connection, sends nacks for buffered messages, - // and closes the subscription after a set timeout. - subscription = topic.subscription(subscriptionNameOrId); - await subscription.close({timeout}); + // Shuts down the gRPC connection, and waits for just before the timeout + // to send nacks for buffered messages. If `timeout` were missing, this + // would wait for the maximum leasing timeout. + subscription = topic.subscription(subscriptionNameOrId, { + closeOptions: { + behavior: SubscriptionCloseBehaviors.WaitForProcessing, + timeout: Duration.from({seconds: 10}), + }, + }); + await subscription.close(); + + // Shuts down the gRPC connection, sends nacks for buffered messages, and waits + // through the timeout for nacks to send. + subscription = topic.subscription(subscriptionNameOrId, { + closeOptions: { + behavior: SubscriptionCloseBehaviors.NackImmediately, + timeout: Duration.from({seconds: 10}), + }, + }); + await subscription.close(); } // -END pubsub_close_subscription_with_timeout] diff --git a/src/index.ts b/src/index.ts index 2c9371069..2b1432cdb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -143,6 +143,8 @@ export { SubscriptionMetadata, SubscriptionOptions, SubscriptionCloseCallback, + SubscriptionCloseOptions, + SubscriptionCloseBehaviors, CreateSubscriptionOptions, CreateSubscriptionCallback, CreateSubscriptionResponse, From 073c6d71566d66b4c2167b9445ad75cb3442f61f Mon Sep 17 00:00:00 2001 From: Owl Bot Date: Thu, 3 Jul 2025 21:50:03 +0000 Subject: [PATCH 15/23] =?UTF-8?q?=F0=9F=A6=89=20Updates=20from=20OwlBot=20?= =?UTF-8?q?post-processor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --- protos/protos.d.ts | 522 ++++++++++++- protos/protos.js | 1821 ++++++++++++++++++++++++++++++++++++++++++-- protos/protos.json | 215 +++++- 3 files changed, 2458 insertions(+), 100 deletions(-) diff --git a/protos/protos.d.ts b/protos/protos.d.ts index cd375d689..7cb0f3891 100644 --- a/protos/protos.d.ts +++ b/protos/protos.d.ts @@ -11351,6 +11351,9 @@ export namespace google { /** CommonLanguageSettings destinations */ destinations?: (google.api.ClientLibraryDestination[]|null); + + /** CommonLanguageSettings selectiveGapicGeneration */ + selectiveGapicGeneration?: (google.api.ISelectiveGapicGeneration|null); } /** Represents a CommonLanguageSettings. */ @@ -11368,6 +11371,9 @@ export namespace google { /** CommonLanguageSettings destinations. */ public destinations: google.api.ClientLibraryDestination[]; + /** CommonLanguageSettings selectiveGapicGeneration. */ + public selectiveGapicGeneration?: (google.api.ISelectiveGapicGeneration|null); + /** * Creates a new CommonLanguageSettings instance using the specified properties. * @param [properties] Properties to set @@ -12068,6 +12074,9 @@ export namespace google { /** PythonSettings common */ common?: (google.api.ICommonLanguageSettings|null); + + /** PythonSettings experimentalFeatures */ + experimentalFeatures?: (google.api.PythonSettings.IExperimentalFeatures|null); } /** Represents a PythonSettings. */ @@ -12082,6 +12091,9 @@ export namespace google { /** PythonSettings common. */ public common?: (google.api.ICommonLanguageSettings|null); + /** PythonSettings experimentalFeatures. */ + public experimentalFeatures?: (google.api.PythonSettings.IExperimentalFeatures|null); + /** * Creates a new PythonSettings instance using the specified properties. * @param [properties] Properties to set @@ -12160,6 +12172,118 @@ export namespace google { public static getTypeUrl(typeUrlPrefix?: string): string; } + namespace PythonSettings { + + /** Properties of an ExperimentalFeatures. */ + interface IExperimentalFeatures { + + /** ExperimentalFeatures restAsyncIoEnabled */ + restAsyncIoEnabled?: (boolean|null); + + /** ExperimentalFeatures protobufPythonicTypesEnabled */ + protobufPythonicTypesEnabled?: (boolean|null); + + /** ExperimentalFeatures unversionedPackageDisabled */ + unversionedPackageDisabled?: (boolean|null); + } + + /** Represents an ExperimentalFeatures. */ + class ExperimentalFeatures implements IExperimentalFeatures { + + /** + * Constructs a new ExperimentalFeatures. + * @param [properties] Properties to set + */ + constructor(properties?: google.api.PythonSettings.IExperimentalFeatures); + + /** ExperimentalFeatures restAsyncIoEnabled. */ + public restAsyncIoEnabled: boolean; + + /** ExperimentalFeatures protobufPythonicTypesEnabled. */ + public protobufPythonicTypesEnabled: boolean; + + /** ExperimentalFeatures unversionedPackageDisabled. */ + public unversionedPackageDisabled: boolean; + + /** + * Creates a new ExperimentalFeatures instance using the specified properties. + * @param [properties] Properties to set + * @returns ExperimentalFeatures instance + */ + public static create(properties?: google.api.PythonSettings.IExperimentalFeatures): google.api.PythonSettings.ExperimentalFeatures; + + /** + * Encodes the specified ExperimentalFeatures message. Does not implicitly {@link google.api.PythonSettings.ExperimentalFeatures.verify|verify} messages. + * @param message ExperimentalFeatures message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: google.api.PythonSettings.IExperimentalFeatures, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified ExperimentalFeatures message, length delimited. Does not implicitly {@link google.api.PythonSettings.ExperimentalFeatures.verify|verify} messages. + * @param message ExperimentalFeatures message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: google.api.PythonSettings.IExperimentalFeatures, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes an ExperimentalFeatures message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns ExperimentalFeatures + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): google.api.PythonSettings.ExperimentalFeatures; + + /** + * Decodes an ExperimentalFeatures message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns ExperimentalFeatures + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): google.api.PythonSettings.ExperimentalFeatures; + + /** + * Verifies an ExperimentalFeatures message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates an ExperimentalFeatures message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns ExperimentalFeatures + */ + public static fromObject(object: { [k: string]: any }): google.api.PythonSettings.ExperimentalFeatures; + + /** + * Creates a plain object from an ExperimentalFeatures message. Also converts values to other types if specified. + * @param message ExperimentalFeatures + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: google.api.PythonSettings.ExperimentalFeatures, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this ExperimentalFeatures to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for ExperimentalFeatures + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } + } + /** Properties of a NodeSettings. */ interface INodeSettings { @@ -12486,6 +12610,9 @@ export namespace google { /** GoSettings common */ common?: (google.api.ICommonLanguageSettings|null); + + /** GoSettings renamedServices */ + renamedServices?: ({ [k: string]: string }|null); } /** Represents a GoSettings. */ @@ -12500,6 +12627,9 @@ export namespace google { /** GoSettings common. */ public common?: (google.api.ICommonLanguageSettings|null); + /** GoSettings renamedServices. */ + public renamedServices: { [k: string]: string }; + /** * Creates a new GoSettings instance using the specified properties. * @param [properties] Properties to set @@ -12824,6 +12954,109 @@ export namespace google { PACKAGE_MANAGER = 20 } + /** Properties of a SelectiveGapicGeneration. */ + interface ISelectiveGapicGeneration { + + /** SelectiveGapicGeneration methods */ + methods?: (string[]|null); + + /** SelectiveGapicGeneration generateOmittedAsInternal */ + generateOmittedAsInternal?: (boolean|null); + } + + /** Represents a SelectiveGapicGeneration. */ + class SelectiveGapicGeneration implements ISelectiveGapicGeneration { + + /** + * Constructs a new SelectiveGapicGeneration. + * @param [properties] Properties to set + */ + constructor(properties?: google.api.ISelectiveGapicGeneration); + + /** SelectiveGapicGeneration methods. */ + public methods: string[]; + + /** SelectiveGapicGeneration generateOmittedAsInternal. */ + public generateOmittedAsInternal: boolean; + + /** + * Creates a new SelectiveGapicGeneration instance using the specified properties. + * @param [properties] Properties to set + * @returns SelectiveGapicGeneration instance + */ + public static create(properties?: google.api.ISelectiveGapicGeneration): google.api.SelectiveGapicGeneration; + + /** + * Encodes the specified SelectiveGapicGeneration message. Does not implicitly {@link google.api.SelectiveGapicGeneration.verify|verify} messages. + * @param message SelectiveGapicGeneration message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: google.api.ISelectiveGapicGeneration, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified SelectiveGapicGeneration message, length delimited. Does not implicitly {@link google.api.SelectiveGapicGeneration.verify|verify} messages. + * @param message SelectiveGapicGeneration message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: google.api.ISelectiveGapicGeneration, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a SelectiveGapicGeneration message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns SelectiveGapicGeneration + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): google.api.SelectiveGapicGeneration; + + /** + * Decodes a SelectiveGapicGeneration message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns SelectiveGapicGeneration + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): google.api.SelectiveGapicGeneration; + + /** + * Verifies a SelectiveGapicGeneration message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a SelectiveGapicGeneration message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns SelectiveGapicGeneration + */ + public static fromObject(object: { [k: string]: any }): google.api.SelectiveGapicGeneration; + + /** + * Creates a plain object from a SelectiveGapicGeneration message. Also converts values to other types if specified. + * @param message SelectiveGapicGeneration + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: google.api.SelectiveGapicGeneration, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this SelectiveGapicGeneration to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for SelectiveGapicGeneration + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } + /** LaunchStage enum. */ enum LaunchStage { LAUNCH_STAGE_UNSPECIFIED = 0, @@ -13205,6 +13438,7 @@ export namespace google { /** Edition enum. */ enum Edition { EDITION_UNKNOWN = 0, + EDITION_LEGACY = 900, EDITION_PROTO2 = 998, EDITION_PROTO3 = 999, EDITION_2023 = 1000, @@ -13235,6 +13469,9 @@ export namespace google { /** FileDescriptorProto weakDependency */ weakDependency?: (number[]|null); + /** FileDescriptorProto optionDependency */ + optionDependency?: (string[]|null); + /** FileDescriptorProto messageType */ messageType?: (google.protobuf.IDescriptorProto[]|null); @@ -13284,6 +13521,9 @@ export namespace google { /** FileDescriptorProto weakDependency. */ public weakDependency: number[]; + /** FileDescriptorProto optionDependency. */ + public optionDependency: string[]; + /** FileDescriptorProto messageType. */ public messageType: google.protobuf.IDescriptorProto[]; @@ -13418,6 +13658,9 @@ export namespace google { /** DescriptorProto reservedName */ reservedName?: (string[]|null); + + /** DescriptorProto visibility */ + visibility?: (google.protobuf.SymbolVisibility|keyof typeof google.protobuf.SymbolVisibility|null); } /** Represents a DescriptorProto. */ @@ -13459,6 +13702,9 @@ export namespace google { /** DescriptorProto reservedName. */ public reservedName: string[]; + /** DescriptorProto visibility. */ + public visibility: (google.protobuf.SymbolVisibility|keyof typeof google.protobuf.SymbolVisibility); + /** * Creates a new DescriptorProto instance using the specified properties. * @param [properties] Properties to set @@ -14306,6 +14552,9 @@ export namespace google { /** EnumDescriptorProto reservedName */ reservedName?: (string[]|null); + + /** EnumDescriptorProto visibility */ + visibility?: (google.protobuf.SymbolVisibility|keyof typeof google.protobuf.SymbolVisibility|null); } /** Represents an EnumDescriptorProto. */ @@ -14332,6 +14581,9 @@ export namespace google { /** EnumDescriptorProto reservedName. */ public reservedName: string[]; + /** EnumDescriptorProto visibility. */ + public visibility: (google.protobuf.SymbolVisibility|keyof typeof google.protobuf.SymbolVisibility); + /** * Creates a new EnumDescriptorProto instance using the specified properties. * @param [properties] Properties to set @@ -15266,6 +15518,9 @@ export namespace google { /** FieldOptions features */ features?: (google.protobuf.IFeatureSet|null); + /** FieldOptions featureSupport */ + featureSupport?: (google.protobuf.FieldOptions.IFeatureSupport|null); + /** FieldOptions uninterpretedOption */ uninterpretedOption?: (google.protobuf.IUninterpretedOption[]|null); @@ -15321,6 +15576,9 @@ export namespace google { /** FieldOptions features. */ public features?: (google.protobuf.IFeatureSet|null); + /** FieldOptions featureSupport. */ + public featureSupport?: (google.protobuf.FieldOptions.IFeatureSupport|null); + /** FieldOptions uninterpretedOption. */ public uninterpretedOption: google.protobuf.IUninterpretedOption[]; @@ -15541,6 +15799,121 @@ export namespace google { */ public static getTypeUrl(typeUrlPrefix?: string): string; } + + /** Properties of a FeatureSupport. */ + interface IFeatureSupport { + + /** FeatureSupport editionIntroduced */ + editionIntroduced?: (google.protobuf.Edition|keyof typeof google.protobuf.Edition|null); + + /** FeatureSupport editionDeprecated */ + editionDeprecated?: (google.protobuf.Edition|keyof typeof google.protobuf.Edition|null); + + /** FeatureSupport deprecationWarning */ + deprecationWarning?: (string|null); + + /** FeatureSupport editionRemoved */ + editionRemoved?: (google.protobuf.Edition|keyof typeof google.protobuf.Edition|null); + } + + /** Represents a FeatureSupport. */ + class FeatureSupport implements IFeatureSupport { + + /** + * Constructs a new FeatureSupport. + * @param [properties] Properties to set + */ + constructor(properties?: google.protobuf.FieldOptions.IFeatureSupport); + + /** FeatureSupport editionIntroduced. */ + public editionIntroduced: (google.protobuf.Edition|keyof typeof google.protobuf.Edition); + + /** FeatureSupport editionDeprecated. */ + public editionDeprecated: (google.protobuf.Edition|keyof typeof google.protobuf.Edition); + + /** FeatureSupport deprecationWarning. */ + public deprecationWarning: string; + + /** FeatureSupport editionRemoved. */ + public editionRemoved: (google.protobuf.Edition|keyof typeof google.protobuf.Edition); + + /** + * Creates a new FeatureSupport instance using the specified properties. + * @param [properties] Properties to set + * @returns FeatureSupport instance + */ + public static create(properties?: google.protobuf.FieldOptions.IFeatureSupport): google.protobuf.FieldOptions.FeatureSupport; + + /** + * Encodes the specified FeatureSupport message. Does not implicitly {@link google.protobuf.FieldOptions.FeatureSupport.verify|verify} messages. + * @param message FeatureSupport message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: google.protobuf.FieldOptions.IFeatureSupport, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified FeatureSupport message, length delimited. Does not implicitly {@link google.protobuf.FieldOptions.FeatureSupport.verify|verify} messages. + * @param message FeatureSupport message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: google.protobuf.FieldOptions.IFeatureSupport, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a FeatureSupport message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns FeatureSupport + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): google.protobuf.FieldOptions.FeatureSupport; + + /** + * Decodes a FeatureSupport message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns FeatureSupport + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): google.protobuf.FieldOptions.FeatureSupport; + + /** + * Verifies a FeatureSupport message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a FeatureSupport message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns FeatureSupport + */ + public static fromObject(object: { [k: string]: any }): google.protobuf.FieldOptions.FeatureSupport; + + /** + * Creates a plain object from a FeatureSupport message. Also converts values to other types if specified. + * @param message FeatureSupport + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: google.protobuf.FieldOptions.FeatureSupport, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this FeatureSupport to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for FeatureSupport + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } } /** Properties of an OneofOptions. */ @@ -15779,6 +16152,9 @@ export namespace google { /** EnumValueOptions debugRedact */ debugRedact?: (boolean|null); + /** EnumValueOptions featureSupport */ + featureSupport?: (google.protobuf.FieldOptions.IFeatureSupport|null); + /** EnumValueOptions uninterpretedOption */ uninterpretedOption?: (google.protobuf.IUninterpretedOption[]|null); } @@ -15801,6 +16177,9 @@ export namespace google { /** EnumValueOptions debugRedact. */ public debugRedact: boolean; + /** EnumValueOptions featureSupport. */ + public featureSupport?: (google.protobuf.FieldOptions.IFeatureSupport|null); + /** EnumValueOptions uninterpretedOption. */ public uninterpretedOption: google.protobuf.IUninterpretedOption[]; @@ -16390,6 +16769,12 @@ export namespace google { /** FeatureSet jsonFormat */ jsonFormat?: (google.protobuf.FeatureSet.JsonFormat|keyof typeof google.protobuf.FeatureSet.JsonFormat|null); + + /** FeatureSet enforceNamingStyle */ + enforceNamingStyle?: (google.protobuf.FeatureSet.EnforceNamingStyle|keyof typeof google.protobuf.FeatureSet.EnforceNamingStyle|null); + + /** FeatureSet defaultSymbolVisibility */ + defaultSymbolVisibility?: (google.protobuf.FeatureSet.VisibilityFeature.DefaultSymbolVisibility|keyof typeof google.protobuf.FeatureSet.VisibilityFeature.DefaultSymbolVisibility|null); } /** Represents a FeatureSet. */ @@ -16419,6 +16804,12 @@ export namespace google { /** FeatureSet jsonFormat. */ public jsonFormat: (google.protobuf.FeatureSet.JsonFormat|keyof typeof google.protobuf.FeatureSet.JsonFormat); + /** FeatureSet enforceNamingStyle. */ + public enforceNamingStyle: (google.protobuf.FeatureSet.EnforceNamingStyle|keyof typeof google.protobuf.FeatureSet.EnforceNamingStyle); + + /** FeatureSet defaultSymbolVisibility. */ + public defaultSymbolVisibility: (google.protobuf.FeatureSet.VisibilityFeature.DefaultSymbolVisibility|keyof typeof google.protobuf.FeatureSet.VisibilityFeature.DefaultSymbolVisibility); + /** * Creates a new FeatureSet instance using the specified properties. * @param [properties] Properties to set @@ -16541,6 +16932,116 @@ export namespace google { ALLOW = 1, LEGACY_BEST_EFFORT = 2 } + + /** EnforceNamingStyle enum. */ + enum EnforceNamingStyle { + ENFORCE_NAMING_STYLE_UNKNOWN = 0, + STYLE2024 = 1, + STYLE_LEGACY = 2 + } + + /** Properties of a VisibilityFeature. */ + interface IVisibilityFeature { + } + + /** Represents a VisibilityFeature. */ + class VisibilityFeature implements IVisibilityFeature { + + /** + * Constructs a new VisibilityFeature. + * @param [properties] Properties to set + */ + constructor(properties?: google.protobuf.FeatureSet.IVisibilityFeature); + + /** + * Creates a new VisibilityFeature instance using the specified properties. + * @param [properties] Properties to set + * @returns VisibilityFeature instance + */ + public static create(properties?: google.protobuf.FeatureSet.IVisibilityFeature): google.protobuf.FeatureSet.VisibilityFeature; + + /** + * Encodes the specified VisibilityFeature message. Does not implicitly {@link google.protobuf.FeatureSet.VisibilityFeature.verify|verify} messages. + * @param message VisibilityFeature message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encode(message: google.protobuf.FeatureSet.IVisibilityFeature, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Encodes the specified VisibilityFeature message, length delimited. Does not implicitly {@link google.protobuf.FeatureSet.VisibilityFeature.verify|verify} messages. + * @param message VisibilityFeature message or plain object to encode + * @param [writer] Writer to encode to + * @returns Writer + */ + public static encodeDelimited(message: google.protobuf.FeatureSet.IVisibilityFeature, writer?: $protobuf.Writer): $protobuf.Writer; + + /** + * Decodes a VisibilityFeature message from the specified reader or buffer. + * @param reader Reader or buffer to decode from + * @param [length] Message length if known beforehand + * @returns VisibilityFeature + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decode(reader: ($protobuf.Reader|Uint8Array), length?: number): google.protobuf.FeatureSet.VisibilityFeature; + + /** + * Decodes a VisibilityFeature message from the specified reader or buffer, length delimited. + * @param reader Reader or buffer to decode from + * @returns VisibilityFeature + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + public static decodeDelimited(reader: ($protobuf.Reader|Uint8Array)): google.protobuf.FeatureSet.VisibilityFeature; + + /** + * Verifies a VisibilityFeature message. + * @param message Plain object to verify + * @returns `null` if valid, otherwise the reason why it is not + */ + public static verify(message: { [k: string]: any }): (string|null); + + /** + * Creates a VisibilityFeature message from a plain object. Also converts values to their respective internal types. + * @param object Plain object + * @returns VisibilityFeature + */ + public static fromObject(object: { [k: string]: any }): google.protobuf.FeatureSet.VisibilityFeature; + + /** + * Creates a plain object from a VisibilityFeature message. Also converts values to other types if specified. + * @param message VisibilityFeature + * @param [options] Conversion options + * @returns Plain object + */ + public static toObject(message: google.protobuf.FeatureSet.VisibilityFeature, options?: $protobuf.IConversionOptions): { [k: string]: any }; + + /** + * Converts this VisibilityFeature to JSON. + * @returns JSON object + */ + public toJSON(): { [k: string]: any }; + + /** + * Gets the default type url for VisibilityFeature + * @param [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns The default type url + */ + public static getTypeUrl(typeUrlPrefix?: string): string; + } + + namespace VisibilityFeature { + + /** DefaultSymbolVisibility enum. */ + enum DefaultSymbolVisibility { + DEFAULT_SYMBOL_VISIBILITY_UNKNOWN = 0, + EXPORT_ALL = 1, + EXPORT_TOP_LEVEL = 2, + LOCAL_ALL = 3, + STRICT = 4 + } + } } /** Properties of a FeatureSetDefaults. */ @@ -16660,8 +17161,11 @@ export namespace google { /** FeatureSetEditionDefault edition */ edition?: (google.protobuf.Edition|keyof typeof google.protobuf.Edition|null); - /** FeatureSetEditionDefault features */ - features?: (google.protobuf.IFeatureSet|null); + /** FeatureSetEditionDefault overridableFeatures */ + overridableFeatures?: (google.protobuf.IFeatureSet|null); + + /** FeatureSetEditionDefault fixedFeatures */ + fixedFeatures?: (google.protobuf.IFeatureSet|null); } /** Represents a FeatureSetEditionDefault. */ @@ -16676,8 +17180,11 @@ export namespace google { /** FeatureSetEditionDefault edition. */ public edition: (google.protobuf.Edition|keyof typeof google.protobuf.Edition); - /** FeatureSetEditionDefault features. */ - public features?: (google.protobuf.IFeatureSet|null); + /** FeatureSetEditionDefault overridableFeatures. */ + public overridableFeatures?: (google.protobuf.IFeatureSet|null); + + /** FeatureSetEditionDefault fixedFeatures. */ + public fixedFeatures?: (google.protobuf.IFeatureSet|null); /** * Creates a new FeatureSetEditionDefault instance using the specified properties. @@ -17210,6 +17717,13 @@ export namespace google { } } + /** SymbolVisibility enum. */ + enum SymbolVisibility { + VISIBILITY_UNSET = 0, + VISIBILITY_LOCAL = 1, + VISIBILITY_EXPORT = 2 + } + /** Properties of a Duration. */ interface IDuration { diff --git a/protos/protos.js b/protos/protos.js index beb45c490..0e183b097 100644 --- a/protos/protos.js +++ b/protos/protos.js @@ -27654,6 +27654,7 @@ * @interface ICommonLanguageSettings * @property {string|null} [referenceDocsUri] CommonLanguageSettings referenceDocsUri * @property {Array.|null} [destinations] CommonLanguageSettings destinations + * @property {google.api.ISelectiveGapicGeneration|null} [selectiveGapicGeneration] CommonLanguageSettings selectiveGapicGeneration */ /** @@ -27688,6 +27689,14 @@ */ CommonLanguageSettings.prototype.destinations = $util.emptyArray; + /** + * CommonLanguageSettings selectiveGapicGeneration. + * @member {google.api.ISelectiveGapicGeneration|null|undefined} selectiveGapicGeneration + * @memberof google.api.CommonLanguageSettings + * @instance + */ + CommonLanguageSettings.prototype.selectiveGapicGeneration = null; + /** * Creates a new CommonLanguageSettings instance using the specified properties. * @function create @@ -27720,6 +27729,8 @@ writer.int32(message.destinations[i]); writer.ldelim(); } + if (message.selectiveGapicGeneration != null && Object.hasOwnProperty.call(message, "selectiveGapicGeneration")) + $root.google.api.SelectiveGapicGeneration.encode(message.selectiveGapicGeneration, writer.uint32(/* id 3, wireType 2 =*/26).fork()).ldelim(); return writer; }; @@ -27771,6 +27782,10 @@ message.destinations.push(reader.int32()); break; } + case 3: { + message.selectiveGapicGeneration = $root.google.api.SelectiveGapicGeneration.decode(reader, reader.uint32()); + break; + } default: reader.skipType(tag & 7); break; @@ -27822,6 +27837,11 @@ break; } } + if (message.selectiveGapicGeneration != null && message.hasOwnProperty("selectiveGapicGeneration")) { + var error = $root.google.api.SelectiveGapicGeneration.verify(message.selectiveGapicGeneration); + if (error) + return "selectiveGapicGeneration." + error; + } return null; }; @@ -27864,6 +27884,11 @@ break; } } + if (object.selectiveGapicGeneration != null) { + if (typeof object.selectiveGapicGeneration !== "object") + throw TypeError(".google.api.CommonLanguageSettings.selectiveGapicGeneration: object expected"); + message.selectiveGapicGeneration = $root.google.api.SelectiveGapicGeneration.fromObject(object.selectiveGapicGeneration); + } return message; }; @@ -27882,8 +27907,10 @@ var object = {}; if (options.arrays || options.defaults) object.destinations = []; - if (options.defaults) + if (options.defaults) { object.referenceDocsUri = ""; + object.selectiveGapicGeneration = null; + } if (message.referenceDocsUri != null && message.hasOwnProperty("referenceDocsUri")) object.referenceDocsUri = message.referenceDocsUri; if (message.destinations && message.destinations.length) { @@ -27891,6 +27918,8 @@ for (var j = 0; j < message.destinations.length; ++j) object.destinations[j] = options.enums === String ? $root.google.api.ClientLibraryDestination[message.destinations[j]] === undefined ? message.destinations[j] : $root.google.api.ClientLibraryDestination[message.destinations[j]] : message.destinations[j]; } + if (message.selectiveGapicGeneration != null && message.hasOwnProperty("selectiveGapicGeneration")) + object.selectiveGapicGeneration = $root.google.api.SelectiveGapicGeneration.toObject(message.selectiveGapicGeneration, options); return object; }; @@ -29713,6 +29742,7 @@ * @memberof google.api * @interface IPythonSettings * @property {google.api.ICommonLanguageSettings|null} [common] PythonSettings common + * @property {google.api.PythonSettings.IExperimentalFeatures|null} [experimentalFeatures] PythonSettings experimentalFeatures */ /** @@ -29738,6 +29768,14 @@ */ PythonSettings.prototype.common = null; + /** + * PythonSettings experimentalFeatures. + * @member {google.api.PythonSettings.IExperimentalFeatures|null|undefined} experimentalFeatures + * @memberof google.api.PythonSettings + * @instance + */ + PythonSettings.prototype.experimentalFeatures = null; + /** * Creates a new PythonSettings instance using the specified properties. * @function create @@ -29764,6 +29802,8 @@ writer = $Writer.create(); if (message.common != null && Object.hasOwnProperty.call(message, "common")) $root.google.api.CommonLanguageSettings.encode(message.common, writer.uint32(/* id 1, wireType 2 =*/10).fork()).ldelim(); + if (message.experimentalFeatures != null && Object.hasOwnProperty.call(message, "experimentalFeatures")) + $root.google.api.PythonSettings.ExperimentalFeatures.encode(message.experimentalFeatures, writer.uint32(/* id 2, wireType 2 =*/18).fork()).ldelim(); return writer; }; @@ -29804,6 +29844,10 @@ message.common = $root.google.api.CommonLanguageSettings.decode(reader, reader.uint32()); break; } + case 2: { + message.experimentalFeatures = $root.google.api.PythonSettings.ExperimentalFeatures.decode(reader, reader.uint32()); + break; + } default: reader.skipType(tag & 7); break; @@ -29844,6 +29888,11 @@ if (error) return "common." + error; } + if (message.experimentalFeatures != null && message.hasOwnProperty("experimentalFeatures")) { + var error = $root.google.api.PythonSettings.ExperimentalFeatures.verify(message.experimentalFeatures); + if (error) + return "experimentalFeatures." + error; + } return null; }; @@ -29864,6 +29913,11 @@ throw TypeError(".google.api.PythonSettings.common: object expected"); message.common = $root.google.api.CommonLanguageSettings.fromObject(object.common); } + if (object.experimentalFeatures != null) { + if (typeof object.experimentalFeatures !== "object") + throw TypeError(".google.api.PythonSettings.experimentalFeatures: object expected"); + message.experimentalFeatures = $root.google.api.PythonSettings.ExperimentalFeatures.fromObject(object.experimentalFeatures); + } return message; }; @@ -29880,10 +29934,14 @@ if (!options) options = {}; var object = {}; - if (options.defaults) + if (options.defaults) { object.common = null; + object.experimentalFeatures = null; + } if (message.common != null && message.hasOwnProperty("common")) object.common = $root.google.api.CommonLanguageSettings.toObject(message.common, options); + if (message.experimentalFeatures != null && message.hasOwnProperty("experimentalFeatures")) + object.experimentalFeatures = $root.google.api.PythonSettings.ExperimentalFeatures.toObject(message.experimentalFeatures, options); return object; }; @@ -29913,6 +29971,258 @@ return typeUrlPrefix + "/google.api.PythonSettings"; }; + PythonSettings.ExperimentalFeatures = (function() { + + /** + * Properties of an ExperimentalFeatures. + * @memberof google.api.PythonSettings + * @interface IExperimentalFeatures + * @property {boolean|null} [restAsyncIoEnabled] ExperimentalFeatures restAsyncIoEnabled + * @property {boolean|null} [protobufPythonicTypesEnabled] ExperimentalFeatures protobufPythonicTypesEnabled + * @property {boolean|null} [unversionedPackageDisabled] ExperimentalFeatures unversionedPackageDisabled + */ + + /** + * Constructs a new ExperimentalFeatures. + * @memberof google.api.PythonSettings + * @classdesc Represents an ExperimentalFeatures. + * @implements IExperimentalFeatures + * @constructor + * @param {google.api.PythonSettings.IExperimentalFeatures=} [properties] Properties to set + */ + function ExperimentalFeatures(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * ExperimentalFeatures restAsyncIoEnabled. + * @member {boolean} restAsyncIoEnabled + * @memberof google.api.PythonSettings.ExperimentalFeatures + * @instance + */ + ExperimentalFeatures.prototype.restAsyncIoEnabled = false; + + /** + * ExperimentalFeatures protobufPythonicTypesEnabled. + * @member {boolean} protobufPythonicTypesEnabled + * @memberof google.api.PythonSettings.ExperimentalFeatures + * @instance + */ + ExperimentalFeatures.prototype.protobufPythonicTypesEnabled = false; + + /** + * ExperimentalFeatures unversionedPackageDisabled. + * @member {boolean} unversionedPackageDisabled + * @memberof google.api.PythonSettings.ExperimentalFeatures + * @instance + */ + ExperimentalFeatures.prototype.unversionedPackageDisabled = false; + + /** + * Creates a new ExperimentalFeatures instance using the specified properties. + * @function create + * @memberof google.api.PythonSettings.ExperimentalFeatures + * @static + * @param {google.api.PythonSettings.IExperimentalFeatures=} [properties] Properties to set + * @returns {google.api.PythonSettings.ExperimentalFeatures} ExperimentalFeatures instance + */ + ExperimentalFeatures.create = function create(properties) { + return new ExperimentalFeatures(properties); + }; + + /** + * Encodes the specified ExperimentalFeatures message. Does not implicitly {@link google.api.PythonSettings.ExperimentalFeatures.verify|verify} messages. + * @function encode + * @memberof google.api.PythonSettings.ExperimentalFeatures + * @static + * @param {google.api.PythonSettings.IExperimentalFeatures} message ExperimentalFeatures message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + ExperimentalFeatures.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.restAsyncIoEnabled != null && Object.hasOwnProperty.call(message, "restAsyncIoEnabled")) + writer.uint32(/* id 1, wireType 0 =*/8).bool(message.restAsyncIoEnabled); + if (message.protobufPythonicTypesEnabled != null && Object.hasOwnProperty.call(message, "protobufPythonicTypesEnabled")) + writer.uint32(/* id 2, wireType 0 =*/16).bool(message.protobufPythonicTypesEnabled); + if (message.unversionedPackageDisabled != null && Object.hasOwnProperty.call(message, "unversionedPackageDisabled")) + writer.uint32(/* id 3, wireType 0 =*/24).bool(message.unversionedPackageDisabled); + return writer; + }; + + /** + * Encodes the specified ExperimentalFeatures message, length delimited. Does not implicitly {@link google.api.PythonSettings.ExperimentalFeatures.verify|verify} messages. + * @function encodeDelimited + * @memberof google.api.PythonSettings.ExperimentalFeatures + * @static + * @param {google.api.PythonSettings.IExperimentalFeatures} message ExperimentalFeatures message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + ExperimentalFeatures.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes an ExperimentalFeatures message from the specified reader or buffer. + * @function decode + * @memberof google.api.PythonSettings.ExperimentalFeatures + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {google.api.PythonSettings.ExperimentalFeatures} ExperimentalFeatures + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + ExperimentalFeatures.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.google.api.PythonSettings.ExperimentalFeatures(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + message.restAsyncIoEnabled = reader.bool(); + break; + } + case 2: { + message.protobufPythonicTypesEnabled = reader.bool(); + break; + } + case 3: { + message.unversionedPackageDisabled = reader.bool(); + break; + } + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes an ExperimentalFeatures message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof google.api.PythonSettings.ExperimentalFeatures + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {google.api.PythonSettings.ExperimentalFeatures} ExperimentalFeatures + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + ExperimentalFeatures.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies an ExperimentalFeatures message. + * @function verify + * @memberof google.api.PythonSettings.ExperimentalFeatures + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + ExperimentalFeatures.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.restAsyncIoEnabled != null && message.hasOwnProperty("restAsyncIoEnabled")) + if (typeof message.restAsyncIoEnabled !== "boolean") + return "restAsyncIoEnabled: boolean expected"; + if (message.protobufPythonicTypesEnabled != null && message.hasOwnProperty("protobufPythonicTypesEnabled")) + if (typeof message.protobufPythonicTypesEnabled !== "boolean") + return "protobufPythonicTypesEnabled: boolean expected"; + if (message.unversionedPackageDisabled != null && message.hasOwnProperty("unversionedPackageDisabled")) + if (typeof message.unversionedPackageDisabled !== "boolean") + return "unversionedPackageDisabled: boolean expected"; + return null; + }; + + /** + * Creates an ExperimentalFeatures message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof google.api.PythonSettings.ExperimentalFeatures + * @static + * @param {Object.} object Plain object + * @returns {google.api.PythonSettings.ExperimentalFeatures} ExperimentalFeatures + */ + ExperimentalFeatures.fromObject = function fromObject(object) { + if (object instanceof $root.google.api.PythonSettings.ExperimentalFeatures) + return object; + var message = new $root.google.api.PythonSettings.ExperimentalFeatures(); + if (object.restAsyncIoEnabled != null) + message.restAsyncIoEnabled = Boolean(object.restAsyncIoEnabled); + if (object.protobufPythonicTypesEnabled != null) + message.protobufPythonicTypesEnabled = Boolean(object.protobufPythonicTypesEnabled); + if (object.unversionedPackageDisabled != null) + message.unversionedPackageDisabled = Boolean(object.unversionedPackageDisabled); + return message; + }; + + /** + * Creates a plain object from an ExperimentalFeatures message. Also converts values to other types if specified. + * @function toObject + * @memberof google.api.PythonSettings.ExperimentalFeatures + * @static + * @param {google.api.PythonSettings.ExperimentalFeatures} message ExperimentalFeatures + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + ExperimentalFeatures.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) { + object.restAsyncIoEnabled = false; + object.protobufPythonicTypesEnabled = false; + object.unversionedPackageDisabled = false; + } + if (message.restAsyncIoEnabled != null && message.hasOwnProperty("restAsyncIoEnabled")) + object.restAsyncIoEnabled = message.restAsyncIoEnabled; + if (message.protobufPythonicTypesEnabled != null && message.hasOwnProperty("protobufPythonicTypesEnabled")) + object.protobufPythonicTypesEnabled = message.protobufPythonicTypesEnabled; + if (message.unversionedPackageDisabled != null && message.hasOwnProperty("unversionedPackageDisabled")) + object.unversionedPackageDisabled = message.unversionedPackageDisabled; + return object; + }; + + /** + * Converts this ExperimentalFeatures to JSON. + * @function toJSON + * @memberof google.api.PythonSettings.ExperimentalFeatures + * @instance + * @returns {Object.} JSON object + */ + ExperimentalFeatures.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for ExperimentalFeatures + * @function getTypeUrl + * @memberof google.api.PythonSettings.ExperimentalFeatures + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + ExperimentalFeatures.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/google.api.PythonSettings.ExperimentalFeatures"; + }; + + return ExperimentalFeatures; + })(); + return PythonSettings; })(); @@ -30789,6 +31099,7 @@ * @memberof google.api * @interface IGoSettings * @property {google.api.ICommonLanguageSettings|null} [common] GoSettings common + * @property {Object.|null} [renamedServices] GoSettings renamedServices */ /** @@ -30800,6 +31111,7 @@ * @param {google.api.IGoSettings=} [properties] Properties to set */ function GoSettings(properties) { + this.renamedServices = {}; if (properties) for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) if (properties[keys[i]] != null) @@ -30814,6 +31126,14 @@ */ GoSettings.prototype.common = null; + /** + * GoSettings renamedServices. + * @member {Object.} renamedServices + * @memberof google.api.GoSettings + * @instance + */ + GoSettings.prototype.renamedServices = $util.emptyObject; + /** * Creates a new GoSettings instance using the specified properties. * @function create @@ -30840,6 +31160,9 @@ writer = $Writer.create(); if (message.common != null && Object.hasOwnProperty.call(message, "common")) $root.google.api.CommonLanguageSettings.encode(message.common, writer.uint32(/* id 1, wireType 2 =*/10).fork()).ldelim(); + if (message.renamedServices != null && Object.hasOwnProperty.call(message, "renamedServices")) + for (var keys = Object.keys(message.renamedServices), i = 0; i < keys.length; ++i) + writer.uint32(/* id 2, wireType 2 =*/18).fork().uint32(/* id 1, wireType 2 =*/10).string(keys[i]).uint32(/* id 2, wireType 2 =*/18).string(message.renamedServices[keys[i]]).ldelim(); return writer; }; @@ -30870,7 +31193,7 @@ GoSettings.decode = function decode(reader, length, error) { if (!(reader instanceof $Reader)) reader = $Reader.create(reader); - var end = length === undefined ? reader.len : reader.pos + length, message = new $root.google.api.GoSettings(); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.google.api.GoSettings(), key, value; while (reader.pos < end) { var tag = reader.uint32(); if (tag === error) @@ -30880,6 +31203,29 @@ message.common = $root.google.api.CommonLanguageSettings.decode(reader, reader.uint32()); break; } + case 2: { + if (message.renamedServices === $util.emptyObject) + message.renamedServices = {}; + var end2 = reader.uint32() + reader.pos; + key = ""; + value = ""; + while (reader.pos < end2) { + var tag2 = reader.uint32(); + switch (tag2 >>> 3) { + case 1: + key = reader.string(); + break; + case 2: + value = reader.string(); + break; + default: + reader.skipType(tag2 & 7); + break; + } + } + message.renamedServices[key] = value; + break; + } default: reader.skipType(tag & 7); break; @@ -30920,6 +31266,14 @@ if (error) return "common." + error; } + if (message.renamedServices != null && message.hasOwnProperty("renamedServices")) { + if (!$util.isObject(message.renamedServices)) + return "renamedServices: object expected"; + var key = Object.keys(message.renamedServices); + for (var i = 0; i < key.length; ++i) + if (!$util.isString(message.renamedServices[key[i]])) + return "renamedServices: string{k:string} expected"; + } return null; }; @@ -30940,6 +31294,13 @@ throw TypeError(".google.api.GoSettings.common: object expected"); message.common = $root.google.api.CommonLanguageSettings.fromObject(object.common); } + if (object.renamedServices) { + if (typeof object.renamedServices !== "object") + throw TypeError(".google.api.GoSettings.renamedServices: object expected"); + message.renamedServices = {}; + for (var keys = Object.keys(object.renamedServices), i = 0; i < keys.length; ++i) + message.renamedServices[keys[i]] = String(object.renamedServices[keys[i]]); + } return message; }; @@ -30956,10 +31317,18 @@ if (!options) options = {}; var object = {}; + if (options.objects || options.defaults) + object.renamedServices = {}; if (options.defaults) object.common = null; if (message.common != null && message.hasOwnProperty("common")) object.common = $root.google.api.CommonLanguageSettings.toObject(message.common, options); + var keys2; + if (message.renamedServices && (keys2 = Object.keys(message.renamedServices)).length) { + object.renamedServices = {}; + for (var j = 0; j < keys2.length; ++j) + object.renamedServices[keys2[j]] = message.renamedServices[keys2[j]]; + } return object; }; @@ -31598,30 +31967,275 @@ return values; })(); - /** - * LaunchStage enum. - * @name google.api.LaunchStage - * @enum {number} - * @property {number} LAUNCH_STAGE_UNSPECIFIED=0 LAUNCH_STAGE_UNSPECIFIED value - * @property {number} UNIMPLEMENTED=6 UNIMPLEMENTED value - * @property {number} PRELAUNCH=7 PRELAUNCH value - * @property {number} EARLY_ACCESS=1 EARLY_ACCESS value - * @property {number} ALPHA=2 ALPHA value - * @property {number} BETA=3 BETA value - * @property {number} GA=4 GA value - * @property {number} DEPRECATED=5 DEPRECATED value - */ - api.LaunchStage = (function() { - var valuesById = {}, values = Object.create(valuesById); - values[valuesById[0] = "LAUNCH_STAGE_UNSPECIFIED"] = 0; - values[valuesById[6] = "UNIMPLEMENTED"] = 6; - values[valuesById[7] = "PRELAUNCH"] = 7; - values[valuesById[1] = "EARLY_ACCESS"] = 1; - values[valuesById[2] = "ALPHA"] = 2; - values[valuesById[3] = "BETA"] = 3; - values[valuesById[4] = "GA"] = 4; - values[valuesById[5] = "DEPRECATED"] = 5; - return values; + api.SelectiveGapicGeneration = (function() { + + /** + * Properties of a SelectiveGapicGeneration. + * @memberof google.api + * @interface ISelectiveGapicGeneration + * @property {Array.|null} [methods] SelectiveGapicGeneration methods + * @property {boolean|null} [generateOmittedAsInternal] SelectiveGapicGeneration generateOmittedAsInternal + */ + + /** + * Constructs a new SelectiveGapicGeneration. + * @memberof google.api + * @classdesc Represents a SelectiveGapicGeneration. + * @implements ISelectiveGapicGeneration + * @constructor + * @param {google.api.ISelectiveGapicGeneration=} [properties] Properties to set + */ + function SelectiveGapicGeneration(properties) { + this.methods = []; + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * SelectiveGapicGeneration methods. + * @member {Array.} methods + * @memberof google.api.SelectiveGapicGeneration + * @instance + */ + SelectiveGapicGeneration.prototype.methods = $util.emptyArray; + + /** + * SelectiveGapicGeneration generateOmittedAsInternal. + * @member {boolean} generateOmittedAsInternal + * @memberof google.api.SelectiveGapicGeneration + * @instance + */ + SelectiveGapicGeneration.prototype.generateOmittedAsInternal = false; + + /** + * Creates a new SelectiveGapicGeneration instance using the specified properties. + * @function create + * @memberof google.api.SelectiveGapicGeneration + * @static + * @param {google.api.ISelectiveGapicGeneration=} [properties] Properties to set + * @returns {google.api.SelectiveGapicGeneration} SelectiveGapicGeneration instance + */ + SelectiveGapicGeneration.create = function create(properties) { + return new SelectiveGapicGeneration(properties); + }; + + /** + * Encodes the specified SelectiveGapicGeneration message. Does not implicitly {@link google.api.SelectiveGapicGeneration.verify|verify} messages. + * @function encode + * @memberof google.api.SelectiveGapicGeneration + * @static + * @param {google.api.ISelectiveGapicGeneration} message SelectiveGapicGeneration message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + SelectiveGapicGeneration.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.methods != null && message.methods.length) + for (var i = 0; i < message.methods.length; ++i) + writer.uint32(/* id 1, wireType 2 =*/10).string(message.methods[i]); + if (message.generateOmittedAsInternal != null && Object.hasOwnProperty.call(message, "generateOmittedAsInternal")) + writer.uint32(/* id 2, wireType 0 =*/16).bool(message.generateOmittedAsInternal); + return writer; + }; + + /** + * Encodes the specified SelectiveGapicGeneration message, length delimited. Does not implicitly {@link google.api.SelectiveGapicGeneration.verify|verify} messages. + * @function encodeDelimited + * @memberof google.api.SelectiveGapicGeneration + * @static + * @param {google.api.ISelectiveGapicGeneration} message SelectiveGapicGeneration message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + SelectiveGapicGeneration.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a SelectiveGapicGeneration message from the specified reader or buffer. + * @function decode + * @memberof google.api.SelectiveGapicGeneration + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {google.api.SelectiveGapicGeneration} SelectiveGapicGeneration + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + SelectiveGapicGeneration.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.google.api.SelectiveGapicGeneration(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + if (!(message.methods && message.methods.length)) + message.methods = []; + message.methods.push(reader.string()); + break; + } + case 2: { + message.generateOmittedAsInternal = reader.bool(); + break; + } + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a SelectiveGapicGeneration message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof google.api.SelectiveGapicGeneration + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {google.api.SelectiveGapicGeneration} SelectiveGapicGeneration + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + SelectiveGapicGeneration.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a SelectiveGapicGeneration message. + * @function verify + * @memberof google.api.SelectiveGapicGeneration + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + SelectiveGapicGeneration.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.methods != null && message.hasOwnProperty("methods")) { + if (!Array.isArray(message.methods)) + return "methods: array expected"; + for (var i = 0; i < message.methods.length; ++i) + if (!$util.isString(message.methods[i])) + return "methods: string[] expected"; + } + if (message.generateOmittedAsInternal != null && message.hasOwnProperty("generateOmittedAsInternal")) + if (typeof message.generateOmittedAsInternal !== "boolean") + return "generateOmittedAsInternal: boolean expected"; + return null; + }; + + /** + * Creates a SelectiveGapicGeneration message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof google.api.SelectiveGapicGeneration + * @static + * @param {Object.} object Plain object + * @returns {google.api.SelectiveGapicGeneration} SelectiveGapicGeneration + */ + SelectiveGapicGeneration.fromObject = function fromObject(object) { + if (object instanceof $root.google.api.SelectiveGapicGeneration) + return object; + var message = new $root.google.api.SelectiveGapicGeneration(); + if (object.methods) { + if (!Array.isArray(object.methods)) + throw TypeError(".google.api.SelectiveGapicGeneration.methods: array expected"); + message.methods = []; + for (var i = 0; i < object.methods.length; ++i) + message.methods[i] = String(object.methods[i]); + } + if (object.generateOmittedAsInternal != null) + message.generateOmittedAsInternal = Boolean(object.generateOmittedAsInternal); + return message; + }; + + /** + * Creates a plain object from a SelectiveGapicGeneration message. Also converts values to other types if specified. + * @function toObject + * @memberof google.api.SelectiveGapicGeneration + * @static + * @param {google.api.SelectiveGapicGeneration} message SelectiveGapicGeneration + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + SelectiveGapicGeneration.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.arrays || options.defaults) + object.methods = []; + if (options.defaults) + object.generateOmittedAsInternal = false; + if (message.methods && message.methods.length) { + object.methods = []; + for (var j = 0; j < message.methods.length; ++j) + object.methods[j] = message.methods[j]; + } + if (message.generateOmittedAsInternal != null && message.hasOwnProperty("generateOmittedAsInternal")) + object.generateOmittedAsInternal = message.generateOmittedAsInternal; + return object; + }; + + /** + * Converts this SelectiveGapicGeneration to JSON. + * @function toJSON + * @memberof google.api.SelectiveGapicGeneration + * @instance + * @returns {Object.} JSON object + */ + SelectiveGapicGeneration.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for SelectiveGapicGeneration + * @function getTypeUrl + * @memberof google.api.SelectiveGapicGeneration + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + SelectiveGapicGeneration.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/google.api.SelectiveGapicGeneration"; + }; + + return SelectiveGapicGeneration; + })(); + + /** + * LaunchStage enum. + * @name google.api.LaunchStage + * @enum {number} + * @property {number} LAUNCH_STAGE_UNSPECIFIED=0 LAUNCH_STAGE_UNSPECIFIED value + * @property {number} UNIMPLEMENTED=6 UNIMPLEMENTED value + * @property {number} PRELAUNCH=7 PRELAUNCH value + * @property {number} EARLY_ACCESS=1 EARLY_ACCESS value + * @property {number} ALPHA=2 ALPHA value + * @property {number} BETA=3 BETA value + * @property {number} GA=4 GA value + * @property {number} DEPRECATED=5 DEPRECATED value + */ + api.LaunchStage = (function() { + var valuesById = {}, values = Object.create(valuesById); + values[valuesById[0] = "LAUNCH_STAGE_UNSPECIFIED"] = 0; + values[valuesById[6] = "UNIMPLEMENTED"] = 6; + values[valuesById[7] = "PRELAUNCH"] = 7; + values[valuesById[1] = "EARLY_ACCESS"] = 1; + values[valuesById[2] = "ALPHA"] = 2; + values[valuesById[3] = "BETA"] = 3; + values[valuesById[4] = "GA"] = 4; + values[valuesById[5] = "DEPRECATED"] = 5; + return values; })(); /** @@ -32583,6 +33197,7 @@ * @name google.protobuf.Edition * @enum {number} * @property {number} EDITION_UNKNOWN=0 EDITION_UNKNOWN value + * @property {number} EDITION_LEGACY=900 EDITION_LEGACY value * @property {number} EDITION_PROTO2=998 EDITION_PROTO2 value * @property {number} EDITION_PROTO3=999 EDITION_PROTO3 value * @property {number} EDITION_2023=1000 EDITION_2023 value @@ -32597,6 +33212,7 @@ protobuf.Edition = (function() { var valuesById = {}, values = Object.create(valuesById); values[valuesById[0] = "EDITION_UNKNOWN"] = 0; + values[valuesById[900] = "EDITION_LEGACY"] = 900; values[valuesById[998] = "EDITION_PROTO2"] = 998; values[valuesById[999] = "EDITION_PROTO3"] = 999; values[valuesById[1000] = "EDITION_2023"] = 1000; @@ -32621,6 +33237,7 @@ * @property {Array.|null} [dependency] FileDescriptorProto dependency * @property {Array.|null} [publicDependency] FileDescriptorProto publicDependency * @property {Array.|null} [weakDependency] FileDescriptorProto weakDependency + * @property {Array.|null} [optionDependency] FileDescriptorProto optionDependency * @property {Array.|null} [messageType] FileDescriptorProto messageType * @property {Array.|null} [enumType] FileDescriptorProto enumType * @property {Array.|null} [service] FileDescriptorProto service @@ -32643,6 +33260,7 @@ this.dependency = []; this.publicDependency = []; this.weakDependency = []; + this.optionDependency = []; this.messageType = []; this.enumType = []; this.service = []; @@ -32693,6 +33311,14 @@ */ FileDescriptorProto.prototype.weakDependency = $util.emptyArray; + /** + * FileDescriptorProto optionDependency. + * @member {Array.} optionDependency + * @memberof google.protobuf.FileDescriptorProto + * @instance + */ + FileDescriptorProto.prototype.optionDependency = $util.emptyArray; + /** * FileDescriptorProto messageType. * @member {Array.} messageType @@ -32814,6 +33440,9 @@ writer.uint32(/* id 12, wireType 2 =*/98).string(message.syntax); if (message.edition != null && Object.hasOwnProperty.call(message, "edition")) writer.uint32(/* id 14, wireType 0 =*/112).int32(message.edition); + if (message.optionDependency != null && message.optionDependency.length) + for (var i = 0; i < message.optionDependency.length; ++i) + writer.uint32(/* id 15, wireType 2 =*/122).string(message.optionDependency[i]); return writer; }; @@ -32886,6 +33515,12 @@ message.weakDependency.push(reader.int32()); break; } + case 15: { + if (!(message.optionDependency && message.optionDependency.length)) + message.optionDependency = []; + message.optionDependency.push(reader.string()); + break; + } case 4: { if (!(message.messageType && message.messageType.length)) message.messageType = []; @@ -32988,6 +33623,13 @@ if (!$util.isInteger(message.weakDependency[i])) return "weakDependency: integer[] expected"; } + if (message.optionDependency != null && message.hasOwnProperty("optionDependency")) { + if (!Array.isArray(message.optionDependency)) + return "optionDependency: array expected"; + for (var i = 0; i < message.optionDependency.length; ++i) + if (!$util.isString(message.optionDependency[i])) + return "optionDependency: string[] expected"; + } if (message.messageType != null && message.hasOwnProperty("messageType")) { if (!Array.isArray(message.messageType)) return "messageType: array expected"; @@ -33042,6 +33684,7 @@ default: return "edition: enum value expected"; case 0: + case 900: case 998: case 999: case 1000: @@ -33094,6 +33737,13 @@ for (var i = 0; i < object.weakDependency.length; ++i) message.weakDependency[i] = object.weakDependency[i] | 0; } + if (object.optionDependency) { + if (!Array.isArray(object.optionDependency)) + throw TypeError(".google.protobuf.FileDescriptorProto.optionDependency: array expected"); + message.optionDependency = []; + for (var i = 0; i < object.optionDependency.length; ++i) + message.optionDependency[i] = String(object.optionDependency[i]); + } if (object.messageType) { if (!Array.isArray(object.messageType)) throw TypeError(".google.protobuf.FileDescriptorProto.messageType: array expected"); @@ -33157,6 +33807,10 @@ case 0: message.edition = 0; break; + case "EDITION_LEGACY": + case 900: + message.edition = 900; + break; case "EDITION_PROTO2": case 998: message.edition = 998; @@ -33222,6 +33876,7 @@ object.extension = []; object.publicDependency = []; object.weakDependency = []; + object.optionDependency = []; } if (options.defaults) { object.name = ""; @@ -33278,6 +33933,11 @@ object.syntax = message.syntax; if (message.edition != null && message.hasOwnProperty("edition")) object.edition = options.enums === String ? $root.google.protobuf.Edition[message.edition] === undefined ? message.edition : $root.google.protobuf.Edition[message.edition] : message.edition; + if (message.optionDependency && message.optionDependency.length) { + object.optionDependency = []; + for (var j = 0; j < message.optionDependency.length; ++j) + object.optionDependency[j] = message.optionDependency[j]; + } return object; }; @@ -33326,6 +33986,7 @@ * @property {google.protobuf.IMessageOptions|null} [options] DescriptorProto options * @property {Array.|null} [reservedRange] DescriptorProto reservedRange * @property {Array.|null} [reservedName] DescriptorProto reservedName + * @property {google.protobuf.SymbolVisibility|null} [visibility] DescriptorProto visibility */ /** @@ -33431,6 +34092,14 @@ */ DescriptorProto.prototype.reservedName = $util.emptyArray; + /** + * DescriptorProto visibility. + * @member {google.protobuf.SymbolVisibility} visibility + * @memberof google.protobuf.DescriptorProto + * @instance + */ + DescriptorProto.prototype.visibility = 0; + /** * Creates a new DescriptorProto instance using the specified properties. * @function create @@ -33483,6 +34152,8 @@ if (message.reservedName != null && message.reservedName.length) for (var i = 0; i < message.reservedName.length; ++i) writer.uint32(/* id 10, wireType 2 =*/82).string(message.reservedName[i]); + if (message.visibility != null && Object.hasOwnProperty.call(message, "visibility")) + writer.uint32(/* id 11, wireType 0 =*/88).int32(message.visibility); return writer; }; @@ -33575,6 +34246,10 @@ message.reservedName.push(reader.string()); break; } + case 11: { + message.visibility = reader.int32(); + break; + } default: reader.skipType(tag & 7); break; @@ -33688,6 +34363,15 @@ if (!$util.isString(message.reservedName[i])) return "reservedName: string[] expected"; } + if (message.visibility != null && message.hasOwnProperty("visibility")) + switch (message.visibility) { + default: + return "visibility: enum value expected"; + case 0: + case 1: + case 2: + break; + } return null; }; @@ -33787,6 +34471,26 @@ for (var i = 0; i < object.reservedName.length; ++i) message.reservedName[i] = String(object.reservedName[i]); } + switch (object.visibility) { + default: + if (typeof object.visibility === "number") { + message.visibility = object.visibility; + break; + } + break; + case "VISIBILITY_UNSET": + case 0: + message.visibility = 0; + break; + case "VISIBILITY_LOCAL": + case 1: + message.visibility = 1; + break; + case "VISIBILITY_EXPORT": + case 2: + message.visibility = 2; + break; + } return message; }; @@ -33816,6 +34520,7 @@ if (options.defaults) { object.name = ""; object.options = null; + object.visibility = options.enums === String ? "VISIBILITY_UNSET" : 0; } if (message.name != null && message.hasOwnProperty("name")) object.name = message.name; @@ -33861,6 +34566,8 @@ for (var j = 0; j < message.reservedName.length; ++j) object.reservedName[j] = message.reservedName[j]; } + if (message.visibility != null && message.hasOwnProperty("visibility")) + object.visibility = options.enums === String ? $root.google.protobuf.SymbolVisibility[message.visibility] === undefined ? message.visibility : $root.google.protobuf.SymbolVisibility[message.visibility] : message.visibility; return object; }; @@ -35905,6 +36612,7 @@ * @property {google.protobuf.IEnumOptions|null} [options] EnumDescriptorProto options * @property {Array.|null} [reservedRange] EnumDescriptorProto reservedRange * @property {Array.|null} [reservedName] EnumDescriptorProto reservedName + * @property {google.protobuf.SymbolVisibility|null} [visibility] EnumDescriptorProto visibility */ /** @@ -35965,6 +36673,14 @@ */ EnumDescriptorProto.prototype.reservedName = $util.emptyArray; + /** + * EnumDescriptorProto visibility. + * @member {google.protobuf.SymbolVisibility} visibility + * @memberof google.protobuf.EnumDescriptorProto + * @instance + */ + EnumDescriptorProto.prototype.visibility = 0; + /** * Creates a new EnumDescriptorProto instance using the specified properties. * @function create @@ -36002,6 +36718,8 @@ if (message.reservedName != null && message.reservedName.length) for (var i = 0; i < message.reservedName.length; ++i) writer.uint32(/* id 5, wireType 2 =*/42).string(message.reservedName[i]); + if (message.visibility != null && Object.hasOwnProperty.call(message, "visibility")) + writer.uint32(/* id 6, wireType 0 =*/48).int32(message.visibility); return writer; }; @@ -36064,6 +36782,10 @@ message.reservedName.push(reader.string()); break; } + case 6: { + message.visibility = reader.int32(); + break; + } default: reader.skipType(tag & 7); break; @@ -36132,6 +36854,15 @@ if (!$util.isString(message.reservedName[i])) return "reservedName: string[] expected"; } + if (message.visibility != null && message.hasOwnProperty("visibility")) + switch (message.visibility) { + default: + return "visibility: enum value expected"; + case 0: + case 1: + case 2: + break; + } return null; }; @@ -36181,6 +36912,26 @@ for (var i = 0; i < object.reservedName.length; ++i) message.reservedName[i] = String(object.reservedName[i]); } + switch (object.visibility) { + default: + if (typeof object.visibility === "number") { + message.visibility = object.visibility; + break; + } + break; + case "VISIBILITY_UNSET": + case 0: + message.visibility = 0; + break; + case "VISIBILITY_LOCAL": + case 1: + message.visibility = 1; + break; + case "VISIBILITY_EXPORT": + case 2: + message.visibility = 2; + break; + } return message; }; @@ -36205,6 +36956,7 @@ if (options.defaults) { object.name = ""; object.options = null; + object.visibility = options.enums === String ? "VISIBILITY_UNSET" : 0; } if (message.name != null && message.hasOwnProperty("name")) object.name = message.name; @@ -36225,6 +36977,8 @@ for (var j = 0; j < message.reservedName.length; ++j) object.reservedName[j] = message.reservedName[j]; } + if (message.visibility != null && message.hasOwnProperty("visibility")) + object.visibility = options.enums === String ? $root.google.protobuf.SymbolVisibility[message.visibility] === undefined ? message.visibility : $root.google.protobuf.SymbolVisibility[message.visibility] : message.visibility; return object; }; @@ -38543,6 +39297,7 @@ * @property {Array.|null} [targets] FieldOptions targets * @property {Array.|null} [editionDefaults] FieldOptions editionDefaults * @property {google.protobuf.IFeatureSet|null} [features] FieldOptions features + * @property {google.protobuf.FieldOptions.IFeatureSupport|null} [featureSupport] FieldOptions featureSupport * @property {Array.|null} [uninterpretedOption] FieldOptions uninterpretedOption * @property {Array.|null} [".google.api.fieldBehavior"] FieldOptions .google.api.fieldBehavior * @property {google.api.IResourceReference|null} [".google.api.resourceReference"] FieldOptions .google.api.resourceReference @@ -38663,6 +39418,14 @@ */ FieldOptions.prototype.features = null; + /** + * FieldOptions featureSupport. + * @member {google.protobuf.FieldOptions.IFeatureSupport|null|undefined} featureSupport + * @memberof google.protobuf.FieldOptions + * @instance + */ + FieldOptions.prototype.featureSupport = null; + /** * FieldOptions uninterpretedOption. * @member {Array.} uninterpretedOption @@ -38737,6 +39500,8 @@ $root.google.protobuf.FieldOptions.EditionDefault.encode(message.editionDefaults[i], writer.uint32(/* id 20, wireType 2 =*/162).fork()).ldelim(); if (message.features != null && Object.hasOwnProperty.call(message, "features")) $root.google.protobuf.FeatureSet.encode(message.features, writer.uint32(/* id 21, wireType 2 =*/170).fork()).ldelim(); + if (message.featureSupport != null && Object.hasOwnProperty.call(message, "featureSupport")) + $root.google.protobuf.FieldOptions.FeatureSupport.encode(message.featureSupport, writer.uint32(/* id 22, wireType 2 =*/178).fork()).ldelim(); if (message.uninterpretedOption != null && message.uninterpretedOption.length) for (var i = 0; i < message.uninterpretedOption.length; ++i) $root.google.protobuf.UninterpretedOption.encode(message.uninterpretedOption[i], writer.uint32(/* id 999, wireType 2 =*/7994).fork()).ldelim(); @@ -38838,6 +39603,10 @@ message.features = $root.google.protobuf.FeatureSet.decode(reader, reader.uint32()); break; } + case 22: { + message.featureSupport = $root.google.protobuf.FieldOptions.FeatureSupport.decode(reader, reader.uint32()); + break; + } case 999: { if (!(message.uninterpretedOption && message.uninterpretedOption.length)) message.uninterpretedOption = []; @@ -38973,6 +39742,11 @@ if (error) return "features." + error; } + if (message.featureSupport != null && message.hasOwnProperty("featureSupport")) { + var error = $root.google.protobuf.FieldOptions.FeatureSupport.verify(message.featureSupport); + if (error) + return "featureSupport." + error; + } if (message.uninterpretedOption != null && message.hasOwnProperty("uninterpretedOption")) { if (!Array.isArray(message.uninterpretedOption)) return "uninterpretedOption: array expected"; @@ -39161,6 +39935,11 @@ throw TypeError(".google.protobuf.FieldOptions.features: object expected"); message.features = $root.google.protobuf.FeatureSet.fromObject(object.features); } + if (object.featureSupport != null) { + if (typeof object.featureSupport !== "object") + throw TypeError(".google.protobuf.FieldOptions.featureSupport: object expected"); + message.featureSupport = $root.google.protobuf.FieldOptions.FeatureSupport.fromObject(object.featureSupport); + } if (object.uninterpretedOption) { if (!Array.isArray(object.uninterpretedOption)) throw TypeError(".google.protobuf.FieldOptions.uninterpretedOption: array expected"); @@ -39258,6 +40037,7 @@ object.debugRedact = false; object.retention = options.enums === String ? "RETENTION_UNKNOWN" : 0; object.features = null; + object.featureSupport = null; object[".google.api.resourceReference"] = null; } if (message.ctype != null && message.hasOwnProperty("ctype")) @@ -39290,6 +40070,8 @@ } if (message.features != null && message.hasOwnProperty("features")) object.features = $root.google.protobuf.FeatureSet.toObject(message.features, options); + if (message.featureSupport != null && message.hasOwnProperty("featureSupport")) + object.featureSupport = $root.google.protobuf.FieldOptions.FeatureSupport.toObject(message.featureSupport, options); if (message.uninterpretedOption && message.uninterpretedOption.length) { object.uninterpretedOption = []; for (var j = 0; j < message.uninterpretedOption.length; ++j) @@ -39562,6 +40344,7 @@ default: return "edition: enum value expected"; case 0: + case 900: case 998: case 999: case 1000: @@ -39603,6 +40386,10 @@ case 0: message.edition = 0; break; + case "EDITION_LEGACY": + case 900: + message.edition = 900; + break; case "EDITION_PROTO2": case 998: message.edition = 998; @@ -39613,93 +40400,575 @@ break; case "EDITION_2023": case 1000: - message.edition = 1000; + message.edition = 1000; + break; + case "EDITION_2024": + case 1001: + message.edition = 1001; + break; + case "EDITION_1_TEST_ONLY": + case 1: + message.edition = 1; + break; + case "EDITION_2_TEST_ONLY": + case 2: + message.edition = 2; + break; + case "EDITION_99997_TEST_ONLY": + case 99997: + message.edition = 99997; + break; + case "EDITION_99998_TEST_ONLY": + case 99998: + message.edition = 99998; + break; + case "EDITION_99999_TEST_ONLY": + case 99999: + message.edition = 99999; + break; + case "EDITION_MAX": + case 2147483647: + message.edition = 2147483647; + break; + } + if (object.value != null) + message.value = String(object.value); + return message; + }; + + /** + * Creates a plain object from an EditionDefault message. Also converts values to other types if specified. + * @function toObject + * @memberof google.protobuf.FieldOptions.EditionDefault + * @static + * @param {google.protobuf.FieldOptions.EditionDefault} message EditionDefault + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + EditionDefault.toObject = function toObject(message, options) { + if (!options) + options = {}; + var object = {}; + if (options.defaults) { + object.value = ""; + object.edition = options.enums === String ? "EDITION_UNKNOWN" : 0; + } + if (message.value != null && message.hasOwnProperty("value")) + object.value = message.value; + if (message.edition != null && message.hasOwnProperty("edition")) + object.edition = options.enums === String ? $root.google.protobuf.Edition[message.edition] === undefined ? message.edition : $root.google.protobuf.Edition[message.edition] : message.edition; + return object; + }; + + /** + * Converts this EditionDefault to JSON. + * @function toJSON + * @memberof google.protobuf.FieldOptions.EditionDefault + * @instance + * @returns {Object.} JSON object + */ + EditionDefault.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for EditionDefault + * @function getTypeUrl + * @memberof google.protobuf.FieldOptions.EditionDefault + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + EditionDefault.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/google.protobuf.FieldOptions.EditionDefault"; + }; + + return EditionDefault; + })(); + + FieldOptions.FeatureSupport = (function() { + + /** + * Properties of a FeatureSupport. + * @memberof google.protobuf.FieldOptions + * @interface IFeatureSupport + * @property {google.protobuf.Edition|null} [editionIntroduced] FeatureSupport editionIntroduced + * @property {google.protobuf.Edition|null} [editionDeprecated] FeatureSupport editionDeprecated + * @property {string|null} [deprecationWarning] FeatureSupport deprecationWarning + * @property {google.protobuf.Edition|null} [editionRemoved] FeatureSupport editionRemoved + */ + + /** + * Constructs a new FeatureSupport. + * @memberof google.protobuf.FieldOptions + * @classdesc Represents a FeatureSupport. + * @implements IFeatureSupport + * @constructor + * @param {google.protobuf.FieldOptions.IFeatureSupport=} [properties] Properties to set + */ + function FeatureSupport(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * FeatureSupport editionIntroduced. + * @member {google.protobuf.Edition} editionIntroduced + * @memberof google.protobuf.FieldOptions.FeatureSupport + * @instance + */ + FeatureSupport.prototype.editionIntroduced = 0; + + /** + * FeatureSupport editionDeprecated. + * @member {google.protobuf.Edition} editionDeprecated + * @memberof google.protobuf.FieldOptions.FeatureSupport + * @instance + */ + FeatureSupport.prototype.editionDeprecated = 0; + + /** + * FeatureSupport deprecationWarning. + * @member {string} deprecationWarning + * @memberof google.protobuf.FieldOptions.FeatureSupport + * @instance + */ + FeatureSupport.prototype.deprecationWarning = ""; + + /** + * FeatureSupport editionRemoved. + * @member {google.protobuf.Edition} editionRemoved + * @memberof google.protobuf.FieldOptions.FeatureSupport + * @instance + */ + FeatureSupport.prototype.editionRemoved = 0; + + /** + * Creates a new FeatureSupport instance using the specified properties. + * @function create + * @memberof google.protobuf.FieldOptions.FeatureSupport + * @static + * @param {google.protobuf.FieldOptions.IFeatureSupport=} [properties] Properties to set + * @returns {google.protobuf.FieldOptions.FeatureSupport} FeatureSupport instance + */ + FeatureSupport.create = function create(properties) { + return new FeatureSupport(properties); + }; + + /** + * Encodes the specified FeatureSupport message. Does not implicitly {@link google.protobuf.FieldOptions.FeatureSupport.verify|verify} messages. + * @function encode + * @memberof google.protobuf.FieldOptions.FeatureSupport + * @static + * @param {google.protobuf.FieldOptions.IFeatureSupport} message FeatureSupport message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + FeatureSupport.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + if (message.editionIntroduced != null && Object.hasOwnProperty.call(message, "editionIntroduced")) + writer.uint32(/* id 1, wireType 0 =*/8).int32(message.editionIntroduced); + if (message.editionDeprecated != null && Object.hasOwnProperty.call(message, "editionDeprecated")) + writer.uint32(/* id 2, wireType 0 =*/16).int32(message.editionDeprecated); + if (message.deprecationWarning != null && Object.hasOwnProperty.call(message, "deprecationWarning")) + writer.uint32(/* id 3, wireType 2 =*/26).string(message.deprecationWarning); + if (message.editionRemoved != null && Object.hasOwnProperty.call(message, "editionRemoved")) + writer.uint32(/* id 4, wireType 0 =*/32).int32(message.editionRemoved); + return writer; + }; + + /** + * Encodes the specified FeatureSupport message, length delimited. Does not implicitly {@link google.protobuf.FieldOptions.FeatureSupport.verify|verify} messages. + * @function encodeDelimited + * @memberof google.protobuf.FieldOptions.FeatureSupport + * @static + * @param {google.protobuf.FieldOptions.IFeatureSupport} message FeatureSupport message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + FeatureSupport.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a FeatureSupport message from the specified reader or buffer. + * @function decode + * @memberof google.protobuf.FieldOptions.FeatureSupport + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {google.protobuf.FieldOptions.FeatureSupport} FeatureSupport + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + FeatureSupport.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.google.protobuf.FieldOptions.FeatureSupport(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + case 1: { + message.editionIntroduced = reader.int32(); + break; + } + case 2: { + message.editionDeprecated = reader.int32(); + break; + } + case 3: { + message.deprecationWarning = reader.string(); + break; + } + case 4: { + message.editionRemoved = reader.int32(); + break; + } + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a FeatureSupport message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof google.protobuf.FieldOptions.FeatureSupport + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {google.protobuf.FieldOptions.FeatureSupport} FeatureSupport + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + FeatureSupport.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a FeatureSupport message. + * @function verify + * @memberof google.protobuf.FieldOptions.FeatureSupport + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + FeatureSupport.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + if (message.editionIntroduced != null && message.hasOwnProperty("editionIntroduced")) + switch (message.editionIntroduced) { + default: + return "editionIntroduced: enum value expected"; + case 0: + case 900: + case 998: + case 999: + case 1000: + case 1001: + case 1: + case 2: + case 99997: + case 99998: + case 99999: + case 2147483647: + break; + } + if (message.editionDeprecated != null && message.hasOwnProperty("editionDeprecated")) + switch (message.editionDeprecated) { + default: + return "editionDeprecated: enum value expected"; + case 0: + case 900: + case 998: + case 999: + case 1000: + case 1001: + case 1: + case 2: + case 99997: + case 99998: + case 99999: + case 2147483647: + break; + } + if (message.deprecationWarning != null && message.hasOwnProperty("deprecationWarning")) + if (!$util.isString(message.deprecationWarning)) + return "deprecationWarning: string expected"; + if (message.editionRemoved != null && message.hasOwnProperty("editionRemoved")) + switch (message.editionRemoved) { + default: + return "editionRemoved: enum value expected"; + case 0: + case 900: + case 998: + case 999: + case 1000: + case 1001: + case 1: + case 2: + case 99997: + case 99998: + case 99999: + case 2147483647: + break; + } + return null; + }; + + /** + * Creates a FeatureSupport message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof google.protobuf.FieldOptions.FeatureSupport + * @static + * @param {Object.} object Plain object + * @returns {google.protobuf.FieldOptions.FeatureSupport} FeatureSupport + */ + FeatureSupport.fromObject = function fromObject(object) { + if (object instanceof $root.google.protobuf.FieldOptions.FeatureSupport) + return object; + var message = new $root.google.protobuf.FieldOptions.FeatureSupport(); + switch (object.editionIntroduced) { + default: + if (typeof object.editionIntroduced === "number") { + message.editionIntroduced = object.editionIntroduced; + break; + } + break; + case "EDITION_UNKNOWN": + case 0: + message.editionIntroduced = 0; + break; + case "EDITION_LEGACY": + case 900: + message.editionIntroduced = 900; + break; + case "EDITION_PROTO2": + case 998: + message.editionIntroduced = 998; + break; + case "EDITION_PROTO3": + case 999: + message.editionIntroduced = 999; + break; + case "EDITION_2023": + case 1000: + message.editionIntroduced = 1000; + break; + case "EDITION_2024": + case 1001: + message.editionIntroduced = 1001; + break; + case "EDITION_1_TEST_ONLY": + case 1: + message.editionIntroduced = 1; + break; + case "EDITION_2_TEST_ONLY": + case 2: + message.editionIntroduced = 2; + break; + case "EDITION_99997_TEST_ONLY": + case 99997: + message.editionIntroduced = 99997; + break; + case "EDITION_99998_TEST_ONLY": + case 99998: + message.editionIntroduced = 99998; + break; + case "EDITION_99999_TEST_ONLY": + case 99999: + message.editionIntroduced = 99999; + break; + case "EDITION_MAX": + case 2147483647: + message.editionIntroduced = 2147483647; + break; + } + switch (object.editionDeprecated) { + default: + if (typeof object.editionDeprecated === "number") { + message.editionDeprecated = object.editionDeprecated; + break; + } + break; + case "EDITION_UNKNOWN": + case 0: + message.editionDeprecated = 0; + break; + case "EDITION_LEGACY": + case 900: + message.editionDeprecated = 900; + break; + case "EDITION_PROTO2": + case 998: + message.editionDeprecated = 998; + break; + case "EDITION_PROTO3": + case 999: + message.editionDeprecated = 999; + break; + case "EDITION_2023": + case 1000: + message.editionDeprecated = 1000; + break; + case "EDITION_2024": + case 1001: + message.editionDeprecated = 1001; + break; + case "EDITION_1_TEST_ONLY": + case 1: + message.editionDeprecated = 1; + break; + case "EDITION_2_TEST_ONLY": + case 2: + message.editionDeprecated = 2; + break; + case "EDITION_99997_TEST_ONLY": + case 99997: + message.editionDeprecated = 99997; + break; + case "EDITION_99998_TEST_ONLY": + case 99998: + message.editionDeprecated = 99998; + break; + case "EDITION_99999_TEST_ONLY": + case 99999: + message.editionDeprecated = 99999; + break; + case "EDITION_MAX": + case 2147483647: + message.editionDeprecated = 2147483647; + break; + } + if (object.deprecationWarning != null) + message.deprecationWarning = String(object.deprecationWarning); + switch (object.editionRemoved) { + default: + if (typeof object.editionRemoved === "number") { + message.editionRemoved = object.editionRemoved; + break; + } + break; + case "EDITION_UNKNOWN": + case 0: + message.editionRemoved = 0; + break; + case "EDITION_LEGACY": + case 900: + message.editionRemoved = 900; + break; + case "EDITION_PROTO2": + case 998: + message.editionRemoved = 998; + break; + case "EDITION_PROTO3": + case 999: + message.editionRemoved = 999; + break; + case "EDITION_2023": + case 1000: + message.editionRemoved = 1000; break; case "EDITION_2024": case 1001: - message.edition = 1001; + message.editionRemoved = 1001; break; case "EDITION_1_TEST_ONLY": case 1: - message.edition = 1; + message.editionRemoved = 1; break; case "EDITION_2_TEST_ONLY": case 2: - message.edition = 2; + message.editionRemoved = 2; break; case "EDITION_99997_TEST_ONLY": case 99997: - message.edition = 99997; + message.editionRemoved = 99997; break; case "EDITION_99998_TEST_ONLY": case 99998: - message.edition = 99998; + message.editionRemoved = 99998; break; case "EDITION_99999_TEST_ONLY": case 99999: - message.edition = 99999; + message.editionRemoved = 99999; break; case "EDITION_MAX": case 2147483647: - message.edition = 2147483647; + message.editionRemoved = 2147483647; break; } - if (object.value != null) - message.value = String(object.value); return message; }; /** - * Creates a plain object from an EditionDefault message. Also converts values to other types if specified. + * Creates a plain object from a FeatureSupport message. Also converts values to other types if specified. * @function toObject - * @memberof google.protobuf.FieldOptions.EditionDefault + * @memberof google.protobuf.FieldOptions.FeatureSupport * @static - * @param {google.protobuf.FieldOptions.EditionDefault} message EditionDefault + * @param {google.protobuf.FieldOptions.FeatureSupport} message FeatureSupport * @param {$protobuf.IConversionOptions} [options] Conversion options * @returns {Object.} Plain object */ - EditionDefault.toObject = function toObject(message, options) { + FeatureSupport.toObject = function toObject(message, options) { if (!options) options = {}; var object = {}; if (options.defaults) { - object.value = ""; - object.edition = options.enums === String ? "EDITION_UNKNOWN" : 0; - } - if (message.value != null && message.hasOwnProperty("value")) - object.value = message.value; - if (message.edition != null && message.hasOwnProperty("edition")) - object.edition = options.enums === String ? $root.google.protobuf.Edition[message.edition] === undefined ? message.edition : $root.google.protobuf.Edition[message.edition] : message.edition; + object.editionIntroduced = options.enums === String ? "EDITION_UNKNOWN" : 0; + object.editionDeprecated = options.enums === String ? "EDITION_UNKNOWN" : 0; + object.deprecationWarning = ""; + object.editionRemoved = options.enums === String ? "EDITION_UNKNOWN" : 0; + } + if (message.editionIntroduced != null && message.hasOwnProperty("editionIntroduced")) + object.editionIntroduced = options.enums === String ? $root.google.protobuf.Edition[message.editionIntroduced] === undefined ? message.editionIntroduced : $root.google.protobuf.Edition[message.editionIntroduced] : message.editionIntroduced; + if (message.editionDeprecated != null && message.hasOwnProperty("editionDeprecated")) + object.editionDeprecated = options.enums === String ? $root.google.protobuf.Edition[message.editionDeprecated] === undefined ? message.editionDeprecated : $root.google.protobuf.Edition[message.editionDeprecated] : message.editionDeprecated; + if (message.deprecationWarning != null && message.hasOwnProperty("deprecationWarning")) + object.deprecationWarning = message.deprecationWarning; + if (message.editionRemoved != null && message.hasOwnProperty("editionRemoved")) + object.editionRemoved = options.enums === String ? $root.google.protobuf.Edition[message.editionRemoved] === undefined ? message.editionRemoved : $root.google.protobuf.Edition[message.editionRemoved] : message.editionRemoved; return object; }; /** - * Converts this EditionDefault to JSON. + * Converts this FeatureSupport to JSON. * @function toJSON - * @memberof google.protobuf.FieldOptions.EditionDefault + * @memberof google.protobuf.FieldOptions.FeatureSupport * @instance * @returns {Object.} JSON object */ - EditionDefault.prototype.toJSON = function toJSON() { + FeatureSupport.prototype.toJSON = function toJSON() { return this.constructor.toObject(this, $protobuf.util.toJSONOptions); }; /** - * Gets the default type url for EditionDefault + * Gets the default type url for FeatureSupport * @function getTypeUrl - * @memberof google.protobuf.FieldOptions.EditionDefault + * @memberof google.protobuf.FieldOptions.FeatureSupport * @static * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") * @returns {string} The default type url */ - EditionDefault.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + FeatureSupport.getTypeUrl = function getTypeUrl(typeUrlPrefix) { if (typeUrlPrefix === undefined) { typeUrlPrefix = "type.googleapis.com"; } - return typeUrlPrefix + "/google.protobuf.FieldOptions.EditionDefault"; + return typeUrlPrefix + "/google.protobuf.FieldOptions.FeatureSupport"; }; - return EditionDefault; + return FeatureSupport; })(); return FieldOptions; @@ -40294,6 +41563,7 @@ * @property {boolean|null} [deprecated] EnumValueOptions deprecated * @property {google.protobuf.IFeatureSet|null} [features] EnumValueOptions features * @property {boolean|null} [debugRedact] EnumValueOptions debugRedact + * @property {google.protobuf.FieldOptions.IFeatureSupport|null} [featureSupport] EnumValueOptions featureSupport * @property {Array.|null} [uninterpretedOption] EnumValueOptions uninterpretedOption */ @@ -40337,6 +41607,14 @@ */ EnumValueOptions.prototype.debugRedact = false; + /** + * EnumValueOptions featureSupport. + * @member {google.protobuf.FieldOptions.IFeatureSupport|null|undefined} featureSupport + * @memberof google.protobuf.EnumValueOptions + * @instance + */ + EnumValueOptions.prototype.featureSupport = null; + /** * EnumValueOptions uninterpretedOption. * @member {Array.} uninterpretedOption @@ -40375,6 +41653,8 @@ $root.google.protobuf.FeatureSet.encode(message.features, writer.uint32(/* id 2, wireType 2 =*/18).fork()).ldelim(); if (message.debugRedact != null && Object.hasOwnProperty.call(message, "debugRedact")) writer.uint32(/* id 3, wireType 0 =*/24).bool(message.debugRedact); + if (message.featureSupport != null && Object.hasOwnProperty.call(message, "featureSupport")) + $root.google.protobuf.FieldOptions.FeatureSupport.encode(message.featureSupport, writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim(); if (message.uninterpretedOption != null && message.uninterpretedOption.length) for (var i = 0; i < message.uninterpretedOption.length; ++i) $root.google.protobuf.UninterpretedOption.encode(message.uninterpretedOption[i], writer.uint32(/* id 999, wireType 2 =*/7994).fork()).ldelim(); @@ -40426,6 +41706,10 @@ message.debugRedact = reader.bool(); break; } + case 4: { + message.featureSupport = $root.google.protobuf.FieldOptions.FeatureSupport.decode(reader, reader.uint32()); + break; + } case 999: { if (!(message.uninterpretedOption && message.uninterpretedOption.length)) message.uninterpretedOption = []; @@ -40478,6 +41762,11 @@ if (message.debugRedact != null && message.hasOwnProperty("debugRedact")) if (typeof message.debugRedact !== "boolean") return "debugRedact: boolean expected"; + if (message.featureSupport != null && message.hasOwnProperty("featureSupport")) { + var error = $root.google.protobuf.FieldOptions.FeatureSupport.verify(message.featureSupport); + if (error) + return "featureSupport." + error; + } if (message.uninterpretedOption != null && message.hasOwnProperty("uninterpretedOption")) { if (!Array.isArray(message.uninterpretedOption)) return "uninterpretedOption: array expected"; @@ -40511,6 +41800,11 @@ } if (object.debugRedact != null) message.debugRedact = Boolean(object.debugRedact); + if (object.featureSupport != null) { + if (typeof object.featureSupport !== "object") + throw TypeError(".google.protobuf.EnumValueOptions.featureSupport: object expected"); + message.featureSupport = $root.google.protobuf.FieldOptions.FeatureSupport.fromObject(object.featureSupport); + } if (object.uninterpretedOption) { if (!Array.isArray(object.uninterpretedOption)) throw TypeError(".google.protobuf.EnumValueOptions.uninterpretedOption: array expected"); @@ -40543,6 +41837,7 @@ object.deprecated = false; object.features = null; object.debugRedact = false; + object.featureSupport = null; } if (message.deprecated != null && message.hasOwnProperty("deprecated")) object.deprecated = message.deprecated; @@ -40550,6 +41845,8 @@ object.features = $root.google.protobuf.FeatureSet.toObject(message.features, options); if (message.debugRedact != null && message.hasOwnProperty("debugRedact")) object.debugRedact = message.debugRedact; + if (message.featureSupport != null && message.hasOwnProperty("featureSupport")) + object.featureSupport = $root.google.protobuf.FieldOptions.FeatureSupport.toObject(message.featureSupport, options); if (message.uninterpretedOption && message.uninterpretedOption.length) { object.uninterpretedOption = []; for (var j = 0; j < message.uninterpretedOption.length; ++j) @@ -41989,6 +43286,8 @@ * @property {google.protobuf.FeatureSet.Utf8Validation|null} [utf8Validation] FeatureSet utf8Validation * @property {google.protobuf.FeatureSet.MessageEncoding|null} [messageEncoding] FeatureSet messageEncoding * @property {google.protobuf.FeatureSet.JsonFormat|null} [jsonFormat] FeatureSet jsonFormat + * @property {google.protobuf.FeatureSet.EnforceNamingStyle|null} [enforceNamingStyle] FeatureSet enforceNamingStyle + * @property {google.protobuf.FeatureSet.VisibilityFeature.DefaultSymbolVisibility|null} [defaultSymbolVisibility] FeatureSet defaultSymbolVisibility */ /** @@ -42054,6 +43353,22 @@ */ FeatureSet.prototype.jsonFormat = 0; + /** + * FeatureSet enforceNamingStyle. + * @member {google.protobuf.FeatureSet.EnforceNamingStyle} enforceNamingStyle + * @memberof google.protobuf.FeatureSet + * @instance + */ + FeatureSet.prototype.enforceNamingStyle = 0; + + /** + * FeatureSet defaultSymbolVisibility. + * @member {google.protobuf.FeatureSet.VisibilityFeature.DefaultSymbolVisibility} defaultSymbolVisibility + * @memberof google.protobuf.FeatureSet + * @instance + */ + FeatureSet.prototype.defaultSymbolVisibility = 0; + /** * Creates a new FeatureSet instance using the specified properties. * @function create @@ -42090,6 +43405,10 @@ writer.uint32(/* id 5, wireType 0 =*/40).int32(message.messageEncoding); if (message.jsonFormat != null && Object.hasOwnProperty.call(message, "jsonFormat")) writer.uint32(/* id 6, wireType 0 =*/48).int32(message.jsonFormat); + if (message.enforceNamingStyle != null && Object.hasOwnProperty.call(message, "enforceNamingStyle")) + writer.uint32(/* id 7, wireType 0 =*/56).int32(message.enforceNamingStyle); + if (message.defaultSymbolVisibility != null && Object.hasOwnProperty.call(message, "defaultSymbolVisibility")) + writer.uint32(/* id 8, wireType 0 =*/64).int32(message.defaultSymbolVisibility); return writer; }; @@ -42150,6 +43469,14 @@ message.jsonFormat = reader.int32(); break; } + case 7: { + message.enforceNamingStyle = reader.int32(); + break; + } + case 8: { + message.defaultSymbolVisibility = reader.int32(); + break; + } default: reader.skipType(tag & 7); break; @@ -42240,6 +43567,26 @@ case 2: break; } + if (message.enforceNamingStyle != null && message.hasOwnProperty("enforceNamingStyle")) + switch (message.enforceNamingStyle) { + default: + return "enforceNamingStyle: enum value expected"; + case 0: + case 1: + case 2: + break; + } + if (message.defaultSymbolVisibility != null && message.hasOwnProperty("defaultSymbolVisibility")) + switch (message.defaultSymbolVisibility) { + default: + return "defaultSymbolVisibility: enum value expected"; + case 0: + case 1: + case 2: + case 3: + case 4: + break; + } return null; }; @@ -42379,6 +43726,54 @@ message.jsonFormat = 2; break; } + switch (object.enforceNamingStyle) { + default: + if (typeof object.enforceNamingStyle === "number") { + message.enforceNamingStyle = object.enforceNamingStyle; + break; + } + break; + case "ENFORCE_NAMING_STYLE_UNKNOWN": + case 0: + message.enforceNamingStyle = 0; + break; + case "STYLE2024": + case 1: + message.enforceNamingStyle = 1; + break; + case "STYLE_LEGACY": + case 2: + message.enforceNamingStyle = 2; + break; + } + switch (object.defaultSymbolVisibility) { + default: + if (typeof object.defaultSymbolVisibility === "number") { + message.defaultSymbolVisibility = object.defaultSymbolVisibility; + break; + } + break; + case "DEFAULT_SYMBOL_VISIBILITY_UNKNOWN": + case 0: + message.defaultSymbolVisibility = 0; + break; + case "EXPORT_ALL": + case 1: + message.defaultSymbolVisibility = 1; + break; + case "EXPORT_TOP_LEVEL": + case 2: + message.defaultSymbolVisibility = 2; + break; + case "LOCAL_ALL": + case 3: + message.defaultSymbolVisibility = 3; + break; + case "STRICT": + case 4: + message.defaultSymbolVisibility = 4; + break; + } return message; }; @@ -42402,6 +43797,8 @@ object.utf8Validation = options.enums === String ? "UTF8_VALIDATION_UNKNOWN" : 0; object.messageEncoding = options.enums === String ? "MESSAGE_ENCODING_UNKNOWN" : 0; object.jsonFormat = options.enums === String ? "JSON_FORMAT_UNKNOWN" : 0; + object.enforceNamingStyle = options.enums === String ? "ENFORCE_NAMING_STYLE_UNKNOWN" : 0; + object.defaultSymbolVisibility = options.enums === String ? "DEFAULT_SYMBOL_VISIBILITY_UNKNOWN" : 0; } if (message.fieldPresence != null && message.hasOwnProperty("fieldPresence")) object.fieldPresence = options.enums === String ? $root.google.protobuf.FeatureSet.FieldPresence[message.fieldPresence] === undefined ? message.fieldPresence : $root.google.protobuf.FeatureSet.FieldPresence[message.fieldPresence] : message.fieldPresence; @@ -42415,6 +43812,10 @@ object.messageEncoding = options.enums === String ? $root.google.protobuf.FeatureSet.MessageEncoding[message.messageEncoding] === undefined ? message.messageEncoding : $root.google.protobuf.FeatureSet.MessageEncoding[message.messageEncoding] : message.messageEncoding; if (message.jsonFormat != null && message.hasOwnProperty("jsonFormat")) object.jsonFormat = options.enums === String ? $root.google.protobuf.FeatureSet.JsonFormat[message.jsonFormat] === undefined ? message.jsonFormat : $root.google.protobuf.FeatureSet.JsonFormat[message.jsonFormat] : message.jsonFormat; + if (message.enforceNamingStyle != null && message.hasOwnProperty("enforceNamingStyle")) + object.enforceNamingStyle = options.enums === String ? $root.google.protobuf.FeatureSet.EnforceNamingStyle[message.enforceNamingStyle] === undefined ? message.enforceNamingStyle : $root.google.protobuf.FeatureSet.EnforceNamingStyle[message.enforceNamingStyle] : message.enforceNamingStyle; + if (message.defaultSymbolVisibility != null && message.hasOwnProperty("defaultSymbolVisibility")) + object.defaultSymbolVisibility = options.enums === String ? $root.google.protobuf.FeatureSet.VisibilityFeature.DefaultSymbolVisibility[message.defaultSymbolVisibility] === undefined ? message.defaultSymbolVisibility : $root.google.protobuf.FeatureSet.VisibilityFeature.DefaultSymbolVisibility[message.defaultSymbolVisibility] : message.defaultSymbolVisibility; return object; }; @@ -42542,6 +43943,219 @@ return values; })(); + /** + * EnforceNamingStyle enum. + * @name google.protobuf.FeatureSet.EnforceNamingStyle + * @enum {number} + * @property {number} ENFORCE_NAMING_STYLE_UNKNOWN=0 ENFORCE_NAMING_STYLE_UNKNOWN value + * @property {number} STYLE2024=1 STYLE2024 value + * @property {number} STYLE_LEGACY=2 STYLE_LEGACY value + */ + FeatureSet.EnforceNamingStyle = (function() { + var valuesById = {}, values = Object.create(valuesById); + values[valuesById[0] = "ENFORCE_NAMING_STYLE_UNKNOWN"] = 0; + values[valuesById[1] = "STYLE2024"] = 1; + values[valuesById[2] = "STYLE_LEGACY"] = 2; + return values; + })(); + + FeatureSet.VisibilityFeature = (function() { + + /** + * Properties of a VisibilityFeature. + * @memberof google.protobuf.FeatureSet + * @interface IVisibilityFeature + */ + + /** + * Constructs a new VisibilityFeature. + * @memberof google.protobuf.FeatureSet + * @classdesc Represents a VisibilityFeature. + * @implements IVisibilityFeature + * @constructor + * @param {google.protobuf.FeatureSet.IVisibilityFeature=} [properties] Properties to set + */ + function VisibilityFeature(properties) { + if (properties) + for (var keys = Object.keys(properties), i = 0; i < keys.length; ++i) + if (properties[keys[i]] != null) + this[keys[i]] = properties[keys[i]]; + } + + /** + * Creates a new VisibilityFeature instance using the specified properties. + * @function create + * @memberof google.protobuf.FeatureSet.VisibilityFeature + * @static + * @param {google.protobuf.FeatureSet.IVisibilityFeature=} [properties] Properties to set + * @returns {google.protobuf.FeatureSet.VisibilityFeature} VisibilityFeature instance + */ + VisibilityFeature.create = function create(properties) { + return new VisibilityFeature(properties); + }; + + /** + * Encodes the specified VisibilityFeature message. Does not implicitly {@link google.protobuf.FeatureSet.VisibilityFeature.verify|verify} messages. + * @function encode + * @memberof google.protobuf.FeatureSet.VisibilityFeature + * @static + * @param {google.protobuf.FeatureSet.IVisibilityFeature} message VisibilityFeature message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + VisibilityFeature.encode = function encode(message, writer) { + if (!writer) + writer = $Writer.create(); + return writer; + }; + + /** + * Encodes the specified VisibilityFeature message, length delimited. Does not implicitly {@link google.protobuf.FeatureSet.VisibilityFeature.verify|verify} messages. + * @function encodeDelimited + * @memberof google.protobuf.FeatureSet.VisibilityFeature + * @static + * @param {google.protobuf.FeatureSet.IVisibilityFeature} message VisibilityFeature message or plain object to encode + * @param {$protobuf.Writer} [writer] Writer to encode to + * @returns {$protobuf.Writer} Writer + */ + VisibilityFeature.encodeDelimited = function encodeDelimited(message, writer) { + return this.encode(message, writer).ldelim(); + }; + + /** + * Decodes a VisibilityFeature message from the specified reader or buffer. + * @function decode + * @memberof google.protobuf.FeatureSet.VisibilityFeature + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @param {number} [length] Message length if known beforehand + * @returns {google.protobuf.FeatureSet.VisibilityFeature} VisibilityFeature + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + VisibilityFeature.decode = function decode(reader, length, error) { + if (!(reader instanceof $Reader)) + reader = $Reader.create(reader); + var end = length === undefined ? reader.len : reader.pos + length, message = new $root.google.protobuf.FeatureSet.VisibilityFeature(); + while (reader.pos < end) { + var tag = reader.uint32(); + if (tag === error) + break; + switch (tag >>> 3) { + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }; + + /** + * Decodes a VisibilityFeature message from the specified reader or buffer, length delimited. + * @function decodeDelimited + * @memberof google.protobuf.FeatureSet.VisibilityFeature + * @static + * @param {$protobuf.Reader|Uint8Array} reader Reader or buffer to decode from + * @returns {google.protobuf.FeatureSet.VisibilityFeature} VisibilityFeature + * @throws {Error} If the payload is not a reader or valid buffer + * @throws {$protobuf.util.ProtocolError} If required fields are missing + */ + VisibilityFeature.decodeDelimited = function decodeDelimited(reader) { + if (!(reader instanceof $Reader)) + reader = new $Reader(reader); + return this.decode(reader, reader.uint32()); + }; + + /** + * Verifies a VisibilityFeature message. + * @function verify + * @memberof google.protobuf.FeatureSet.VisibilityFeature + * @static + * @param {Object.} message Plain object to verify + * @returns {string|null} `null` if valid, otherwise the reason why it is not + */ + VisibilityFeature.verify = function verify(message) { + if (typeof message !== "object" || message === null) + return "object expected"; + return null; + }; + + /** + * Creates a VisibilityFeature message from a plain object. Also converts values to their respective internal types. + * @function fromObject + * @memberof google.protobuf.FeatureSet.VisibilityFeature + * @static + * @param {Object.} object Plain object + * @returns {google.protobuf.FeatureSet.VisibilityFeature} VisibilityFeature + */ + VisibilityFeature.fromObject = function fromObject(object) { + if (object instanceof $root.google.protobuf.FeatureSet.VisibilityFeature) + return object; + return new $root.google.protobuf.FeatureSet.VisibilityFeature(); + }; + + /** + * Creates a plain object from a VisibilityFeature message. Also converts values to other types if specified. + * @function toObject + * @memberof google.protobuf.FeatureSet.VisibilityFeature + * @static + * @param {google.protobuf.FeatureSet.VisibilityFeature} message VisibilityFeature + * @param {$protobuf.IConversionOptions} [options] Conversion options + * @returns {Object.} Plain object + */ + VisibilityFeature.toObject = function toObject() { + return {}; + }; + + /** + * Converts this VisibilityFeature to JSON. + * @function toJSON + * @memberof google.protobuf.FeatureSet.VisibilityFeature + * @instance + * @returns {Object.} JSON object + */ + VisibilityFeature.prototype.toJSON = function toJSON() { + return this.constructor.toObject(this, $protobuf.util.toJSONOptions); + }; + + /** + * Gets the default type url for VisibilityFeature + * @function getTypeUrl + * @memberof google.protobuf.FeatureSet.VisibilityFeature + * @static + * @param {string} [typeUrlPrefix] your custom typeUrlPrefix(default "type.googleapis.com") + * @returns {string} The default type url + */ + VisibilityFeature.getTypeUrl = function getTypeUrl(typeUrlPrefix) { + if (typeUrlPrefix === undefined) { + typeUrlPrefix = "type.googleapis.com"; + } + return typeUrlPrefix + "/google.protobuf.FeatureSet.VisibilityFeature"; + }; + + /** + * DefaultSymbolVisibility enum. + * @name google.protobuf.FeatureSet.VisibilityFeature.DefaultSymbolVisibility + * @enum {number} + * @property {number} DEFAULT_SYMBOL_VISIBILITY_UNKNOWN=0 DEFAULT_SYMBOL_VISIBILITY_UNKNOWN value + * @property {number} EXPORT_ALL=1 EXPORT_ALL value + * @property {number} EXPORT_TOP_LEVEL=2 EXPORT_TOP_LEVEL value + * @property {number} LOCAL_ALL=3 LOCAL_ALL value + * @property {number} STRICT=4 STRICT value + */ + VisibilityFeature.DefaultSymbolVisibility = (function() { + var valuesById = {}, values = Object.create(valuesById); + values[valuesById[0] = "DEFAULT_SYMBOL_VISIBILITY_UNKNOWN"] = 0; + values[valuesById[1] = "EXPORT_ALL"] = 1; + values[valuesById[2] = "EXPORT_TOP_LEVEL"] = 2; + values[valuesById[3] = "LOCAL_ALL"] = 3; + values[valuesById[4] = "STRICT"] = 4; + return values; + })(); + + return VisibilityFeature; + })(); + return FeatureSet; })(); @@ -42726,6 +44340,7 @@ default: return "minimumEdition: enum value expected"; case 0: + case 900: case 998: case 999: case 1000: @@ -42743,6 +44358,7 @@ default: return "maximumEdition: enum value expected"; case 0: + case 900: case 998: case 999: case 1000: @@ -42791,6 +44407,10 @@ case 0: message.minimumEdition = 0; break; + case "EDITION_LEGACY": + case 900: + message.minimumEdition = 900; + break; case "EDITION_PROTO2": case 998: message.minimumEdition = 998; @@ -42843,6 +44463,10 @@ case 0: message.maximumEdition = 0; break; + case "EDITION_LEGACY": + case 900: + message.maximumEdition = 900; + break; case "EDITION_PROTO2": case 998: message.maximumEdition = 998; @@ -42951,7 +44575,8 @@ * @memberof google.protobuf.FeatureSetDefaults * @interface IFeatureSetEditionDefault * @property {google.protobuf.Edition|null} [edition] FeatureSetEditionDefault edition - * @property {google.protobuf.IFeatureSet|null} [features] FeatureSetEditionDefault features + * @property {google.protobuf.IFeatureSet|null} [overridableFeatures] FeatureSetEditionDefault overridableFeatures + * @property {google.protobuf.IFeatureSet|null} [fixedFeatures] FeatureSetEditionDefault fixedFeatures */ /** @@ -42978,12 +44603,20 @@ FeatureSetEditionDefault.prototype.edition = 0; /** - * FeatureSetEditionDefault features. - * @member {google.protobuf.IFeatureSet|null|undefined} features + * FeatureSetEditionDefault overridableFeatures. + * @member {google.protobuf.IFeatureSet|null|undefined} overridableFeatures + * @memberof google.protobuf.FeatureSetDefaults.FeatureSetEditionDefault + * @instance + */ + FeatureSetEditionDefault.prototype.overridableFeatures = null; + + /** + * FeatureSetEditionDefault fixedFeatures. + * @member {google.protobuf.IFeatureSet|null|undefined} fixedFeatures * @memberof google.protobuf.FeatureSetDefaults.FeatureSetEditionDefault * @instance */ - FeatureSetEditionDefault.prototype.features = null; + FeatureSetEditionDefault.prototype.fixedFeatures = null; /** * Creates a new FeatureSetEditionDefault instance using the specified properties. @@ -43009,10 +44642,12 @@ FeatureSetEditionDefault.encode = function encode(message, writer) { if (!writer) writer = $Writer.create(); - if (message.features != null && Object.hasOwnProperty.call(message, "features")) - $root.google.protobuf.FeatureSet.encode(message.features, writer.uint32(/* id 2, wireType 2 =*/18).fork()).ldelim(); if (message.edition != null && Object.hasOwnProperty.call(message, "edition")) writer.uint32(/* id 3, wireType 0 =*/24).int32(message.edition); + if (message.overridableFeatures != null && Object.hasOwnProperty.call(message, "overridableFeatures")) + $root.google.protobuf.FeatureSet.encode(message.overridableFeatures, writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim(); + if (message.fixedFeatures != null && Object.hasOwnProperty.call(message, "fixedFeatures")) + $root.google.protobuf.FeatureSet.encode(message.fixedFeatures, writer.uint32(/* id 5, wireType 2 =*/42).fork()).ldelim(); return writer; }; @@ -43053,8 +44688,12 @@ message.edition = reader.int32(); break; } - case 2: { - message.features = $root.google.protobuf.FeatureSet.decode(reader, reader.uint32()); + case 4: { + message.overridableFeatures = $root.google.protobuf.FeatureSet.decode(reader, reader.uint32()); + break; + } + case 5: { + message.fixedFeatures = $root.google.protobuf.FeatureSet.decode(reader, reader.uint32()); break; } default: @@ -43097,6 +44736,7 @@ default: return "edition: enum value expected"; case 0: + case 900: case 998: case 999: case 1000: @@ -43109,10 +44749,15 @@ case 2147483647: break; } - if (message.features != null && message.hasOwnProperty("features")) { - var error = $root.google.protobuf.FeatureSet.verify(message.features); + if (message.overridableFeatures != null && message.hasOwnProperty("overridableFeatures")) { + var error = $root.google.protobuf.FeatureSet.verify(message.overridableFeatures); + if (error) + return "overridableFeatures." + error; + } + if (message.fixedFeatures != null && message.hasOwnProperty("fixedFeatures")) { + var error = $root.google.protobuf.FeatureSet.verify(message.fixedFeatures); if (error) - return "features." + error; + return "fixedFeatures." + error; } return null; }; @@ -43140,6 +44785,10 @@ case 0: message.edition = 0; break; + case "EDITION_LEGACY": + case 900: + message.edition = 900; + break; case "EDITION_PROTO2": case 998: message.edition = 998; @@ -43181,10 +44830,15 @@ message.edition = 2147483647; break; } - if (object.features != null) { - if (typeof object.features !== "object") - throw TypeError(".google.protobuf.FeatureSetDefaults.FeatureSetEditionDefault.features: object expected"); - message.features = $root.google.protobuf.FeatureSet.fromObject(object.features); + if (object.overridableFeatures != null) { + if (typeof object.overridableFeatures !== "object") + throw TypeError(".google.protobuf.FeatureSetDefaults.FeatureSetEditionDefault.overridableFeatures: object expected"); + message.overridableFeatures = $root.google.protobuf.FeatureSet.fromObject(object.overridableFeatures); + } + if (object.fixedFeatures != null) { + if (typeof object.fixedFeatures !== "object") + throw TypeError(".google.protobuf.FeatureSetDefaults.FeatureSetEditionDefault.fixedFeatures: object expected"); + message.fixedFeatures = $root.google.protobuf.FeatureSet.fromObject(object.fixedFeatures); } return message; }; @@ -43203,13 +44857,16 @@ options = {}; var object = {}; if (options.defaults) { - object.features = null; object.edition = options.enums === String ? "EDITION_UNKNOWN" : 0; + object.overridableFeatures = null; + object.fixedFeatures = null; } - if (message.features != null && message.hasOwnProperty("features")) - object.features = $root.google.protobuf.FeatureSet.toObject(message.features, options); if (message.edition != null && message.hasOwnProperty("edition")) object.edition = options.enums === String ? $root.google.protobuf.Edition[message.edition] === undefined ? message.edition : $root.google.protobuf.Edition[message.edition] : message.edition; + if (message.overridableFeatures != null && message.hasOwnProperty("overridableFeatures")) + object.overridableFeatures = $root.google.protobuf.FeatureSet.toObject(message.overridableFeatures, options); + if (message.fixedFeatures != null && message.hasOwnProperty("fixedFeatures")) + object.fixedFeatures = $root.google.protobuf.FeatureSet.toObject(message.fixedFeatures, options); return object; }; @@ -44424,6 +46081,22 @@ return GeneratedCodeInfo; })(); + /** + * SymbolVisibility enum. + * @name google.protobuf.SymbolVisibility + * @enum {number} + * @property {number} VISIBILITY_UNSET=0 VISIBILITY_UNSET value + * @property {number} VISIBILITY_LOCAL=1 VISIBILITY_LOCAL value + * @property {number} VISIBILITY_EXPORT=2 VISIBILITY_EXPORT value + */ + protobuf.SymbolVisibility = (function() { + var valuesById = {}, values = Object.create(valuesById); + values[valuesById[0] = "VISIBILITY_UNSET"] = 0; + values[valuesById[1] = "VISIBILITY_LOCAL"] = 1; + values[valuesById[2] = "VISIBILITY_EXPORT"] = 2; + return values; + })(); + protobuf.Duration = (function() { /** diff --git a/protos/protos.json b/protos/protos.json index 17de0553e..e9562c561 100644 --- a/protos/protos.json +++ b/protos/protos.json @@ -3291,8 +3291,7 @@ "java_multiple_files": true, "java_outer_classname": "ResourceProto", "java_package": "com.google.api", - "objc_class_prefix": "GAPI", - "cc_enable_arenas": true + "objc_class_prefix": "GAPI" }, "nested": { "http": { @@ -3416,6 +3415,10 @@ "rule": "repeated", "type": "ClientLibraryDestination", "id": 2 + }, + "selectiveGapicGeneration": { + "type": "SelectiveGapicGeneration", + "id": 3 } } }, @@ -3556,6 +3559,28 @@ "common": { "type": "CommonLanguageSettings", "id": 1 + }, + "experimentalFeatures": { + "type": "ExperimentalFeatures", + "id": 2 + } + }, + "nested": { + "ExperimentalFeatures": { + "fields": { + "restAsyncIoEnabled": { + "type": "bool", + "id": 1 + }, + "protobufPythonicTypesEnabled": { + "type": "bool", + "id": 2 + }, + "unversionedPackageDisabled": { + "type": "bool", + "id": 3 + } + } } } }, @@ -3613,6 +3638,11 @@ "common": { "type": "CommonLanguageSettings", "id": 1 + }, + "renamedServices": { + "keyType": "string", + "type": "string", + "id": 2 } } }, @@ -3674,6 +3704,19 @@ "PACKAGE_MANAGER": 20 } }, + "SelectiveGapicGeneration": { + "fields": { + "methods": { + "rule": "repeated", + "type": "string", + "id": 1 + }, + "generateOmittedAsInternal": { + "type": "bool", + "id": 2 + } + } + }, "LaunchStage": { "values": { "LAUNCH_STAGE_UNSPECIFIED": 0, @@ -3806,12 +3849,19 @@ "type": "FileDescriptorProto", "id": 1 } - } + }, + "extensions": [ + [ + 536000000, + 536000000 + ] + ] }, "Edition": { "edition": "proto2", "values": { "EDITION_UNKNOWN": 0, + "EDITION_LEGACY": 900, "EDITION_PROTO2": 998, "EDITION_PROTO3": 999, "EDITION_2023": 1000, @@ -3850,6 +3900,11 @@ "type": "int32", "id": 11 }, + "optionDependency": { + "rule": "repeated", + "type": "string", + "id": 15 + }, "messageType": { "rule": "repeated", "type": "DescriptorProto", @@ -3938,6 +3993,10 @@ "rule": "repeated", "type": "string", "id": 10 + }, + "visibility": { + "type": "SymbolVisibility", + "id": 11 } }, "nested": { @@ -4163,6 +4222,10 @@ "rule": "repeated", "type": "string", "id": 5 + }, + "visibility": { + "type": "SymbolVisibility", + "id": 6 } }, "nested": { @@ -4377,6 +4440,7 @@ 42, 42 ], + "php_generic_services", [ 38, 38 @@ -4512,7 +4576,8 @@ "type": "bool", "id": 10, "options": { - "default": false + "default": false, + "deprecated": true } }, "debugRedact": { @@ -4540,6 +4605,10 @@ "type": "FeatureSet", "id": 21 }, + "featureSupport": { + "type": "FeatureSupport", + "id": 22 + }, "uninterpretedOption": { "rule": "repeated", "type": "UninterpretedOption", @@ -4609,6 +4678,26 @@ "id": 2 } } + }, + "FeatureSupport": { + "fields": { + "editionIntroduced": { + "type": "Edition", + "id": 1 + }, + "editionDeprecated": { + "type": "Edition", + "id": 2 + }, + "deprecationWarning": { + "type": "string", + "id": 3 + }, + "editionRemoved": { + "type": "Edition", + "id": 4 + } + } } } }, @@ -4697,6 +4786,10 @@ "default": false } }, + "featureSupport": { + "type": "FieldOptions.FeatureSupport", + "id": 4 + }, "uninterpretedOption": { "rule": "repeated", "type": "UninterpretedOption", @@ -4839,6 +4932,7 @@ "options": { "retention": "RETENTION_RUNTIME", "targets": "TARGET_TYPE_FILE", + "feature_support.edition_introduced": "EDITION_2023", "edition_defaults.edition": "EDITION_2023", "edition_defaults.value": "EXPLICIT" } @@ -4849,6 +4943,7 @@ "options": { "retention": "RETENTION_RUNTIME", "targets": "TARGET_TYPE_FILE", + "feature_support.edition_introduced": "EDITION_2023", "edition_defaults.edition": "EDITION_PROTO3", "edition_defaults.value": "OPEN" } @@ -4859,6 +4954,7 @@ "options": { "retention": "RETENTION_RUNTIME", "targets": "TARGET_TYPE_FILE", + "feature_support.edition_introduced": "EDITION_2023", "edition_defaults.edition": "EDITION_PROTO3", "edition_defaults.value": "PACKED" } @@ -4869,6 +4965,7 @@ "options": { "retention": "RETENTION_RUNTIME", "targets": "TARGET_TYPE_FILE", + "feature_support.edition_introduced": "EDITION_2023", "edition_defaults.edition": "EDITION_PROTO3", "edition_defaults.value": "VERIFY" } @@ -4879,7 +4976,8 @@ "options": { "retention": "RETENTION_RUNTIME", "targets": "TARGET_TYPE_FILE", - "edition_defaults.edition": "EDITION_PROTO2", + "feature_support.edition_introduced": "EDITION_2023", + "edition_defaults.edition": "EDITION_LEGACY", "edition_defaults.value": "LENGTH_PREFIXED" } }, @@ -4889,27 +4987,38 @@ "options": { "retention": "RETENTION_RUNTIME", "targets": "TARGET_TYPE_FILE", + "feature_support.edition_introduced": "EDITION_2023", "edition_defaults.edition": "EDITION_PROTO3", "edition_defaults.value": "ALLOW" } + }, + "enforceNamingStyle": { + "type": "EnforceNamingStyle", + "id": 7, + "options": { + "retention": "RETENTION_SOURCE", + "targets": "TARGET_TYPE_METHOD", + "feature_support.edition_introduced": "EDITION_2024", + "edition_defaults.edition": "EDITION_2024", + "edition_defaults.value": "STYLE2024" + } + }, + "defaultSymbolVisibility": { + "type": "VisibilityFeature.DefaultSymbolVisibility", + "id": 8, + "options": { + "retention": "RETENTION_SOURCE", + "targets": "TARGET_TYPE_FILE", + "feature_support.edition_introduced": "EDITION_2024", + "edition_defaults.edition": "EDITION_2024", + "edition_defaults.value": "EXPORT_TOP_LEVEL" + } } }, "extensions": [ [ 1000, - 1000 - ], - [ - 1001, - 1001 - ], - [ - 1002, - 1002 - ], - [ - 9990, - 9990 + 9994 ], [ 9995, @@ -4954,7 +5063,13 @@ "UTF8_VALIDATION_UNKNOWN": 0, "VERIFY": 2, "NONE": 3 - } + }, + "reserved": [ + [ + 1, + 1 + ] + ] }, "MessageEncoding": { "values": { @@ -4969,6 +5084,33 @@ "ALLOW": 1, "LEGACY_BEST_EFFORT": 2 } + }, + "EnforceNamingStyle": { + "values": { + "ENFORCE_NAMING_STYLE_UNKNOWN": 0, + "STYLE2024": 1, + "STYLE_LEGACY": 2 + } + }, + "VisibilityFeature": { + "fields": {}, + "reserved": [ + [ + 1, + 536870911 + ] + ], + "nested": { + "DefaultSymbolVisibility": { + "values": { + "DEFAULT_SYMBOL_VISIBILITY_UNKNOWN": 0, + "EXPORT_ALL": 1, + "EXPORT_TOP_LEVEL": 2, + "LOCAL_ALL": 3, + "STRICT": 4 + } + } + } } } }, @@ -4996,11 +5138,26 @@ "type": "Edition", "id": 3 }, - "features": { + "overridableFeatures": { "type": "FeatureSet", - "id": 2 + "id": 4 + }, + "fixedFeatures": { + "type": "FeatureSet", + "id": 5 } - } + }, + "reserved": [ + [ + 1, + 1 + ], + [ + 2, + 2 + ], + "features" + ] } } }, @@ -5013,6 +5170,12 @@ "id": 1 } }, + "extensions": [ + [ + 536000000, + 536000000 + ] + ], "nested": { "Location": { "fields": { @@ -5098,6 +5261,14 @@ } } }, + "SymbolVisibility": { + "edition": "proto2", + "values": { + "VISIBILITY_UNSET": 0, + "VISIBILITY_LOCAL": 1, + "VISIBILITY_EXPORT": 2 + } + }, "Duration": { "fields": { "seconds": { From bcad5e7493dabe58a8203b6aa0336b658220a925 Mon Sep 17 00:00:00 2001 From: Owl Bot Date: Wed, 16 Jul 2025 17:53:08 +0000 Subject: [PATCH 16/23] =?UTF-8?q?=F0=9F=A6=89=20Updates=20from=20OwlBot=20?= =?UTF-8?q?post-processor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --- README.md | 1 + samples/README.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/README.md b/README.md index 4972a7433..6bbb20f75 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/nodejs-pubsub/tree | Sample | Source Code | Try it | | --------------------------- | --------------------------------- | ------ | +| Close Subscription with Timeout | [source code](https://github.com/googleapis/nodejs-pubsub/blob/main/samples/closeSubscriptionWithTimeout.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-pubsub&page=editor&open_in_editor=samples/closeSubscriptionWithTimeout.js,samples/README.md) | | Commit an Avro-Based Schema | [source code](https://github.com/googleapis/nodejs-pubsub/blob/main/samples/commitAvroSchema.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-pubsub&page=editor&open_in_editor=samples/commitAvroSchema.js,samples/README.md) | | Commit an Proto-Based Schema | [source code](https://github.com/googleapis/nodejs-pubsub/blob/main/samples/commitProtoSchema.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-pubsub&page=editor&open_in_editor=samples/commitProtoSchema.js,samples/README.md) | | Create an Avro based Schema | [source code](https://github.com/googleapis/nodejs-pubsub/blob/main/samples/createAvroSchema.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-pubsub&page=editor&open_in_editor=samples/createAvroSchema.js,samples/README.md) | diff --git a/samples/README.md b/samples/README.md index 7635d0758..5d312dda1 100644 --- a/samples/README.md +++ b/samples/README.md @@ -20,6 +20,7 @@ guides. * [Before you begin](#before-you-begin) * [Samples](#samples) + * [Close Subscription with Timeout](#close-subscription-with-timeout) * [Commit an Avro-Based Schema](#commit-an-avro-based-schema) * [Commit an Proto-Based Schema](#commit-an-proto-based-schema) * [Create an Avro based Schema](#create-an-avro-based-schema) @@ -108,6 +109,25 @@ Before running the samples, make sure you've followed the steps outlined in +### Close Subscription with Timeout + +Demonstrates closing a subscription with a specified timeout for graceful shutdown. + +View the [source code](https://github.com/googleapis/nodejs-pubsub/blob/main/samples/closeSubscriptionWithTimeout.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-pubsub&page=editor&open_in_editor=samples/closeSubscriptionWithTimeout.js,samples/README.md) + +__Usage:__ + + +`node closeSubscriptionWithTimeout.js ` + + +----- + + + + ### Commit an Avro-Based Schema Commits a new schema definition revision on a project, using Avro From f28be03e0c0cfae3e7c3ee9f3e3ca4bfddcce5db Mon Sep 17 00:00:00 2001 From: Owl Bot Date: Wed, 16 Jul 2025 17:54:04 +0000 Subject: [PATCH 17/23] =?UTF-8?q?=F0=9F=A6=89=20Updates=20from=20OwlBot=20?= =?UTF-8?q?post-processor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md --- README.md | 1 + samples/README.md | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/README.md b/README.md index 4972a7433..6bbb20f75 100644 --- a/README.md +++ b/README.md @@ -123,6 +123,7 @@ Samples are in the [`samples/`](https://github.com/googleapis/nodejs-pubsub/tree | Sample | Source Code | Try it | | --------------------------- | --------------------------------- | ------ | +| Close Subscription with Timeout | [source code](https://github.com/googleapis/nodejs-pubsub/blob/main/samples/closeSubscriptionWithTimeout.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-pubsub&page=editor&open_in_editor=samples/closeSubscriptionWithTimeout.js,samples/README.md) | | Commit an Avro-Based Schema | [source code](https://github.com/googleapis/nodejs-pubsub/blob/main/samples/commitAvroSchema.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-pubsub&page=editor&open_in_editor=samples/commitAvroSchema.js,samples/README.md) | | Commit an Proto-Based Schema | [source code](https://github.com/googleapis/nodejs-pubsub/blob/main/samples/commitProtoSchema.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-pubsub&page=editor&open_in_editor=samples/commitProtoSchema.js,samples/README.md) | | Create an Avro based Schema | [source code](https://github.com/googleapis/nodejs-pubsub/blob/main/samples/createAvroSchema.js) | [![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-pubsub&page=editor&open_in_editor=samples/createAvroSchema.js,samples/README.md) | diff --git a/samples/README.md b/samples/README.md index 7635d0758..5d312dda1 100644 --- a/samples/README.md +++ b/samples/README.md @@ -20,6 +20,7 @@ guides. * [Before you begin](#before-you-begin) * [Samples](#samples) + * [Close Subscription with Timeout](#close-subscription-with-timeout) * [Commit an Avro-Based Schema](#commit-an-avro-based-schema) * [Commit an Proto-Based Schema](#commit-an-proto-based-schema) * [Create an Avro based Schema](#create-an-avro-based-schema) @@ -108,6 +109,25 @@ Before running the samples, make sure you've followed the steps outlined in +### Close Subscription with Timeout + +Demonstrates closing a subscription with a specified timeout for graceful shutdown. + +View the [source code](https://github.com/googleapis/nodejs-pubsub/blob/main/samples/closeSubscriptionWithTimeout.js). + +[![Open in Cloud Shell][shell_img]](https://console.cloud.google.com/cloudshell/open?git_repo=https://github.com/googleapis/nodejs-pubsub&page=editor&open_in_editor=samples/closeSubscriptionWithTimeout.js,samples/README.md) + +__Usage:__ + + +`node closeSubscriptionWithTimeout.js ` + + +----- + + + + ### Commit an Avro-Based Schema Commits a new schema definition revision on a project, using Avro From 8866c98dd55d2ed42ccb489f46cd6a8945aef980 Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Wed, 6 Aug 2025 16:01:32 -0400 Subject: [PATCH 18/23] fix: no need to check isEmpty on remove --- src/lease-manager.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lease-manager.ts b/src/lease-manager.ts index e5462b078..9ee6df342 100644 --- a/src/lease-manager.ts +++ b/src/lease-manager.ts @@ -179,7 +179,6 @@ export class LeaseManager extends EventEmitter { } const wasFull = this.isFull(); - const wasEmpty = this.isEmpty(); this._messages.delete(message); this.bytes -= message.length; @@ -193,7 +192,7 @@ export class LeaseManager extends EventEmitter { this._dispense(this._pending.shift()!); } - if (!wasEmpty && this.isEmpty()) { + if (this.isEmpty()) { this.emit('empty'); } From 791a7f04212a6e1b6fa8ed26048228cfb12db658 Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Wed, 6 Aug 2025 16:10:31 -0400 Subject: [PATCH 19/23] chore: remove unneeded promise skip code --- src/subscriber.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/subscriber.ts b/src/subscriber.ts index c41943b62..f2812928c 100644 --- a/src/subscriber.ts +++ b/src/subscriber.ts @@ -1362,11 +1362,6 @@ export class Subscriber extends EventEmitter { promises.push(this._modAcks.onDrain()); } - if (!promises.length) { - // Nothing to wait for. - return; - } - // Wait for the flush promises. await Promise.all(promises); } From 36e53613379e2f55203242276229101837ad5e3b Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Wed, 6 Aug 2025 16:33:51 -0400 Subject: [PATCH 20/23] fix: substantially clarify the awaitWithTimeout interface --- src/subscriber.ts | 29 ++++++++++++++++------------ src/util.ts | 35 +++++++++++++++++++++++++-------- test/util.ts | 49 +++++++++++++++++++++++++---------------------- 3 files changed, 70 insertions(+), 43 deletions(-) diff --git a/src/subscriber.ts b/src/subscriber.ts index f2812928c..c6e975348 100644 --- a/src/subscriber.ts +++ b/src/subscriber.ts @@ -991,14 +991,18 @@ export class Subscriber extends EventEmitter { this._inventory.on('empty', r); }); - try { - await awaitWithTimeout(emptyPromise, waitTimeout); - } catch (e) { + const resultCompletion = await awaitWithTimeout( + emptyPromise, + waitTimeout, + ); + if (resultCompletion.exception || resultCompletion.timedOut) { // Don't try to deal with errors at this point, just warn-log. - const err = e as [unknown, boolean]; - if (err[1] === false) { + if (resultCompletion.timedOut === false) { // This wasn't a timeout. - logs.debug.warn('Error during Subscriber.close(): %j', err[0]); + logs.debug.warn( + 'Error during Subscriber.close(): %j', + resultCompletion.exception, + ); } } } @@ -1019,14 +1023,15 @@ export class Subscriber extends EventEmitter { // Wait for user callbacks to complete. const flushCompleted = this._waitForFlush(); - try { - await awaitWithTimeout(flushCompleted, timeout); - } catch (e) { + const flushResult = await awaitWithTimeout(flushCompleted, timeout); + if (flushResult.exception || flushResult.timedOut) { // Don't try to deal with errors at this point, just warn-log. - const err = e as [unknown, boolean]; - if (err[1] === false) { + if (flushResult.timedOut === false) { // This wasn't a timeout. - logs.debug.warn('Error during Subscriber.close(): %j', err[0]); + logs.debug.warn( + 'Error during Subscriber.close(): %j', + flushResult.exception, + ); } } diff --git a/src/util.ts b/src/util.ts index 5320dc4be..9ee6a5e28 100644 --- a/src/util.ts +++ b/src/util.ts @@ -88,18 +88,30 @@ export function addToBucket(map: Map, bucket: T, item: U) { const timeoutToken: unique symbol = Symbol('pubsub promise timeout'); /** - * Awaits on Promise with a timeout. Resolves on success, rejects on timeout. + * Return value from `awaitWithTimeout`. There are several variations on what + * might happen here, so this bundles it all up into a "report". + */ +export interface TimeoutReturn { + returnedValue?: T; + exception?: Error; + timedOut: boolean; +} + +/** + * Awaits on Promise with a timeout. Resolves on the passed promise resolving or + * rejecting, or on timeout. * * @param promise An existing Promise returning type T. * @param timeout A timeout value as a Duration; if the timeout is exceeded while - * waiting for the promise, the Promise this function returns will reject. - * @returns In any case, a tuple with the first item being the T value or an - * error value, and the second item being true if the timeout was exceeded. + * waiting for the promise, the Promise this function returns will resolve, but + * with `timedOut` set. + * @returns A TimeoutReturn with the returned value, if applicable, an exception if + * the promise rejected, or the timedOut set to true if it timed out. */ export async function awaitWithTimeout( promise: Promise, timeout: Duration, -): Promise<[T, boolean]> { +): Promise> { let timeoutId: NodeJS.Timeout | undefined; const timeoutPromise = new Promise((_, rej) => { timeoutId = setTimeout(() => rej(timeoutToken), timeout.milliseconds); @@ -107,10 +119,17 @@ export async function awaitWithTimeout( try { const value = await Promise.race([timeoutPromise, promise]); clearTimeout(timeoutId); - return [value, false]; + return { + returnedValue: value, + timedOut: false, + }; } catch (e) { - // The timeout passed. + const err: Error | symbol = e as unknown as Error | symbol; + // The timeout passed or the promise rejected. clearTimeout(timeoutId); - throw [e, e === timeoutToken]; + return { + exception: (err !== timeoutToken ? err : undefined) as Error | undefined, + timedOut: err === timeoutToken, + }; } } diff --git a/test/util.ts b/test/util.ts index 5355cefc1..ba8d0e63d 100644 --- a/test/util.ts +++ b/test/util.ts @@ -83,12 +83,15 @@ describe('utils', () => { Duration.from({seconds: 1}), ); fakeTimers.tick(500); - try { - const result = await awaitPromise; - assert.deepStrictEqual(result, [testString, false]); - } catch (e) { - assert.strictEqual(e, null, 'timeout was triggered, improperly'); - } + + const result = await awaitPromise; + assert.strictEqual(result.returnedValue, testString); + assert.strictEqual(result.exception, undefined); + assert.strictEqual( + result.timedOut, + false, + 'timeout was triggered, improperly', + ); }); it('handles non-timeout errors properly', async () => { @@ -104,12 +107,14 @@ describe('utils', () => { Duration.from({seconds: 1}), ); fakeTimers.tick(500); - try { - const result = await awaitPromise; - assert.strictEqual(result, null, 'non-error was triggered, improperly'); - } catch (e) { - assert.deepStrictEqual(e, [testString, false]); - } + + const result = await awaitPromise; + assert.strictEqual( + result.exception, + testString, + 'non-error was triggered, improperly', + ); + assert.strictEqual(result.timedOut, false); }); it('handles timeout properly', async () => { @@ -125,17 +130,15 @@ describe('utils', () => { Duration.from({seconds: 1}), ); fakeTimers.tick(1500); - try { - const result = await awaitPromise; - assert.strictEqual( - result, - null, - 'timeout was not triggered, improperly', - ); - } catch (e) { - const err: unknown[] = e as unknown[]; - assert.strictEqual(err[1], true); - } + + const result = await awaitPromise; + assert.strictEqual( + result.timedOut, + true, + 'timeout was not triggered, improperly', + ); + + assert.strictEqual(result.timedOut, true); }); }); }); From d3a348924c7e86ab78d749f81b54ab8357620652 Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Wed, 6 Aug 2025 16:42:00 -0400 Subject: [PATCH 21/23] chore: hoist timeout logic into its own method --- src/subscriber.ts | 46 ++++++++++++++++++++-------------------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/subscriber.ts b/src/subscriber.ts index c6e975348..8b06c794f 100644 --- a/src/subscriber.ts +++ b/src/subscriber.ts @@ -932,6 +932,23 @@ export class Subscriber extends EventEmitter { return AckResponses.Success; } + async #awaitTimeoutAndCheck( + promise: Promise, + timeout: Duration, + ): Promise { + const result = await awaitWithTimeout(promise, timeout); + if (result.exception || result.timedOut) { + // Don't try to deal with errors at this point, just warn-log. + if (result.timedOut === false) { + // This wasn't a timeout. + logs.debug.warn( + 'Error during Subscriber.close(): %j', + result.exception, + ); + } + } + } + /** * Closes the subscriber, stopping the reception of new messages and shutting * down the underlying stream. The behavior of the returned Promise will depend @@ -987,24 +1004,11 @@ export class Subscriber extends EventEmitter { ) { const waitTimeout = timeout.subtract(finalNackTimeout); - const emptyPromise = new Promise(r => { + const emptyPromise = new Promise(r => { this._inventory.on('empty', r); }); - const resultCompletion = await awaitWithTimeout( - emptyPromise, - waitTimeout, - ); - if (resultCompletion.exception || resultCompletion.timedOut) { - // Don't try to deal with errors at this point, just warn-log. - if (resultCompletion.timedOut === false) { - // This wasn't a timeout. - logs.debug.warn( - 'Error during Subscriber.close(): %j', - resultCompletion.exception, - ); - } - } + await this.#awaitTimeoutAndCheck(emptyPromise, waitTimeout); } // Now we head into immediate shutdown mode with what time is left. @@ -1023,17 +1027,7 @@ export class Subscriber extends EventEmitter { // Wait for user callbacks to complete. const flushCompleted = this._waitForFlush(); - const flushResult = await awaitWithTimeout(flushCompleted, timeout); - if (flushResult.exception || flushResult.timedOut) { - // Don't try to deal with errors at this point, just warn-log. - if (flushResult.timedOut === false) { - // This wasn't a timeout. - logs.debug.warn( - 'Error during Subscriber.close(): %j', - flushResult.exception, - ); - } - } + await this.#awaitTimeoutAndCheck(flushCompleted, timeout); // Clean up OTel spans for any remaining messages. remaining.forEach(m => { From a2859351b07857d58f4fcb969cf311c0939d7e90 Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:00:02 -0400 Subject: [PATCH 22/23] fix: tests were leaking EventEmitter handlers --- test/lease-manager.ts | 23 +++++++++++++---------- test/message-queues.ts | 4 ++++ test/publisher/message-queues.ts | 2 ++ test/subscriber.ts | 10 +++++++--- test/test-utils.ts | 15 +++++++++++++-- 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/test/lease-manager.ts b/test/lease-manager.ts index 8cd44c7b3..0d74a3963 100644 --- a/test/lease-manager.ts +++ b/test/lease-manager.ts @@ -108,6 +108,8 @@ describe('LeaseManager', () => { let LeaseManager: typeof leaseTypes.LeaseManager; let leaseManager: leaseTypes.LeaseManager; + let fakeLog: FakeLog | undefined; + before(() => { LeaseManager = proxyquire('../src/lease-manager.js', { os: fakeos, @@ -121,6 +123,7 @@ describe('LeaseManager', () => { }); afterEach(() => { + fakeLog?.remove(); leaseManager.clear(); sandbox.restore(); }); @@ -197,20 +200,20 @@ describe('LeaseManager', () => { fakeMessage.id = 'a'; fakeMessage.ackId = 'b'; - const fakeLog = new FakeLog(leaseTypes.logs.callbackDelivery); + fakeLog = new FakeLog(leaseTypes.logs.callbackDelivery); leaseManager.setOptions({ allowExcessMessages: true, }); subscriber.on('message', () => { - assert.strictEqual(fakeLog.called, true); + assert.strictEqual(fakeLog!.called, true); assert.strictEqual( - fakeLog.fields!.severity, + fakeLog!.fields!.severity, loggingUtils.LogSeverity.INFO, ); - assert.strictEqual(fakeLog.args![1] as string, 'a'); - assert.strictEqual(fakeLog.args![2] as string, 'b'); + assert.strictEqual(fakeLog!.args![1] as string, 'a'); + assert.strictEqual(fakeLog!.args![2] as string, 'b'); done(); }); @@ -222,7 +225,7 @@ describe('LeaseManager', () => { fakeMessage.id = 'a'; fakeMessage.ackId = 'b'; - const fakeLog = new FakeLog(leaseTypes.logs.callbackExceptions); + fakeLog = new FakeLog(leaseTypes.logs.callbackExceptions); leaseManager.setOptions({ allowExcessMessages: true, @@ -285,7 +288,7 @@ describe('LeaseManager', () => { const pendingStub = sandbox.stub(leaseManager, 'pending'); pendingStub.get(() => 0); leaseManager.setOptions({allowExcessMessages: false}); - const fakeLog = new FakeLog(leaseTypes.logs.subscriberFlowControl); + fakeLog = new FakeLog(leaseTypes.logs.subscriberFlowControl); leaseManager.add(fakeMessage); assert.strictEqual(fakeLog.called, true); @@ -392,7 +395,7 @@ describe('LeaseManager', () => { const removeStub = sandbox.stub(leaseManager, 'remove'); const modAckStub = sandbox.stub(goodMessage, 'modAck'); - const fakeLog = new FakeLog(leaseTypes.logs.expiry); + fakeLog = new FakeLog(leaseTypes.logs.expiry); leaseManager.add(goodMessage as {} as Message); clock.tick(halfway); @@ -474,7 +477,7 @@ describe('LeaseManager', () => { }); it('should log if it was full and is now empty', () => { - const fakeLog = new FakeLog(leaseTypes.logs.subscriberFlowControl); + fakeLog = new FakeLog(leaseTypes.logs.subscriberFlowControl); const pendingStub = sandbox.stub(leaseManager, 'pending'); pendingStub.get(() => 0); leaseManager.add(new FakeMessage() as {} as Message); @@ -613,7 +616,7 @@ describe('LeaseManager', () => { const pending = new FakeMessage() as {} as Message; leaseManager.setOptions({allowExcessMessages: false, maxMessages: 1}); - const fakeLog = new FakeLog(leaseTypes.logs.subscriberFlowControl); + fakeLog = new FakeLog(leaseTypes.logs.subscriberFlowControl); leaseManager.add(temp); leaseManager.add(pending); diff --git a/test/message-queues.ts b/test/message-queues.ts index bc88de2d5..f91129e7d 100644 --- a/test/message-queues.ts +++ b/test/message-queues.ts @@ -415,6 +415,8 @@ describe('MessageQueues', () => { messages.forEach(message => ackQueue.add(message as Message)); await ackQueue.flush('logtest'); + fakeLog.remove(); + assert.strictEqual(fakeLog.called, true); assert.strictEqual( fakeLog.fields!.severity, @@ -674,6 +676,8 @@ describe('MessageQueues', () => { messages.forEach(message => modAckQueue.add(message as Message)); await modAckQueue.flush('logtest'); + fakeLog.remove(); + assert.strictEqual(fakeLog.called, true); assert.strictEqual( fakeLog.fields!.severity, diff --git a/test/publisher/message-queues.ts b/test/publisher/message-queues.ts index 89136b0f0..7c810a9be 100644 --- a/test/publisher/message-queues.ts +++ b/test/publisher/message-queues.ts @@ -184,6 +184,8 @@ describe('Message Queues', () => { sandbox.stub(topic, 'request'); const fakeLog = new FakeLog(q.logs.publishBatch); void queue._publish(messages, callbacks, 0, 'test'); + fakeLog.remove(); + assert.strictEqual(fakeLog.called, true); assert.strictEqual( fakeLog.fields!.severity, diff --git a/test/subscriber.ts b/test/subscriber.ts index 092c09f66..4e6917b15 100644 --- a/test/subscriber.ts +++ b/test/subscriber.ts @@ -206,6 +206,8 @@ describe('Subscriber', () => { let Subscriber: typeof s.Subscriber; let subscriber: s.Subscriber; + let fakeLog: FakeLog | undefined; + beforeEach(() => { sandbox = sinon.createSandbox(); fakeProjectify = { @@ -243,6 +245,8 @@ describe('Subscriber', () => { }); afterEach(async () => { + fakeLog?.remove(); + fakeLog = undefined; sandbox.restore(); await subscriber.close(); tracing.setGloballyEnabled(false); @@ -441,7 +445,7 @@ describe('Subscriber', () => { }); it('should log on ack completion', async () => { - const fakeLog = new FakeLog(s.logs.ackNack); + fakeLog = new FakeLog(s.logs.ackNack); await subscriber.ack(message); @@ -459,7 +463,7 @@ describe('Subscriber', () => { message.received = 0; sandbox.stub(histogram, 'percentile').withArgs(99).returns(10); - const fakeLog = new FakeLog(s.logs.slowAck); + fakeLog = new FakeLog(s.logs.slowAck); await subscriber.ack(message); @@ -926,7 +930,7 @@ describe('Subscriber', () => { }); it('should log on ack completion', async () => { - const fakeLog = new FakeLog(s.logs.ackNack); + fakeLog = new FakeLog(s.logs.ackNack); await subscriber.nack(message); diff --git a/test/test-utils.ts b/test/test-utils.ts index a2f20946a..f171d4685 100644 --- a/test/test-utils.ts +++ b/test/test-utils.ts @@ -63,12 +63,23 @@ export class FakeLog { fields?: loggingUtils.LogFields; args?: unknown[]; called = false; + log: loggingUtils.AdhocDebugLogFunction; + listener: (lf: loggingUtils.LogFields, a: unknown[]) => void; constructor(log: loggingUtils.AdhocDebugLogFunction) { - log.on('log', (lf, a) => { + this.log = log; + this.listener = (lf: loggingUtils.LogFields, a: unknown[]) => { this.fields = lf; this.args = a; this.called = true; - }); + }; + this.log.on('log', this.listener); + } + + remove() { + // This really ought to be properly exposed, but since it's not, we'll + // do this for now to keep the tests from being leaky. + const instance = (this.log as loggingUtils.AdhocDebugLogFunction).instance; + instance.off('log', this.listener); } } From abeeb6c600e540b282de8523d75c83d3147e273c Mon Sep 17 00:00:00 2001 From: feywind <57276408+feywind@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:05:44 -0400 Subject: [PATCH 23/23] chore: change constant to CONSTANT_CASE --- src/subscriber.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/subscriber.ts b/src/subscriber.ts index 8b06c794f..c12996862 100644 --- a/src/subscriber.ts +++ b/src/subscriber.ts @@ -83,7 +83,7 @@ export interface SubscriberCloseOptions { * Specifies how long before the final close timeout, in WaitForProcessing mode, * that we should give up and start shutting down cleanly. */ -const finalNackTimeout = Duration.from({seconds: 1}); +const FINAL_NACK_TIMEOUT = Duration.from({seconds: 1}); /** * Thrown when an error is detected in an ack/nack/modack call, when @@ -982,13 +982,13 @@ export class Subscriber extends EventEmitter { ); // If the user specified a zero timeout, just bail immediately. - if (Math.floor(timeout.milliseconds) === 0) { + if (!timeout.milliseconds) { this._inventory.clear(); return; } // Warn the user if the timeout is too short for NackImmediately. - if (Duration.compare(timeout, finalNackTimeout) < 0) { + if (Duration.compare(timeout, FINAL_NACK_TIMEOUT) < 0) { logs.debug.warn( 'Subscriber.close() timeout is less than the final shutdown time (%i ms). This may result in lost nacks.', timeout.milliseconds, @@ -1002,7 +1002,7 @@ export class Subscriber extends EventEmitter { behavior === SubscriberCloseBehaviors.WaitForProcessing && !this._inventory.isEmpty ) { - const waitTimeout = timeout.subtract(finalNackTimeout); + const waitTimeout = timeout.subtract(FINAL_NACK_TIMEOUT); const emptyPromise = new Promise(r => { this._inventory.on('empty', r);