mirror of https://github.com/veypi/OneAuth.git
nats
parent
c187b078b0
commit
85aab14cba
Binary file not shown.
Before Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 859 B |
Binary file not shown.
Before Width: | Height: | Size: 2.0 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB |
@ -0,0 +1,40 @@
|
||||
/*
|
||||
* nats.ts
|
||||
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||
* 2023-10-16 16:18
|
||||
* Distributed under terms of the MIT license.
|
||||
*/
|
||||
|
||||
import axios from 'axios'
|
||||
import util from '../libs/util'
|
||||
import { connect, StringCodec } from '../libs/nats.ws'
|
||||
|
||||
|
||||
|
||||
|
||||
const sc = StringCodec();
|
||||
const nc = await connect({
|
||||
servers: 'ws://127.0.0.1:4221',
|
||||
authenticator: function(nonce?: string) {
|
||||
let nkey = 'UCXFAAVMCPTATZUZX6H24YF6FI3NKPQBPLM6BNN2EDFPNSUUEZPNFKEL'
|
||||
// let nre = nkeyAuthenticator(nkey_seed)
|
||||
let res = {
|
||||
nkey: nkey,
|
||||
sig: async function() {
|
||||
let response = await axios.post('/api/app/nats/token/', { token: util.getToken(), nonce: nonce });
|
||||
console.log(response)
|
||||
return response.data
|
||||
}
|
||||
};
|
||||
return res
|
||||
} as any
|
||||
})
|
||||
|
||||
nc.publish('msg', '123')
|
||||
const sub = nc.subscribe("msg");
|
||||
(async () => {
|
||||
for await (const m of sub) {
|
||||
console.log(`[${sub.getProcessed()}]: ${sc.decode(m.data)}`);
|
||||
}
|
||||
console.log("subscription closed");
|
||||
})();
|
@ -0,0 +1,22 @@
|
||||
/*
|
||||
* oaer.ts
|
||||
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||
* 2023-10-16 21:20
|
||||
* Distributed under terms of the MIT license.
|
||||
*/
|
||||
|
||||
|
||||
// import '@veypi/oaer'
|
||||
import oaer from '@veypi/oaer'
|
||||
import bus from 'src/libs/bus'
|
||||
import util from 'src/libs/util'
|
||||
|
||||
oaer.set({
|
||||
token: util.getToken(),
|
||||
host: 'http://' + window.location.host,
|
||||
uuid: 'FR9P5t8debxc11aFF',
|
||||
})
|
||||
|
||||
bus.on('token', (t: any) => {
|
||||
oaer.set({ token: t })
|
||||
})
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,153 @@
|
||||
/*
|
||||
* Copyright 2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
export {
|
||||
checkJsError,
|
||||
isFlowControlMsg,
|
||||
isHeartbeatMsg,
|
||||
millis,
|
||||
nanos,
|
||||
} from "./jsutil.ts";
|
||||
|
||||
export {
|
||||
AdvisoryKind,
|
||||
consumerOpts,
|
||||
DirectMsgHeaders,
|
||||
isConsumerOptsBuilder,
|
||||
JsHeaders,
|
||||
RepublishHeaders,
|
||||
} from "./types.ts";
|
||||
|
||||
export type {
|
||||
Advisory,
|
||||
Closed,
|
||||
ConsumerInfoable,
|
||||
ConsumerOpts,
|
||||
ConsumerOptsBuilder,
|
||||
Consumers,
|
||||
Destroyable,
|
||||
JetStreamClient,
|
||||
JetStreamManager,
|
||||
JetStreamManagerOptions,
|
||||
JetStreamOptions,
|
||||
JetStreamPublishOptions,
|
||||
JetStreamPullSubscription,
|
||||
JetStreamSubscription,
|
||||
JetStreamSubscriptionInfoable,
|
||||
JetStreamSubscriptionOptions,
|
||||
JsMsgCallback,
|
||||
KV,
|
||||
KvCodec,
|
||||
KvCodecs,
|
||||
KvEntry,
|
||||
KvLimits,
|
||||
KvOptions,
|
||||
KvPutOptions,
|
||||
KvStatus,
|
||||
KvWatchInclude,
|
||||
KvWatchOptions,
|
||||
ObjectInfo,
|
||||
ObjectResult,
|
||||
ObjectStore,
|
||||
ObjectStoreLink,
|
||||
ObjectStoreMeta,
|
||||
ObjectStoreMetaOptions,
|
||||
ObjectStoreOptions,
|
||||
ObjectStorePutOpts,
|
||||
ObjectStoreStatus,
|
||||
PubAck,
|
||||
Pullable,
|
||||
RoKV,
|
||||
StoredMsg,
|
||||
Stream,
|
||||
StreamAPI,
|
||||
Streams,
|
||||
Views,
|
||||
} from "./types.ts";
|
||||
|
||||
export type { StreamNames } from "./jsbaseclient_api.ts";
|
||||
export type {
|
||||
AccountLimits,
|
||||
ApiPagedRequest,
|
||||
ClusterInfo,
|
||||
ConsumerConfig,
|
||||
ConsumerInfo,
|
||||
ConsumerUpdateConfig,
|
||||
ExternalStream,
|
||||
JetStreamAccountStats,
|
||||
JetStreamApiStats,
|
||||
JetStreamUsageAccountLimits,
|
||||
LastForMsgRequest,
|
||||
LostStreamData,
|
||||
MsgDeleteRequest,
|
||||
MsgRequest,
|
||||
PeerInfo,
|
||||
Placement,
|
||||
PullOptions,
|
||||
PurgeBySeq,
|
||||
PurgeBySubject,
|
||||
PurgeOpts,
|
||||
PurgeResponse,
|
||||
PurgeTrimOpts,
|
||||
Republish,
|
||||
SeqMsgRequest,
|
||||
SequenceInfo,
|
||||
StreamAlternate,
|
||||
StreamConfig,
|
||||
StreamConsumerLimits,
|
||||
StreamInfo,
|
||||
StreamSource,
|
||||
StreamSourceInfo,
|
||||
StreamState,
|
||||
StreamUpdateConfig,
|
||||
SubjectTransformConfig,
|
||||
} from "./jsapi_types.ts";
|
||||
|
||||
export type { JsMsg } from "./jsmsg.ts";
|
||||
export type { Lister } from "./jslister.ts";
|
||||
|
||||
export {
|
||||
AckPolicy,
|
||||
DeliverPolicy,
|
||||
DiscardPolicy,
|
||||
ReplayPolicy,
|
||||
RetentionPolicy,
|
||||
StorageType,
|
||||
StoreCompression,
|
||||
} from "./jsapi_types.ts";
|
||||
|
||||
export type { ConsumerAPI } from "./jsmconsumer_api.ts";
|
||||
export type { DeliveryInfo, StreamInfoRequestOptions } from "./jsapi_types.ts";
|
||||
|
||||
export type {
|
||||
ConsumeBytes,
|
||||
ConsumeCallback,
|
||||
ConsumeMessages,
|
||||
ConsumeOptions,
|
||||
Consumer,
|
||||
ConsumerCallbackFn,
|
||||
ConsumerMessages,
|
||||
ConsumerStatus,
|
||||
Expires,
|
||||
FetchBytes,
|
||||
FetchMessages,
|
||||
FetchOptions,
|
||||
IdleHeartbeat,
|
||||
MaxBytes,
|
||||
MaxMessages,
|
||||
OrderedConsumerOptions,
|
||||
ThresholdBytes,
|
||||
ThresholdMessages,
|
||||
} from "./consumer.ts";
|
||||
export { ConsumerDebugEvents, ConsumerEvents } from "./consumer.ts";
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright 2021-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Empty } from "../nats-base-client/encoders.ts";
|
||||
import { Codec, JSONCodec } from "../nats-base-client/codec.ts";
|
||||
import { extend } from "../nats-base-client/util.ts";
|
||||
import { NatsConnectionImpl } from "../nats-base-client/nats.ts";
|
||||
import { checkJsErrorCode } from "./jsutil.ts";
|
||||
import {
|
||||
JetStreamOptions,
|
||||
Msg,
|
||||
NatsConnection,
|
||||
RequestOptions,
|
||||
} from "../nats-base-client/core.ts";
|
||||
import { ApiResponse } from "./jsapi_types.ts";
|
||||
|
||||
const defaultPrefix = "$JS.API";
|
||||
const defaultTimeout = 5000;
|
||||
|
||||
export function defaultJsOptions(opts?: JetStreamOptions): JetStreamOptions {
|
||||
opts = opts || {} as JetStreamOptions;
|
||||
if (opts.domain) {
|
||||
opts.apiPrefix = `$JS.${opts.domain}.API`;
|
||||
delete opts.domain;
|
||||
}
|
||||
return extend({ apiPrefix: defaultPrefix, timeout: defaultTimeout }, opts);
|
||||
}
|
||||
|
||||
export interface StreamNames {
|
||||
streams: string[];
|
||||
}
|
||||
|
||||
export interface StreamNameBySubject {
|
||||
subject: string;
|
||||
}
|
||||
|
||||
export class BaseApiClient {
|
||||
nc: NatsConnectionImpl;
|
||||
opts: JetStreamOptions;
|
||||
prefix: string;
|
||||
timeout: number;
|
||||
jc: Codec<unknown>;
|
||||
|
||||
constructor(nc: NatsConnection, opts?: JetStreamOptions) {
|
||||
this.nc = nc as NatsConnectionImpl;
|
||||
this.opts = defaultJsOptions(opts);
|
||||
this._parseOpts();
|
||||
this.prefix = this.opts.apiPrefix!;
|
||||
this.timeout = this.opts.timeout!;
|
||||
this.jc = JSONCodec();
|
||||
}
|
||||
|
||||
getOptions(): JetStreamOptions {
|
||||
return Object.assign({}, this.opts);
|
||||
}
|
||||
|
||||
_parseOpts() {
|
||||
let prefix = this.opts.apiPrefix;
|
||||
if (!prefix || prefix.length === 0) {
|
||||
throw new Error("invalid empty prefix");
|
||||
}
|
||||
const c = prefix[prefix.length - 1];
|
||||
if (c === ".") {
|
||||
prefix = prefix.substr(0, prefix.length - 1);
|
||||
}
|
||||
this.opts.apiPrefix = prefix;
|
||||
}
|
||||
|
||||
async _request(
|
||||
subj: string,
|
||||
data: unknown = null,
|
||||
opts?: RequestOptions,
|
||||
): Promise<unknown> {
|
||||
opts = opts || {} as RequestOptions;
|
||||
opts.timeout = this.timeout;
|
||||
|
||||
let a: Uint8Array = Empty;
|
||||
if (data) {
|
||||
a = this.jc.encode(data);
|
||||
}
|
||||
|
||||
const m = await this.nc.request(
|
||||
subj,
|
||||
a,
|
||||
opts,
|
||||
);
|
||||
return this.parseJsResponse(m);
|
||||
}
|
||||
|
||||
async findStream(subject: string): Promise<string> {
|
||||
const q = { subject } as StreamNameBySubject;
|
||||
const r = await this._request(`${this.prefix}.STREAM.NAMES`, q);
|
||||
const names = r as StreamNames;
|
||||
if (!names.streams || names.streams.length !== 1) {
|
||||
throw new Error("no stream matches subject");
|
||||
}
|
||||
return names.streams[0];
|
||||
}
|
||||
|
||||
getConnection(): NatsConnection {
|
||||
return this.nc;
|
||||
}
|
||||
|
||||
parseJsResponse(m: Msg): unknown {
|
||||
const v = this.jc.decode(m.data);
|
||||
const r = v as ApiResponse;
|
||||
if (r.error) {
|
||||
const err = checkJsErrorCode(r.error.code, r.error.description);
|
||||
if (err !== null) {
|
||||
err.api_error = r.error;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return v;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,117 @@
|
||||
/*
|
||||
* Copyright 2021-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { BaseApiClient } from "./jsbaseclient_api.ts";
|
||||
import {
|
||||
ApiPaged,
|
||||
ApiPagedRequest,
|
||||
ApiResponse,
|
||||
ConsumerListResponse,
|
||||
StreamListResponse,
|
||||
} from "./jsapi_types.ts";
|
||||
|
||||
/**
|
||||
* An interface for listing. Returns a promise with typed list.
|
||||
*/
|
||||
export interface Lister<T> {
|
||||
[Symbol.asyncIterator](): AsyncIterator<T>;
|
||||
|
||||
next(): Promise<T[]>;
|
||||
}
|
||||
|
||||
export type ListerFieldFilter<T> = (v: unknown) => T[];
|
||||
|
||||
export class ListerImpl<T> implements Lister<T>, AsyncIterable<T> {
|
||||
err?: Error;
|
||||
offset: number;
|
||||
pageInfo: ApiPaged;
|
||||
subject: string;
|
||||
jsm: BaseApiClient;
|
||||
filter: ListerFieldFilter<T>;
|
||||
payload: unknown;
|
||||
|
||||
constructor(
|
||||
subject: string,
|
||||
filter: ListerFieldFilter<T>,
|
||||
jsm: BaseApiClient,
|
||||
payload?: unknown,
|
||||
) {
|
||||
if (!subject) {
|
||||
throw new Error("subject is required");
|
||||
}
|
||||
this.subject = subject;
|
||||
this.jsm = jsm;
|
||||
this.offset = 0;
|
||||
this.pageInfo = {} as ApiPaged;
|
||||
this.filter = filter;
|
||||
this.payload = payload || {};
|
||||
}
|
||||
|
||||
async next(): Promise<T[]> {
|
||||
if (this.err) {
|
||||
return [];
|
||||
}
|
||||
if (this.pageInfo && this.offset >= this.pageInfo.total) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const offset = { offset: this.offset } as ApiPagedRequest;
|
||||
if (this.payload) {
|
||||
Object.assign(offset, this.payload);
|
||||
}
|
||||
try {
|
||||
const r = await this.jsm._request(
|
||||
this.subject,
|
||||
offset,
|
||||
{ timeout: this.jsm.timeout },
|
||||
);
|
||||
this.pageInfo = r as ApiPaged;
|
||||
// offsets are reported in total, so need to count
|
||||
// all the entries returned
|
||||
this.offset += this.countResponse(r as ApiResponse);
|
||||
const a = this.filter(r);
|
||||
return a;
|
||||
} catch (err) {
|
||||
this.err = err;
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
countResponse(r?: ApiResponse): number {
|
||||
switch (r?.type) {
|
||||
case "io.nats.jetstream.api.v1.stream_names_response":
|
||||
case "io.nats.jetstream.api.v1.stream_list_response":
|
||||
return (r as StreamListResponse).streams.length;
|
||||
case "io.nats.jetstream.api.v1.consumer_list_response":
|
||||
return (r as ConsumerListResponse).consumers.length;
|
||||
default:
|
||||
console.error(
|
||||
`jslister.ts: unknown API response for paged output: ${r?.type}`,
|
||||
);
|
||||
// has to be a stream...
|
||||
return (r as StreamListResponse).streams?.length || 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
async *[Symbol.asyncIterator]() {
|
||||
let page = await this.next();
|
||||
while (page.length > 0) {
|
||||
for (const item of page) {
|
||||
yield item;
|
||||
}
|
||||
page = await this.next();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,173 @@
|
||||
/*
|
||||
* Copyright 2021-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { BaseApiClient } from "./jsbaseclient_api.ts";
|
||||
import { StreamAPIImpl } from "./jsmstream_api.ts";
|
||||
import { ConsumerAPI, ConsumerAPIImpl } from "./jsmconsumer_api.ts";
|
||||
import { QueuedIteratorImpl } from "../nats-base-client/queued_iterator.ts";
|
||||
import {
|
||||
Advisory,
|
||||
AdvisoryKind,
|
||||
DirectMsg,
|
||||
DirectMsgHeaders,
|
||||
DirectStreamAPI,
|
||||
JetStreamClient,
|
||||
JetStreamManager,
|
||||
StoredMsg,
|
||||
StreamAPI,
|
||||
} from "./types.ts";
|
||||
import {
|
||||
JetStreamOptions,
|
||||
Msg,
|
||||
MsgHdrs,
|
||||
NatsConnection,
|
||||
ReviverFn,
|
||||
} from "../nats-base-client/core.ts";
|
||||
import {
|
||||
AccountInfoResponse,
|
||||
ApiResponse,
|
||||
DirectMsgRequest,
|
||||
JetStreamAccountStats,
|
||||
LastForMsgRequest,
|
||||
} from "./jsapi_types.ts";
|
||||
import { checkJsError, validateStreamName } from "./jsutil.ts";
|
||||
import { Empty, TD } from "../nats-base-client/encoders.ts";
|
||||
import { Codec, JSONCodec } from "../nats-base-client/codec.ts";
|
||||
|
||||
export class DirectStreamAPIImpl extends BaseApiClient
|
||||
implements DirectStreamAPI {
|
||||
constructor(nc: NatsConnection, opts?: JetStreamOptions) {
|
||||
super(nc, opts);
|
||||
}
|
||||
|
||||
async getMessage(
|
||||
stream: string,
|
||||
query: DirectMsgRequest,
|
||||
): Promise<StoredMsg> {
|
||||
validateStreamName(stream);
|
||||
// if doing a last_by_subj request, we append the subject
|
||||
// this allows last_by_subj to be subject to permissions (KV)
|
||||
let qq: DirectMsgRequest | null = query;
|
||||
const { last_by_subj } = qq as LastForMsgRequest;
|
||||
if (last_by_subj) {
|
||||
qq = null;
|
||||
}
|
||||
|
||||
const payload = qq ? this.jc.encode(qq) : Empty;
|
||||
const pre = this.opts.apiPrefix || "$JS.API";
|
||||
const subj = last_by_subj
|
||||
? `${pre}.DIRECT.GET.${stream}.${last_by_subj}`
|
||||
: `${pre}.DIRECT.GET.${stream}`;
|
||||
|
||||
const r = await this.nc.request(
|
||||
subj,
|
||||
payload,
|
||||
);
|
||||
|
||||
// response is not a JS.API response
|
||||
const err = checkJsError(r);
|
||||
if (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
const dm = new DirectMsgImpl(r);
|
||||
return Promise.resolve(dm);
|
||||
}
|
||||
}
|
||||
|
||||
export class DirectMsgImpl implements DirectMsg {
|
||||
data: Uint8Array;
|
||||
header: MsgHdrs;
|
||||
static jc?: Codec<unknown>;
|
||||
|
||||
constructor(m: Msg) {
|
||||
if (!m.headers) {
|
||||
throw new Error("headers expected");
|
||||
}
|
||||
this.data = m.data;
|
||||
this.header = m.headers;
|
||||
}
|
||||
|
||||
get subject(): string {
|
||||
return this.header.get(DirectMsgHeaders.Subject);
|
||||
}
|
||||
|
||||
get seq(): number {
|
||||
const v = this.header.get(DirectMsgHeaders.Sequence);
|
||||
return typeof v === "string" ? parseInt(v) : 0;
|
||||
}
|
||||
|
||||
get time(): Date {
|
||||
return new Date(Date.parse(this.timestamp));
|
||||
}
|
||||
|
||||
get timestamp(): string {
|
||||
return this.header.get(DirectMsgHeaders.TimeStamp);
|
||||
}
|
||||
|
||||
get stream(): string {
|
||||
return this.header.get(DirectMsgHeaders.Stream);
|
||||
}
|
||||
|
||||
json<T = unknown>(reviver?: ReviverFn): T {
|
||||
return JSONCodec<T>(reviver).decode(this.data);
|
||||
}
|
||||
|
||||
string(): string {
|
||||
return TD.decode(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
export class JetStreamManagerImpl extends BaseApiClient
|
||||
implements JetStreamManager {
|
||||
streams: StreamAPI;
|
||||
consumers: ConsumerAPI;
|
||||
direct: DirectStreamAPI;
|
||||
constructor(nc: NatsConnection, opts?: JetStreamOptions) {
|
||||
super(nc, opts);
|
||||
this.streams = new StreamAPIImpl(nc, opts);
|
||||
this.consumers = new ConsumerAPIImpl(nc, opts);
|
||||
this.direct = new DirectStreamAPIImpl(nc, opts);
|
||||
}
|
||||
|
||||
async getAccountInfo(): Promise<JetStreamAccountStats> {
|
||||
const r = await this._request(`${this.prefix}.INFO`);
|
||||
return r as AccountInfoResponse;
|
||||
}
|
||||
|
||||
jetstream(): JetStreamClient {
|
||||
return this.nc.jetstream(this.getOptions());
|
||||
}
|
||||
|
||||
advisories(): AsyncIterable<Advisory> {
|
||||
const iter = new QueuedIteratorImpl<Advisory>();
|
||||
this.nc.subscribe(`$JS.EVENT.ADVISORY.>`, {
|
||||
callback: (err, msg) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
try {
|
||||
const d = this.parseJsResponse(msg) as ApiResponse;
|
||||
const chunks = d.type.split(".");
|
||||
const kind = chunks[chunks.length - 1];
|
||||
iter.push({ kind: kind as AdvisoryKind, data: d });
|
||||
} catch (err) {
|
||||
iter.stop(err);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return iter;
|
||||
}
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
/*
|
||||
* Copyright 2021-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { BaseApiClient } from "./jsbaseclient_api.ts";
|
||||
import { Lister, ListerFieldFilter, ListerImpl } from "./jslister.ts";
|
||||
import {
|
||||
minValidation,
|
||||
validateDurableName,
|
||||
validateStreamName,
|
||||
} from "./jsutil.ts";
|
||||
import { NatsConnectionImpl } from "../nats-base-client/nats.ts";
|
||||
import { Feature } from "../nats-base-client/semver.ts";
|
||||
import { JetStreamOptions, NatsConnection } from "../nats-base-client/core.ts";
|
||||
import {
|
||||
ConsumerApiAction,
|
||||
ConsumerConfig,
|
||||
ConsumerInfo,
|
||||
ConsumerListResponse,
|
||||
ConsumerUpdateConfig,
|
||||
CreateConsumerRequest,
|
||||
SuccessResponse,
|
||||
} from "./jsapi_types.ts";
|
||||
|
||||
export interface ConsumerAPI {
|
||||
/**
|
||||
* Returns the ConsumerInfo for the specified consumer in the specified stream.
|
||||
* @param stream
|
||||
* @param consumer
|
||||
*/
|
||||
info(stream: string, consumer: string): Promise<ConsumerInfo>;
|
||||
|
||||
/**
|
||||
* Adds a new consumer to the specified stream with the specified consumer options.
|
||||
* @param stream
|
||||
* @param cfg
|
||||
*/
|
||||
add(stream: string, cfg: Partial<ConsumerConfig>): Promise<ConsumerInfo>;
|
||||
|
||||
/**
|
||||
* Updates the consumer configuration for the specified consumer on the specified
|
||||
* stream that has the specified durable name.
|
||||
* @param stream
|
||||
* @param durable
|
||||
* @param cfg
|
||||
*/
|
||||
update(
|
||||
stream: string,
|
||||
durable: string,
|
||||
cfg: Partial<ConsumerUpdateConfig>,
|
||||
): Promise<ConsumerInfo>;
|
||||
|
||||
/**
|
||||
* Deletes the specified consumer name/durable from the specified stream.
|
||||
* @param stream
|
||||
* @param consumer
|
||||
*/
|
||||
delete(stream: string, consumer: string): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Lists all the consumers on the specfied streams
|
||||
* @param stream
|
||||
*/
|
||||
list(stream: string): Lister<ConsumerInfo>;
|
||||
}
|
||||
|
||||
export class ConsumerAPIImpl extends BaseApiClient implements ConsumerAPI {
|
||||
constructor(nc: NatsConnection, opts?: JetStreamOptions) {
|
||||
super(nc, opts);
|
||||
}
|
||||
|
||||
async add(
|
||||
stream: string,
|
||||
cfg: ConsumerConfig,
|
||||
action = ConsumerApiAction.Create,
|
||||
): Promise<ConsumerInfo> {
|
||||
validateStreamName(stream);
|
||||
|
||||
if (cfg.deliver_group && cfg.flow_control) {
|
||||
throw new Error(
|
||||
"jetstream flow control is not supported with queue groups",
|
||||
);
|
||||
}
|
||||
if (cfg.deliver_group && cfg.idle_heartbeat) {
|
||||
throw new Error(
|
||||
"jetstream idle heartbeat is not supported with queue groups",
|
||||
);
|
||||
}
|
||||
|
||||
const cr = {} as CreateConsumerRequest;
|
||||
cr.config = cfg;
|
||||
cr.stream_name = stream;
|
||||
cr.action = action;
|
||||
|
||||
if (cr.config.durable_name) {
|
||||
validateDurableName(cr.config.durable_name);
|
||||
}
|
||||
|
||||
const nci = this.nc as NatsConnectionImpl;
|
||||
let { min, ok: newAPI } = nci.features.get(
|
||||
Feature.JS_NEW_CONSUMER_CREATE_API,
|
||||
);
|
||||
|
||||
const name = cfg.name === "" ? undefined : cfg.name;
|
||||
if (name && !newAPI) {
|
||||
throw new Error(`consumer 'name' requires server ${min}`);
|
||||
}
|
||||
if (name) {
|
||||
try {
|
||||
minValidation("name", name);
|
||||
} catch (err) {
|
||||
// if we have a cannot contain the message, massage a bit
|
||||
const m = err.message;
|
||||
const idx = m.indexOf("cannot contain");
|
||||
if (idx !== -1) {
|
||||
throw new Error(`consumer 'name' ${m.substring(idx)}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
let subj;
|
||||
let consumerName = "";
|
||||
// new api doesn't support multiple filter subjects
|
||||
// this delayed until here because the consumer in an update could have
|
||||
// been created with the new API, and have a `name`
|
||||
if (Array.isArray(cfg.filter_subjects)) {
|
||||
const { min, ok } = nci.features.get(Feature.JS_MULTIPLE_CONSUMER_FILTER);
|
||||
if (!ok) {
|
||||
throw new Error(`consumer 'filter_subjects' requires server ${min}`);
|
||||
}
|
||||
newAPI = false;
|
||||
}
|
||||
if (cfg.metadata) {
|
||||
const { min, ok } = nci.features.get(Feature.JS_STREAM_CONSUMER_METADATA);
|
||||
if (!ok) {
|
||||
throw new Error(`consumer 'metadata' requires server ${min}`);
|
||||
}
|
||||
}
|
||||
if (newAPI) {
|
||||
consumerName = cfg.name ?? cfg.durable_name ?? "";
|
||||
}
|
||||
if (consumerName !== "") {
|
||||
let fs = cfg.filter_subject ?? undefined;
|
||||
if (fs === ">") {
|
||||
fs = undefined;
|
||||
}
|
||||
subj = fs !== undefined
|
||||
? `${this.prefix}.CONSUMER.CREATE.${stream}.${consumerName}.${fs}`
|
||||
: `${this.prefix}.CONSUMER.CREATE.${stream}.${consumerName}`;
|
||||
} else {
|
||||
subj = cfg.durable_name
|
||||
? `${this.prefix}.CONSUMER.DURABLE.CREATE.${stream}.${cfg.durable_name}`
|
||||
: `${this.prefix}.CONSUMER.CREATE.${stream}`;
|
||||
}
|
||||
|
||||
const r = await this._request(subj, cr);
|
||||
return r as ConsumerInfo;
|
||||
}
|
||||
|
||||
async update(
|
||||
stream: string,
|
||||
durable: string,
|
||||
cfg: ConsumerUpdateConfig,
|
||||
): Promise<ConsumerInfo> {
|
||||
const ci = await this.info(stream, durable);
|
||||
const changable = cfg as ConsumerConfig;
|
||||
return this.add(
|
||||
stream,
|
||||
Object.assign(ci.config, changable),
|
||||
ConsumerApiAction.Update,
|
||||
);
|
||||
}
|
||||
|
||||
async info(stream: string, name: string): Promise<ConsumerInfo> {
|
||||
validateStreamName(stream);
|
||||
validateDurableName(name);
|
||||
const r = await this._request(
|
||||
`${this.prefix}.CONSUMER.INFO.${stream}.${name}`,
|
||||
);
|
||||
return r as ConsumerInfo;
|
||||
}
|
||||
|
||||
async delete(stream: string, name: string): Promise<boolean> {
|
||||
validateStreamName(stream);
|
||||
validateDurableName(name);
|
||||
const r = await this._request(
|
||||
`${this.prefix}.CONSUMER.DELETE.${stream}.${name}`,
|
||||
);
|
||||
const cr = r as SuccessResponse;
|
||||
return cr.success;
|
||||
}
|
||||
|
||||
list(stream: string): Lister<ConsumerInfo> {
|
||||
validateStreamName(stream);
|
||||
const filter: ListerFieldFilter<ConsumerInfo> = (
|
||||
v: unknown,
|
||||
): ConsumerInfo[] => {
|
||||
const clr = v as ConsumerListResponse;
|
||||
return clr.consumers;
|
||||
};
|
||||
const subj = `${this.prefix}.CONSUMER.LIST.${stream}`;
|
||||
return new ListerImpl<ConsumerInfo>(subj, filter, this);
|
||||
}
|
||||
}
|
@ -0,0 +1,292 @@
|
||||
/*
|
||||
* Copyright 2021-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { DataBuffer } from "../nats-base-client/databuffer.ts";
|
||||
import { JSONCodec, StringCodec } from "../nats-base-client/codec.ts";
|
||||
import { MsgImpl } from "../nats-base-client/msg.ts";
|
||||
import { ProtocolHandler } from "../nats-base-client/protocol.ts";
|
||||
import { RequestOne } from "../nats-base-client/request.ts";
|
||||
import { nanos } from "./jsutil.ts";
|
||||
import { Msg, MsgHdrs, RequestOptions } from "../nats-base-client/core.ts";
|
||||
import { DeliveryInfo, PullOptions } from "./jsapi_types.ts";
|
||||
|
||||
export const ACK = Uint8Array.of(43, 65, 67, 75);
|
||||
const NAK = Uint8Array.of(45, 78, 65, 75);
|
||||
const WPI = Uint8Array.of(43, 87, 80, 73);
|
||||
const NXT = Uint8Array.of(43, 78, 88, 84);
|
||||
const TERM = Uint8Array.of(43, 84, 69, 82, 77);
|
||||
const SPACE = Uint8Array.of(32);
|
||||
|
||||
/**
|
||||
* Represents a message stored in JetStream
|
||||
*/
|
||||
export interface JsMsg {
|
||||
/**
|
||||
* True if the message was redelivered
|
||||
*/
|
||||
redelivered: boolean;
|
||||
/**
|
||||
* The delivery info for the message
|
||||
*/
|
||||
info: DeliveryInfo;
|
||||
/**
|
||||
* The sequence number for the message
|
||||
*/
|
||||
seq: number;
|
||||
/**
|
||||
* Any headers associated with the message
|
||||
*/
|
||||
headers: MsgHdrs | undefined;
|
||||
/**
|
||||
* The message's data
|
||||
*/
|
||||
data: Uint8Array;
|
||||
/**
|
||||
* The subject on which the message was published
|
||||
*/
|
||||
subject: string;
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
sid: number;
|
||||
|
||||
/**
|
||||
* Indicate to the JetStream server that the message was processed
|
||||
* successfully.
|
||||
*/
|
||||
ack(): void;
|
||||
|
||||
/**
|
||||
* Indicate to the JetStream server that processing of the message
|
||||
* failed, and that it should be resent after the spefied number of
|
||||
* milliseconds.
|
||||
* @param millis
|
||||
*/
|
||||
nak(millis?: number): void;
|
||||
|
||||
/**
|
||||
* Indicate to the JetStream server that processing of the message
|
||||
* is on going, and that the ack wait timer for the message should be
|
||||
* reset preventing a redelivery.
|
||||
*/
|
||||
working(): void;
|
||||
|
||||
/**
|
||||
* !! this is an experimental feature - and could be removed
|
||||
*
|
||||
* next() combines ack() and pull(), requires the subject for a
|
||||
* subscription processing to process a message is provided
|
||||
* (can be the same) however, because the ability to specify
|
||||
* how long to keep the request open can be specified, this
|
||||
* functionality doesn't work well with iterators, as an error
|
||||
* (408s) are expected and needed to re-trigger a pull in case
|
||||
* there was a timeout. In an iterator, the error will close
|
||||
* the iterator, requiring a subscription to be reset.
|
||||
*/
|
||||
next(subj: string, ro?: Partial<PullOptions>): void;
|
||||
|
||||
/**
|
||||
* Indicate to the JetStream server that processing of the message
|
||||
* failed and that the message should not be sent to the consumer again.
|
||||
*/
|
||||
term(): void;
|
||||
|
||||
/**
|
||||
* Indicate to the JetStream server that the message was processed
|
||||
* successfully and that the JetStream server should acknowledge back
|
||||
* that the acknowledgement was received.
|
||||
*/
|
||||
ackAck(): Promise<boolean>;
|
||||
|
||||
/**
|
||||
* Convenience method to parse the message payload as JSON. This method
|
||||
* will throw an exception if there's a parsing error;
|
||||
*/
|
||||
json<T>(): T;
|
||||
|
||||
/**
|
||||
* Convenience method to parse the message payload as string. This method
|
||||
* may throw an exception if there's a conversion error
|
||||
*/
|
||||
string(): string;
|
||||
}
|
||||
|
||||
export function toJsMsg(m: Msg): JsMsg {
|
||||
return new JsMsgImpl(m);
|
||||
}
|
||||
|
||||
export function parseInfo(s: string): DeliveryInfo {
|
||||
const tokens = s.split(".");
|
||||
if (tokens.length === 9) {
|
||||
tokens.splice(2, 0, "_", "");
|
||||
}
|
||||
|
||||
if (
|
||||
(tokens.length < 11) || tokens[0] !== "$JS" || tokens[1] !== "ACK"
|
||||
) {
|
||||
throw new Error(`not js message`);
|
||||
}
|
||||
|
||||
// old
|
||||
// "$JS.ACK.<stream>.<consumer>.<redeliveryCount><streamSeq><deliverySequence>.<timestamp>.<pending>"
|
||||
// new
|
||||
// $JS.ACK.<domain>.<accounthash>.<stream>.<consumer>.<redeliveryCount>.<streamSeq>.<deliverySequence>.<timestamp>.<pending>.<random>
|
||||
const di = {} as DeliveryInfo;
|
||||
// if domain is "_", replace with blank
|
||||
di.domain = tokens[2] === "_" ? "" : tokens[2];
|
||||
di.account_hash = tokens[3];
|
||||
di.stream = tokens[4];
|
||||
di.consumer = tokens[5];
|
||||
di.redeliveryCount = parseInt(tokens[6], 10);
|
||||
di.redelivered = di.redeliveryCount > 1;
|
||||
di.streamSequence = parseInt(tokens[7], 10);
|
||||
di.deliverySequence = parseInt(tokens[8], 10);
|
||||
di.timestampNanos = parseInt(tokens[9], 10);
|
||||
di.pending = parseInt(tokens[10], 10);
|
||||
return di;
|
||||
}
|
||||
|
||||
export class JsMsgImpl implements JsMsg {
|
||||
msg: Msg;
|
||||
di?: DeliveryInfo;
|
||||
didAck: boolean;
|
||||
|
||||
constructor(msg: Msg) {
|
||||
this.msg = msg;
|
||||
this.didAck = false;
|
||||
}
|
||||
|
||||
get subject(): string {
|
||||
return this.msg.subject;
|
||||
}
|
||||
|
||||
get sid(): number {
|
||||
return this.msg.sid;
|
||||
}
|
||||
|
||||
get data(): Uint8Array {
|
||||
return this.msg.data;
|
||||
}
|
||||
|
||||
get headers(): MsgHdrs {
|
||||
return this.msg.headers!;
|
||||
}
|
||||
|
||||
get info(): DeliveryInfo {
|
||||
if (!this.di) {
|
||||
this.di = parseInfo(this.reply);
|
||||
}
|
||||
return this.di;
|
||||
}
|
||||
|
||||
get redelivered(): boolean {
|
||||
return this.info.redeliveryCount > 1;
|
||||
}
|
||||
|
||||
get reply(): string {
|
||||
return this.msg.reply || "";
|
||||
}
|
||||
|
||||
get seq(): number {
|
||||
return this.info.streamSequence;
|
||||
}
|
||||
|
||||
doAck(payload: Uint8Array) {
|
||||
if (!this.didAck) {
|
||||
// all acks are final with the exception of +WPI
|
||||
this.didAck = !this.isWIP(payload);
|
||||
this.msg.respond(payload);
|
||||
}
|
||||
}
|
||||
|
||||
isWIP(p: Uint8Array) {
|
||||
return p.length === 4 && p[0] === WPI[0] && p[1] === WPI[1] &&
|
||||
p[2] === WPI[2] && p[3] === WPI[3];
|
||||
}
|
||||
|
||||
// this has to dig into the internals as the message has access
|
||||
// to the protocol but not the high-level client.
|
||||
async ackAck(): Promise<boolean> {
|
||||
if (!this.didAck) {
|
||||
this.didAck = true;
|
||||
if (this.msg.reply) {
|
||||
const mi = this.msg as MsgImpl;
|
||||
const proto = mi.publisher as unknown as ProtocolHandler;
|
||||
const r = new RequestOne(proto.muxSubscriptions, this.msg.reply);
|
||||
proto.request(r);
|
||||
try {
|
||||
proto.publish(
|
||||
this.msg.reply,
|
||||
ACK,
|
||||
{
|
||||
reply: `${proto.muxSubscriptions.baseInbox}${r.token}`,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
r.cancel(err);
|
||||
}
|
||||
try {
|
||||
await Promise.race([r.timer, r.deferred]);
|
||||
return true;
|
||||
} catch (err) {
|
||||
r.cancel(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
ack() {
|
||||
this.doAck(ACK);
|
||||
}
|
||||
|
||||
nak(millis?: number) {
|
||||
let payload = NAK;
|
||||
if (millis) {
|
||||
payload = StringCodec().encode(
|
||||
`-NAK ${JSON.stringify({ delay: nanos(millis) })}`,
|
||||
);
|
||||
}
|
||||
this.doAck(payload);
|
||||
}
|
||||
|
||||
working() {
|
||||
this.doAck(WPI);
|
||||
}
|
||||
|
||||
next(subj: string, opts: Partial<PullOptions> = { batch: 1 }) {
|
||||
const args: Partial<PullOptions> = {};
|
||||
args.batch = opts.batch || 1;
|
||||
args.no_wait = opts.no_wait || false;
|
||||
if (opts.expires && opts.expires > 0) {
|
||||
args.expires = nanos(opts.expires);
|
||||
}
|
||||
const data = JSONCodec().encode(args);
|
||||
const payload = DataBuffer.concat(NXT, SPACE, data);
|
||||
const reqOpts = subj ? { reply: subj } as RequestOptions : undefined;
|
||||
this.msg.respond(payload, reqOpts);
|
||||
}
|
||||
|
||||
term() {
|
||||
this.doAck(TERM);
|
||||
}
|
||||
|
||||
json<T = unknown>(): T {
|
||||
return this.msg.json();
|
||||
}
|
||||
|
||||
string(): string {
|
||||
return this.msg.string();
|
||||
}
|
||||
}
|
@ -0,0 +1,592 @@
|
||||
/*
|
||||
* Copyright 2021-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Empty, MsgHdrs } from "../nats-base-client/types.ts";
|
||||
import { BaseApiClient, StreamNames } from "./jsbaseclient_api.ts";
|
||||
import { Lister, ListerFieldFilter, ListerImpl } from "./jslister.ts";
|
||||
import { validateStreamName } from "./jsutil.ts";
|
||||
import { headers, MsgHdrsImpl } from "../nats-base-client/headers.ts";
|
||||
import { KvStatusImpl } from "./kv.ts";
|
||||
import { ObjectStoreStatusImpl, osPrefix } from "./objectstore.ts";
|
||||
import { Codec, JSONCodec } from "../nats-base-client/codec.ts";
|
||||
import { TD } from "../nats-base-client/encoders.ts";
|
||||
import { Feature } from "../nats-base-client/semver.ts";
|
||||
import { NatsConnectionImpl } from "../nats-base-client/nats.ts";
|
||||
import {
|
||||
Consumers,
|
||||
kvPrefix,
|
||||
KvStatus,
|
||||
ObjectStoreStatus,
|
||||
StoredMsg,
|
||||
Stream,
|
||||
StreamAPI,
|
||||
Streams,
|
||||
} from "./types.ts";
|
||||
import {
|
||||
JetStreamOptions,
|
||||
NatsConnection,
|
||||
ReviverFn,
|
||||
} from "../nats-base-client/core.ts";
|
||||
import {
|
||||
ApiPagedRequest,
|
||||
ExternalStream,
|
||||
MsgDeleteRequest,
|
||||
MsgRequest,
|
||||
PurgeBySeq,
|
||||
PurgeOpts,
|
||||
PurgeResponse,
|
||||
PurgeTrimOpts,
|
||||
StreamAlternate,
|
||||
StreamConfig,
|
||||
StreamInfo,
|
||||
StreamInfoRequestOptions,
|
||||
StreamListResponse,
|
||||
StreamMsgResponse,
|
||||
StreamSource,
|
||||
StreamUpdateConfig,
|
||||
SuccessResponse,
|
||||
} from "./jsapi_types.ts";
|
||||
import {
|
||||
Consumer,
|
||||
OrderedConsumerOptions,
|
||||
OrderedPullConsumerImpl,
|
||||
PullConsumerImpl,
|
||||
} from "./consumer.ts";
|
||||
import { ConsumerAPI, ConsumerAPIImpl } from "./jsmconsumer_api.ts";
|
||||
|
||||
export function convertStreamSourceDomain(s?: StreamSource) {
|
||||
if (s === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const { domain } = s;
|
||||
if (domain === undefined) {
|
||||
return s;
|
||||
}
|
||||
const copy = Object.assign({}, s) as StreamSource;
|
||||
delete copy.domain;
|
||||
|
||||
if (domain === "") {
|
||||
return copy;
|
||||
}
|
||||
if (copy.external) {
|
||||
throw new Error("domain and external are both set");
|
||||
}
|
||||
copy.external = { api: `$JS.${domain}.API` } as ExternalStream;
|
||||
return copy;
|
||||
}
|
||||
|
||||
export class ConsumersImpl implements Consumers {
|
||||
api: ConsumerAPI;
|
||||
notified: boolean;
|
||||
|
||||
constructor(api: ConsumerAPI) {
|
||||
this.api = api;
|
||||
this.notified = false;
|
||||
}
|
||||
|
||||
checkVersion(): Promise<void> {
|
||||
const fv = (this.api as ConsumerAPIImpl).nc.features.get(
|
||||
Feature.JS_SIMPLIFICATION,
|
||||
);
|
||||
if (!fv.ok) {
|
||||
return Promise.reject(
|
||||
new Error(
|
||||
`consumers framework is only supported on servers ${fv.min} or better`,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async get(
|
||||
stream: string,
|
||||
name: string | Partial<OrderedConsumerOptions> = {},
|
||||
): Promise<Consumer> {
|
||||
if (typeof name === "object") {
|
||||
return this.ordered(stream, name);
|
||||
}
|
||||
// check we have support for pending msgs and header notifications
|
||||
await this.checkVersion();
|
||||
|
||||
return this.api.info(stream, name)
|
||||
.then((ci) => {
|
||||
if (ci.config.deliver_subject !== undefined) {
|
||||
return Promise.reject(new Error("push consumer not supported"));
|
||||
}
|
||||
return new PullConsumerImpl(this.api, ci);
|
||||
})
|
||||
.catch((err) => {
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
|
||||
async ordered(
|
||||
stream: string,
|
||||
opts?: Partial<OrderedConsumerOptions>,
|
||||
): Promise<Consumer> {
|
||||
await this.checkVersion();
|
||||
|
||||
const impl = this.api as ConsumerAPIImpl;
|
||||
const sapi = new StreamAPIImpl(impl.nc, impl.opts);
|
||||
return sapi.info(stream)
|
||||
.then((_si) => {
|
||||
return Promise.resolve(
|
||||
new OrderedPullConsumerImpl(this.api, stream, opts),
|
||||
);
|
||||
})
|
||||
.catch((err) => {
|
||||
return Promise.reject(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class StreamImpl implements Stream {
|
||||
api: StreamAPIImpl;
|
||||
_info: StreamInfo;
|
||||
|
||||
constructor(api: StreamAPI, info: StreamInfo) {
|
||||
this.api = api as StreamAPIImpl;
|
||||
this._info = info;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this._info.config.name;
|
||||
}
|
||||
|
||||
alternates(): Promise<StreamAlternate[]> {
|
||||
return this.info()
|
||||
.then((si) => {
|
||||
return si.alternates ? si.alternates : [];
|
||||
});
|
||||
}
|
||||
|
||||
async best(): Promise<Stream> {
|
||||
await this.info();
|
||||
if (this._info.alternates) {
|
||||
const asi = await this.api.info(this._info.alternates[0].name);
|
||||
return new StreamImpl(this.api, asi);
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
info(
|
||||
cached = false,
|
||||
opts?: Partial<StreamInfoRequestOptions>,
|
||||
): Promise<StreamInfo> {
|
||||
if (cached) {
|
||||
return Promise.resolve(this._info);
|
||||
}
|
||||
return this.api.info(this.name, opts)
|
||||
.then((si) => {
|
||||
this._info = si;
|
||||
return this._info;
|
||||
});
|
||||
}
|
||||
|
||||
getConsumer(
|
||||
name?: string | Partial<OrderedConsumerOptions>,
|
||||
): Promise<Consumer> {
|
||||
return new ConsumersImpl(new ConsumerAPIImpl(this.api.nc, this.api.opts))
|
||||
.get(this.name, name);
|
||||
}
|
||||
|
||||
getMessage(query: MsgRequest): Promise<StoredMsg> {
|
||||
return this.api.getMessage(this.name, query);
|
||||
}
|
||||
|
||||
deleteMessage(seq: number, erase?: boolean): Promise<boolean> {
|
||||
return this.api.deleteMessage(this.name, seq, erase);
|
||||
}
|
||||
}
|
||||
|
||||
export class StreamAPIImpl extends BaseApiClient implements StreamAPI {
|
||||
constructor(nc: NatsConnection, opts?: JetStreamOptions) {
|
||||
super(nc, opts);
|
||||
}
|
||||
|
||||
checkStreamConfigVersions(cfg: Partial<StreamConfig>) {
|
||||
const nci = this.nc as NatsConnectionImpl;
|
||||
if (cfg.metadata) {
|
||||
const { min, ok } = nci.features.get(Feature.JS_STREAM_CONSUMER_METADATA);
|
||||
if (!ok) {
|
||||
throw new Error(`stream 'metadata' requires server ${min}`);
|
||||
}
|
||||
}
|
||||
if (cfg.first_seq) {
|
||||
const { min, ok } = nci.features.get(Feature.JS_STREAM_FIRST_SEQ);
|
||||
if (!ok) {
|
||||
throw new Error(`stream 'first_seq' requires server ${min}`);
|
||||
}
|
||||
}
|
||||
if (cfg.subject_transform) {
|
||||
const { min, ok } = nci.features.get(Feature.JS_STREAM_SUBJECT_TRANSFORM);
|
||||
if (!ok) {
|
||||
throw new Error(`stream 'subject_transform' requires server ${min}`);
|
||||
}
|
||||
}
|
||||
if (cfg.compression) {
|
||||
const { min, ok } = nci.features.get(Feature.JS_STREAM_COMPRESSION);
|
||||
if (!ok) {
|
||||
throw new Error(`stream 'compression' requires server ${min}`);
|
||||
}
|
||||
}
|
||||
if (cfg.consumer_limits) {
|
||||
const { min, ok } = nci.features.get(Feature.JS_DEFAULT_CONSUMER_LIMITS);
|
||||
if (!ok) {
|
||||
throw new Error(`stream 'consumer_limits' requires server ${min}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateStreamSource(
|
||||
context: string,
|
||||
src: Partial<StreamSource>,
|
||||
): void {
|
||||
const count = src.subject_transforms?.length || 0;
|
||||
if (count > 0) {
|
||||
const { min, ok } = nci.features.get(
|
||||
Feature.JS_STREAM_SOURCE_SUBJECT_TRANSFORM,
|
||||
);
|
||||
if (!ok) {
|
||||
throw new Error(
|
||||
`${context} 'subject_transforms' requires server ${min}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg.sources) {
|
||||
cfg.sources.forEach((src) => {
|
||||
validateStreamSource("stream sources", src);
|
||||
});
|
||||
}
|
||||
|
||||
if (cfg.mirror) {
|
||||
validateStreamSource("stream mirror", cfg.mirror);
|
||||
}
|
||||
}
|
||||
|
||||
async add(cfg = {} as Partial<StreamConfig>): Promise<StreamInfo> {
|
||||
this.checkStreamConfigVersions(cfg);
|
||||
validateStreamName(cfg.name);
|
||||
cfg.mirror = convertStreamSourceDomain(cfg.mirror);
|
||||
//@ts-ignore: the sources are either set or not - so no item should be undefined in the list
|
||||
cfg.sources = cfg.sources?.map(convertStreamSourceDomain);
|
||||
const r = await this._request(
|
||||
`${this.prefix}.STREAM.CREATE.${cfg.name}`,
|
||||
cfg,
|
||||
);
|
||||
const si = r as StreamInfo;
|
||||
this._fixInfo(si);
|
||||
|
||||
return si;
|
||||
}
|
||||
|
||||
async delete(stream: string): Promise<boolean> {
|
||||
validateStreamName(stream);
|
||||
const r = await this._request(`${this.prefix}.STREAM.DELETE.${stream}`);
|
||||
const cr = r as SuccessResponse;
|
||||
return cr.success;
|
||||
}
|
||||
|
||||
async update(
|
||||
name: string,
|
||||
cfg = {} as Partial<StreamUpdateConfig>,
|
||||
): Promise<StreamInfo> {
|
||||
if (typeof name === "object") {
|
||||
const sc = name as StreamConfig;
|
||||
name = sc.name;
|
||||
cfg = sc;
|
||||
console.trace(
|
||||
`\u001B[33m >> streams.update(config: StreamConfig) api changed to streams.update(name: string, config: StreamUpdateConfig) - this shim will be removed - update your code. \u001B[0m`,
|
||||
);
|
||||
}
|
||||
this.checkStreamConfigVersions(cfg);
|
||||
validateStreamName(name);
|
||||
const old = await this.info(name);
|
||||
const update = Object.assign(old.config, cfg);
|
||||
update.mirror = convertStreamSourceDomain(update.mirror);
|
||||
//@ts-ignore: the sources are either set or not - so no item should be undefined in the list
|
||||
update.sources = update.sources?.map(convertStreamSourceDomain);
|
||||
|
||||
const r = await this._request(
|
||||
`${this.prefix}.STREAM.UPDATE.${name}`,
|
||||
update,
|
||||
);
|
||||
const si = r as StreamInfo;
|
||||
this._fixInfo(si);
|
||||
return si;
|
||||
}
|
||||
|
||||
async info(
|
||||
name: string,
|
||||
data?: Partial<StreamInfoRequestOptions>,
|
||||
): Promise<StreamInfo> {
|
||||
validateStreamName(name);
|
||||
const subj = `${this.prefix}.STREAM.INFO.${name}`;
|
||||
const r = await this._request(subj, data);
|
||||
let si = r as StreamInfo;
|
||||
let { total, limit } = si;
|
||||
|
||||
// check how many subjects we got in the first request
|
||||
let have = si.state.subjects
|
||||
? Object.getOwnPropertyNames(si.state.subjects).length
|
||||
: 1;
|
||||
|
||||
// if the response is paged, we have a large list of subjects
|
||||
// handle the paging and return a StreamInfo with all of it
|
||||
if (total && total > have) {
|
||||
const infos: StreamInfo[] = [si];
|
||||
const paged = data || {} as unknown as ApiPagedRequest;
|
||||
let i = 0;
|
||||
// total could change, so it is possible to have collected
|
||||
// more that the total
|
||||
while (total > have) {
|
||||
i++;
|
||||
paged.offset = limit * i;
|
||||
const r = await this._request(subj, paged) as StreamInfo;
|
||||
// update it in case it changed
|
||||
total = r.total;
|
||||
infos.push(r);
|
||||
const count = Object.getOwnPropertyNames(r.state.subjects).length;
|
||||
have += count;
|
||||
// if request returns less than limit it is done
|
||||
if (count < limit) {
|
||||
// done
|
||||
break;
|
||||
}
|
||||
}
|
||||
// collect all the subjects
|
||||
let subjects = {};
|
||||
for (let i = 0; i < infos.length; i++) {
|
||||
si = infos[i];
|
||||
if (si.state.subjects) {
|
||||
subjects = Object.assign(subjects, si.state.subjects);
|
||||
}
|
||||
}
|
||||
// don't give the impression we paged
|
||||
si.offset = 0;
|
||||
si.total = 0;
|
||||
si.limit = 0;
|
||||
si.state.subjects = subjects;
|
||||
}
|
||||
this._fixInfo(si);
|
||||
return si;
|
||||
}
|
||||
|
||||
list(subject = ""): Lister<StreamInfo> {
|
||||
const payload = subject?.length ? { subject } : {};
|
||||
const listerFilter: ListerFieldFilter<StreamInfo> = (
|
||||
v: unknown,
|
||||
): StreamInfo[] => {
|
||||
const slr = v as StreamListResponse;
|
||||
slr.streams.forEach((si) => {
|
||||
this._fixInfo(si);
|
||||
});
|
||||
return slr.streams;
|
||||
};
|
||||
const subj = `${this.prefix}.STREAM.LIST`;
|
||||
return new ListerImpl<StreamInfo>(subj, listerFilter, this, payload);
|
||||
}
|
||||
|
||||
// FIXME: init of sealed, deny_delete, deny_purge shouldn't be necessary
|
||||
// https://github.com/nats-io/nats-server/issues/2633
|
||||
_fixInfo(si: StreamInfo) {
|
||||
si.config.sealed = si.config.sealed || false;
|
||||
si.config.deny_delete = si.config.deny_delete || false;
|
||||
si.config.deny_purge = si.config.deny_purge || false;
|
||||
si.config.allow_rollup_hdrs = si.config.allow_rollup_hdrs || false;
|
||||
}
|
||||
|
||||
async purge(name: string, opts?: PurgeOpts): Promise<PurgeResponse> {
|
||||
if (opts) {
|
||||
const { keep, seq } = opts as PurgeBySeq & PurgeTrimOpts;
|
||||
if (typeof keep === "number" && typeof seq === "number") {
|
||||
throw new Error("can specify one of keep or seq");
|
||||
}
|
||||
}
|
||||
validateStreamName(name);
|
||||
const v = await this._request(`${this.prefix}.STREAM.PURGE.${name}`, opts);
|
||||
return v as PurgeResponse;
|
||||
}
|
||||
|
||||
async deleteMessage(
|
||||
stream: string,
|
||||
seq: number,
|
||||
erase = true,
|
||||
): Promise<boolean> {
|
||||
validateStreamName(stream);
|
||||
const dr = { seq } as MsgDeleteRequest;
|
||||
if (!erase) {
|
||||
dr.no_erase = true;
|
||||
}
|
||||
const r = await this._request(
|
||||
`${this.prefix}.STREAM.MSG.DELETE.${stream}`,
|
||||
dr,
|
||||
);
|
||||
const cr = r as SuccessResponse;
|
||||
return cr.success;
|
||||
}
|
||||
|
||||
async getMessage(stream: string, query: MsgRequest): Promise<StoredMsg> {
|
||||
validateStreamName(stream);
|
||||
const r = await this._request(
|
||||
`${this.prefix}.STREAM.MSG.GET.${stream}`,
|
||||
query,
|
||||
);
|
||||
const sm = r as StreamMsgResponse;
|
||||
return new StoredMsgImpl(sm);
|
||||
}
|
||||
|
||||
find(subject: string): Promise<string> {
|
||||
return this.findStream(subject);
|
||||
}
|
||||
|
||||
listKvs(): Lister<KvStatus> {
|
||||
const filter: ListerFieldFilter<KvStatus> = (
|
||||
v: unknown,
|
||||
): KvStatus[] => {
|
||||
const slr = v as StreamListResponse;
|
||||
const kvStreams = slr.streams.filter((v) => {
|
||||
return v.config.name.startsWith(kvPrefix);
|
||||
});
|
||||
kvStreams.forEach((si) => {
|
||||
this._fixInfo(si);
|
||||
});
|
||||
let cluster = "";
|
||||
if (kvStreams.length) {
|
||||
cluster = this.nc.info?.cluster ?? "";
|
||||
}
|
||||
const status = kvStreams.map((si) => {
|
||||
return new KvStatusImpl(si, cluster);
|
||||
});
|
||||
return status;
|
||||
};
|
||||
const subj = `${this.prefix}.STREAM.LIST`;
|
||||
return new ListerImpl<KvStatus>(subj, filter, this);
|
||||
}
|
||||
|
||||
listObjectStores(): Lister<ObjectStoreStatus> {
|
||||
const filter: ListerFieldFilter<ObjectStoreStatus> = (
|
||||
v: unknown,
|
||||
): ObjectStoreStatus[] => {
|
||||
const slr = v as StreamListResponse;
|
||||
const objStreams = slr.streams.filter((v) => {
|
||||
return v.config.name.startsWith(osPrefix);
|
||||
});
|
||||
objStreams.forEach((si) => {
|
||||
this._fixInfo(si);
|
||||
});
|
||||
const status = objStreams.map((si) => {
|
||||
return new ObjectStoreStatusImpl(si);
|
||||
});
|
||||
return status;
|
||||
};
|
||||
const subj = `${this.prefix}.STREAM.LIST`;
|
||||
return new ListerImpl<ObjectStoreStatus>(subj, filter, this);
|
||||
}
|
||||
|
||||
names(subject = ""): Lister<string> {
|
||||
const payload = subject?.length ? { subject } : {};
|
||||
const listerFilter: ListerFieldFilter<string> = (
|
||||
v: unknown,
|
||||
): string[] => {
|
||||
const sr = v as StreamNames;
|
||||
return sr.streams;
|
||||
};
|
||||
const subj = `${this.prefix}.STREAM.NAMES`;
|
||||
return new ListerImpl<string>(subj, listerFilter, this, payload);
|
||||
}
|
||||
|
||||
async get(name: string): Promise<Stream> {
|
||||
const si = await this.info(name);
|
||||
return Promise.resolve(new StreamImpl(this, si));
|
||||
}
|
||||
}
|
||||
|
||||
export class StoredMsgImpl implements StoredMsg {
|
||||
_header?: MsgHdrs;
|
||||
smr: StreamMsgResponse;
|
||||
static jc?: Codec<unknown>;
|
||||
|
||||
constructor(smr: StreamMsgResponse) {
|
||||
this.smr = smr;
|
||||
}
|
||||
|
||||
get subject(): string {
|
||||
return this.smr.message.subject;
|
||||
}
|
||||
|
||||
get seq(): number {
|
||||
return this.smr.message.seq;
|
||||
}
|
||||
|
||||
get timestamp(): string {
|
||||
return this.smr.message.time;
|
||||
}
|
||||
|
||||
get time(): Date {
|
||||
return new Date(Date.parse(this.timestamp));
|
||||
}
|
||||
|
||||
get data(): Uint8Array {
|
||||
return this.smr.message.data ? this._parse(this.smr.message.data) : Empty;
|
||||
}
|
||||
|
||||
get header(): MsgHdrs {
|
||||
if (!this._header) {
|
||||
if (this.smr.message.hdrs) {
|
||||
const hd = this._parse(this.smr.message.hdrs);
|
||||
this._header = MsgHdrsImpl.decode(hd);
|
||||
} else {
|
||||
this._header = headers();
|
||||
}
|
||||
}
|
||||
return this._header;
|
||||
}
|
||||
|
||||
_parse(s: string): Uint8Array {
|
||||
const bs = atob(s);
|
||||
const len = bs.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = bs.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
json<T = unknown>(reviver?: ReviverFn): T {
|
||||
return JSONCodec<T>(reviver).decode(this.data);
|
||||
}
|
||||
|
||||
string(): string {
|
||||
return TD.decode(this.data);
|
||||
}
|
||||
}
|
||||
|
||||
export class StreamsImpl implements Streams {
|
||||
api: StreamAPIImpl;
|
||||
|
||||
constructor(api: StreamAPI) {
|
||||
this.api = api as StreamAPIImpl;
|
||||
}
|
||||
|
||||
get(stream: string): Promise<Stream> {
|
||||
return this.api.info(stream)
|
||||
.then((si) => {
|
||||
return new StreamImpl(this.api, si);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,236 @@
|
||||
/*
|
||||
* Copyright 2021-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { Empty } from "../nats-base-client/encoders.ts";
|
||||
import { MsgArg } from "../nats-base-client/parser.ts";
|
||||
import { headers, MsgHdrsImpl } from "../nats-base-client/headers.ts";
|
||||
import { MsgImpl } from "../nats-base-client/msg.ts";
|
||||
import {
|
||||
ErrorCode,
|
||||
Msg,
|
||||
Nanos,
|
||||
NatsError,
|
||||
Publisher,
|
||||
} from "../nats-base-client/core.ts";
|
||||
|
||||
export function validateDurableName(name?: string) {
|
||||
return minValidation("durable", name);
|
||||
}
|
||||
|
||||
export function validateStreamName(name?: string) {
|
||||
return minValidation("stream", name);
|
||||
}
|
||||
|
||||
export function minValidation(context: string, name = "") {
|
||||
// minimum validation on streams/consumers matches nats cli
|
||||
if (name === "") {
|
||||
throw Error(`${context} name required`);
|
||||
}
|
||||
const bad = [".", "*", ">", "/", "\\", " ", "\t", "\n", "\r"];
|
||||
bad.forEach((v) => {
|
||||
if (name.indexOf(v) !== -1) {
|
||||
// make the error have a meaningful character
|
||||
switch (v) {
|
||||
case "\n":
|
||||
v = "\\n";
|
||||
break;
|
||||
case "\r":
|
||||
v = "\\r";
|
||||
break;
|
||||
case "\t":
|
||||
v = "\\t";
|
||||
break;
|
||||
default:
|
||||
// nothing
|
||||
}
|
||||
throw Error(
|
||||
`invalid ${context} name - ${context} name cannot contain '${v}'`,
|
||||
);
|
||||
}
|
||||
});
|
||||
return "";
|
||||
}
|
||||
|
||||
export function validateName(context: string, name = "") {
|
||||
if (name === "") {
|
||||
throw Error(`${context} name required`);
|
||||
}
|
||||
const m = validName(name);
|
||||
if (m.length) {
|
||||
throw new Error(`invalid ${context} name - ${context} name ${m}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function validName(name = ""): string {
|
||||
if (name === "") {
|
||||
throw Error(`name required`);
|
||||
}
|
||||
const RE = /^[-\w]+$/g;
|
||||
const m = name.match(RE);
|
||||
if (m === null) {
|
||||
for (const c of name.split("")) {
|
||||
const mm = c.match(RE);
|
||||
if (mm === null) {
|
||||
return `cannot contain '${c}'`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the specified millis into Nanos
|
||||
* @param millis
|
||||
*/
|
||||
export function nanos(millis: number): Nanos {
|
||||
return millis * 1000000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the specified Nanos into millis
|
||||
* @param ns
|
||||
*/
|
||||
export function millis(ns: Nanos) {
|
||||
return Math.floor(ns / 1000000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the message is a flow control message
|
||||
* @param msg
|
||||
*/
|
||||
export function isFlowControlMsg(msg: Msg): boolean {
|
||||
if (msg.data.length > 0) {
|
||||
return false;
|
||||
}
|
||||
const h = msg.headers;
|
||||
if (!h) {
|
||||
return false;
|
||||
}
|
||||
return h.code >= 100 && h.code < 200;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the message is a heart beat message
|
||||
* @param msg
|
||||
*/
|
||||
export function isHeartbeatMsg(msg: Msg): boolean {
|
||||
return isFlowControlMsg(msg) && msg.headers?.description === "Idle Heartbeat";
|
||||
}
|
||||
|
||||
export function newJsErrorMsg(
|
||||
code: number,
|
||||
description: string,
|
||||
subject: string,
|
||||
): Msg {
|
||||
const h = headers(code, description) as MsgHdrsImpl;
|
||||
|
||||
const arg = { hdr: 1, sid: 0, size: 0 } as MsgArg;
|
||||
const msg = new MsgImpl(arg, Empty, {} as Publisher);
|
||||
msg._headers = h;
|
||||
msg._subject = subject;
|
||||
|
||||
return msg;
|
||||
}
|
||||
|
||||
export function checkJsError(msg: Msg): NatsError | null {
|
||||
// JS error only if no payload - otherwise assume it is application data
|
||||
if (msg.data.length !== 0) {
|
||||
return null;
|
||||
}
|
||||
const h = msg.headers;
|
||||
if (!h) {
|
||||
return null;
|
||||
}
|
||||
return checkJsErrorCode(h.code, h.description);
|
||||
}
|
||||
|
||||
export enum Js409Errors {
|
||||
MaxBatchExceeded = "exceeded maxrequestbatch of",
|
||||
MaxExpiresExceeded = "exceeded maxrequestexpires of",
|
||||
MaxBytesExceeded = "exceeded maxrequestmaxbytes of",
|
||||
MaxMessageSizeExceeded = "message size exceeds maxbytes",
|
||||
PushConsumer = "consumer is push based",
|
||||
MaxWaitingExceeded = "exceeded maxwaiting", // not terminal
|
||||
IdleHeartbeatMissed = "idle heartbeats missed",
|
||||
ConsumerDeleted = "consumer deleted",
|
||||
// FIXME: consumer deleted - instead of no responder (terminal error)
|
||||
// leadership changed -
|
||||
}
|
||||
|
||||
let MAX_WAITING_FAIL = false;
|
||||
export function setMaxWaitingToFail(tf: boolean) {
|
||||
MAX_WAITING_FAIL = tf;
|
||||
}
|
||||
|
||||
export function isTerminal409(err: NatsError): boolean {
|
||||
if (err.code !== ErrorCode.JetStream409) {
|
||||
return false;
|
||||
}
|
||||
const fatal = [
|
||||
Js409Errors.MaxBatchExceeded,
|
||||
Js409Errors.MaxExpiresExceeded,
|
||||
Js409Errors.MaxBytesExceeded,
|
||||
Js409Errors.MaxMessageSizeExceeded,
|
||||
Js409Errors.PushConsumer,
|
||||
Js409Errors.IdleHeartbeatMissed,
|
||||
Js409Errors.ConsumerDeleted,
|
||||
];
|
||||
if (MAX_WAITING_FAIL) {
|
||||
fatal.push(Js409Errors.MaxWaitingExceeded);
|
||||
}
|
||||
|
||||
return fatal.find((s) => {
|
||||
return err.message.indexOf(s) !== -1;
|
||||
}) !== undefined;
|
||||
}
|
||||
|
||||
export function checkJsErrorCode(
|
||||
code: number,
|
||||
description = "",
|
||||
): NatsError | null {
|
||||
if (code < 300) {
|
||||
return null;
|
||||
}
|
||||
description = description.toLowerCase();
|
||||
switch (code) {
|
||||
case 404:
|
||||
// 404 for jetstream will provide different messages ensure we
|
||||
// keep whatever the server returned
|
||||
return new NatsError(description, ErrorCode.JetStream404NoMessages);
|
||||
case 408:
|
||||
return new NatsError(description, ErrorCode.JetStream408RequestTimeout);
|
||||
case 409: {
|
||||
// the description can be exceeded max waiting or max ack pending, which are
|
||||
// recoverable, but can also be terminal errors where the request exceeds
|
||||
// some value in the consumer configuration
|
||||
const ec = description.startsWith(Js409Errors.IdleHeartbeatMissed)
|
||||
? ErrorCode.JetStreamIdleHeartBeat
|
||||
: ErrorCode.JetStream409;
|
||||
return new NatsError(
|
||||
description,
|
||||
ec,
|
||||
);
|
||||
}
|
||||
case 503:
|
||||
return NatsError.errorForCode(
|
||||
ErrorCode.JetStreamNotEnabled,
|
||||
new Error(description),
|
||||
);
|
||||
default:
|
||||
if (description === "") {
|
||||
description = ErrorCode.Unknown;
|
||||
}
|
||||
return new NatsError(description, `${code}`);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Copyright 2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
export {
|
||||
checkJsError,
|
||||
isFlowControlMsg,
|
||||
isHeartbeatMsg,
|
||||
millis,
|
||||
nanos,
|
||||
} from "./internal_mod.ts";
|
||||
|
||||
export {
|
||||
AckPolicy,
|
||||
AdvisoryKind,
|
||||
ConsumerDebugEvents,
|
||||
ConsumerEvents,
|
||||
DeliverPolicy,
|
||||
DirectMsgHeaders,
|
||||
DiscardPolicy,
|
||||
JsHeaders,
|
||||
ReplayPolicy,
|
||||
RepublishHeaders,
|
||||
RetentionPolicy,
|
||||
StorageType,
|
||||
StoreCompression,
|
||||
} from "./internal_mod.ts";
|
||||
|
||||
export type {
|
||||
AccountLimits,
|
||||
Advisory,
|
||||
ApiPagedRequest,
|
||||
Closed,
|
||||
ClusterInfo,
|
||||
ConsumeBytes,
|
||||
ConsumeCallback,
|
||||
ConsumeMessages,
|
||||
ConsumeOptions,
|
||||
Consumer,
|
||||
ConsumerAPI,
|
||||
ConsumerCallbackFn,
|
||||
ConsumerConfig,
|
||||
ConsumerInfo,
|
||||
ConsumerInfoable,
|
||||
ConsumerMessages,
|
||||
ConsumerOpts,
|
||||
ConsumerOptsBuilder,
|
||||
Consumers,
|
||||
ConsumerStatus,
|
||||
ConsumerUpdateConfig,
|
||||
DeliveryInfo,
|
||||
Destroyable,
|
||||
Expires,
|
||||
ExternalStream,
|
||||
FetchBytes,
|
||||
FetchMessages,
|
||||
FetchOptions,
|
||||
IdleHeartbeat,
|
||||
JetStreamAccountStats,
|
||||
JetStreamApiStats,
|
||||
JetStreamClient,
|
||||
JetStreamManager,
|
||||
JetStreamManagerOptions,
|
||||
JetStreamOptions,
|
||||
JetStreamPublishOptions,
|
||||
JetStreamPullSubscription,
|
||||
JetStreamSubscription,
|
||||
JetStreamSubscriptionOptions,
|
||||
JetStreamUsageAccountLimits,
|
||||
JsMsg,
|
||||
JsMsgCallback,
|
||||
KV,
|
||||
KvCodec,
|
||||
KvCodecs,
|
||||
KvEntry,
|
||||
KvLimits,
|
||||
KvOptions,
|
||||
KvPutOptions,
|
||||
KvStatus,
|
||||
KvWatchInclude,
|
||||
KvWatchOptions,
|
||||
LastForMsgRequest,
|
||||
Lister,
|
||||
LostStreamData,
|
||||
MaxBytes,
|
||||
MaxMessages,
|
||||
MsgDeleteRequest,
|
||||
MsgRequest,
|
||||
ObjectInfo,
|
||||
ObjectResult,
|
||||
ObjectStore,
|
||||
ObjectStoreLink,
|
||||
ObjectStoreMeta,
|
||||
ObjectStoreMetaOptions,
|
||||
ObjectStoreOptions,
|
||||
ObjectStorePutOpts,
|
||||
ObjectStoreStatus,
|
||||
OrderedConsumerOptions,
|
||||
PeerInfo,
|
||||
Placement,
|
||||
PubAck,
|
||||
Pullable,
|
||||
PullOptions,
|
||||
PurgeBySeq,
|
||||
PurgeBySubject,
|
||||
PurgeOpts,
|
||||
PurgeResponse,
|
||||
PurgeTrimOpts,
|
||||
Republish,
|
||||
RoKV,
|
||||
SeqMsgRequest,
|
||||
SequenceInfo,
|
||||
StoredMsg,
|
||||
Stream,
|
||||
StreamAlternate,
|
||||
StreamAPI,
|
||||
StreamConfig,
|
||||
StreamConsumerLimits,
|
||||
StreamInfo,
|
||||
StreamInfoRequestOptions,
|
||||
StreamNames,
|
||||
Streams,
|
||||
StreamSource,
|
||||
StreamSourceInfo,
|
||||
StreamState,
|
||||
StreamUpdateConfig,
|
||||
SubjectTransformConfig,
|
||||
ThresholdBytes,
|
||||
ThresholdMessages,
|
||||
Views,
|
||||
} from "./internal_mod.ts";
|
||||
export { consumerOpts } from "./types.ts";
|
@ -0,0 +1,812 @@
|
||||
/*
|
||||
* Copyright 2022-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { validateBucket } from "./kv.ts";
|
||||
import { Base64UrlPaddedCodec } from "../nats-base-client/base64.ts";
|
||||
import { JSONCodec } from "../nats-base-client/codec.ts";
|
||||
import { nuid } from "../nats-base-client/nuid.ts";
|
||||
import { deferred } from "../nats-base-client/util.ts";
|
||||
import { DataBuffer } from "../nats-base-client/databuffer.ts";
|
||||
import { headers, MsgHdrsImpl } from "../nats-base-client/headers.ts";
|
||||
import {
|
||||
consumerOpts,
|
||||
JetStreamClient,
|
||||
JetStreamManager,
|
||||
JsHeaders,
|
||||
ObjectInfo,
|
||||
ObjectResult,
|
||||
ObjectStore,
|
||||
ObjectStoreMeta,
|
||||
ObjectStoreMetaOptions,
|
||||
ObjectStoreOptions,
|
||||
ObjectStorePutOpts,
|
||||
ObjectStoreStatus,
|
||||
PubAck,
|
||||
} from "./types.ts";
|
||||
import { QueuedIteratorImpl } from "../nats-base-client/queued_iterator.ts";
|
||||
import { SHA256 } from "../nats-base-client/sha256.js";
|
||||
|
||||
import {
|
||||
MsgHdrs,
|
||||
NatsConnection,
|
||||
NatsError,
|
||||
QueuedIterator,
|
||||
} from "../nats-base-client/core.ts";
|
||||
import {
|
||||
DiscardPolicy,
|
||||
PurgeResponse,
|
||||
StorageType,
|
||||
StreamConfig,
|
||||
StreamInfo,
|
||||
StreamInfoRequestOptions,
|
||||
} from "./jsapi_types.ts";
|
||||
import { JsMsg } from "./jsmsg.ts";
|
||||
import { PubHeaders } from "./jsclient.ts";
|
||||
|
||||
export const osPrefix = "OBJ_";
|
||||
export const digestType = "SHA-256=";
|
||||
|
||||
export function objectStoreStreamName(bucket: string): string {
|
||||
validateBucket(bucket);
|
||||
return `${osPrefix}${bucket}`;
|
||||
}
|
||||
|
||||
export function objectStoreBucketName(stream: string): string {
|
||||
if (stream.startsWith(osPrefix)) {
|
||||
return stream.substring(4);
|
||||
}
|
||||
return stream;
|
||||
}
|
||||
|
||||
export class ObjectStoreStatusImpl implements ObjectStoreStatus {
|
||||
si: StreamInfo;
|
||||
backingStore: string;
|
||||
|
||||
constructor(si: StreamInfo) {
|
||||
this.si = si;
|
||||
this.backingStore = "JetStream";
|
||||
}
|
||||
get bucket(): string {
|
||||
return objectStoreBucketName(this.si.config.name);
|
||||
}
|
||||
get description(): string {
|
||||
return this.si.config.description ?? "";
|
||||
}
|
||||
get ttl(): number {
|
||||
return this.si.config.max_age;
|
||||
}
|
||||
get storage(): StorageType {
|
||||
return this.si.config.storage;
|
||||
}
|
||||
get replicas(): number {
|
||||
return this.si.config.num_replicas;
|
||||
}
|
||||
get sealed(): boolean {
|
||||
return this.si.config.sealed;
|
||||
}
|
||||
get size(): number {
|
||||
return this.si.state.bytes;
|
||||
}
|
||||
get streamInfo(): StreamInfo {
|
||||
return this.si;
|
||||
}
|
||||
get metadata(): Record<string, string> | undefined {
|
||||
return this.si.config.metadata;
|
||||
}
|
||||
}
|
||||
|
||||
export type ServerObjectStoreMeta = {
|
||||
name: string;
|
||||
description?: string;
|
||||
headers?: Record<string, string[]>;
|
||||
options?: ObjectStoreMetaOptions;
|
||||
};
|
||||
|
||||
export type ServerObjectInfo = {
|
||||
bucket: string;
|
||||
nuid: string;
|
||||
size: number;
|
||||
chunks: number;
|
||||
digest: string;
|
||||
deleted?: boolean;
|
||||
mtime: string;
|
||||
revision: number;
|
||||
metadata?: Record<string, string>;
|
||||
} & ServerObjectStoreMeta;
|
||||
|
||||
class ObjectInfoImpl implements ObjectInfo {
|
||||
info: ServerObjectInfo;
|
||||
hdrs!: MsgHdrs;
|
||||
constructor(oi: ServerObjectInfo) {
|
||||
this.info = oi;
|
||||
}
|
||||
get name(): string {
|
||||
return this.info.name;
|
||||
}
|
||||
get description(): string {
|
||||
return this.info.description ?? "";
|
||||
}
|
||||
get headers(): MsgHdrs {
|
||||
if (!this.hdrs) {
|
||||
this.hdrs = MsgHdrsImpl.fromRecord(this.info.headers || {});
|
||||
}
|
||||
return this.hdrs;
|
||||
}
|
||||
get options(): ObjectStoreMetaOptions | undefined {
|
||||
return this.info.options;
|
||||
}
|
||||
get bucket(): string {
|
||||
return this.info.bucket;
|
||||
}
|
||||
get chunks(): number {
|
||||
return this.info.chunks;
|
||||
}
|
||||
get deleted(): boolean {
|
||||
return this.info.deleted ?? false;
|
||||
}
|
||||
get digest(): string {
|
||||
return this.info.digest;
|
||||
}
|
||||
get mtime(): string {
|
||||
return this.info.mtime;
|
||||
}
|
||||
get nuid(): string {
|
||||
return this.info.nuid;
|
||||
}
|
||||
get size(): number {
|
||||
return this.info.size;
|
||||
}
|
||||
get revision(): number {
|
||||
return this.info.revision;
|
||||
}
|
||||
get metadata(): Record<string, string> {
|
||||
return this.info.metadata || {};
|
||||
}
|
||||
isLink() {
|
||||
return (this.info.options?.link !== undefined) &&
|
||||
(this.info.options?.link !== null);
|
||||
}
|
||||
}
|
||||
|
||||
function toServerObjectStoreMeta(
|
||||
meta: Partial<ObjectStoreMeta>,
|
||||
): ServerObjectStoreMeta {
|
||||
const v = {
|
||||
name: meta.name,
|
||||
description: meta.description ?? "",
|
||||
options: meta.options,
|
||||
metadata: meta.metadata,
|
||||
} as ServerObjectStoreMeta;
|
||||
|
||||
if (meta.headers) {
|
||||
const mhi = meta.headers as MsgHdrsImpl;
|
||||
v.headers = mhi.toRecord();
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
function emptyReadableStream(): ReadableStream {
|
||||
return new ReadableStream({
|
||||
pull(c) {
|
||||
c.enqueue(new Uint8Array(0));
|
||||
c.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export class ObjectStoreImpl implements ObjectStore {
|
||||
jsm: JetStreamManager;
|
||||
js: JetStreamClient;
|
||||
stream!: string;
|
||||
name: string;
|
||||
|
||||
constructor(name: string, jsm: JetStreamManager, js: JetStreamClient) {
|
||||
this.name = name;
|
||||
this.jsm = jsm;
|
||||
this.js = js;
|
||||
}
|
||||
|
||||
_checkNotEmpty(name: string): { name: string; error?: Error } {
|
||||
if (!name || name.length === 0) {
|
||||
return { name, error: new Error("name cannot be empty") };
|
||||
}
|
||||
return { name };
|
||||
}
|
||||
|
||||
async info(name: string): Promise<ObjectInfo | null> {
|
||||
const info = await this.rawInfo(name);
|
||||
return info ? new ObjectInfoImpl(info) : null;
|
||||
}
|
||||
|
||||
async list(): Promise<ObjectInfo[]> {
|
||||
const buf: ObjectInfo[] = [];
|
||||
const iter = await this.watch({
|
||||
ignoreDeletes: true,
|
||||
includeHistory: true,
|
||||
});
|
||||
for await (const info of iter) {
|
||||
// watch will give a null when it has initialized
|
||||
// for us that is the hint we are done
|
||||
if (info === null) {
|
||||
break;
|
||||
}
|
||||
buf.push(info);
|
||||
}
|
||||
return Promise.resolve(buf);
|
||||
}
|
||||
|
||||
async rawInfo(name: string): Promise<ServerObjectInfo | null> {
|
||||
const { name: obj, error } = this._checkNotEmpty(name);
|
||||
if (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const meta = this._metaSubject(obj);
|
||||
try {
|
||||
const m = await this.jsm.streams.getMessage(this.stream, {
|
||||
last_by_subj: meta,
|
||||
});
|
||||
const jc = JSONCodec<ServerObjectInfo>();
|
||||
const soi = jc.decode(m.data) as ServerObjectInfo;
|
||||
soi.revision = m.seq;
|
||||
return soi;
|
||||
} catch (err) {
|
||||
if (err.code === "404") {
|
||||
return null;
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
async _si(
|
||||
opts?: Partial<StreamInfoRequestOptions>,
|
||||
): Promise<StreamInfo | null> {
|
||||
try {
|
||||
return await this.jsm.streams.info(this.stream, opts);
|
||||
} catch (err) {
|
||||
const nerr = err as NatsError;
|
||||
if (nerr.code === "404") {
|
||||
return null;
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
async seal(): Promise<ObjectStoreStatus> {
|
||||
let info = await this._si();
|
||||
if (info === null) {
|
||||
return Promise.reject(new Error("object store not found"));
|
||||
}
|
||||
info.config.sealed = true;
|
||||
info = await this.jsm.streams.update(this.stream, info.config);
|
||||
return Promise.resolve(new ObjectStoreStatusImpl(info));
|
||||
}
|
||||
|
||||
async status(
|
||||
opts?: Partial<StreamInfoRequestOptions>,
|
||||
): Promise<ObjectStoreStatus> {
|
||||
const info = await this._si(opts);
|
||||
if (info === null) {
|
||||
return Promise.reject(new Error("object store not found"));
|
||||
}
|
||||
return Promise.resolve(new ObjectStoreStatusImpl(info));
|
||||
}
|
||||
|
||||
destroy(): Promise<boolean> {
|
||||
return this.jsm.streams.delete(this.stream);
|
||||
}
|
||||
|
||||
async _put(
|
||||
meta: ObjectStoreMeta,
|
||||
rs: ReadableStream<Uint8Array> | null,
|
||||
opts?: ObjectStorePutOpts,
|
||||
): Promise<ObjectInfo> {
|
||||
const jsopts = this.js.getOptions();
|
||||
opts = opts || { timeout: jsopts.timeout };
|
||||
opts.timeout = opts.timeout || jsopts.timeout;
|
||||
opts.previousRevision = opts.previousRevision ?? undefined;
|
||||
const { timeout, previousRevision } = opts;
|
||||
const si = (this.js as unknown as { nc: NatsConnection }).nc.info;
|
||||
const maxPayload = si?.max_payload || 1024;
|
||||
meta = meta || {} as ObjectStoreMeta;
|
||||
meta.options = meta.options || {};
|
||||
let maxChunk = meta.options?.max_chunk_size || 128 * 1024;
|
||||
maxChunk = maxChunk > maxPayload ? maxPayload : maxChunk;
|
||||
meta.options.max_chunk_size = maxChunk;
|
||||
|
||||
const old = await this.info(meta.name);
|
||||
const { name: n, error } = this._checkNotEmpty(meta.name);
|
||||
if (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const id = nuid.next();
|
||||
const chunkSubj = this._chunkSubject(id);
|
||||
const metaSubj = this._metaSubject(n);
|
||||
|
||||
const info = Object.assign({
|
||||
bucket: this.name,
|
||||
nuid: id,
|
||||
size: 0,
|
||||
chunks: 0,
|
||||
}, toServerObjectStoreMeta(meta)) as ServerObjectInfo;
|
||||
|
||||
const d = deferred<ObjectInfo>();
|
||||
|
||||
const proms: Promise<unknown>[] = [];
|
||||
const db = new DataBuffer();
|
||||
try {
|
||||
const reader = rs ? rs.getReader() : null;
|
||||
const sha = new SHA256();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = reader
|
||||
? await reader.read()
|
||||
: { done: true, value: undefined };
|
||||
if (done) {
|
||||
// put any partial chunk in
|
||||
if (db.size() > 0) {
|
||||
const payload = db.drain();
|
||||
sha.update(payload);
|
||||
info.chunks!++;
|
||||
info.size! += payload.length;
|
||||
proms.push(this.js.publish(chunkSubj, payload, { timeout }));
|
||||
}
|
||||
// wait for all the chunks to write
|
||||
await Promise.all(proms);
|
||||
proms.length = 0;
|
||||
|
||||
// prepare the metadata
|
||||
info.mtime = new Date().toISOString();
|
||||
const digest = sha.digest("base64");
|
||||
const pad = digest.length % 3;
|
||||
const padding = pad > 0 ? "=".repeat(pad) : "";
|
||||
info.digest = `${digestType}${digest}${padding}`;
|
||||
info.deleted = false;
|
||||
|
||||
// trailing md for the object
|
||||
const h = headers();
|
||||
if (typeof previousRevision === "number") {
|
||||
h.set(
|
||||
PubHeaders.ExpectedLastSubjectSequenceHdr,
|
||||
`${previousRevision}`,
|
||||
);
|
||||
}
|
||||
h.set(JsHeaders.RollupHdr, JsHeaders.RollupValueSubject);
|
||||
|
||||
// try to update the metadata
|
||||
const pa = await this.js.publish(metaSubj, JSONCodec().encode(info), {
|
||||
headers: h,
|
||||
timeout,
|
||||
});
|
||||
// update the revision to point to the sequence where we inserted
|
||||
info.revision = pa.seq;
|
||||
|
||||
// if we are here, the new entry is live
|
||||
if (old) {
|
||||
try {
|
||||
await this.jsm.streams.purge(this.stream, {
|
||||
filter: `$O.${this.name}.C.${old.nuid}`,
|
||||
});
|
||||
} catch (_err) {
|
||||
// rejecting here, would mean send the wrong signal
|
||||
// the update succeeded, but cleanup of old chunks failed.
|
||||
}
|
||||
}
|
||||
|
||||
// resolve the ObjectInfo
|
||||
d.resolve(new ObjectInfoImpl(info!));
|
||||
// stop
|
||||
break;
|
||||
}
|
||||
if (value) {
|
||||
db.fill(value);
|
||||
while (db.size() > maxChunk) {
|
||||
info.chunks!++;
|
||||
info.size! += maxChunk;
|
||||
const payload = db.drain(meta.options.max_chunk_size);
|
||||
sha.update(payload);
|
||||
proms.push(
|
||||
this.js.publish(chunkSubj, payload, { timeout }),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
// we failed, remove any partials
|
||||
await this.jsm.streams.purge(this.stream, { filter: chunkSubj });
|
||||
d.reject(err);
|
||||
}
|
||||
|
||||
return d;
|
||||
}
|
||||
|
||||
putBlob(
|
||||
meta: ObjectStoreMeta,
|
||||
data: Uint8Array | null,
|
||||
opts?: ObjectStorePutOpts,
|
||||
): Promise<ObjectInfo> {
|
||||
function readableStreamFrom(data: Uint8Array): ReadableStream<Uint8Array> {
|
||||
return new ReadableStream<Uint8Array>({
|
||||
pull(controller) {
|
||||
controller.enqueue(data);
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
if (data === null) {
|
||||
data = new Uint8Array(0);
|
||||
}
|
||||
return this.put(meta, readableStreamFrom(data), opts);
|
||||
}
|
||||
|
||||
put(
|
||||
meta: ObjectStoreMeta,
|
||||
rs: ReadableStream<Uint8Array> | null,
|
||||
opts?: ObjectStorePutOpts,
|
||||
): Promise<ObjectInfo> {
|
||||
if (meta?.options?.link) {
|
||||
return Promise.reject(
|
||||
new Error("link cannot be set when putting the object in bucket"),
|
||||
);
|
||||
}
|
||||
return this._put(meta, rs, opts);
|
||||
}
|
||||
|
||||
async getBlob(name: string): Promise<Uint8Array | null> {
|
||||
async function fromReadableStream(
|
||||
rs: ReadableStream<Uint8Array>,
|
||||
): Promise<Uint8Array> {
|
||||
const buf = new DataBuffer();
|
||||
const reader = rs.getReader();
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
return buf.drain();
|
||||
}
|
||||
if (value && value.length) {
|
||||
buf.fill(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const r = await this.get(name);
|
||||
if (r === null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
const vs = await Promise.all([r.error, fromReadableStream(r.data)]);
|
||||
if (vs[0]) {
|
||||
return Promise.reject(vs[0]);
|
||||
} else {
|
||||
return Promise.resolve(vs[1]);
|
||||
}
|
||||
}
|
||||
|
||||
async get(name: string): Promise<ObjectResult | null> {
|
||||
const info = await this.rawInfo(name);
|
||||
if (info === null) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (info.deleted) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
if (info.options && info.options.link) {
|
||||
const ln = info.options.link.name || "";
|
||||
if (ln === "") {
|
||||
throw new Error("link is a bucket");
|
||||
}
|
||||
const os = info.options.link.bucket !== this.name
|
||||
? await ObjectStoreImpl.create(
|
||||
this.js,
|
||||
info.options.link.bucket,
|
||||
)
|
||||
: this;
|
||||
return os.get(ln);
|
||||
}
|
||||
|
||||
const d = deferred<Error | null>();
|
||||
|
||||
const r: Partial<ObjectResult> = {
|
||||
info: new ObjectInfoImpl(info),
|
||||
error: d,
|
||||
};
|
||||
if (info.size === 0) {
|
||||
r.data = emptyReadableStream();
|
||||
d.resolve(null);
|
||||
return Promise.resolve(r as ObjectResult);
|
||||
}
|
||||
|
||||
let controller: ReadableStreamDefaultController;
|
||||
|
||||
const oc = consumerOpts();
|
||||
oc.orderedConsumer();
|
||||
const sha = new SHA256();
|
||||
const subj = `$O.${this.name}.C.${info.nuid}`;
|
||||
const sub = await this.js.subscribe(subj, oc);
|
||||
(async () => {
|
||||
for await (const jm of sub) {
|
||||
if (jm.data.length > 0) {
|
||||
sha.update(jm.data);
|
||||
controller!.enqueue(jm.data);
|
||||
}
|
||||
if (jm.info.pending === 0) {
|
||||
const hash = sha.digest("base64");
|
||||
// go pads the hash - which should be multiple of 3 - otherwise pads with '='
|
||||
const pad = hash.length % 3;
|
||||
const padding = pad > 0 ? "=".repeat(pad) : "";
|
||||
const digest = `${digestType}${hash}${padding}`;
|
||||
if (digest !== info.digest) {
|
||||
controller!.error(
|
||||
new Error(
|
||||
`received a corrupt object, digests do not match received: ${info.digest} calculated ${digest}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
controller!.close();
|
||||
}
|
||||
sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
})()
|
||||
.then(() => {
|
||||
d.resolve();
|
||||
})
|
||||
.catch((err) => {
|
||||
controller!.error(err);
|
||||
d.reject(err);
|
||||
});
|
||||
|
||||
r.data = new ReadableStream({
|
||||
start(c) {
|
||||
controller = c;
|
||||
},
|
||||
cancel() {
|
||||
sub.unsubscribe();
|
||||
},
|
||||
});
|
||||
|
||||
return r as ObjectResult;
|
||||
}
|
||||
|
||||
linkStore(name: string, bucket: ObjectStore): Promise<ObjectInfo> {
|
||||
if (!(bucket instanceof ObjectStoreImpl)) {
|
||||
return Promise.reject("bucket required");
|
||||
}
|
||||
const osi = bucket as ObjectStoreImpl;
|
||||
const { name: n, error } = this._checkNotEmpty(name);
|
||||
if (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
const meta = {
|
||||
name: n,
|
||||
options: { link: { bucket: osi.name } },
|
||||
};
|
||||
return this._put(meta, null);
|
||||
}
|
||||
|
||||
async link(name: string, info: ObjectInfo): Promise<ObjectInfo> {
|
||||
const { name: n, error } = this._checkNotEmpty(name);
|
||||
if (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
if (info.deleted) {
|
||||
return Promise.reject(new Error("src object is deleted"));
|
||||
}
|
||||
if ((info as ObjectInfoImpl).isLink()) {
|
||||
return Promise.reject(new Error("src object is a link"));
|
||||
}
|
||||
const dest = await this.rawInfo(name);
|
||||
if (dest !== null && !dest.deleted) {
|
||||
return Promise.reject(
|
||||
new Error("an object already exists with that name"),
|
||||
);
|
||||
}
|
||||
|
||||
const link = { bucket: info.bucket, name: info.name };
|
||||
const mm = {
|
||||
name: n,
|
||||
bucket: info.bucket,
|
||||
options: { link: link },
|
||||
} as ObjectStoreMeta;
|
||||
await this.js.publish(this._metaSubject(name), JSON.stringify(mm));
|
||||
const i = await this.info(name);
|
||||
return Promise.resolve(i!);
|
||||
}
|
||||
|
||||
async delete(name: string): Promise<PurgeResponse> {
|
||||
const info = await this.rawInfo(name);
|
||||
if (info === null) {
|
||||
return Promise.resolve({ purged: 0, success: false });
|
||||
}
|
||||
info.deleted = true;
|
||||
info.size = 0;
|
||||
info.chunks = 0;
|
||||
info.digest = "";
|
||||
|
||||
const jc = JSONCodec();
|
||||
const h = headers();
|
||||
h.set(JsHeaders.RollupHdr, JsHeaders.RollupValueSubject);
|
||||
|
||||
await this.js.publish(this._metaSubject(info.name), jc.encode(info), {
|
||||
headers: h,
|
||||
});
|
||||
return this.jsm.streams.purge(this.stream, {
|
||||
filter: this._chunkSubject(info.nuid),
|
||||
});
|
||||
}
|
||||
|
||||
async update(
|
||||
name: string,
|
||||
meta: Partial<ObjectStoreMeta> = {},
|
||||
): Promise<PubAck> {
|
||||
const info = await this.rawInfo(name);
|
||||
if (info === null) {
|
||||
return Promise.reject(new Error("object not found"));
|
||||
}
|
||||
if (info.deleted) {
|
||||
return Promise.reject(
|
||||
new Error("cannot update meta for a deleted object"),
|
||||
);
|
||||
}
|
||||
meta.name = meta.name ?? info.name;
|
||||
const { name: n, error } = this._checkNotEmpty(meta.name);
|
||||
if (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
if (name !== meta.name) {
|
||||
const i = await this.info(meta.name);
|
||||
if (i && !i.deleted) {
|
||||
return Promise.reject(
|
||||
new Error("an object already exists with that name"),
|
||||
);
|
||||
}
|
||||
}
|
||||
meta.name = n;
|
||||
const ii = Object.assign({}, info, toServerObjectStoreMeta(meta!));
|
||||
// if the name changed, delete the old meta
|
||||
const ack = await this.js.publish(
|
||||
this._metaSubject(ii.name),
|
||||
JSON.stringify(ii),
|
||||
);
|
||||
if (name !== meta.name) {
|
||||
await this.jsm.streams.purge(this.stream, {
|
||||
filter: this._metaSubject(name),
|
||||
});
|
||||
}
|
||||
return Promise.resolve(ack);
|
||||
}
|
||||
|
||||
async watch(opts: Partial<
|
||||
{
|
||||
ignoreDeletes?: boolean;
|
||||
includeHistory?: boolean;
|
||||
}
|
||||
> = {}): Promise<QueuedIterator<ObjectInfo | null>> {
|
||||
opts.includeHistory = opts.includeHistory ?? false;
|
||||
opts.ignoreDeletes = opts.ignoreDeletes ?? false;
|
||||
let initialized = false;
|
||||
const qi = new QueuedIteratorImpl<ObjectInfo | null>();
|
||||
const subj = this._metaSubjectAll();
|
||||
try {
|
||||
await this.jsm.streams.getMessage(this.stream, { last_by_subj: subj });
|
||||
} catch (err) {
|
||||
if (err.code === "404") {
|
||||
qi.push(null);
|
||||
initialized = true;
|
||||
} else {
|
||||
qi.stop(err);
|
||||
}
|
||||
}
|
||||
const jc = JSONCodec<ObjectInfo>();
|
||||
const copts = consumerOpts();
|
||||
copts.orderedConsumer();
|
||||
if (opts.includeHistory) {
|
||||
copts.deliverLastPerSubject();
|
||||
} else {
|
||||
// FIXME: Go's implementation doesn't seem correct - if history is not desired
|
||||
// the watch should only be giving notifications on new entries
|
||||
initialized = true;
|
||||
copts.deliverNew();
|
||||
}
|
||||
copts.callback((err: NatsError | null, jm: JsMsg | null) => {
|
||||
if (err) {
|
||||
qi.stop(err);
|
||||
return;
|
||||
}
|
||||
if (jm !== null) {
|
||||
const oi = jc.decode(jm.data);
|
||||
if (oi.deleted && opts.ignoreDeletes === true) {
|
||||
// do nothing
|
||||
} else {
|
||||
qi.push(oi);
|
||||
}
|
||||
if (jm.info?.pending === 0 && !initialized) {
|
||||
initialized = true;
|
||||
qi.push(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const sub = await this.js.subscribe(subj, copts);
|
||||
qi._data = sub;
|
||||
qi.iterClosed.then(() => {
|
||||
sub.unsubscribe();
|
||||
});
|
||||
sub.closed.then(() => {
|
||||
qi.stop();
|
||||
}).catch((err) => {
|
||||
qi.stop(err);
|
||||
});
|
||||
|
||||
return qi;
|
||||
}
|
||||
|
||||
_chunkSubject(id: string) {
|
||||
return `$O.${this.name}.C.${id}`;
|
||||
}
|
||||
|
||||
_metaSubject(n: string): string {
|
||||
return `$O.${this.name}.M.${Base64UrlPaddedCodec.encode(n)}`;
|
||||
}
|
||||
|
||||
_metaSubjectAll(): string {
|
||||
return `$O.${this.name}.M.>`;
|
||||
}
|
||||
|
||||
async init(opts: Partial<ObjectStoreOptions> = {}): Promise<void> {
|
||||
try {
|
||||
this.stream = objectStoreStreamName(this.name);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
const max_age = opts?.ttl || 0;
|
||||
delete opts.ttl;
|
||||
const sc = Object.assign({ max_age }, opts) as StreamConfig;
|
||||
sc.name = this.stream;
|
||||
sc.allow_direct = true;
|
||||
sc.allow_rollup_hdrs = true;
|
||||
sc.discard = DiscardPolicy.New;
|
||||
sc.subjects = [`$O.${this.name}.C.>`, `$O.${this.name}.M.>`];
|
||||
if (opts.placement) {
|
||||
sc.placement = opts.placement;
|
||||
}
|
||||
if (opts.metadata) {
|
||||
sc.metadata = opts.metadata;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.jsm.streams.info(sc.name);
|
||||
} catch (err) {
|
||||
if (err.message === "stream not found") {
|
||||
await this.jsm.streams.add(sc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async create(
|
||||
js: JetStreamClient,
|
||||
name: string,
|
||||
opts: Partial<ObjectStoreOptions> = {},
|
||||
): Promise<ObjectStore> {
|
||||
const jsm = await js.jetstreamManager();
|
||||
const os = new ObjectStoreImpl(name, jsm, js);
|
||||
await os.init(opts);
|
||||
return Promise.resolve(os);
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2020-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { nkeys } from "./nkeys.ts";
|
||||
import { TD, TE } from "./encoders.ts";
|
||||
import {
|
||||
Auth,
|
||||
Authenticator,
|
||||
ErrorCode,
|
||||
JwtAuth,
|
||||
NatsError,
|
||||
NKeyAuth,
|
||||
NoAuth,
|
||||
TokenAuth,
|
||||
UserPass,
|
||||
} from "./core.ts";
|
||||
|
||||
export function multiAuthenticator(authenticators: Authenticator[]) {
|
||||
return (nonce?: string): Auth => {
|
||||
let auth: Partial<NoAuth & TokenAuth & UserPass & NKeyAuth & JwtAuth> = {};
|
||||
authenticators.forEach((a) => {
|
||||
const args = a(nonce) || {};
|
||||
auth = Object.assign(auth, args);
|
||||
});
|
||||
return auth as Auth;
|
||||
};
|
||||
}
|
||||
|
||||
export function noAuthFn(): Authenticator {
|
||||
return (): NoAuth => {
|
||||
return;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a user/pass authenticator for the specified user and optional password
|
||||
* @param { string | () => string } user
|
||||
* @param {string | () => string } pass
|
||||
* @return {UserPass}
|
||||
*/
|
||||
export function usernamePasswordAuthenticator(
|
||||
user: string | (() => string),
|
||||
pass?: string | (() => string),
|
||||
): Authenticator {
|
||||
return (): UserPass => {
|
||||
const u = typeof user === "function" ? user() : user;
|
||||
const p = typeof pass === "function" ? pass() : pass;
|
||||
return { user: u, pass: p };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a token authenticator for the specified token
|
||||
* @param { string | () => string } token
|
||||
* @return {TokenAuth}
|
||||
*/
|
||||
export function tokenAuthenticator(
|
||||
token: string | (() => string),
|
||||
): Authenticator {
|
||||
return (): TokenAuth => {
|
||||
const auth_token = typeof token === "function" ? token() : token;
|
||||
return { auth_token };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Authenticator that returns a NKeyAuth based that uses the
|
||||
* specified seed or function returning a seed.
|
||||
* @param {Uint8Array | (() => Uint8Array)} seed - the nkey seed
|
||||
* @return {NKeyAuth}
|
||||
*/
|
||||
export function nkeyAuthenticator(
|
||||
seed?: Uint8Array | (() => Uint8Array),
|
||||
): Authenticator {
|
||||
return (nonce?: string): NKeyAuth => {
|
||||
const s = typeof seed === "function" ? seed() : seed;
|
||||
const kp = s ? nkeys.fromSeed(s) : undefined;
|
||||
const nkey = kp ? kp.getPublicKey() : "";
|
||||
const challenge = TE.encode(nonce || "");
|
||||
const sigBytes = kp !== undefined && nonce ? kp.sign(challenge) : undefined;
|
||||
const sig = sigBytes ? nkeys.encode(sigBytes) : "";
|
||||
return { nkey, sig };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Authenticator function that returns a JwtAuth.
|
||||
* If a seed is provided, the public key, and signature are
|
||||
* calculated.
|
||||
*
|
||||
* @param {string | ()=>string} ajwt - the jwt
|
||||
* @param {Uint8Array | ()=> Uint8Array } seed - the optional nkey seed
|
||||
* @return {Authenticator}
|
||||
*/
|
||||
export function jwtAuthenticator(
|
||||
ajwt: string | (() => string),
|
||||
seed?: Uint8Array | (() => Uint8Array),
|
||||
): Authenticator {
|
||||
return (
|
||||
nonce?: string,
|
||||
): JwtAuth => {
|
||||
const jwt = typeof ajwt === "function" ? ajwt() : ajwt;
|
||||
const fn = nkeyAuthenticator(seed);
|
||||
const { nkey, sig } = fn(nonce) as NKeyAuth;
|
||||
return { jwt, nkey, sig };
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Authenticator function that returns a JwtAuth.
|
||||
* This is a convenience Authenticator that parses the
|
||||
* specified creds and delegates to the jwtAuthenticator.
|
||||
* @param {Uint8Array | () => Uint8Array } creds - the contents of a creds file or a function that returns the creds
|
||||
* @returns {JwtAuth}
|
||||
*/
|
||||
export function credsAuthenticator(
|
||||
creds: Uint8Array | (() => Uint8Array),
|
||||
): Authenticator {
|
||||
const fn = typeof creds !== "function" ? () => creds : creds;
|
||||
const parse = () => {
|
||||
const CREDS =
|
||||
/\s*(?:(?:[-]{3,}[^\n]*[-]{3,}\n)(.+)(?:\n\s*[-]{3,}[^\n]*[-]{3,}\n))/ig;
|
||||
const s = TD.decode(fn());
|
||||
// get the JWT
|
||||
let m = CREDS.exec(s);
|
||||
if (!m) {
|
||||
throw NatsError.errorForCode(ErrorCode.BadCreds);
|
||||
}
|
||||
const jwt = m[1].trim();
|
||||
// get the nkey
|
||||
m = CREDS.exec(s);
|
||||
if (!m) {
|
||||
throw NatsError.errorForCode(ErrorCode.BadCreds);
|
||||
}
|
||||
if (!m) {
|
||||
throw NatsError.errorForCode(ErrorCode.BadCreds);
|
||||
}
|
||||
const seed = TE.encode(m[1].trim());
|
||||
|
||||
return { jwt, seed };
|
||||
};
|
||||
|
||||
const jwtFn = () => {
|
||||
const { jwt } = parse();
|
||||
return jwt;
|
||||
};
|
||||
const nkeyFn = () => {
|
||||
const { seed } = parse();
|
||||
return seed;
|
||||
};
|
||||
|
||||
return jwtAuthenticator(jwtFn, nkeyFn);
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
export class Base64Codec {
|
||||
static encode(bytes: string | Uint8Array): string {
|
||||
if (typeof bytes === "string") {
|
||||
return btoa(bytes);
|
||||
}
|
||||
const a = Array.from(bytes);
|
||||
return btoa(String.fromCharCode(...a));
|
||||
}
|
||||
|
||||
static decode(s: string, binary = false): Uint8Array | string {
|
||||
const bin = atob(s);
|
||||
if (!binary) {
|
||||
return bin;
|
||||
}
|
||||
return Uint8Array.from(bin, (c) => c.charCodeAt(0));
|
||||
}
|
||||
}
|
||||
|
||||
export class Base64UrlCodec {
|
||||
static encode(bytes: string | Uint8Array): string {
|
||||
return Base64UrlCodec.toB64URLEncoding(Base64Codec.encode(bytes));
|
||||
}
|
||||
|
||||
static decode(s: string, binary = false): Uint8Array | string {
|
||||
return Base64Codec.decode(Base64UrlCodec.fromB64URLEncoding(s), binary);
|
||||
}
|
||||
|
||||
static toB64URLEncoding(b64str: string): string {
|
||||
return b64str
|
||||
.replace(/=/g, "")
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_");
|
||||
}
|
||||
|
||||
static fromB64URLEncoding(b64str: string): string {
|
||||
// pads are % 4, but not necessary on decoding
|
||||
return b64str
|
||||
.replace(/_/g, "/")
|
||||
.replace(/-/g, "+");
|
||||
}
|
||||
}
|
||||
|
||||
export class Base64UrlPaddedCodec {
|
||||
static encode(bytes: string | Uint8Array): string {
|
||||
return Base64UrlPaddedCodec.toB64URLEncoding(Base64Codec.encode(bytes));
|
||||
}
|
||||
|
||||
static decode(s: string, binary = false): Uint8Array | string {
|
||||
return Base64UrlPaddedCodec.decode(
|
||||
Base64UrlPaddedCodec.fromB64URLEncoding(s),
|
||||
binary,
|
||||
);
|
||||
}
|
||||
|
||||
static toB64URLEncoding(b64str: string): string {
|
||||
return b64str
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_");
|
||||
}
|
||||
|
||||
static fromB64URLEncoding(b64str: string): string {
|
||||
// pads are % 4, but not necessary on decoding
|
||||
return b64str
|
||||
.replace(/_/g, "/")
|
||||
.replace(/-/g, "+");
|
||||
}
|
||||
}
|
@ -0,0 +1,430 @@
|
||||
/*
|
||||
* Copyright 2020-2022 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Empty } from "./types.ts";
|
||||
import { nuid } from "./nuid.ts";
|
||||
import { deferred, Perf } from "./util.ts";
|
||||
import type { NatsConnectionImpl } from "./nats.ts";
|
||||
import { ErrorCode, NatsConnection, NatsError } from "./core.ts";
|
||||
|
||||
export class Metric {
|
||||
name: string;
|
||||
duration: number;
|
||||
date: number;
|
||||
payload: number;
|
||||
msgs: number;
|
||||
lang!: string;
|
||||
version!: string;
|
||||
bytes: number;
|
||||
asyncRequests?: boolean;
|
||||
min?: number;
|
||||
max?: number;
|
||||
|
||||
constructor(name: string, duration: number) {
|
||||
this.name = name;
|
||||
this.duration = duration;
|
||||
this.date = Date.now();
|
||||
this.payload = 0;
|
||||
this.msgs = 0;
|
||||
this.bytes = 0;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
const sec = (this.duration) / 1000;
|
||||
const mps = Math.round(this.msgs / sec);
|
||||
const label = this.asyncRequests ? "asyncRequests" : "";
|
||||
let minmax = "";
|
||||
if (this.max) {
|
||||
minmax = `${this.min}/${this.max}`;
|
||||
}
|
||||
|
||||
return `${this.name}${label ? " [asyncRequests]" : ""} ${
|
||||
humanizeNumber(mps)
|
||||
} msgs/sec - [${sec.toFixed(2)} secs] ~ ${
|
||||
throughput(this.bytes, sec)
|
||||
} ${minmax}`;
|
||||
}
|
||||
|
||||
toCsv(): string {
|
||||
return `"${this.name}",${
|
||||
new Date(this.date).toISOString()
|
||||
},${this.lang},${this.version},${this.msgs},${this.payload},${this.bytes},${this.duration},${
|
||||
this.asyncRequests ? this.asyncRequests : false
|
||||
}\n`;
|
||||
}
|
||||
|
||||
static header(): string {
|
||||
return `Test,Date,Lang,Version,Count,MsgPayload,Bytes,Millis,Async\n`;
|
||||
}
|
||||
}
|
||||
|
||||
export interface BenchOpts {
|
||||
callbacks?: boolean;
|
||||
msgs?: number;
|
||||
size?: number;
|
||||
subject?: string;
|
||||
asyncRequests?: boolean;
|
||||
pub?: boolean;
|
||||
sub?: boolean;
|
||||
rep?: boolean;
|
||||
req?: boolean;
|
||||
}
|
||||
|
||||
export class Bench {
|
||||
nc: NatsConnection;
|
||||
callbacks: boolean;
|
||||
msgs: number;
|
||||
size: number;
|
||||
subject: string;
|
||||
asyncRequests?: boolean;
|
||||
pub?: boolean;
|
||||
sub?: boolean;
|
||||
req?: boolean;
|
||||
rep?: boolean;
|
||||
perf: Perf;
|
||||
payload: Uint8Array;
|
||||
|
||||
constructor(
|
||||
nc: NatsConnection,
|
||||
opts: BenchOpts = {
|
||||
msgs: 100000,
|
||||
size: 128,
|
||||
subject: "",
|
||||
asyncRequests: false,
|
||||
pub: false,
|
||||
sub: false,
|
||||
req: false,
|
||||
rep: false,
|
||||
},
|
||||
) {
|
||||
this.nc = nc;
|
||||
this.callbacks = opts.callbacks || false;
|
||||
this.msgs = opts.msgs || 0;
|
||||
this.size = opts.size || 0;
|
||||
this.subject = opts.subject || nuid.next();
|
||||
this.asyncRequests = opts.asyncRequests || false;
|
||||
this.pub = opts.pub || false;
|
||||
this.sub = opts.sub || false;
|
||||
this.req = opts.req || false;
|
||||
this.rep = opts.rep || false;
|
||||
this.perf = new Perf();
|
||||
this.payload = this.size ? new Uint8Array(this.size) : Empty;
|
||||
|
||||
if (!this.pub && !this.sub && !this.req && !this.rep) {
|
||||
throw new Error("no bench option selected");
|
||||
}
|
||||
}
|
||||
|
||||
async run(): Promise<Metric[]> {
|
||||
this.nc.closed()
|
||||
.then((err) => {
|
||||
if (err) {
|
||||
throw new NatsError(
|
||||
`bench closed with an error: ${err.message}`,
|
||||
ErrorCode.Unknown,
|
||||
err,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (this.callbacks) {
|
||||
await this.runCallbacks();
|
||||
} else {
|
||||
await this.runAsync();
|
||||
}
|
||||
return this.processMetrics();
|
||||
}
|
||||
|
||||
processMetrics(): Metric[] {
|
||||
const nc = this.nc as NatsConnectionImpl;
|
||||
const { lang, version } = nc.protocol.transport;
|
||||
|
||||
if (this.pub && this.sub) {
|
||||
this.perf.measure("pubsub", "pubStart", "subStop");
|
||||
}
|
||||
if (this.req && this.rep) {
|
||||
this.perf.measure("reqrep", "reqStart", "reqStop");
|
||||
}
|
||||
|
||||
const measures = this.perf.getEntries();
|
||||
const pubsub = measures.find((m) => m.name === "pubsub");
|
||||
const reqrep = measures.find((m) => m.name === "reqrep");
|
||||
const req = measures.find((m) => m.name === "req");
|
||||
const rep = measures.find((m) => m.name === "rep");
|
||||
const pub = measures.find((m) => m.name === "pub");
|
||||
const sub = measures.find((m) => m.name === "sub");
|
||||
|
||||
const stats = this.nc.stats();
|
||||
|
||||
const metrics: Metric[] = [];
|
||||
if (pubsub) {
|
||||
const { name, duration } = pubsub;
|
||||
const m = new Metric(name, duration);
|
||||
m.msgs = this.msgs * 2;
|
||||
m.bytes = stats.inBytes + stats.outBytes;
|
||||
m.lang = lang;
|
||||
m.version = version;
|
||||
m.payload = this.payload.length;
|
||||
metrics.push(m);
|
||||
}
|
||||
|
||||
if (reqrep) {
|
||||
const { name, duration } = reqrep;
|
||||
const m = new Metric(name, duration);
|
||||
m.msgs = this.msgs * 2;
|
||||
m.bytes = stats.inBytes + stats.outBytes;
|
||||
m.lang = lang;
|
||||
m.version = version;
|
||||
m.payload = this.payload.length;
|
||||
metrics.push(m);
|
||||
}
|
||||
|
||||
if (pub) {
|
||||
const { name, duration } = pub;
|
||||
const m = new Metric(name, duration);
|
||||
m.msgs = this.msgs;
|
||||
m.bytes = stats.outBytes;
|
||||
m.lang = lang;
|
||||
m.version = version;
|
||||
m.payload = this.payload.length;
|
||||
metrics.push(m);
|
||||
}
|
||||
|
||||
if (sub) {
|
||||
const { name, duration } = sub;
|
||||
const m = new Metric(name, duration);
|
||||
m.msgs = this.msgs;
|
||||
m.bytes = stats.inBytes;
|
||||
m.lang = lang;
|
||||
m.version = version;
|
||||
m.payload = this.payload.length;
|
||||
metrics.push(m);
|
||||
}
|
||||
|
||||
if (rep) {
|
||||
const { name, duration } = rep;
|
||||
const m = new Metric(name, duration);
|
||||
m.msgs = this.msgs;
|
||||
m.bytes = stats.inBytes + stats.outBytes;
|
||||
m.lang = lang;
|
||||
m.version = version;
|
||||
m.payload = this.payload.length;
|
||||
metrics.push(m);
|
||||
}
|
||||
|
||||
if (req) {
|
||||
const { name, duration } = req;
|
||||
const m = new Metric(name, duration);
|
||||
m.msgs = this.msgs;
|
||||
m.bytes = stats.inBytes + stats.outBytes;
|
||||
m.lang = lang;
|
||||
m.version = version;
|
||||
m.payload = this.payload.length;
|
||||
metrics.push(m);
|
||||
}
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
async runCallbacks(): Promise<void> {
|
||||
const jobs: Promise<void>[] = [];
|
||||
|
||||
if (this.sub) {
|
||||
const d = deferred<void>();
|
||||
jobs.push(d);
|
||||
let i = 0;
|
||||
this.nc.subscribe(this.subject, {
|
||||
max: this.msgs,
|
||||
callback: () => {
|
||||
i++;
|
||||
if (i === 1) {
|
||||
this.perf.mark("subStart");
|
||||
}
|
||||
if (i === this.msgs) {
|
||||
this.perf.mark("subStop");
|
||||
this.perf.measure("sub", "subStart", "subStop");
|
||||
d.resolve();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (this.rep) {
|
||||
const d = deferred<void>();
|
||||
jobs.push(d);
|
||||
let i = 0;
|
||||
this.nc.subscribe(this.subject, {
|
||||
max: this.msgs,
|
||||
callback: (_, m) => {
|
||||
m.respond(this.payload);
|
||||
i++;
|
||||
if (i === 1) {
|
||||
this.perf.mark("repStart");
|
||||
}
|
||||
if (i === this.msgs) {
|
||||
this.perf.mark("repStop");
|
||||
this.perf.measure("rep", "repStart", "repStop");
|
||||
d.resolve();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (this.pub) {
|
||||
const job = (async () => {
|
||||
this.perf.mark("pubStart");
|
||||
for (let i = 0; i < this.msgs; i++) {
|
||||
this.nc.publish(this.subject, this.payload);
|
||||
}
|
||||
await this.nc.flush();
|
||||
this.perf.mark("pubStop");
|
||||
this.perf.measure("pub", "pubStart", "pubStop");
|
||||
})();
|
||||
jobs.push(job);
|
||||
}
|
||||
|
||||
if (this.req) {
|
||||
const job = (async () => {
|
||||
if (this.asyncRequests) {
|
||||
this.perf.mark("reqStart");
|
||||
const a = [];
|
||||
for (let i = 0; i < this.msgs; i++) {
|
||||
a.push(
|
||||
this.nc.request(this.subject, this.payload, { timeout: 20000 }),
|
||||
);
|
||||
}
|
||||
await Promise.all(a);
|
||||
this.perf.mark("reqStop");
|
||||
this.perf.measure("req", "reqStart", "reqStop");
|
||||
} else {
|
||||
this.perf.mark("reqStart");
|
||||
for (let i = 0; i < this.msgs; i++) {
|
||||
await this.nc.request(this.subject);
|
||||
}
|
||||
this.perf.mark("reqStop");
|
||||
this.perf.measure("req", "reqStart", "reqStop");
|
||||
}
|
||||
})();
|
||||
jobs.push(job);
|
||||
}
|
||||
|
||||
await Promise.all(jobs);
|
||||
}
|
||||
|
||||
async runAsync(): Promise<void> {
|
||||
const jobs: Promise<void>[] = [];
|
||||
|
||||
if (this.rep) {
|
||||
let first = false;
|
||||
const sub = this.nc.subscribe(this.subject, { max: this.msgs });
|
||||
const job = (async () => {
|
||||
for await (const m of sub) {
|
||||
if (!first) {
|
||||
this.perf.mark("repStart");
|
||||
first = true;
|
||||
}
|
||||
m.respond(this.payload);
|
||||
}
|
||||
await this.nc.flush();
|
||||
this.perf.mark("repStop");
|
||||
this.perf.measure("rep", "repStart", "repStop");
|
||||
})();
|
||||
jobs.push(job);
|
||||
}
|
||||
|
||||
if (this.sub) {
|
||||
let first = false;
|
||||
const sub = this.nc.subscribe(this.subject, { max: this.msgs });
|
||||
const job = (async () => {
|
||||
for await (const _m of sub) {
|
||||
if (!first) {
|
||||
this.perf.mark("subStart");
|
||||
first = true;
|
||||
}
|
||||
}
|
||||
this.perf.mark("subStop");
|
||||
this.perf.measure("sub", "subStart", "subStop");
|
||||
})();
|
||||
jobs.push(job);
|
||||
}
|
||||
|
||||
if (this.pub) {
|
||||
const job = (async () => {
|
||||
this.perf.mark("pubStart");
|
||||
for (let i = 0; i < this.msgs; i++) {
|
||||
this.nc.publish(this.subject, this.payload);
|
||||
}
|
||||
await this.nc.flush();
|
||||
this.perf.mark("pubStop");
|
||||
this.perf.measure("pub", "pubStart", "pubStop");
|
||||
})();
|
||||
jobs.push(job);
|
||||
}
|
||||
|
||||
if (this.req) {
|
||||
const job = (async () => {
|
||||
if (this.asyncRequests) {
|
||||
this.perf.mark("reqStart");
|
||||
const a = [];
|
||||
for (let i = 0; i < this.msgs; i++) {
|
||||
a.push(
|
||||
this.nc.request(this.subject, this.payload, { timeout: 20000 }),
|
||||
);
|
||||
}
|
||||
await Promise.all(a);
|
||||
this.perf.mark("reqStop");
|
||||
this.perf.measure("req", "reqStart", "reqStop");
|
||||
} else {
|
||||
this.perf.mark("reqStart");
|
||||
for (let i = 0; i < this.msgs; i++) {
|
||||
await this.nc.request(this.subject);
|
||||
}
|
||||
this.perf.mark("reqStop");
|
||||
this.perf.measure("req", "reqStart", "reqStop");
|
||||
}
|
||||
})();
|
||||
jobs.push(job);
|
||||
}
|
||||
|
||||
await Promise.all(jobs);
|
||||
}
|
||||
}
|
||||
|
||||
export function throughput(bytes: number, seconds: number): string {
|
||||
return `${humanizeBytes(bytes / seconds)}/sec`;
|
||||
}
|
||||
|
||||
export function msgThroughput(msgs: number, seconds: number): string {
|
||||
return `${(Math.floor(msgs / seconds))} msgs/sec`;
|
||||
}
|
||||
|
||||
export function humanizeBytes(bytes: number, si = false): string {
|
||||
const base = si ? 1000 : 1024;
|
||||
const pre = si
|
||||
? ["k", "M", "G", "T", "P", "E"]
|
||||
: ["K", "M", "G", "T", "P", "E"];
|
||||
const post = si ? "iB" : "B";
|
||||
|
||||
if (bytes < base) {
|
||||
return `${bytes.toFixed(2)} ${post}`;
|
||||
}
|
||||
const exp = parseInt(Math.log(bytes) / Math.log(base) + "");
|
||||
const index = parseInt((exp - 1) + "");
|
||||
return `${(bytes / Math.pow(base, exp)).toFixed(2)} ${pre[index]}${post}`;
|
||||
}
|
||||
|
||||
function humanizeNumber(n: number) {
|
||||
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2020-2022 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { TD, TE } from "./encoders.ts";
|
||||
import { ErrorCode, NatsError } from "./core.ts";
|
||||
|
||||
export interface Codec<T> {
|
||||
/**
|
||||
* Encode T to an Uint8Array suitable for including in a message payload.
|
||||
* @param d
|
||||
*/
|
||||
encode(d: T): Uint8Array;
|
||||
|
||||
/**
|
||||
* Decode an Uint8Array from a message payload into a T
|
||||
* @param a
|
||||
*/
|
||||
decode(a: Uint8Array): T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Codec} for encoding strings to a message payload
|
||||
* and decoding message payloads into strings.
|
||||
* @constructor
|
||||
*/
|
||||
export function StringCodec(): Codec<string> {
|
||||
return {
|
||||
encode(d: string): Uint8Array {
|
||||
return TE.encode(d);
|
||||
},
|
||||
decode(a: Uint8Array): string {
|
||||
return TD.decode(a);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a {@link Codec} for encoding JavaScript object to JSON and
|
||||
* serialize them to an Uint8Array, and conversely, from an
|
||||
* Uint8Array to JSON to a JavaScript Object.
|
||||
* @param reviver
|
||||
* @constructor
|
||||
*/
|
||||
export function JSONCodec<T = unknown>(
|
||||
reviver?: (this: unknown, key: string, value: unknown) => unknown,
|
||||
): Codec<T> {
|
||||
return {
|
||||
encode(d: T): Uint8Array {
|
||||
try {
|
||||
if (d === undefined) {
|
||||
// @ts-ignore: json will not handle undefined
|
||||
d = null;
|
||||
}
|
||||
return TE.encode(JSON.stringify(d));
|
||||
} catch (err) {
|
||||
throw NatsError.errorForCode(ErrorCode.BadJson, err);
|
||||
}
|
||||
},
|
||||
|
||||
decode(a: Uint8Array): T {
|
||||
try {
|
||||
return JSON.parse(TD.decode(a), reviver);
|
||||
} catch (err) {
|
||||
throw NatsError.errorForCode(ErrorCode.BadJson, err);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2018-2021 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { TD, TE } from "./encoders.ts";
|
||||
|
||||
export class DataBuffer {
|
||||
buffers: Uint8Array[];
|
||||
byteLength: number;
|
||||
|
||||
constructor() {
|
||||
this.buffers = [];
|
||||
this.byteLength = 0;
|
||||
}
|
||||
|
||||
static concat(...bufs: Uint8Array[]): Uint8Array {
|
||||
let max = 0;
|
||||
for (let i = 0; i < bufs.length; i++) {
|
||||
max += bufs[i].length;
|
||||
}
|
||||
const out = new Uint8Array(max);
|
||||
let index = 0;
|
||||
for (let i = 0; i < bufs.length; i++) {
|
||||
out.set(bufs[i], index);
|
||||
index += bufs[i].length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
static fromAscii(m: string): Uint8Array {
|
||||
if (!m) {
|
||||
m = "";
|
||||
}
|
||||
return TE.encode(m);
|
||||
}
|
||||
|
||||
static toAscii(a: Uint8Array): string {
|
||||
return TD.decode(a);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.buffers.length = 0;
|
||||
this.byteLength = 0;
|
||||
}
|
||||
|
||||
pack(): void {
|
||||
if (this.buffers.length > 1) {
|
||||
const v = new Uint8Array(this.byteLength);
|
||||
let index = 0;
|
||||
for (let i = 0; i < this.buffers.length; i++) {
|
||||
v.set(this.buffers[i], index);
|
||||
index += this.buffers[i].length;
|
||||
}
|
||||
this.buffers.length = 0;
|
||||
this.buffers.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
shift(): Uint8Array {
|
||||
if (this.buffers.length) {
|
||||
const a = this.buffers.shift();
|
||||
if (a) {
|
||||
this.byteLength -= a.length;
|
||||
return a;
|
||||
}
|
||||
}
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
drain(n?: number): Uint8Array {
|
||||
if (this.buffers.length) {
|
||||
this.pack();
|
||||
const v = this.buffers.pop();
|
||||
if (v) {
|
||||
const max = this.byteLength;
|
||||
if (n === undefined || n > max) {
|
||||
n = max;
|
||||
}
|
||||
const d = v.subarray(0, n);
|
||||
if (max > n) {
|
||||
this.buffers.push(v.subarray(n));
|
||||
}
|
||||
this.byteLength = max - n;
|
||||
return d;
|
||||
}
|
||||
}
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
fill(a: Uint8Array, ...bufs: Uint8Array[]): void {
|
||||
if (a) {
|
||||
this.buffers.push(a);
|
||||
this.byteLength += a.length;
|
||||
}
|
||||
for (let i = 0; i < bufs.length; i++) {
|
||||
if (bufs[i] && bufs[i].length) {
|
||||
this.buffers.push(bufs[i]);
|
||||
this.byteLength += bufs[i].length;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
peek(): Uint8Array {
|
||||
if (this.buffers.length) {
|
||||
this.pack();
|
||||
return this.buffers[0];
|
||||
}
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return this.byteLength;
|
||||
}
|
||||
|
||||
length(): number {
|
||||
return this.buffers.length;
|
||||
}
|
||||
}
|
@ -0,0 +1,248 @@
|
||||
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
|
||||
|
||||
// This code has been ported almost directly from Go's src/bytes/buffer.go
|
||||
// Copyright 2009 The Go Authors. All rights reserved. BSD license.
|
||||
// https://github.com/golang/go/blob/master/LICENSE
|
||||
|
||||
// This code removes all Deno specific functionality to enable its use
|
||||
// in a browser environment
|
||||
|
||||
//@internal
|
||||
import { TE } from "./encoders.ts";
|
||||
|
||||
export class AssertionError extends Error {
|
||||
constructor(msg?: string) {
|
||||
super(msg);
|
||||
this.name = "AssertionError";
|
||||
}
|
||||
}
|
||||
|
||||
export interface Reader {
|
||||
read(p: Uint8Array): number | null;
|
||||
}
|
||||
|
||||
export interface Writer {
|
||||
write(p: Uint8Array): number;
|
||||
}
|
||||
|
||||
// @internal
|
||||
export function assert(cond: unknown, msg = "Assertion failed."): asserts cond {
|
||||
if (!cond) {
|
||||
throw new AssertionError(msg);
|
||||
}
|
||||
}
|
||||
|
||||
// MIN_READ is the minimum ArrayBuffer size passed to a read call by
|
||||
// buffer.ReadFrom. As long as the Buffer has at least MIN_READ bytes beyond
|
||||
// what is required to hold the contents of r, readFrom() will not grow the
|
||||
// underlying buffer.
|
||||
const MIN_READ = 32 * 1024;
|
||||
|
||||
export const MAX_SIZE = 2 ** 32 - 2;
|
||||
|
||||
// `off` is the offset into `dst` where it will at which to begin writing values
|
||||
// from `src`.
|
||||
// Returns the number of bytes copied.
|
||||
function copy(src: Uint8Array, dst: Uint8Array, off = 0): number {
|
||||
const r = dst.byteLength - off;
|
||||
if (src.byteLength > r) {
|
||||
src = src.subarray(0, r);
|
||||
}
|
||||
dst.set(src, off);
|
||||
return src.byteLength;
|
||||
}
|
||||
|
||||
export function concat(origin?: Uint8Array, b?: Uint8Array): Uint8Array {
|
||||
if (origin === undefined && b === undefined) {
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
if (origin === undefined) {
|
||||
return b!;
|
||||
}
|
||||
if (b === undefined) {
|
||||
return origin;
|
||||
}
|
||||
const output = new Uint8Array(origin.length + b.length);
|
||||
output.set(origin, 0);
|
||||
output.set(b, origin.length);
|
||||
return output;
|
||||
}
|
||||
|
||||
export function append(origin: Uint8Array, b: number): Uint8Array {
|
||||
return concat(origin, Uint8Array.of(b));
|
||||
}
|
||||
|
||||
export class DenoBuffer implements Reader, Writer {
|
||||
_buf: Uint8Array; // contents are the bytes _buf[off : len(_buf)]
|
||||
_off: number; // read at _buf[off], write at _buf[_buf.byteLength]
|
||||
|
||||
constructor(ab?: ArrayBuffer) {
|
||||
this._off = 0;
|
||||
if (ab == null) {
|
||||
this._buf = new Uint8Array(0);
|
||||
return;
|
||||
}
|
||||
|
||||
this._buf = new Uint8Array(ab);
|
||||
}
|
||||
|
||||
bytes(options: { copy?: boolean } = { copy: true }): Uint8Array {
|
||||
if (options.copy === false) return this._buf.subarray(this._off);
|
||||
return this._buf.slice(this._off);
|
||||
}
|
||||
|
||||
empty(): boolean {
|
||||
return this._buf.byteLength <= this._off;
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this._buf.byteLength - this._off;
|
||||
}
|
||||
|
||||
get capacity(): number {
|
||||
return this._buf.buffer.byteLength;
|
||||
}
|
||||
|
||||
truncate(n: number): void {
|
||||
if (n === 0) {
|
||||
this.reset();
|
||||
return;
|
||||
}
|
||||
if (n < 0 || n > this.length) {
|
||||
throw Error("bytes.Buffer: truncation out of range");
|
||||
}
|
||||
this._reslice(this._off + n);
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this._reslice(0);
|
||||
this._off = 0;
|
||||
}
|
||||
|
||||
_tryGrowByReslice(n: number): number {
|
||||
const l = this._buf.byteLength;
|
||||
if (n <= this.capacity - l) {
|
||||
this._reslice(l + n);
|
||||
return l;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
_reslice(len: number): void {
|
||||
assert(len <= this._buf.buffer.byteLength);
|
||||
this._buf = new Uint8Array(this._buf.buffer, 0, len);
|
||||
}
|
||||
|
||||
readByte(): number | null {
|
||||
const a = new Uint8Array(1);
|
||||
if (this.read(a)) {
|
||||
return a[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
read(p: Uint8Array): number | null {
|
||||
if (this.empty()) {
|
||||
// Buffer is empty, reset to recover space.
|
||||
this.reset();
|
||||
if (p.byteLength === 0) {
|
||||
// this edge case is tested in 'bufferReadEmptyAtEOF' test
|
||||
return 0;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const nread = copy(this._buf.subarray(this._off), p);
|
||||
this._off += nread;
|
||||
return nread;
|
||||
}
|
||||
|
||||
writeByte(n: number): number {
|
||||
return this.write(Uint8Array.of(n));
|
||||
}
|
||||
|
||||
writeString(s: string): number {
|
||||
return this.write(TE.encode(s));
|
||||
}
|
||||
|
||||
write(p: Uint8Array): number {
|
||||
const m = this._grow(p.byteLength);
|
||||
return copy(p, this._buf, m);
|
||||
}
|
||||
|
||||
_grow(n: number): number {
|
||||
const m = this.length;
|
||||
// If buffer is empty, reset to recover space.
|
||||
if (m === 0 && this._off !== 0) {
|
||||
this.reset();
|
||||
}
|
||||
// Fast: Try to _grow by means of a _reslice.
|
||||
const i = this._tryGrowByReslice(n);
|
||||
if (i >= 0) {
|
||||
return i;
|
||||
}
|
||||
const c = this.capacity;
|
||||
if (n <= Math.floor(c / 2) - m) {
|
||||
// We can slide things down instead of allocating a new
|
||||
// ArrayBuffer. We only need m+n <= c to slide, but
|
||||
// we instead let capacity get twice as large so we
|
||||
// don't spend all our time copying.
|
||||
copy(this._buf.subarray(this._off), this._buf);
|
||||
} else if (c + n > MAX_SIZE) {
|
||||
throw new Error("The buffer cannot be grown beyond the maximum size.");
|
||||
} else {
|
||||
// Not enough space anywhere, we need to allocate.
|
||||
const buf = new Uint8Array(Math.min(2 * c + n, MAX_SIZE));
|
||||
copy(this._buf.subarray(this._off), buf);
|
||||
this._buf = buf;
|
||||
}
|
||||
// Restore this.off and len(this._buf).
|
||||
this._off = 0;
|
||||
this._reslice(Math.min(m + n, MAX_SIZE));
|
||||
return m;
|
||||
}
|
||||
|
||||
grow(n: number): void {
|
||||
if (n < 0) {
|
||||
throw Error("Buffer._grow: negative count");
|
||||
}
|
||||
const m = this._grow(n);
|
||||
this._reslice(m);
|
||||
}
|
||||
|
||||
readFrom(r: Reader): number {
|
||||
let n = 0;
|
||||
const tmp = new Uint8Array(MIN_READ);
|
||||
while (true) {
|
||||
const shouldGrow = this.capacity - this.length < MIN_READ;
|
||||
// read into tmp buffer if there's not enough room
|
||||
// otherwise read directly into the internal buffer
|
||||
const buf = shouldGrow
|
||||
? tmp
|
||||
: new Uint8Array(this._buf.buffer, this.length);
|
||||
|
||||
const nread = r.read(buf);
|
||||
if (nread === null) {
|
||||
return n;
|
||||
}
|
||||
|
||||
// write will grow if needed
|
||||
if (shouldGrow) this.write(buf.subarray(0, nread));
|
||||
else this._reslice(this.length + nread);
|
||||
|
||||
n += nread;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function readAll(r: Reader): Uint8Array {
|
||||
const buf = new DenoBuffer();
|
||||
buf.readFrom(r);
|
||||
return buf.bytes();
|
||||
}
|
||||
|
||||
export function writeAll(w: Writer, arr: Uint8Array): void {
|
||||
let nwritten = 0;
|
||||
while (nwritten < arr.length) {
|
||||
nwritten += w.write(arr.subarray(nwritten));
|
||||
}
|
||||
}
|
@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright 2020 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
export const Empty = new Uint8Array(0);
|
||||
|
||||
export const TE = new TextEncoder();
|
||||
export const TD = new TextDecoder();
|
||||
|
||||
function concat(...bufs: Uint8Array[]): Uint8Array {
|
||||
let max = 0;
|
||||
for (let i = 0; i < bufs.length; i++) {
|
||||
max += bufs[i].length;
|
||||
}
|
||||
const out = new Uint8Array(max);
|
||||
let index = 0;
|
||||
for (let i = 0; i < bufs.length; i++) {
|
||||
out.set(bufs[i], index);
|
||||
index += bufs[i].length;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function encode(...a: string[]): Uint8Array {
|
||||
const bufs = [];
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
bufs.push(TE.encode(a[i]));
|
||||
}
|
||||
if (bufs.length === 0) {
|
||||
return Empty;
|
||||
}
|
||||
if (bufs.length === 1) {
|
||||
return bufs[0];
|
||||
}
|
||||
return concat(...bufs);
|
||||
}
|
||||
|
||||
export function decode(a: Uint8Array): string {
|
||||
if (!a || a.length === 0) {
|
||||
return "";
|
||||
}
|
||||
return TD.decode(a);
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
/*
|
||||
* Copyright 2018-2021 The NATS Authors
|
||||
* 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.
|
||||
*/
|
@ -0,0 +1,299 @@
|
||||
/*
|
||||
* Copyright 2020-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Heavily inspired by Golang's https://golang.org/src/net/http/header.go
|
||||
|
||||
import { TD, TE } from "./encoders.ts";
|
||||
import { ErrorCode, Match, MsgHdrs, NatsError } from "./core.ts";
|
||||
|
||||
// https://www.ietf.org/rfc/rfc822.txt
|
||||
// 3.1.2. STRUCTURE OF HEADER FIELDS
|
||||
//
|
||||
// Once a field has been unfolded, it may be viewed as being com-
|
||||
// posed of a field-name followed by a colon (":"), followed by a
|
||||
// field-body, and terminated by a carriage-return/line-feed.
|
||||
// The field-name must be composed of printable ASCII characters
|
||||
// (i.e., characters that have values between 33. and 126.,
|
||||
// decimal, except colon). The field-body may be composed of any
|
||||
// ASCII characters, except CR or LF. (While CR and/or LF may be
|
||||
// present in the actual text, they are removed by the action of
|
||||
// unfolding the field.)
|
||||
export function canonicalMIMEHeaderKey(k: string): string {
|
||||
const a = 97;
|
||||
const A = 65;
|
||||
const Z = 90;
|
||||
const z = 122;
|
||||
const dash = 45;
|
||||
const colon = 58;
|
||||
const start = 33;
|
||||
const end = 126;
|
||||
const toLower = a - A;
|
||||
|
||||
let upper = true;
|
||||
const buf: number[] = new Array(k.length);
|
||||
for (let i = 0; i < k.length; i++) {
|
||||
let c = k.charCodeAt(i);
|
||||
if (c === colon || c < start || c > end) {
|
||||
throw new NatsError(
|
||||
`'${k[i]}' is not a valid character for a header key`,
|
||||
ErrorCode.BadHeader,
|
||||
);
|
||||
}
|
||||
if (upper && a <= c && c <= z) {
|
||||
c -= toLower;
|
||||
} else if (!upper && A <= c && c <= Z) {
|
||||
c += toLower;
|
||||
}
|
||||
buf[i] = c;
|
||||
upper = c == dash;
|
||||
}
|
||||
return String.fromCharCode(...buf);
|
||||
}
|
||||
|
||||
export function headers(code = 0, description = ""): MsgHdrs {
|
||||
if ((code === 0 && description !== "") || (code > 0 && description === "")) {
|
||||
throw new Error("setting status requires both code and description");
|
||||
}
|
||||
return new MsgHdrsImpl(code, description);
|
||||
}
|
||||
|
||||
const HEADER = "NATS/1.0";
|
||||
|
||||
export class MsgHdrsImpl implements MsgHdrs {
|
||||
_code: number;
|
||||
headers: Map<string, string[]>;
|
||||
_description: string;
|
||||
|
||||
constructor(code = 0, description = "") {
|
||||
this._code = code;
|
||||
this._description = description;
|
||||
this.headers = new Map();
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this.headers.entries();
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return this.headers.size;
|
||||
}
|
||||
|
||||
equals(mh: MsgHdrsImpl): boolean {
|
||||
if (
|
||||
mh && this.headers.size === mh.headers.size &&
|
||||
this._code === mh._code
|
||||
) {
|
||||
for (const [k, v] of this.headers) {
|
||||
const a = mh.values(k);
|
||||
if (v.length !== a.length) {
|
||||
return false;
|
||||
}
|
||||
const vv = [...v].sort();
|
||||
const aa = [...a].sort();
|
||||
for (let i = 0; i < vv.length; i++) {
|
||||
if (vv[i] !== aa[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static decode(a: Uint8Array): MsgHdrsImpl {
|
||||
const mh = new MsgHdrsImpl();
|
||||
const s = TD.decode(a);
|
||||
const lines = s.split("\r\n");
|
||||
const h = lines[0];
|
||||
if (h !== HEADER) {
|
||||
// malformed headers could add extra space without adding a code or description
|
||||
let str = h.replace(HEADER, "").trim();
|
||||
if (str.length > 0) {
|
||||
mh._code = parseInt(str, 10);
|
||||
if (isNaN(mh._code)) {
|
||||
mh._code = 0;
|
||||
}
|
||||
const scode = mh._code.toString();
|
||||
str = str.replace(scode, "");
|
||||
mh._description = str.trim();
|
||||
}
|
||||
}
|
||||
if (lines.length >= 1) {
|
||||
lines.slice(1).map((s) => {
|
||||
if (s) {
|
||||
const idx = s.indexOf(":");
|
||||
if (idx > -1) {
|
||||
const k = s.slice(0, idx);
|
||||
const v = s.slice(idx + 1).trim();
|
||||
mh.append(k, v);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return mh;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
if (this.headers.size === 0 && this._code === 0) {
|
||||
return "";
|
||||
}
|
||||
let s = HEADER;
|
||||
if (this._code > 0 && this._description !== "") {
|
||||
s += ` ${this._code} ${this._description}`;
|
||||
}
|
||||
for (const [k, v] of this.headers) {
|
||||
for (let i = 0; i < v.length; i++) {
|
||||
s = `${s}\r\n${k}: ${v[i]}`;
|
||||
}
|
||||
}
|
||||
return `${s}\r\n\r\n`;
|
||||
}
|
||||
|
||||
encode(): Uint8Array {
|
||||
return TE.encode(this.toString());
|
||||
}
|
||||
|
||||
static validHeaderValue(k: string): string {
|
||||
const inv = /[\r\n]/;
|
||||
if (inv.test(k)) {
|
||||
throw new NatsError(
|
||||
"invalid header value - \\r and \\n are not allowed.",
|
||||
ErrorCode.BadHeader,
|
||||
);
|
||||
}
|
||||
return k.trim();
|
||||
}
|
||||
|
||||
keys(): string[] {
|
||||
const keys = [];
|
||||
for (const sk of this.headers.keys()) {
|
||||
keys.push(sk);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
findKeys(k: string, match = Match.Exact): string[] {
|
||||
const keys = this.keys();
|
||||
switch (match) {
|
||||
case Match.Exact:
|
||||
return keys.filter((v) => {
|
||||
return v === k;
|
||||
});
|
||||
case Match.CanonicalMIME:
|
||||
k = canonicalMIMEHeaderKey(k);
|
||||
return keys.filter((v) => {
|
||||
return v === k;
|
||||
});
|
||||
default: {
|
||||
const lci = k.toLowerCase();
|
||||
return keys.filter((v) => {
|
||||
return lci === v.toLowerCase();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get(k: string, match = Match.Exact): string {
|
||||
const keys = this.findKeys(k, match);
|
||||
if (keys.length) {
|
||||
const v = this.headers.get(keys[0]);
|
||||
if (v) {
|
||||
return Array.isArray(v) ? v[0] : v;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
has(k: string, match = Match.Exact): boolean {
|
||||
return this.findKeys(k, match).length > 0;
|
||||
}
|
||||
|
||||
set(k: string, v: string, match = Match.Exact): void {
|
||||
this.delete(k, match);
|
||||
this.append(k, v, match);
|
||||
}
|
||||
|
||||
append(k: string, v: string, match = Match.Exact): void {
|
||||
// validate the key
|
||||
const ck = canonicalMIMEHeaderKey(k);
|
||||
if (match === Match.CanonicalMIME) {
|
||||
k = ck;
|
||||
}
|
||||
// if we get non-sensical ignores/etc, we should try
|
||||
// to do the right thing and use the first key that matches
|
||||
const keys = this.findKeys(k, match);
|
||||
k = keys.length > 0 ? keys[0] : k;
|
||||
|
||||
const value = MsgHdrsImpl.validHeaderValue(v);
|
||||
let a = this.headers.get(k);
|
||||
if (!a) {
|
||||
a = [];
|
||||
this.headers.set(k, a);
|
||||
}
|
||||
a.push(value);
|
||||
}
|
||||
|
||||
values(k: string, match = Match.Exact): string[] {
|
||||
const buf: string[] = [];
|
||||
const keys = this.findKeys(k, match);
|
||||
keys.forEach((v) => {
|
||||
const values = this.headers.get(v);
|
||||
if (values) {
|
||||
buf.push(...values);
|
||||
}
|
||||
});
|
||||
return buf;
|
||||
}
|
||||
|
||||
delete(k: string, match = Match.Exact): void {
|
||||
const keys = this.findKeys(k, match);
|
||||
keys.forEach((v) => {
|
||||
this.headers.delete(v);
|
||||
});
|
||||
}
|
||||
|
||||
get hasError() {
|
||||
return this._code >= 300;
|
||||
}
|
||||
|
||||
get status(): string {
|
||||
return `${this._code} ${this._description}`.trim();
|
||||
}
|
||||
|
||||
toRecord(): Record<string, string[]> {
|
||||
const data = {} as Record<string, string[]>;
|
||||
this.keys().forEach((v) => {
|
||||
data[v] = this.values(v);
|
||||
});
|
||||
return data;
|
||||
}
|
||||
|
||||
get code(): number {
|
||||
return this._code;
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this._description;
|
||||
}
|
||||
|
||||
static fromRecord(r: Record<string, string[]>): MsgHdrs {
|
||||
const h = new MsgHdrsImpl();
|
||||
for (const k in r) {
|
||||
h.headers.set(k, r[k]);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
}
|
@ -0,0 +1,92 @@
|
||||
/*
|
||||
* Copyright 2020-2021 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { Deferred, deferred } from "./util.ts";
|
||||
|
||||
import { DebugEvents, Status } from "./core.ts";
|
||||
|
||||
export interface PH {
|
||||
flush(p?: Deferred<void>): Promise<void>;
|
||||
disconnect(): void;
|
||||
dispatchStatus(status: Status): void;
|
||||
}
|
||||
|
||||
export class Heartbeat {
|
||||
ph: PH;
|
||||
interval: number;
|
||||
maxOut: number;
|
||||
timer?: number;
|
||||
pendings: Promise<void>[];
|
||||
|
||||
constructor(ph: PH, interval: number, maxOut: number) {
|
||||
this.ph = ph;
|
||||
this.interval = interval;
|
||||
this.maxOut = maxOut;
|
||||
this.pendings = [];
|
||||
}
|
||||
|
||||
// api to start the heartbeats, since this can be
|
||||
// spuriously called from dial, ensure we don't
|
||||
// leak timers
|
||||
start() {
|
||||
this.cancel();
|
||||
this._schedule();
|
||||
}
|
||||
|
||||
// api for canceling the heartbeats, if stale is
|
||||
// true it will initiate a client disconnect
|
||||
cancel(stale?: boolean) {
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer);
|
||||
this.timer = undefined;
|
||||
}
|
||||
this._reset();
|
||||
if (stale) {
|
||||
this.ph.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
_schedule() {
|
||||
// @ts-ignore: node is not a number - we treat this opaquely
|
||||
this.timer = setTimeout(() => {
|
||||
this.ph.dispatchStatus(
|
||||
{ type: DebugEvents.PingTimer, data: `${this.pendings.length + 1}` },
|
||||
);
|
||||
if (this.pendings.length === this.maxOut) {
|
||||
this.cancel(true);
|
||||
return;
|
||||
}
|
||||
const ping = deferred<void>();
|
||||
this.ph.flush(ping)
|
||||
.then(() => {
|
||||
this._reset();
|
||||
})
|
||||
.catch(() => {
|
||||
// we disconnected - pongs were rejected
|
||||
this.cancel();
|
||||
});
|
||||
this.pendings.push(ping);
|
||||
this._schedule();
|
||||
}, this.interval);
|
||||
}
|
||||
|
||||
_reset() {
|
||||
// clear pendings after resolving them
|
||||
this.pendings = this.pendings.filter((p) => {
|
||||
const d = p as Deferred<void>;
|
||||
d.resolve();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright 2022 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Called with the number of missed heartbeats.
|
||||
* If the function returns true, the monitor will cancel monitoring.
|
||||
*/
|
||||
export type IdleHeartbeatFn = (n: number) => boolean;
|
||||
|
||||
/**
|
||||
* IdleHeartbeatOptions
|
||||
*/
|
||||
export type IdleHeartbeatOptions = {
|
||||
/**
|
||||
* @field maxOut - optional maximum number of missed heartbeats before notifying (default is 2)
|
||||
*/
|
||||
maxOut: number;
|
||||
/**
|
||||
* @field cancelAfter - optional timer to auto cancel monitoring in millis
|
||||
*/
|
||||
cancelAfter: number;
|
||||
};
|
||||
|
||||
export class IdleHeartbeat {
|
||||
interval: number;
|
||||
maxOut: number;
|
||||
cancelAfter: number;
|
||||
timer?: number;
|
||||
autoCancelTimer?: number;
|
||||
last!: number;
|
||||
missed: number;
|
||||
count: number;
|
||||
callback: IdleHeartbeatFn;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
* @param interval in millis to check
|
||||
* @param cb a callback to report when heartbeats are missed
|
||||
* @param opts monitor options @see IdleHeartbeatOptions
|
||||
*/
|
||||
constructor(
|
||||
interval: number,
|
||||
cb: IdleHeartbeatFn,
|
||||
opts: Partial<IdleHeartbeatOptions> = { maxOut: 2 },
|
||||
) {
|
||||
this.interval = interval;
|
||||
this.maxOut = opts?.maxOut || 2;
|
||||
this.cancelAfter = opts?.cancelAfter || 0;
|
||||
this.last = Date.now();
|
||||
this.missed = 0;
|
||||
this.count = 0;
|
||||
this.callback = cb;
|
||||
|
||||
this._schedule();
|
||||
}
|
||||
|
||||
/**
|
||||
* cancel monitoring
|
||||
*/
|
||||
cancel() {
|
||||
if (this.autoCancelTimer) {
|
||||
clearTimeout(this.autoCancelTimer);
|
||||
}
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
this.timer = 0;
|
||||
this.autoCancelTimer = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* work signals that there was work performed
|
||||
*/
|
||||
work() {
|
||||
this.last = Date.now();
|
||||
this.missed = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* internal api to change the interval, cancelAfter and maxOut
|
||||
* @param interval
|
||||
* @param cancelAfter
|
||||
* @param maxOut
|
||||
*/
|
||||
_change(interval: number, cancelAfter = 0, maxOut = 2) {
|
||||
this.interval = interval;
|
||||
this.maxOut = maxOut;
|
||||
this.cancelAfter = cancelAfter;
|
||||
this.restart();
|
||||
}
|
||||
|
||||
/**
|
||||
* cancels and restarts the monitoring
|
||||
*/
|
||||
restart() {
|
||||
this.cancel();
|
||||
this._schedule();
|
||||
}
|
||||
|
||||
/**
|
||||
* internal api called to start monitoring
|
||||
*/
|
||||
_schedule() {
|
||||
if (this.cancelAfter > 0) {
|
||||
// @ts-ignore: in node is not a number - we treat this opaquely
|
||||
this.autoCancelTimer = setTimeout(() => {
|
||||
this.cancel();
|
||||
}, this.cancelAfter);
|
||||
}
|
||||
// @ts-ignore: in node is not a number - we treat this opaquely
|
||||
this.timer = setInterval(() => {
|
||||
this.count++;
|
||||
if ((Date.now() - this.last) > this.interval) {
|
||||
this.missed++;
|
||||
}
|
||||
if (this.missed >= this.maxOut) {
|
||||
try {
|
||||
if (this.callback(this.missed) === true) {
|
||||
this.cancel();
|
||||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
}, this.interval);
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
export { NatsConnectionImpl } from "./nats";
|
||||
export { Nuid, nuid } from "./nuid";
|
||||
|
||||
export type { ServiceClient, TypedSubscriptionOptions } from "./types";
|
||||
|
||||
export { MsgImpl } from "./msg";
|
||||
export { setTransportFactory } from "./transport";
|
||||
export type { Transport, TransportFactory } from "./transport";
|
||||
export { Connect, INFO, ProtocolHandler } from "./protocol";
|
||||
export type { Deferred, Perf, Timeout } from "./util";
|
||||
export { collect, deferred, delay, extend, render, timeout } from "./util";
|
||||
export { canonicalMIMEHeaderKey, headers, MsgHdrsImpl } from "./headers";
|
||||
export { Heartbeat } from "./heartbeats";
|
||||
export type { PH } from "./heartbeats";
|
||||
export { MuxSubscription } from "./muxsubscription";
|
||||
export { DataBuffer } from "./databuffer";
|
||||
export {
|
||||
buildAuthenticator,
|
||||
checkOptions,
|
||||
checkUnsupportedOption,
|
||||
} from "./options";
|
||||
export { RequestOne } from "./request";
|
||||
export {
|
||||
credsAuthenticator,
|
||||
jwtAuthenticator,
|
||||
nkeyAuthenticator,
|
||||
tokenAuthenticator,
|
||||
usernamePasswordAuthenticator,
|
||||
} from "./authenticator";
|
||||
export type { Codec } from "./codec";
|
||||
export { JSONCodec, StringCodec } from "./codec";
|
||||
export * from "./nkeys";
|
||||
export type {
|
||||
DispatchedFn,
|
||||
IngestionFilterFn,
|
||||
IngestionFilterFnResult,
|
||||
ProtocolFilterFn,
|
||||
} from "./queued_iterator";
|
||||
export { QueuedIteratorImpl } from "./queued_iterator";
|
||||
export type { ParserEvent } from "./parser";
|
||||
export { Kind, Parser, State } from "./parser";
|
||||
export { DenoBuffer, MAX_SIZE, readAll, writeAll } from "./denobuffer";
|
||||
export { Bench, Metric } from "./bench";
|
||||
export type { BenchOpts } from "./bench";
|
||||
export { TD, TE } from "./encoders";
|
||||
export { isIP, parseIP } from "./ipparser";
|
||||
export { TypedSubscription } from "./typedsub";
|
||||
export type { MsgAdapter, TypedCallback } from "./typedsub";
|
||||
export {
|
||||
Base64KeyCodec,
|
||||
Bucket,
|
||||
defaultBucketOpts,
|
||||
NoopKvCodecs,
|
||||
} from "../jetstream/kv";
|
||||
|
||||
export type { SemVer } from "./semver";
|
||||
|
||||
export { compare, parseSemVer } from "./semver";
|
||||
|
||||
export { Empty } from "./types";
|
||||
export { extractProtocolMessage } from "./transport";
|
||||
|
||||
export type {
|
||||
ApiError,
|
||||
Auth,
|
||||
Authenticator,
|
||||
ConnectionOptions,
|
||||
Dispatcher,
|
||||
Endpoint,
|
||||
EndpointInfo,
|
||||
EndpointOptions,
|
||||
EndpointStats,
|
||||
JwtAuth,
|
||||
Msg,
|
||||
MsgHdrs,
|
||||
NamedEndpointStats,
|
||||
Nanos,
|
||||
NatsConnection,
|
||||
NKeyAuth,
|
||||
NoAuth,
|
||||
Payload,
|
||||
PublishOptions,
|
||||
QueuedIterator,
|
||||
Request,
|
||||
RequestManyOptions,
|
||||
RequestOptions,
|
||||
ReviverFn,
|
||||
Server,
|
||||
ServerInfo,
|
||||
ServersChanged,
|
||||
Service,
|
||||
ServiceConfig,
|
||||
ServiceGroup,
|
||||
ServiceHandler,
|
||||
ServiceIdentity,
|
||||
ServiceInfo,
|
||||
ServiceMetadata,
|
||||
ServiceMsg,
|
||||
ServiceResponse,
|
||||
ServicesAPI,
|
||||
ServiceStats,
|
||||
Stats,
|
||||
Status,
|
||||
Sub,
|
||||
SubOpts,
|
||||
Subscription,
|
||||
SubscriptionOptions,
|
||||
SyncIterator,
|
||||
TlsOptions,
|
||||
TokenAuth,
|
||||
UserPass,
|
||||
} from "./core";
|
||||
export {
|
||||
createInbox,
|
||||
DebugEvents,
|
||||
ErrorCode,
|
||||
Events,
|
||||
isNatsError,
|
||||
Match,
|
||||
NatsError,
|
||||
RequestStrategy,
|
||||
ServiceError,
|
||||
ServiceErrorCodeHeader,
|
||||
ServiceErrorHeader,
|
||||
ServiceResponseType,
|
||||
ServiceVerb,
|
||||
syncIterator,
|
||||
} from "./core";
|
||||
export { SubscriptionImpl, Subscriptions } from "./protocol";
|
@ -0,0 +1,215 @@
|
||||
/*
|
||||
* Copyright 2020-2021 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// JavaScript port of go net/ip/ParseIP
|
||||
// https://github.com/golang/go/blob/master/src/net/ip.go
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
const IPv4LEN = 4;
|
||||
const IPv6LEN = 16;
|
||||
const ASCII0 = 48;
|
||||
const ASCII9 = 57;
|
||||
const ASCIIA = 65;
|
||||
const ASCIIF = 70;
|
||||
const ASCIIa = 97;
|
||||
const ASCIIf = 102;
|
||||
const big = 0xFFFFFF;
|
||||
|
||||
export function ipV4(a: number, b: number, c: number, d: number): Uint8Array {
|
||||
const ip = new Uint8Array(IPv6LEN);
|
||||
const prefix = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff];
|
||||
prefix.forEach((v, idx) => {
|
||||
ip[idx] = v;
|
||||
});
|
||||
ip[12] = a;
|
||||
ip[13] = b;
|
||||
ip[14] = c;
|
||||
ip[15] = d;
|
||||
|
||||
return ip;
|
||||
}
|
||||
|
||||
export function isIP(h: string) {
|
||||
return parseIP(h) !== undefined;
|
||||
}
|
||||
|
||||
export function parseIP(h: string): Uint8Array | undefined {
|
||||
for (let i = 0; i < h.length; i++) {
|
||||
switch (h[i]) {
|
||||
case ".":
|
||||
return parseIPv4(h);
|
||||
case ":":
|
||||
return parseIPv6(h);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
function parseIPv4(s: string): Uint8Array | undefined {
|
||||
const ip = new Uint8Array(IPv4LEN);
|
||||
for (let i = 0; i < IPv4LEN; i++) {
|
||||
if (s.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (i > 0) {
|
||||
if (s[0] !== ".") {
|
||||
return undefined;
|
||||
}
|
||||
s = s.substring(1);
|
||||
}
|
||||
const { n, c, ok } = dtoi(s);
|
||||
if (!ok || n > 0xFF) {
|
||||
return undefined;
|
||||
}
|
||||
s = s.substring(c);
|
||||
ip[i] = n;
|
||||
}
|
||||
return ipV4(ip[0], ip[1], ip[2], ip[3]);
|
||||
}
|
||||
|
||||
function parseIPv6(s: string): Uint8Array | undefined {
|
||||
const ip = new Uint8Array(IPv6LEN);
|
||||
let ellipsis = -1;
|
||||
|
||||
if (s.length >= 2 && s[0] === ":" && s[1] === ":") {
|
||||
ellipsis = 0;
|
||||
s = s.substring(2);
|
||||
if (s.length === 0) {
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
|
||||
let i = 0;
|
||||
while (i < IPv6LEN) {
|
||||
const { n, c, ok } = xtoi(s);
|
||||
if (!ok || n > 0xFFFF) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (c < s.length && s[c] === ".") {
|
||||
if (ellipsis < 0 && i != IPv6LEN - IPv4LEN) {
|
||||
return undefined;
|
||||
}
|
||||
if (i + IPv4LEN > IPv6LEN) {
|
||||
return undefined;
|
||||
}
|
||||
const ip4 = parseIPv4(s);
|
||||
if (ip4 === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
ip[i] = ip4[12];
|
||||
ip[i + 1] = ip4[13];
|
||||
ip[i + 2] = ip4[14];
|
||||
ip[i + 3] = ip4[15];
|
||||
s = "";
|
||||
i += IPv4LEN;
|
||||
break;
|
||||
}
|
||||
|
||||
ip[i] = n >> 8;
|
||||
ip[i + 1] = n;
|
||||
i += 2;
|
||||
|
||||
s = s.substring(c);
|
||||
if (s.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (s[0] !== ":" || s.length == 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
s = s.substring(1);
|
||||
|
||||
if (s[0] === ":") {
|
||||
if (ellipsis >= 0) {
|
||||
return undefined;
|
||||
}
|
||||
ellipsis = i;
|
||||
s = s.substring(1);
|
||||
if (s.length === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s.length !== 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (i < IPv6LEN) {
|
||||
if (ellipsis < 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const n = IPv6LEN - i;
|
||||
for (let j = i - 1; j >= ellipsis; j--) {
|
||||
ip[j + n] = ip[j];
|
||||
}
|
||||
for (let j = ellipsis + n - 1; j >= ellipsis; j--) {
|
||||
ip[j] = 0;
|
||||
}
|
||||
} else if (ellipsis >= 0) {
|
||||
return undefined;
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
function dtoi(s: string): { n: number; c: number; ok: boolean } {
|
||||
let i = 0;
|
||||
let n = 0;
|
||||
for (
|
||||
i = 0;
|
||||
i < s.length && ASCII0 <= s.charCodeAt(i) && s.charCodeAt(i) <= ASCII9;
|
||||
i++
|
||||
) {
|
||||
n = n * 10 + (s.charCodeAt(i) - ASCII0);
|
||||
if (n >= big) {
|
||||
return { n: big, c: i, ok: false };
|
||||
}
|
||||
}
|
||||
if (i === 0) {
|
||||
return { n: 0, c: 0, ok: false };
|
||||
}
|
||||
return { n: n, c: i, ok: true };
|
||||
}
|
||||
|
||||
function xtoi(s: string): { n: number; c: number; ok: boolean } {
|
||||
let n = 0;
|
||||
let i = 0;
|
||||
for (i = 0; i < s.length; i++) {
|
||||
if (ASCII0 <= s.charCodeAt(i) && s.charCodeAt(i) <= ASCII9) {
|
||||
n *= 16;
|
||||
n += s.charCodeAt(i) - ASCII0;
|
||||
} else if (ASCIIa <= s.charCodeAt(i) && s.charCodeAt(i) <= ASCIIf) {
|
||||
n *= 16;
|
||||
n += (s.charCodeAt(i) - ASCIIa) + 10;
|
||||
} else if (ASCIIA <= s.charCodeAt(i) && s.charCodeAt(i) <= ASCIIF) {
|
||||
n *= 16;
|
||||
n += (s.charCodeAt(i) - ASCIIA) + 10;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
if (n >= big) {
|
||||
return { n: 0, c: i, ok: false };
|
||||
}
|
||||
}
|
||||
if (i === 0) {
|
||||
return { n: 0, c: i, ok: false };
|
||||
}
|
||||
return { n: n, c: i, ok: true };
|
||||
}
|
@ -0,0 +1,102 @@
|
||||
/*
|
||||
* mod.ts
|
||||
* Copyright (C) 2023 veypi <i@veypi.com>
|
||||
* 2023-10-16 20:16
|
||||
* Distributed under terms of the MIT license.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
export {
|
||||
Bench,
|
||||
buildAuthenticator,
|
||||
canonicalMIMEHeaderKey,
|
||||
createInbox,
|
||||
credsAuthenticator,
|
||||
DebugEvents,
|
||||
deferred,
|
||||
Empty,
|
||||
ErrorCode,
|
||||
Events,
|
||||
headers,
|
||||
JSONCodec,
|
||||
jwtAuthenticator,
|
||||
Match,
|
||||
Metric,
|
||||
MsgHdrsImpl,
|
||||
NatsError,
|
||||
nkeyAuthenticator,
|
||||
nkeys,
|
||||
Nuid,
|
||||
nuid,
|
||||
RequestStrategy,
|
||||
ServiceError,
|
||||
ServiceErrorCodeHeader,
|
||||
ServiceErrorHeader,
|
||||
ServiceResponseType,
|
||||
ServiceVerb,
|
||||
StringCodec,
|
||||
syncIterator,
|
||||
tokenAuthenticator,
|
||||
usernamePasswordAuthenticator,
|
||||
} from "./internal_mod";
|
||||
|
||||
export type {
|
||||
ApiError,
|
||||
Auth,
|
||||
Authenticator,
|
||||
BenchOpts,
|
||||
Codec,
|
||||
ConnectionOptions,
|
||||
Deferred,
|
||||
DispatchedFn,
|
||||
Endpoint,
|
||||
EndpointInfo,
|
||||
EndpointOptions,
|
||||
EndpointStats,
|
||||
IngestionFilterFn,
|
||||
IngestionFilterFnResult,
|
||||
JwtAuth,
|
||||
Msg,
|
||||
MsgAdapter,
|
||||
MsgHdrs,
|
||||
NamedEndpointStats,
|
||||
Nanos,
|
||||
NatsConnection,
|
||||
NKeyAuth,
|
||||
NoAuth,
|
||||
Payload,
|
||||
Perf,
|
||||
ProtocolFilterFn,
|
||||
PublishOptions,
|
||||
QueuedIterator,
|
||||
RequestManyOptions,
|
||||
RequestOptions,
|
||||
ReviverFn,
|
||||
ServerInfo,
|
||||
ServersChanged,
|
||||
Service,
|
||||
ServiceClient,
|
||||
ServiceConfig,
|
||||
ServiceGroup,
|
||||
ServiceHandler,
|
||||
ServiceIdentity,
|
||||
ServiceInfo,
|
||||
ServiceMetadata,
|
||||
ServiceMsg,
|
||||
ServiceResponse,
|
||||
ServicesAPI,
|
||||
ServiceStats,
|
||||
Stats,
|
||||
Status,
|
||||
Sub,
|
||||
SubOpts,
|
||||
Subscription,
|
||||
SubscriptionOptions,
|
||||
SyncIterator,
|
||||
TlsOptions,
|
||||
TokenAuth,
|
||||
TypedCallback,
|
||||
TypedSubscriptionOptions,
|
||||
UserPass,
|
||||
} from "./internal_mod";
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2020-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { MsgHdrsImpl } from "./headers.ts";
|
||||
import type { MsgArg } from "./parser.ts";
|
||||
import { Empty, TD } from "./encoders.ts";
|
||||
import { Codec, JSONCodec } from "./codec.ts";
|
||||
import {
|
||||
ErrorCode,
|
||||
Msg,
|
||||
MsgHdrs,
|
||||
NatsError,
|
||||
Publisher,
|
||||
ReviverFn,
|
||||
} from "./core.ts";
|
||||
|
||||
export function isRequestError(msg: Msg): NatsError | null {
|
||||
// NATS core only considers errors 503s on messages that have no payload
|
||||
// everything else simply forwarded as part of the message and is considered
|
||||
// application level information
|
||||
if (msg && msg.data.length === 0 && msg.headers?.code === 503) {
|
||||
return NatsError.errorForCode(ErrorCode.NoResponders);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export class MsgImpl implements Msg {
|
||||
_headers?: MsgHdrs;
|
||||
_msg: MsgArg;
|
||||
_rdata: Uint8Array;
|
||||
_reply!: string;
|
||||
_subject!: string;
|
||||
publisher: Publisher;
|
||||
static jc: Codec<unknown>;
|
||||
|
||||
constructor(msg: MsgArg, data: Uint8Array, publisher: Publisher) {
|
||||
this._msg = msg;
|
||||
this._rdata = data;
|
||||
this.publisher = publisher;
|
||||
}
|
||||
|
||||
get subject(): string {
|
||||
if (this._subject) {
|
||||
return this._subject;
|
||||
}
|
||||
this._subject = TD.decode(this._msg.subject);
|
||||
return this._subject;
|
||||
}
|
||||
|
||||
get reply(): string {
|
||||
if (this._reply) {
|
||||
return this._reply;
|
||||
}
|
||||
this._reply = TD.decode(this._msg.reply);
|
||||
return this._reply;
|
||||
}
|
||||
|
||||
get sid(): number {
|
||||
return this._msg.sid;
|
||||
}
|
||||
|
||||
get headers(): MsgHdrs | undefined {
|
||||
if (this._msg.hdr > -1 && !this._headers) {
|
||||
const buf = this._rdata.subarray(0, this._msg.hdr);
|
||||
this._headers = MsgHdrsImpl.decode(buf);
|
||||
}
|
||||
return this._headers;
|
||||
}
|
||||
|
||||
get data(): Uint8Array {
|
||||
if (!this._rdata) {
|
||||
return new Uint8Array(0);
|
||||
}
|
||||
return this._msg.hdr > -1
|
||||
? this._rdata.subarray(this._msg.hdr)
|
||||
: this._rdata;
|
||||
}
|
||||
|
||||
// eslint-ignore-next-line @typescript-eslint/no-explicit-any
|
||||
respond(
|
||||
data: Uint8Array = Empty,
|
||||
opts?: { headers?: MsgHdrs; reply?: string },
|
||||
): boolean {
|
||||
if (this.reply) {
|
||||
this.publisher.publish(this.reply, data, opts);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size(): number {
|
||||
const subj = this._msg.subject.length;
|
||||
const reply = this._msg.reply?.length || 0;
|
||||
const payloadAndHeaders = this._msg.size === -1 ? 0 : this._msg.size;
|
||||
return subj + reply + payloadAndHeaders;
|
||||
}
|
||||
|
||||
json<T = unknown>(reviver?: ReviverFn): T {
|
||||
return JSONCodec<T>(reviver).decode(this.data);
|
||||
}
|
||||
|
||||
string(): string {
|
||||
return TD.decode(this.data);
|
||||
}
|
||||
}
|
@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2020-2021 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { isRequestError } from "./msg.ts";
|
||||
import { createInbox, ErrorCode, Msg, NatsError, Request } from "./core.ts";
|
||||
|
||||
export class MuxSubscription {
|
||||
baseInbox!: string;
|
||||
reqs: Map<string, Request>;
|
||||
|
||||
constructor() {
|
||||
this.reqs = new Map<string, Request>();
|
||||
}
|
||||
|
||||
size(): number {
|
||||
return this.reqs.size;
|
||||
}
|
||||
|
||||
init(prefix?: string): string {
|
||||
this.baseInbox = `${createInbox(prefix)}.`;
|
||||
return this.baseInbox;
|
||||
}
|
||||
|
||||
add(r: Request) {
|
||||
if (!isNaN(r.received)) {
|
||||
r.received = 0;
|
||||
}
|
||||
this.reqs.set(r.token, r);
|
||||
}
|
||||
|
||||
get(token: string): Request | undefined {
|
||||
return this.reqs.get(token);
|
||||
}
|
||||
|
||||
cancel(r: Request): void {
|
||||
this.reqs.delete(r.token);
|
||||
}
|
||||
|
||||
getToken(m: Msg): string | null {
|
||||
const s = m.subject || "";
|
||||
if (s.indexOf(this.baseInbox) === 0) {
|
||||
return s.substring(this.baseInbox.length);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
all(): Request[] {
|
||||
return Array.from(this.reqs.values());
|
||||
}
|
||||
|
||||
handleError(isMuxPermissionError: boolean, err?: NatsError): boolean {
|
||||
if (err && err.permissionContext) {
|
||||
if (isMuxPermissionError) {
|
||||
// one or more requests queued but mux cannot process them
|
||||
this.all().forEach((r) => {
|
||||
r.resolver(err, {} as Msg);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const ctx = err.permissionContext;
|
||||
if (ctx.operation === "publish") {
|
||||
const req = this.all().find((s) => {
|
||||
return s.requestSubject === ctx.subject;
|
||||
});
|
||||
if (req) {
|
||||
req.resolver(err, {} as Msg);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
dispatcher() {
|
||||
return (err: NatsError | null, m: Msg) => {
|
||||
const token = this.getToken(m);
|
||||
if (token) {
|
||||
const r = this.get(token);
|
||||
if (r) {
|
||||
if (err === null && m.headers) {
|
||||
err = isRequestError(m);
|
||||
}
|
||||
r.resolver(err, m);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
close() {
|
||||
const err = NatsError.errorForCode(ErrorCode.Timeout);
|
||||
this.reqs.forEach((req) => {
|
||||
req.resolver(err, {} as Msg);
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,545 @@
|
||||
/*
|
||||
* Copyright 2018-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { deferred } from "./util";
|
||||
import { ProtocolHandler, SubscriptionImpl } from "./protocol";
|
||||
import { Empty } from "./encoders";
|
||||
import { NatsError, ServiceClient } from "./types";
|
||||
|
||||
import type { SemVer } from "./semver";
|
||||
import { Features, parseSemVer } from "./semver";
|
||||
|
||||
import { parseOptions } from "./options";
|
||||
import { QueuedIteratorImpl } from "./queued_iterator";
|
||||
import {
|
||||
RequestMany,
|
||||
RequestManyOptionsInternal,
|
||||
RequestOne,
|
||||
} from "./request";
|
||||
import { isRequestError } from "./msg";
|
||||
import { JetStreamManagerImpl } from "../jetstream/jsm";
|
||||
import { JetStreamClientImpl } from "../jetstream/jsclient";
|
||||
import { ServiceImpl } from "./service";
|
||||
import { ServiceClientImpl } from "./serviceclient";
|
||||
import { JetStreamClient, JetStreamManager } from "../jetstream/types";
|
||||
import {
|
||||
ConnectionOptions,
|
||||
createInbox,
|
||||
ErrorCode,
|
||||
JetStreamManagerOptions,
|
||||
JetStreamOptions,
|
||||
Msg,
|
||||
NatsConnection,
|
||||
Payload,
|
||||
PublishOptions,
|
||||
QueuedIterator,
|
||||
RequestManyOptions,
|
||||
RequestOptions,
|
||||
RequestStrategy,
|
||||
ServerInfo,
|
||||
Service,
|
||||
ServiceConfig,
|
||||
ServicesAPI,
|
||||
Stats,
|
||||
Status,
|
||||
Subscription,
|
||||
SubscriptionOptions,
|
||||
} from "./core";
|
||||
|
||||
export class NatsConnectionImpl implements NatsConnection {
|
||||
options: ConnectionOptions;
|
||||
protocol!: ProtocolHandler;
|
||||
draining: boolean;
|
||||
listeners: QueuedIterator<Status>[];
|
||||
_services!: ServicesAPI;
|
||||
|
||||
private constructor(opts: ConnectionOptions) {
|
||||
this.draining = false;
|
||||
this.options = parseOptions(opts);
|
||||
this.listeners = [];
|
||||
}
|
||||
|
||||
public static connect(opts: ConnectionOptions = {}): Promise<NatsConnection> {
|
||||
return new Promise<NatsConnection>((resolve, reject) => {
|
||||
const nc = new NatsConnectionImpl(opts);
|
||||
ProtocolHandler.connect(nc.options, nc)
|
||||
.then((ph: ProtocolHandler) => {
|
||||
nc.protocol = ph;
|
||||
(async function() {
|
||||
for await (const s of ph.status()) {
|
||||
nc.listeners.forEach((l) => {
|
||||
l.push(s);
|
||||
});
|
||||
}
|
||||
})();
|
||||
resolve(nc);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
closed(): Promise<void | Error> {
|
||||
return this.protocol.closed;
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.protocol.close();
|
||||
}
|
||||
|
||||
_check(subject: string, sub: boolean, pub: boolean) {
|
||||
if (this.isClosed()) {
|
||||
throw NatsError.errorForCode(ErrorCode.ConnectionClosed);
|
||||
}
|
||||
if (sub && this.isDraining()) {
|
||||
throw NatsError.errorForCode(ErrorCode.ConnectionDraining);
|
||||
}
|
||||
if (pub && this.protocol.noMorePublishing) {
|
||||
throw NatsError.errorForCode(ErrorCode.ConnectionDraining);
|
||||
}
|
||||
subject = subject || "";
|
||||
if (subject.length === 0) {
|
||||
throw NatsError.errorForCode(ErrorCode.BadSubject);
|
||||
}
|
||||
}
|
||||
|
||||
publish(
|
||||
subject: string,
|
||||
data?: Payload,
|
||||
options?: PublishOptions,
|
||||
): void {
|
||||
this._check(subject, false, true);
|
||||
this.protocol.publish(subject, data, options);
|
||||
}
|
||||
|
||||
subscribe(
|
||||
subject: string,
|
||||
opts: SubscriptionOptions = {},
|
||||
): Subscription {
|
||||
this._check(subject, true, false);
|
||||
const sub = new SubscriptionImpl(this.protocol, subject, opts);
|
||||
this.protocol.subscribe(sub);
|
||||
return sub;
|
||||
}
|
||||
|
||||
_resub(s: Subscription, subject: string, max?: number) {
|
||||
this._check(subject, true, false);
|
||||
const si = s as SubscriptionImpl;
|
||||
// FIXME: need way of understanding a callbacks processed
|
||||
// count without it, we cannot really do much - ie
|
||||
// for rejected messages, the count would be lower, etc.
|
||||
// To handle cases were for example KV is building a map
|
||||
// the consumer would say how many messages we need to do
|
||||
// a proper build before we can handle updates.
|
||||
si.max = max; // this might clear it
|
||||
if (max) {
|
||||
// we cannot auto-unsub, because we don't know the
|
||||
// number of messages we processed vs received
|
||||
// allow the auto-unsub on processMsg to work if they
|
||||
// we were called with a new max
|
||||
si.max = max + si.received;
|
||||
}
|
||||
this.protocol.resub(si, subject);
|
||||
}
|
||||
|
||||
// possibilities are:
|
||||
// stop on error or any non-100 status
|
||||
// AND:
|
||||
// - wait for timer
|
||||
// - wait for n messages or timer
|
||||
// - wait for unknown messages, done when empty or reset timer expires (with possible alt wait)
|
||||
// - wait for unknown messages, done when an empty payload is received or timer expires (with possible alt wait)
|
||||
requestMany(
|
||||
subject: string,
|
||||
data: Payload = Empty,
|
||||
opts: Partial<RequestManyOptions> = { maxWait: 1000, maxMessages: -1 },
|
||||
): Promise<QueuedIterator<Msg>> {
|
||||
try {
|
||||
this._check(subject, true, true);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
|
||||
opts.strategy = opts.strategy || RequestStrategy.Timer;
|
||||
opts.maxWait = opts.maxWait || 1000;
|
||||
if (opts.maxWait < 1) {
|
||||
return Promise.reject(new NatsError("timeout", ErrorCode.InvalidOption));
|
||||
}
|
||||
|
||||
// the iterator for user results
|
||||
const qi = new QueuedIteratorImpl<Msg>();
|
||||
function stop(err?: Error) {
|
||||
//@ts-ignore: stop function
|
||||
qi.push(() => {
|
||||
qi.stop(err);
|
||||
});
|
||||
}
|
||||
|
||||
// callback for the subscription or the mux handler
|
||||
// simply pushes errors and messages into the iterator
|
||||
function callback(err: Error | null, msg: Msg | null) {
|
||||
if (err || msg === null) {
|
||||
stop(err === null ? undefined : err);
|
||||
} else {
|
||||
qi.push(msg);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.noMux) {
|
||||
// we setup a subscription and manage it
|
||||
const stack = new Error().stack;
|
||||
let max = typeof opts.maxMessages === "number" && opts.maxMessages > 0
|
||||
? opts.maxMessages
|
||||
: -1;
|
||||
|
||||
const sub = this.subscribe(createInbox(this.options.inboxPrefix), {
|
||||
callback: (err, msg) => {
|
||||
// we only expect runtime errors or a no responders
|
||||
if (
|
||||
msg?.data?.length === 0 &&
|
||||
msg?.headers?.status === ErrorCode.NoResponders
|
||||
) {
|
||||
err = NatsError.errorForCode(ErrorCode.NoResponders);
|
||||
}
|
||||
// augment any error with the current stack to provide context
|
||||
// for the error on the suer code
|
||||
if (err) {
|
||||
err.stack += `\n\n${stack}`;
|
||||
cancel(err);
|
||||
return;
|
||||
}
|
||||
// push the message
|
||||
callback(null, msg);
|
||||
// see if the m request is completed
|
||||
if (opts.strategy === RequestStrategy.Count) {
|
||||
max--;
|
||||
if (max === 0) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
if (opts.strategy === RequestStrategy.JitterTimer) {
|
||||
clearTimers();
|
||||
timer = setTimeout(() => {
|
||||
cancel();
|
||||
}, 300);
|
||||
}
|
||||
if (opts.strategy === RequestStrategy.SentinelMsg) {
|
||||
if (msg && msg.data.length === 0) {
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
sub.closed
|
||||
.then(() => {
|
||||
stop();
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
qi.stop(err);
|
||||
});
|
||||
|
||||
const cancel = (err?: Error) => {
|
||||
if (err) {
|
||||
//@ts-ignore: error
|
||||
qi.push(() => {
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
clearTimers();
|
||||
sub.drain()
|
||||
.then(() => {
|
||||
stop();
|
||||
})
|
||||
.catch((_err: Error) => {
|
||||
stop();
|
||||
});
|
||||
};
|
||||
|
||||
qi.iterClosed
|
||||
.then(() => {
|
||||
clearTimers();
|
||||
sub?.unsubscribe();
|
||||
})
|
||||
.catch((_err) => {
|
||||
clearTimers();
|
||||
sub?.unsubscribe();
|
||||
});
|
||||
|
||||
try {
|
||||
this.publish(subject, data, { reply: sub.getSubject() });
|
||||
} catch (err) {
|
||||
cancel(err);
|
||||
}
|
||||
|
||||
let timer = setTimeout(() => {
|
||||
cancel();
|
||||
}, opts.maxWait);
|
||||
|
||||
const clearTimers = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// the ingestion is the RequestMany
|
||||
const rmo = opts as RequestManyOptionsInternal;
|
||||
rmo.callback = callback;
|
||||
|
||||
qi.iterClosed.then(() => {
|
||||
r.cancel();
|
||||
}).catch((err) => {
|
||||
r.cancel(err);
|
||||
});
|
||||
|
||||
const r = new RequestMany(this.protocol.muxSubscriptions, subject, rmo);
|
||||
this.protocol.request(r);
|
||||
|
||||
try {
|
||||
this.publish(
|
||||
subject,
|
||||
data,
|
||||
{
|
||||
reply: `${this.protocol.muxSubscriptions.baseInbox}${r.token}`,
|
||||
headers: opts.headers,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
r.cancel(err);
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve(qi);
|
||||
}
|
||||
|
||||
request(
|
||||
subject: string,
|
||||
data?: Payload,
|
||||
opts: RequestOptions = { timeout: 1000, noMux: false },
|
||||
): Promise<Msg> {
|
||||
try {
|
||||
this._check(subject, true, true);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
opts.timeout = opts.timeout || 1000;
|
||||
if (opts.timeout < 1) {
|
||||
return Promise.reject(new NatsError("timeout", ErrorCode.InvalidOption));
|
||||
}
|
||||
if (!opts.noMux && opts.reply) {
|
||||
return Promise.reject(
|
||||
new NatsError(
|
||||
"reply can only be used with noMux",
|
||||
ErrorCode.InvalidOption,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.noMux) {
|
||||
const inbox = opts.reply
|
||||
? opts.reply
|
||||
: createInbox(this.options.inboxPrefix);
|
||||
const d = deferred<Msg>();
|
||||
const errCtx = new Error();
|
||||
const sub = this.subscribe(
|
||||
inbox,
|
||||
{
|
||||
max: 1,
|
||||
timeout: opts.timeout,
|
||||
callback: (err, msg) => {
|
||||
if (err) {
|
||||
// timeouts from `timeout()` will have the proper stack
|
||||
if (err.code !== ErrorCode.Timeout) {
|
||||
err.stack += `\n\n${errCtx.stack}`;
|
||||
}
|
||||
d.reject(err);
|
||||
} else {
|
||||
err = isRequestError(msg);
|
||||
if (err) {
|
||||
// if we failed here, help the developer by showing what failed
|
||||
err.stack += `\n\n${errCtx.stack}`;
|
||||
d.reject(err);
|
||||
} else {
|
||||
d.resolve(msg);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
(sub as SubscriptionImpl).requestSubject = subject;
|
||||
this.protocol.publish(subject, data, {
|
||||
reply: inbox,
|
||||
headers: opts.headers,
|
||||
});
|
||||
return d;
|
||||
} else {
|
||||
const r = new RequestOne(this.protocol.muxSubscriptions, subject, opts);
|
||||
this.protocol.request(r);
|
||||
|
||||
try {
|
||||
this.publish(
|
||||
subject,
|
||||
data,
|
||||
{
|
||||
reply: `${this.protocol.muxSubscriptions.baseInbox}${r.token}`,
|
||||
headers: opts.headers,
|
||||
},
|
||||
);
|
||||
} catch (err) {
|
||||
r.cancel(err);
|
||||
}
|
||||
|
||||
const p = Promise.race([r.timer, r.deferred]);
|
||||
p.catch(() => {
|
||||
r.cancel();
|
||||
});
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
/** *
|
||||
* Flushes to the server. Promise resolves when round-trip completes.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
flush(): Promise<void> {
|
||||
if (this.isClosed()) {
|
||||
return Promise.reject(
|
||||
NatsError.errorForCode(ErrorCode.ConnectionClosed),
|
||||
);
|
||||
}
|
||||
return this.protocol.flush();
|
||||
}
|
||||
|
||||
drain(): Promise<void> {
|
||||
if (this.isClosed()) {
|
||||
return Promise.reject(
|
||||
NatsError.errorForCode(ErrorCode.ConnectionClosed),
|
||||
);
|
||||
}
|
||||
if (this.isDraining()) {
|
||||
return Promise.reject(
|
||||
NatsError.errorForCode(ErrorCode.ConnectionDraining),
|
||||
);
|
||||
}
|
||||
this.draining = true;
|
||||
return this.protocol.drain();
|
||||
}
|
||||
|
||||
isClosed(): boolean {
|
||||
return this.protocol.isClosed();
|
||||
}
|
||||
|
||||
isDraining(): boolean {
|
||||
return this.draining;
|
||||
}
|
||||
|
||||
getServer(): string {
|
||||
const srv = this.protocol.getServer();
|
||||
return srv ? srv.listen : "";
|
||||
}
|
||||
|
||||
status(): AsyncIterable<Status> {
|
||||
const iter = new QueuedIteratorImpl<Status>();
|
||||
iter.iterClosed.then(() => {
|
||||
const idx = this.listeners.indexOf(iter);
|
||||
this.listeners.splice(idx, 1);
|
||||
});
|
||||
this.listeners.push(iter);
|
||||
return iter;
|
||||
}
|
||||
|
||||
get info(): ServerInfo | undefined {
|
||||
return this.protocol.isClosed() ? undefined : this.protocol.info;
|
||||
}
|
||||
|
||||
stats(): Stats {
|
||||
return {
|
||||
inBytes: this.protocol.inBytes,
|
||||
outBytes: this.protocol.outBytes,
|
||||
inMsgs: this.protocol.inMsgs,
|
||||
outMsgs: this.protocol.outMsgs,
|
||||
};
|
||||
}
|
||||
|
||||
async jetstreamManager(
|
||||
opts: JetStreamManagerOptions = {},
|
||||
): Promise<JetStreamManager> {
|
||||
const adm = new JetStreamManagerImpl(this, opts);
|
||||
if (opts.checkAPI !== false) {
|
||||
try {
|
||||
await adm.getAccountInfo();
|
||||
} catch (err) {
|
||||
const ne = err as NatsError;
|
||||
if (ne.code === ErrorCode.NoResponders) {
|
||||
ne.code = ErrorCode.JetStreamNotEnabled;
|
||||
}
|
||||
throw ne;
|
||||
}
|
||||
}
|
||||
return adm;
|
||||
}
|
||||
|
||||
jetstream(
|
||||
opts: JetStreamOptions = {},
|
||||
): JetStreamClient {
|
||||
return new JetStreamClientImpl(this, opts);
|
||||
}
|
||||
|
||||
getServerVersion(): SemVer | undefined {
|
||||
const info = this.info;
|
||||
return info ? parseSemVer(info.version) : undefined;
|
||||
}
|
||||
|
||||
async rtt(): Promise<number> {
|
||||
if (!this.protocol._closed && !this.protocol.connected) {
|
||||
throw NatsError.errorForCode(ErrorCode.Disconnect);
|
||||
}
|
||||
const start = Date.now();
|
||||
await this.flush();
|
||||
return Date.now() - start;
|
||||
}
|
||||
|
||||
get features(): Features {
|
||||
return this.protocol.features;
|
||||
}
|
||||
|
||||
get services(): ServicesAPI {
|
||||
if (!this._services) {
|
||||
this._services = new ServicesFactory(this);
|
||||
}
|
||||
return this._services;
|
||||
}
|
||||
}
|
||||
|
||||
export class ServicesFactory implements ServicesAPI {
|
||||
nc: NatsConnection;
|
||||
constructor(nc: NatsConnection) {
|
||||
this.nc = nc;
|
||||
}
|
||||
|
||||
add(config: ServiceConfig): Promise<Service> {
|
||||
try {
|
||||
const s = new ServiceImpl(this.nc, config);
|
||||
return s.start();
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
client(opts?: RequestManyOptions, prefix?: string): ServiceClient {
|
||||
return new ServiceClientImpl(this.nc, opts, prefix);
|
||||
}
|
||||
}
|
@ -0,0 +1,2 @@
|
||||
// export * as nkeys from "https://raw.githubusercontent.com/nats-io/nkeys.js/v1.0.4/modules/esm/mod.ts";
|
||||
export * as nkeys from '../nkey/mod'
|
@ -0,0 +1,126 @@
|
||||
/*
|
||||
* Copyright 2016-2021 The NATS Authors
|
||||
* 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";
|
||||
|
||||
const digits = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const base = 36;
|
||||
const preLen = 12;
|
||||
const seqLen = 10;
|
||||
const maxSeq = 3656158440062976; // base^seqLen == 36^10
|
||||
const minInc = 33;
|
||||
const maxInc = 333;
|
||||
const totalLen = preLen + seqLen;
|
||||
|
||||
function _getRandomValues(a: Uint8Array) {
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
a[i] = Math.floor(Math.random() * 255);
|
||||
}
|
||||
}
|
||||
|
||||
function fillRandom(a: Uint8Array) {
|
||||
if (globalThis?.crypto?.getRandomValues) {
|
||||
globalThis.crypto.getRandomValues(a);
|
||||
} else {
|
||||
_getRandomValues(a);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and initialize a nuid.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
export class Nuid {
|
||||
buf: Uint8Array;
|
||||
seq!: number;
|
||||
inc!: number;
|
||||
|
||||
constructor() {
|
||||
this.buf = new Uint8Array(totalLen);
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes a nuid with a crypto random prefix,
|
||||
* and pseudo-random sequence and increment.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
private init() {
|
||||
this.setPre();
|
||||
this.initSeqAndInc();
|
||||
this.fillSeq();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the pseudo randmon sequence number and the increment range.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
private initSeqAndInc() {
|
||||
this.seq = Math.floor(Math.random() * maxSeq);
|
||||
this.inc = Math.floor(Math.random() * (maxInc - minInc) + minInc);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the prefix from crypto random bytes. Converts to base36.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
private setPre() {
|
||||
const cbuf = new Uint8Array(preLen);
|
||||
fillRandom(cbuf);
|
||||
for (let i = 0; i < preLen; i++) {
|
||||
const di = cbuf[i] % base;
|
||||
this.buf[i] = digits.charCodeAt(di);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the sequence part of the nuid as base36 from this.seq.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
private fillSeq() {
|
||||
let n = this.seq;
|
||||
for (let i = totalLen - 1; i >= preLen; i--) {
|
||||
this.buf[i] = digits.charCodeAt(n % base);
|
||||
n = Math.floor(n / base);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next nuid.
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
next(): string {
|
||||
this.seq += this.inc;
|
||||
if (this.seq > maxSeq) {
|
||||
this.setPre();
|
||||
this.initSeqAndInc();
|
||||
}
|
||||
this.fillSeq();
|
||||
// @ts-ignore - Uint8Arrays can be an argument
|
||||
return String.fromCharCode.apply(String, this.buf);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.init();
|
||||
}
|
||||
}
|
||||
|
||||
export const nuid = new Nuid();
|
@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright 2021-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { extend } from "./util.ts";
|
||||
import { defaultPort, getResolveFn } from "./transport.ts";
|
||||
import { createInbox, ServerInfo } from "./core.ts";
|
||||
import {
|
||||
multiAuthenticator,
|
||||
noAuthFn,
|
||||
tokenAuthenticator,
|
||||
usernamePasswordAuthenticator,
|
||||
} from "./authenticator.ts";
|
||||
import {
|
||||
Authenticator,
|
||||
ConnectionOptions,
|
||||
DEFAULT_HOST,
|
||||
ErrorCode,
|
||||
NatsError,
|
||||
} from "./core.ts";
|
||||
|
||||
export const DEFAULT_MAX_RECONNECT_ATTEMPTS = 10;
|
||||
export const DEFAULT_JITTER = 100;
|
||||
export const DEFAULT_JITTER_TLS = 1000;
|
||||
// Ping interval
|
||||
export const DEFAULT_PING_INTERVAL = 2 * 60 * 1000; // 2 minutes
|
||||
export const DEFAULT_MAX_PING_OUT = 2;
|
||||
|
||||
// DISCONNECT Parameters, 2 sec wait, 10 tries
|
||||
export const DEFAULT_RECONNECT_TIME_WAIT = 2 * 1000;
|
||||
|
||||
export function defaultOptions(): ConnectionOptions {
|
||||
return {
|
||||
maxPingOut: DEFAULT_MAX_PING_OUT,
|
||||
maxReconnectAttempts: DEFAULT_MAX_RECONNECT_ATTEMPTS,
|
||||
noRandomize: false,
|
||||
pedantic: false,
|
||||
pingInterval: DEFAULT_PING_INTERVAL,
|
||||
reconnect: true,
|
||||
reconnectJitter: DEFAULT_JITTER,
|
||||
reconnectJitterTLS: DEFAULT_JITTER_TLS,
|
||||
reconnectTimeWait: DEFAULT_RECONNECT_TIME_WAIT,
|
||||
tls: undefined,
|
||||
verbose: false,
|
||||
waitOnFirstConnect: false,
|
||||
ignoreAuthErrorAbort: false,
|
||||
} as ConnectionOptions;
|
||||
}
|
||||
|
||||
export function buildAuthenticator(
|
||||
opts: ConnectionOptions,
|
||||
): Authenticator {
|
||||
const buf: Authenticator[] = [];
|
||||
// jwtAuthenticator is created by the user, since it
|
||||
// will require possibly reading files which
|
||||
// some of the clients are simply unable to do
|
||||
if (typeof opts.authenticator === "function") {
|
||||
buf.push(opts.authenticator);
|
||||
}
|
||||
if (Array.isArray(opts.authenticator)) {
|
||||
buf.push(...opts.authenticator);
|
||||
}
|
||||
if (opts.token) {
|
||||
buf.push(tokenAuthenticator(opts.token));
|
||||
}
|
||||
if (opts.user) {
|
||||
buf.push(usernamePasswordAuthenticator(opts.user, opts.pass));
|
||||
}
|
||||
return buf.length === 0 ? noAuthFn() : multiAuthenticator(buf);
|
||||
}
|
||||
|
||||
export function parseOptions(opts?: ConnectionOptions): ConnectionOptions {
|
||||
const dhp = `${DEFAULT_HOST}:${defaultPort()}`;
|
||||
opts = opts || { servers: [dhp] };
|
||||
opts.servers = opts.servers || [];
|
||||
if (typeof opts.servers === "string") {
|
||||
opts.servers = [opts.servers];
|
||||
}
|
||||
|
||||
if (opts.servers.length > 0 && opts.port) {
|
||||
throw new NatsError(
|
||||
"port and servers options are mutually exclusive",
|
||||
ErrorCode.InvalidOption,
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.servers.length === 0 && opts.port) {
|
||||
opts.servers = [`${DEFAULT_HOST}:${opts.port}`];
|
||||
}
|
||||
if (opts.servers && opts.servers.length === 0) {
|
||||
opts.servers = [dhp];
|
||||
}
|
||||
const options = extend(defaultOptions(), opts);
|
||||
|
||||
options.authenticator = buildAuthenticator(options);
|
||||
|
||||
["reconnectDelayHandler", "authenticator"].forEach((n) => {
|
||||
if (options[n] && typeof options[n] !== "function") {
|
||||
throw new NatsError(
|
||||
`${n} option should be a function`,
|
||||
ErrorCode.NotFunction,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (!options.reconnectDelayHandler) {
|
||||
options.reconnectDelayHandler = () => {
|
||||
let extra = options.tls
|
||||
? options.reconnectJitterTLS
|
||||
: options.reconnectJitter;
|
||||
if (extra) {
|
||||
extra++;
|
||||
extra = Math.floor(Math.random() * extra);
|
||||
}
|
||||
return options.reconnectTimeWait + extra;
|
||||
};
|
||||
}
|
||||
|
||||
if (options.inboxPrefix) {
|
||||
try {
|
||||
createInbox(options.inboxPrefix);
|
||||
} catch (err) {
|
||||
throw new NatsError(err.message, ErrorCode.ApiError);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.resolve) {
|
||||
if (typeof getResolveFn() !== "function") {
|
||||
throw new NatsError(
|
||||
`'resolve' is not supported on this client`,
|
||||
ErrorCode.InvalidOption,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function checkOptions(info: ServerInfo, options: ConnectionOptions) {
|
||||
const { proto, tls_required: tlsRequired, tls_available: tlsAvailable } =
|
||||
info;
|
||||
if ((proto === undefined || proto < 1) && options.noEcho) {
|
||||
throw new NatsError("noEcho", ErrorCode.ServerOptionNotAvailable);
|
||||
}
|
||||
const tls = tlsRequired || tlsAvailable || false;
|
||||
if (options.tls && !tls) {
|
||||
throw new NatsError("tls", ErrorCode.ServerOptionNotAvailable);
|
||||
}
|
||||
}
|
||||
|
||||
export function checkUnsupportedOption(prop: string, v?: string) {
|
||||
if (v) {
|
||||
throw new NatsError(prop, ErrorCode.InvalidOption);
|
||||
}
|
||||
}
|
@ -0,0 +1,748 @@
|
||||
// deno-lint-ignore-file no-undef
|
||||
/*
|
||||
* Copyright 2020-2021 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { DenoBuffer } from "./denobuffer.ts";
|
||||
import { TD } from "./encoders.ts";
|
||||
import { Dispatcher } from "./core.ts";
|
||||
|
||||
export enum Kind {
|
||||
OK,
|
||||
ERR,
|
||||
MSG,
|
||||
INFO,
|
||||
PING,
|
||||
PONG,
|
||||
}
|
||||
|
||||
export interface ParserEvent {
|
||||
kind: Kind;
|
||||
msg?: MsgArg;
|
||||
data?: Uint8Array;
|
||||
}
|
||||
|
||||
export function describe(e: ParserEvent): string {
|
||||
let ks: string;
|
||||
let data = "";
|
||||
|
||||
switch (e.kind) {
|
||||
case Kind.MSG:
|
||||
ks = "MSG";
|
||||
break;
|
||||
case Kind.OK:
|
||||
ks = "OK";
|
||||
break;
|
||||
case Kind.ERR:
|
||||
ks = "ERR";
|
||||
data = TD.decode(e.data);
|
||||
break;
|
||||
case Kind.PING:
|
||||
ks = "PING";
|
||||
break;
|
||||
case Kind.PONG:
|
||||
ks = "PONG";
|
||||
break;
|
||||
case Kind.INFO:
|
||||
ks = "INFO";
|
||||
data = TD.decode(e.data);
|
||||
}
|
||||
return `${ks}: ${data}`;
|
||||
}
|
||||
|
||||
export interface MsgArg {
|
||||
subject: Uint8Array;
|
||||
reply?: Uint8Array;
|
||||
sid: number;
|
||||
hdr: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
function newMsgArg(): MsgArg {
|
||||
const ma = {} as MsgArg;
|
||||
ma.sid = -1;
|
||||
ma.hdr = -1;
|
||||
ma.size = -1;
|
||||
|
||||
return ma;
|
||||
}
|
||||
|
||||
const ASCII_0 = 48;
|
||||
const ASCII_9 = 57;
|
||||
|
||||
// This is an almost verbatim port of the Go NATS parser
|
||||
// https://github.com/nats-io/nats.go/blob/master/parser.go
|
||||
export class Parser {
|
||||
dispatcher: Dispatcher<ParserEvent>;
|
||||
state: State;
|
||||
as: number;
|
||||
drop: number;
|
||||
hdr: number;
|
||||
ma!: MsgArg;
|
||||
argBuf?: DenoBuffer;
|
||||
msgBuf?: DenoBuffer;
|
||||
|
||||
constructor(dispatcher: Dispatcher<ParserEvent>) {
|
||||
this.dispatcher = dispatcher;
|
||||
this.state = State.OP_START;
|
||||
this.as = 0;
|
||||
this.drop = 0;
|
||||
this.hdr = 0;
|
||||
}
|
||||
|
||||
parse(buf: Uint8Array): void {
|
||||
let i: number;
|
||||
for (i = 0; i < buf.length; i++) {
|
||||
const b = buf[i];
|
||||
switch (this.state) {
|
||||
case State.OP_START:
|
||||
switch (b) {
|
||||
case cc.M:
|
||||
case cc.m:
|
||||
this.state = State.OP_M;
|
||||
this.hdr = -1;
|
||||
this.ma = newMsgArg();
|
||||
break;
|
||||
case cc.H:
|
||||
case cc.h:
|
||||
this.state = State.OP_H;
|
||||
this.hdr = 0;
|
||||
this.ma = newMsgArg();
|
||||
break;
|
||||
case cc.P:
|
||||
case cc.p:
|
||||
this.state = State.OP_P;
|
||||
break;
|
||||
case cc.PLUS:
|
||||
this.state = State.OP_PLUS;
|
||||
break;
|
||||
case cc.MINUS:
|
||||
this.state = State.OP_MINUS;
|
||||
break;
|
||||
case cc.I:
|
||||
case cc.i:
|
||||
this.state = State.OP_I;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_H:
|
||||
switch (b) {
|
||||
case cc.M:
|
||||
case cc.m:
|
||||
this.state = State.OP_M;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_M:
|
||||
switch (b) {
|
||||
case cc.S:
|
||||
case cc.s:
|
||||
this.state = State.OP_MS;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_MS:
|
||||
switch (b) {
|
||||
case cc.G:
|
||||
case cc.g:
|
||||
this.state = State.OP_MSG;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_MSG:
|
||||
switch (b) {
|
||||
case cc.SPACE:
|
||||
case cc.TAB:
|
||||
this.state = State.OP_MSG_SPC;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_MSG_SPC:
|
||||
switch (b) {
|
||||
case cc.SPACE:
|
||||
case cc.TAB:
|
||||
continue;
|
||||
default:
|
||||
this.state = State.MSG_ARG;
|
||||
this.as = i;
|
||||
}
|
||||
break;
|
||||
case State.MSG_ARG:
|
||||
switch (b) {
|
||||
case cc.CR:
|
||||
this.drop = 1;
|
||||
break;
|
||||
case cc.NL: {
|
||||
const arg: Uint8Array = this.argBuf
|
||||
? this.argBuf.bytes()
|
||||
: buf.subarray(this.as, i - this.drop);
|
||||
this.processMsgArgs(arg);
|
||||
this.drop = 0;
|
||||
this.as = i + 1;
|
||||
this.state = State.MSG_PAYLOAD;
|
||||
|
||||
// jump ahead with the index. If this overruns
|
||||
// what is left we fall out and process a split buffer.
|
||||
i = this.as + this.ma.size - 1;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (this.argBuf) {
|
||||
this.argBuf.writeByte(b);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case State.MSG_PAYLOAD:
|
||||
if (this.msgBuf) {
|
||||
if (this.msgBuf.length >= this.ma.size) {
|
||||
const data = this.msgBuf.bytes({ copy: false });
|
||||
this.dispatcher.push(
|
||||
{ kind: Kind.MSG, msg: this.ma, data: data },
|
||||
);
|
||||
this.argBuf = undefined;
|
||||
this.msgBuf = undefined;
|
||||
this.state = State.MSG_END;
|
||||
} else {
|
||||
let toCopy = this.ma.size - this.msgBuf.length;
|
||||
const avail = buf.length - i;
|
||||
|
||||
if (avail < toCopy) {
|
||||
toCopy = avail;
|
||||
}
|
||||
|
||||
if (toCopy > 0) {
|
||||
this.msgBuf.write(buf.subarray(i, i + toCopy));
|
||||
i = (i + toCopy) - 1;
|
||||
} else {
|
||||
this.msgBuf.writeByte(b);
|
||||
}
|
||||
}
|
||||
} else if (i - this.as >= this.ma.size) {
|
||||
this.dispatcher.push(
|
||||
{ kind: Kind.MSG, msg: this.ma, data: buf.subarray(this.as, i) },
|
||||
);
|
||||
this.argBuf = undefined;
|
||||
this.msgBuf = undefined;
|
||||
this.state = State.MSG_END;
|
||||
}
|
||||
break;
|
||||
case State.MSG_END:
|
||||
switch (b) {
|
||||
case cc.NL:
|
||||
this.drop = 0;
|
||||
this.as = i + 1;
|
||||
this.state = State.OP_START;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
case State.OP_PLUS:
|
||||
switch (b) {
|
||||
case cc.O:
|
||||
case cc.o:
|
||||
this.state = State.OP_PLUS_O;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_PLUS_O:
|
||||
switch (b) {
|
||||
case cc.K:
|
||||
case cc.k:
|
||||
this.state = State.OP_PLUS_OK;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_PLUS_OK:
|
||||
switch (b) {
|
||||
case cc.NL:
|
||||
this.dispatcher.push({ kind: Kind.OK });
|
||||
this.drop = 0;
|
||||
this.state = State.OP_START;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case State.OP_MINUS:
|
||||
switch (b) {
|
||||
case cc.E:
|
||||
case cc.e:
|
||||
this.state = State.OP_MINUS_E;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_MINUS_E:
|
||||
switch (b) {
|
||||
case cc.R:
|
||||
case cc.r:
|
||||
this.state = State.OP_MINUS_ER;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_MINUS_ER:
|
||||
switch (b) {
|
||||
case cc.R:
|
||||
case cc.r:
|
||||
this.state = State.OP_MINUS_ERR;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_MINUS_ERR:
|
||||
switch (b) {
|
||||
case cc.SPACE:
|
||||
case cc.TAB:
|
||||
this.state = State.OP_MINUS_ERR_SPC;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_MINUS_ERR_SPC:
|
||||
switch (b) {
|
||||
case cc.SPACE:
|
||||
case cc.TAB:
|
||||
continue;
|
||||
default:
|
||||
this.state = State.MINUS_ERR_ARG;
|
||||
this.as = i;
|
||||
}
|
||||
break;
|
||||
case State.MINUS_ERR_ARG:
|
||||
switch (b) {
|
||||
case cc.CR:
|
||||
this.drop = 1;
|
||||
break;
|
||||
case cc.NL: {
|
||||
let arg: Uint8Array;
|
||||
if (this.argBuf) {
|
||||
arg = this.argBuf.bytes();
|
||||
this.argBuf = undefined;
|
||||
} else {
|
||||
arg = buf.subarray(this.as, i - this.drop);
|
||||
}
|
||||
this.dispatcher.push({ kind: Kind.ERR, data: arg });
|
||||
this.drop = 0;
|
||||
this.as = i + 1;
|
||||
this.state = State.OP_START;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (this.argBuf) {
|
||||
this.argBuf.write(Uint8Array.of(b));
|
||||
}
|
||||
}
|
||||
break;
|
||||
case State.OP_P:
|
||||
switch (b) {
|
||||
case cc.I:
|
||||
case cc.i:
|
||||
this.state = State.OP_PI;
|
||||
break;
|
||||
case cc.O:
|
||||
case cc.o:
|
||||
this.state = State.OP_PO;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_PO:
|
||||
switch (b) {
|
||||
case cc.N:
|
||||
case cc.n:
|
||||
this.state = State.OP_PON;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_PON:
|
||||
switch (b) {
|
||||
case cc.G:
|
||||
case cc.g:
|
||||
this.state = State.OP_PONG;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_PONG:
|
||||
switch (b) {
|
||||
case cc.NL:
|
||||
this.dispatcher.push({ kind: Kind.PONG });
|
||||
this.drop = 0;
|
||||
this.state = State.OP_START;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case State.OP_PI:
|
||||
switch (b) {
|
||||
case cc.N:
|
||||
case cc.n:
|
||||
this.state = State.OP_PIN;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_PIN:
|
||||
switch (b) {
|
||||
case cc.G:
|
||||
case cc.g:
|
||||
this.state = State.OP_PING;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_PING:
|
||||
switch (b) {
|
||||
case cc.NL:
|
||||
this.dispatcher.push({ kind: Kind.PING });
|
||||
this.drop = 0;
|
||||
this.state = State.OP_START;
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case State.OP_I:
|
||||
switch (b) {
|
||||
case cc.N:
|
||||
case cc.n:
|
||||
this.state = State.OP_IN;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_IN:
|
||||
switch (b) {
|
||||
case cc.F:
|
||||
case cc.f:
|
||||
this.state = State.OP_INF;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_INF:
|
||||
switch (b) {
|
||||
case cc.O:
|
||||
case cc.o:
|
||||
this.state = State.OP_INFO;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_INFO:
|
||||
switch (b) {
|
||||
case cc.SPACE:
|
||||
case cc.TAB:
|
||||
this.state = State.OP_INFO_SPC;
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
break;
|
||||
case State.OP_INFO_SPC:
|
||||
switch (b) {
|
||||
case cc.SPACE:
|
||||
case cc.TAB:
|
||||
continue;
|
||||
default:
|
||||
this.state = State.INFO_ARG;
|
||||
this.as = i;
|
||||
}
|
||||
break;
|
||||
case State.INFO_ARG:
|
||||
switch (b) {
|
||||
case cc.CR:
|
||||
this.drop = 1;
|
||||
break;
|
||||
case cc.NL: {
|
||||
let arg: Uint8Array;
|
||||
if (this.argBuf) {
|
||||
arg = this.argBuf.bytes();
|
||||
this.argBuf = undefined;
|
||||
} else {
|
||||
arg = buf.subarray(this.as, i - this.drop);
|
||||
}
|
||||
this.dispatcher.push({ kind: Kind.INFO, data: arg });
|
||||
this.drop = 0;
|
||||
this.as = i + 1;
|
||||
this.state = State.OP_START;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (this.argBuf) {
|
||||
this.argBuf.writeByte(b);
|
||||
}
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw this.fail(buf.subarray(i));
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
(this.state === State.MSG_ARG || this.state === State.MINUS_ERR_ARG ||
|
||||
this.state === State.INFO_ARG) && !this.argBuf
|
||||
) {
|
||||
this.argBuf = new DenoBuffer(buf.subarray(this.as, i - this.drop));
|
||||
}
|
||||
|
||||
if (this.state === State.MSG_PAYLOAD && !this.msgBuf) {
|
||||
if (!this.argBuf) {
|
||||
this.cloneMsgArg();
|
||||
}
|
||||
this.msgBuf = new DenoBuffer(buf.subarray(this.as));
|
||||
}
|
||||
}
|
||||
|
||||
cloneMsgArg() {
|
||||
const s = this.ma.subject.length;
|
||||
const r = this.ma.reply ? this.ma.reply.length : 0;
|
||||
const buf = new Uint8Array(s + r);
|
||||
buf.set(this.ma.subject);
|
||||
if (this.ma.reply) {
|
||||
buf.set(this.ma.reply, s);
|
||||
}
|
||||
this.argBuf = new DenoBuffer(buf);
|
||||
this.ma.subject = buf.subarray(0, s);
|
||||
if (this.ma.reply) {
|
||||
this.ma.reply = buf.subarray(s);
|
||||
}
|
||||
}
|
||||
|
||||
processMsgArgs(arg: Uint8Array): void {
|
||||
if (this.hdr >= 0) {
|
||||
return this.processHeaderMsgArgs(arg);
|
||||
}
|
||||
|
||||
const args: Uint8Array[] = [];
|
||||
let start = -1;
|
||||
for (let i = 0; i < arg.length; i++) {
|
||||
const b = arg[i];
|
||||
switch (b) {
|
||||
case cc.SPACE:
|
||||
case cc.TAB:
|
||||
case cc.CR:
|
||||
case cc.NL:
|
||||
if (start >= 0) {
|
||||
args.push(arg.subarray(start, i));
|
||||
start = -1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (start < 0) {
|
||||
start = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (start >= 0) {
|
||||
args.push(arg.subarray(start));
|
||||
}
|
||||
|
||||
switch (args.length) {
|
||||
case 3:
|
||||
this.ma.subject = args[0];
|
||||
this.ma.sid = this.protoParseInt(args[1]);
|
||||
this.ma.reply = undefined;
|
||||
this.ma.size = this.protoParseInt(args[2]);
|
||||
break;
|
||||
case 4:
|
||||
this.ma.subject = args[0];
|
||||
this.ma.sid = this.protoParseInt(args[1]);
|
||||
this.ma.reply = args[2];
|
||||
this.ma.size = this.protoParseInt(args[3]);
|
||||
break;
|
||||
default:
|
||||
throw this.fail(arg, "processMsgArgs Parse Error");
|
||||
}
|
||||
|
||||
if (this.ma.sid < 0) {
|
||||
throw this.fail(arg, "processMsgArgs Bad or Missing Sid Error");
|
||||
}
|
||||
if (this.ma.size < 0) {
|
||||
throw this.fail(arg, "processMsgArgs Bad or Missing Size Error");
|
||||
}
|
||||
}
|
||||
|
||||
fail(data: Uint8Array, label = ""): Error {
|
||||
if (!label) {
|
||||
label = `parse error [${this.state}]`;
|
||||
} else {
|
||||
label = `${label} [${this.state}]`;
|
||||
}
|
||||
|
||||
return new Error(`${label}: ${TD.decode(data)}`);
|
||||
}
|
||||
|
||||
processHeaderMsgArgs(arg: Uint8Array): void {
|
||||
const args: Uint8Array[] = [];
|
||||
let start = -1;
|
||||
for (let i = 0; i < arg.length; i++) {
|
||||
const b = arg[i];
|
||||
switch (b) {
|
||||
case cc.SPACE:
|
||||
case cc.TAB:
|
||||
case cc.CR:
|
||||
case cc.NL:
|
||||
if (start >= 0) {
|
||||
args.push(arg.subarray(start, i));
|
||||
start = -1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (start < 0) {
|
||||
start = i;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (start >= 0) {
|
||||
args.push(arg.subarray(start));
|
||||
}
|
||||
|
||||
switch (args.length) {
|
||||
case 4:
|
||||
this.ma.subject = args[0];
|
||||
this.ma.sid = this.protoParseInt(args[1]);
|
||||
this.ma.reply = undefined;
|
||||
this.ma.hdr = this.protoParseInt(args[2]);
|
||||
this.ma.size = this.protoParseInt(args[3]);
|
||||
break;
|
||||
case 5:
|
||||
this.ma.subject = args[0];
|
||||
this.ma.sid = this.protoParseInt(args[1]);
|
||||
this.ma.reply = args[2];
|
||||
this.ma.hdr = this.protoParseInt(args[3]);
|
||||
this.ma.size = this.protoParseInt(args[4]);
|
||||
break;
|
||||
default:
|
||||
throw this.fail(arg, "processHeaderMsgArgs Parse Error");
|
||||
}
|
||||
|
||||
if (this.ma.sid < 0) {
|
||||
throw this.fail(arg, "processHeaderMsgArgs Bad or Missing Sid Error");
|
||||
}
|
||||
if (this.ma.hdr < 0 || this.ma.hdr > this.ma.size) {
|
||||
throw this.fail(
|
||||
arg,
|
||||
"processHeaderMsgArgs Bad or Missing Header Size Error",
|
||||
);
|
||||
}
|
||||
if (this.ma.size < 0) {
|
||||
throw this.fail(arg, "processHeaderMsgArgs Bad or Missing Size Error");
|
||||
}
|
||||
}
|
||||
|
||||
protoParseInt(a: Uint8Array): number {
|
||||
if (a.length === 0) {
|
||||
return -1;
|
||||
}
|
||||
let n = 0;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] < ASCII_0 || a[i] > ASCII_9) {
|
||||
return -1;
|
||||
}
|
||||
n = n * 10 + (a[i] - ASCII_0);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
}
|
||||
|
||||
export enum State {
|
||||
OP_START = 0,
|
||||
OP_PLUS,
|
||||
OP_PLUS_O,
|
||||
OP_PLUS_OK,
|
||||
OP_MINUS,
|
||||
OP_MINUS_E,
|
||||
OP_MINUS_ER,
|
||||
OP_MINUS_ERR,
|
||||
OP_MINUS_ERR_SPC,
|
||||
MINUS_ERR_ARG,
|
||||
OP_M,
|
||||
OP_MS,
|
||||
OP_MSG,
|
||||
OP_MSG_SPC,
|
||||
MSG_ARG,
|
||||
MSG_PAYLOAD,
|
||||
MSG_END,
|
||||
OP_H,
|
||||
OP_P,
|
||||
OP_PI,
|
||||
OP_PIN,
|
||||
OP_PING,
|
||||
OP_PO,
|
||||
OP_PON,
|
||||
OP_PONG,
|
||||
OP_I,
|
||||
OP_IN,
|
||||
OP_INF,
|
||||
OP_INFO,
|
||||
OP_INFO_SPC,
|
||||
INFO_ARG,
|
||||
}
|
||||
|
||||
enum cc {
|
||||
CR = "\r".charCodeAt(0),
|
||||
E = "E".charCodeAt(0),
|
||||
e = "e".charCodeAt(0),
|
||||
F = "F".charCodeAt(0),
|
||||
f = "f".charCodeAt(0),
|
||||
G = "G".charCodeAt(0),
|
||||
g = "g".charCodeAt(0),
|
||||
H = "H".charCodeAt(0),
|
||||
h = "h".charCodeAt(0),
|
||||
I = "I".charCodeAt(0),
|
||||
i = "i".charCodeAt(0),
|
||||
K = "K".charCodeAt(0),
|
||||
k = "k".charCodeAt(0),
|
||||
M = "M".charCodeAt(0),
|
||||
m = "m".charCodeAt(0),
|
||||
MINUS = "-".charCodeAt(0),
|
||||
N = "N".charCodeAt(0),
|
||||
n = "n".charCodeAt(0),
|
||||
NL = "\n".charCodeAt(0),
|
||||
O = "O".charCodeAt(0),
|
||||
o = "o".charCodeAt(0),
|
||||
P = "P".charCodeAt(0),
|
||||
p = "p".charCodeAt(0),
|
||||
PLUS = "+".charCodeAt(0),
|
||||
R = "R".charCodeAt(0),
|
||||
r = "r".charCodeAt(0),
|
||||
S = "S".charCodeAt(0),
|
||||
s = "s".charCodeAt(0),
|
||||
SPACE = " ".charCodeAt(0),
|
||||
TAB = "\t".charCodeAt(0),
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright 2020-2022 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { Deferred, deferred } from "./util.ts";
|
||||
import { ErrorCode, NatsError, QueuedIterator } from "./core.ts";
|
||||
|
||||
export type IngestionFilterFnResult = { ingest: boolean; protocol: boolean };
|
||||
|
||||
/**
|
||||
* IngestionFilterFn prevents a value from being ingested by the
|
||||
* iterator. It is executed on `push`. If ingest is false the value
|
||||
* shouldn't be pushed. If protcol is true, the value is a protcol
|
||||
* value
|
||||
*
|
||||
* @param: data is the value
|
||||
* @src: is the source of the data if set.
|
||||
*/
|
||||
export type IngestionFilterFn<T = unknown> = (
|
||||
data: T | null,
|
||||
src?: unknown,
|
||||
) => IngestionFilterFnResult;
|
||||
/**
|
||||
* ProtocolFilterFn allows filtering of values that shouldn't be presented
|
||||
* to the iterator. ProtocolFilterFn is executed when a value is about to be presented
|
||||
*
|
||||
* @param data: the value
|
||||
* @returns boolean: true if the value should presented to the iterator
|
||||
*/
|
||||
export type ProtocolFilterFn<T = unknown> = (data: T | null) => boolean;
|
||||
/**
|
||||
* DispatcherFn allows for values to be processed after being presented
|
||||
* to the iterator. Note that if the ProtocolFilter rejected the value
|
||||
* it will _not_ be presented to the DispatchedFn. Any processing should
|
||||
* instead have been handled by the ProtocolFilterFn.
|
||||
* @param data: the value
|
||||
*/
|
||||
export type DispatchedFn<T = unknown> = (data: T | null) => void;
|
||||
|
||||
export class QueuedIteratorImpl<T> implements QueuedIterator<T> {
|
||||
inflight: number;
|
||||
processed: number;
|
||||
// FIXME: this is updated by the protocol
|
||||
received: number;
|
||||
noIterator: boolean;
|
||||
iterClosed: Deferred<void>;
|
||||
done: boolean;
|
||||
signal: Deferred<void>;
|
||||
yields: T[];
|
||||
filtered: number;
|
||||
pendingFiltered: number;
|
||||
ingestionFilterFn?: IngestionFilterFn<T>;
|
||||
protocolFilterFn?: ProtocolFilterFn<T>;
|
||||
dispatchedFn?: DispatchedFn<T>;
|
||||
ctx?: unknown;
|
||||
_data?: unknown; //data is for use by extenders in any way they like
|
||||
err?: Error;
|
||||
time: number;
|
||||
yielding: boolean;
|
||||
|
||||
constructor() {
|
||||
this.inflight = 0;
|
||||
this.filtered = 0;
|
||||
this.pendingFiltered = 0;
|
||||
this.processed = 0;
|
||||
this.received = 0;
|
||||
this.noIterator = false;
|
||||
this.done = false;
|
||||
this.signal = deferred<void>();
|
||||
this.yields = [];
|
||||
this.iterClosed = deferred<void>();
|
||||
this.time = 0;
|
||||
this.yielding = false;
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator]() {
|
||||
return this.iterate();
|
||||
}
|
||||
|
||||
push(v: T): void {
|
||||
if (this.done) {
|
||||
return;
|
||||
}
|
||||
if (typeof v === "function") {
|
||||
this.yields.push(v);
|
||||
this.signal.resolve();
|
||||
return;
|
||||
}
|
||||
const { ingest, protocol } = this.ingestionFilterFn
|
||||
? this.ingestionFilterFn(v, this.ctx || this)
|
||||
: { ingest: true, protocol: false };
|
||||
if (ingest) {
|
||||
if (protocol) {
|
||||
this.filtered++;
|
||||
this.pendingFiltered++;
|
||||
}
|
||||
this.yields.push(v);
|
||||
this.signal.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
async *iterate(): AsyncIterableIterator<T> {
|
||||
if (this.noIterator) {
|
||||
throw new NatsError("unsupported iterator", ErrorCode.ApiError);
|
||||
}
|
||||
if (this.yielding) {
|
||||
throw new NatsError("already yielding", ErrorCode.ApiError);
|
||||
}
|
||||
this.yielding = true;
|
||||
try {
|
||||
while (true) {
|
||||
if (this.yields.length === 0) {
|
||||
await this.signal;
|
||||
}
|
||||
if (this.err) {
|
||||
throw this.err;
|
||||
}
|
||||
const yields = this.yields;
|
||||
this.inflight = yields.length;
|
||||
this.yields = [];
|
||||
for (let i = 0; i < yields.length; i++) {
|
||||
if (typeof yields[i] === "function") {
|
||||
const fn = yields[i] as unknown as () => void;
|
||||
try {
|
||||
fn();
|
||||
} catch (err) {
|
||||
// failed on the invocation - fail the iterator
|
||||
// so they know to fix the callback
|
||||
throw err;
|
||||
}
|
||||
// fn could have also set an error
|
||||
if (this.err) {
|
||||
throw this.err;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// only pass messages that pass the filter
|
||||
const ok = this.protocolFilterFn
|
||||
? this.protocolFilterFn(yields[i])
|
||||
: true;
|
||||
if (ok) {
|
||||
this.processed++;
|
||||
const start = Date.now();
|
||||
yield yields[i];
|
||||
this.time = Date.now() - start;
|
||||
if (this.dispatchedFn && yields[i]) {
|
||||
this.dispatchedFn(yields[i]);
|
||||
}
|
||||
} else {
|
||||
this.pendingFiltered--;
|
||||
}
|
||||
this.inflight--;
|
||||
}
|
||||
// yielding could have paused and microtask
|
||||
// could have added messages. Prevent allocations
|
||||
// if possible
|
||||
if (this.done) {
|
||||
break;
|
||||
} else if (this.yields.length === 0) {
|
||||
yields.length = 0;
|
||||
this.yields = yields;
|
||||
this.signal = deferred();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// the iterator used break/return
|
||||
this.stop();
|
||||
}
|
||||
}
|
||||
|
||||
stop(err?: Error): void {
|
||||
if (this.done) {
|
||||
return;
|
||||
}
|
||||
this.err = err;
|
||||
this.done = true;
|
||||
this.signal.resolve();
|
||||
this.iterClosed.resolve();
|
||||
}
|
||||
|
||||
getProcessed(): number {
|
||||
return this.noIterator ? this.received : this.processed;
|
||||
}
|
||||
|
||||
getPending(): number {
|
||||
return this.yields.length + this.inflight - this.pendingFiltered;
|
||||
}
|
||||
|
||||
getReceived(): number {
|
||||
return this.received - this.filtered;
|
||||
}
|
||||
}
|
@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright 2020-2021 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { Deferred, deferred, Timeout, timeout } from "./util.ts";
|
||||
import { MuxSubscription } from "./muxsubscription.ts";
|
||||
import { nuid } from "./nuid.ts";
|
||||
import {
|
||||
ErrorCode,
|
||||
Msg,
|
||||
NatsError,
|
||||
Request,
|
||||
RequestManyOptions,
|
||||
RequestOptions,
|
||||
RequestStrategy,
|
||||
} from "./core.ts";
|
||||
|
||||
export class BaseRequest {
|
||||
token: string;
|
||||
received: number;
|
||||
ctx: Error;
|
||||
requestSubject: string;
|
||||
mux: MuxSubscription;
|
||||
|
||||
constructor(
|
||||
mux: MuxSubscription,
|
||||
requestSubject: string,
|
||||
) {
|
||||
this.mux = mux;
|
||||
this.requestSubject = requestSubject;
|
||||
this.received = 0;
|
||||
this.token = nuid.next();
|
||||
this.ctx = new Error();
|
||||
}
|
||||
}
|
||||
|
||||
export interface RequestManyOptionsInternal extends RequestManyOptions {
|
||||
callback: (err: Error | null, msg: Msg | null) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Request expects multiple message response
|
||||
* the request ends when the timer expires,
|
||||
* an error arrives or an expected count of messages
|
||||
* arrives, end is signaled by a null message
|
||||
*/
|
||||
export class RequestMany extends BaseRequest implements Request {
|
||||
callback!: (err: Error | null, msg: Msg | null) => void;
|
||||
done: Deferred<void>;
|
||||
timer: number;
|
||||
max: number;
|
||||
opts: Partial<RequestManyOptionsInternal>;
|
||||
constructor(
|
||||
mux: MuxSubscription,
|
||||
requestSubject: string,
|
||||
opts: Partial<RequestManyOptions> = { maxWait: 1000 },
|
||||
) {
|
||||
super(mux, requestSubject);
|
||||
this.opts = opts;
|
||||
if (typeof this.opts.callback !== "function") {
|
||||
throw new Error("callback is required");
|
||||
}
|
||||
this.callback = this.opts.callback;
|
||||
|
||||
this.max = typeof opts.maxMessages === "number" && opts.maxMessages > 0
|
||||
? opts.maxMessages
|
||||
: -1;
|
||||
this.done = deferred();
|
||||
this.done.then(() => {
|
||||
this.callback(null, null);
|
||||
});
|
||||
// @ts-ignore: node is not a number
|
||||
this.timer = setTimeout(() => {
|
||||
this.cancel();
|
||||
}, opts.maxWait);
|
||||
}
|
||||
|
||||
cancel(err?: NatsError): void {
|
||||
if (err) {
|
||||
this.callback(err, null);
|
||||
}
|
||||
clearTimeout(this.timer);
|
||||
this.mux.cancel(this);
|
||||
this.done.resolve();
|
||||
}
|
||||
|
||||
resolver(err: Error | null, msg: Msg): void {
|
||||
if (err) {
|
||||
err.stack += `\n\n${this.ctx.stack}`;
|
||||
this.cancel(err as NatsError);
|
||||
} else {
|
||||
this.callback(null, msg);
|
||||
if (this.opts.strategy === RequestStrategy.Count) {
|
||||
this.max--;
|
||||
if (this.max === 0) {
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.opts.strategy === RequestStrategy.JitterTimer) {
|
||||
clearTimeout(this.timer);
|
||||
// @ts-ignore: node is not a number
|
||||
this.timer = setTimeout(() => {
|
||||
this.cancel();
|
||||
}, this.opts.jitter || 300);
|
||||
}
|
||||
|
||||
if (this.opts.strategy === RequestStrategy.SentinelMsg) {
|
||||
if (msg && msg.data.length === 0) {
|
||||
this.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class RequestOne extends BaseRequest implements Request {
|
||||
deferred: Deferred<Msg>;
|
||||
timer: Timeout<Msg>;
|
||||
|
||||
constructor(
|
||||
mux: MuxSubscription,
|
||||
requestSubject: string,
|
||||
opts: RequestOptions = { timeout: 1000 },
|
||||
) {
|
||||
super(mux, requestSubject);
|
||||
// extend(this, opts);
|
||||
this.deferred = deferred();
|
||||
this.timer = timeout<Msg>(opts.timeout);
|
||||
}
|
||||
|
||||
resolver(err: Error | null, msg: Msg): void {
|
||||
if (this.timer) {
|
||||
this.timer.cancel();
|
||||
}
|
||||
if (err) {
|
||||
err.stack += `\n\n${this.ctx.stack}`;
|
||||
this.deferred.reject(err);
|
||||
} else {
|
||||
this.deferred.resolve(msg);
|
||||
}
|
||||
this.cancel();
|
||||
}
|
||||
|
||||
cancel(err?: NatsError): void {
|
||||
if (this.timer) {
|
||||
this.timer.cancel();
|
||||
}
|
||||
this.mux.cancel(this);
|
||||
this.deferred.reject(
|
||||
err ? err : NatsError.errorForCode(ErrorCode.Cancelled),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,157 @@
|
||||
/*
|
||||
* Copyright 2022-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export type SemVer = { major: number; minor: number; micro: number };
|
||||
export function parseSemVer(
|
||||
s = "",
|
||||
): SemVer {
|
||||
const m = s.match(/(\d+).(\d+).(\d+)/);
|
||||
if (m) {
|
||||
return {
|
||||
major: parseInt(m[1]),
|
||||
minor: parseInt(m[2]),
|
||||
micro: parseInt(m[3]),
|
||||
};
|
||||
}
|
||||
throw new Error(`'${s}' is not a semver value`);
|
||||
}
|
||||
export function compare(a: SemVer, b: SemVer): number {
|
||||
if (a.major < b.major) return -1;
|
||||
if (a.major > b.major) return 1;
|
||||
if (a.minor < b.minor) return -1;
|
||||
if (a.minor > b.minor) return 1;
|
||||
if (a.micro < b.micro) return -1;
|
||||
if (a.micro > b.micro) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export enum Feature {
|
||||
JS_KV = "js_kv",
|
||||
JS_OBJECTSTORE = "js_objectstore",
|
||||
JS_PULL_MAX_BYTES = "js_pull_max_bytes",
|
||||
JS_NEW_CONSUMER_CREATE_API = "js_new_consumer_create",
|
||||
JS_ALLOW_DIRECT = "js_allow_direct",
|
||||
JS_MULTIPLE_CONSUMER_FILTER = "js_multiple_consumer_filter",
|
||||
JS_SIMPLIFICATION = "js_simplification",
|
||||
JS_STREAM_CONSUMER_METADATA = "js_stream_consumer_metadata",
|
||||
JS_CONSUMER_FILTER_SUBJECTS = "js_consumer_filter_subjects",
|
||||
JS_STREAM_FIRST_SEQ = "js_stream_first_seq",
|
||||
JS_STREAM_SUBJECT_TRANSFORM = "js_stream_subject_transform",
|
||||
JS_STREAM_SOURCE_SUBJECT_TRANSFORM = "js_stream_source_subject_transform",
|
||||
JS_STREAM_COMPRESSION = "js_stream_compression",
|
||||
JS_DEFAULT_CONSUMER_LIMITS = "js_default_consumer_limits",
|
||||
}
|
||||
|
||||
type FeatureVersion = {
|
||||
ok: boolean;
|
||||
min: string;
|
||||
};
|
||||
|
||||
export class Features {
|
||||
server!: SemVer;
|
||||
features: Map<Feature, FeatureVersion>;
|
||||
disabled: Feature[];
|
||||
constructor(v: SemVer) {
|
||||
this.features = new Map<Feature, FeatureVersion>();
|
||||
this.disabled = [];
|
||||
this.update(v);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all disabled entries
|
||||
*/
|
||||
resetDisabled() {
|
||||
this.disabled.length = 0;
|
||||
this.update(this.server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables a particular feature.
|
||||
* @param f
|
||||
*/
|
||||
disable(f: Feature) {
|
||||
this.disabled.push(f);
|
||||
this.update(this.server);
|
||||
}
|
||||
|
||||
isDisabled(f: Feature) {
|
||||
return this.disabled.indexOf(f) !== -1;
|
||||
}
|
||||
|
||||
update(v: SemVer | string) {
|
||||
if (typeof v === "string") {
|
||||
v = parseSemVer(v);
|
||||
}
|
||||
this.server = v;
|
||||
this.set(Feature.JS_KV, "2.6.2");
|
||||
this.set(Feature.JS_OBJECTSTORE, "2.6.3");
|
||||
this.set(Feature.JS_PULL_MAX_BYTES, "2.8.3");
|
||||
this.set(Feature.JS_NEW_CONSUMER_CREATE_API, "2.9.0");
|
||||
this.set(Feature.JS_ALLOW_DIRECT, "2.9.0");
|
||||
this.set(Feature.JS_MULTIPLE_CONSUMER_FILTER, "2.10.0");
|
||||
this.set(Feature.JS_SIMPLIFICATION, "2.9.4");
|
||||
this.set(Feature.JS_STREAM_CONSUMER_METADATA, "2.10.0");
|
||||
this.set(Feature.JS_CONSUMER_FILTER_SUBJECTS, "2.10.0");
|
||||
this.set(Feature.JS_STREAM_FIRST_SEQ, "2.10.0");
|
||||
this.set(Feature.JS_STREAM_SUBJECT_TRANSFORM, "2.10.0");
|
||||
this.set(Feature.JS_STREAM_SOURCE_SUBJECT_TRANSFORM, "2.10.0");
|
||||
this.set(Feature.JS_STREAM_COMPRESSION, "2.10.0");
|
||||
this.set(Feature.JS_DEFAULT_CONSUMER_LIMITS, "2.10.0");
|
||||
|
||||
this.disabled.forEach((f) => {
|
||||
this.features.delete(f);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a feature that requires a particular server version.
|
||||
* @param f
|
||||
* @param requires
|
||||
*/
|
||||
set(f: Feature, requires: string) {
|
||||
this.features.set(f, {
|
||||
min: requires,
|
||||
ok: compare(this.server, parseSemVer(requires)) >= 0,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the feature is available and the min server
|
||||
* version that supports it.
|
||||
* @param f
|
||||
*/
|
||||
get(f: Feature): FeatureVersion {
|
||||
return this.features.get(f) || { min: "unknown", ok: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the feature is supported
|
||||
* @param f
|
||||
*/
|
||||
supports(f: Feature): boolean {
|
||||
return this.get(f)?.ok || false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the server is at least the specified version
|
||||
* @param v
|
||||
*/
|
||||
require(v: SemVer | string): boolean {
|
||||
if (typeof v === "string") {
|
||||
v = parseSemVer(v);
|
||||
}
|
||||
return compare(this.server, v) >= 0;
|
||||
}
|
||||
}
|
@ -0,0 +1,328 @@
|
||||
/*
|
||||
* Copyright 2018-2022 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { defaultPort, getUrlParseFn } from "./transport.ts";
|
||||
import { shuffle } from "./util.ts";
|
||||
import { isIP } from "./ipparser.ts";
|
||||
import {
|
||||
DEFAULT_HOST,
|
||||
DEFAULT_PORT,
|
||||
DnsResolveFn,
|
||||
Server,
|
||||
ServerInfo,
|
||||
ServersChanged,
|
||||
} from "./core.ts";
|
||||
|
||||
export function isIPV4OrHostname(hp: string): boolean {
|
||||
if (hp.indexOf(".") !== -1) {
|
||||
return true;
|
||||
}
|
||||
if (hp.indexOf("[") !== -1 || hp.indexOf("::") !== -1) {
|
||||
return false;
|
||||
}
|
||||
// if we have a plain hostname or host:port
|
||||
if (hp.split(":").length <= 2) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isIPV6(hp: string) {
|
||||
return !isIPV4OrHostname(hp);
|
||||
}
|
||||
|
||||
function filterIpv6MappedToIpv4(hp: string): string {
|
||||
const prefix = "::FFFF:";
|
||||
const idx = hp.toUpperCase().indexOf(prefix);
|
||||
if (idx !== -1 && hp.indexOf(".") !== -1) {
|
||||
// we have something like: ::FFFF:127.0.0.1 or [::FFFF:127.0.0.1]:4222
|
||||
let ip = hp.substring(idx + prefix.length);
|
||||
ip = ip.replace("[", "");
|
||||
return ip.replace("]", "");
|
||||
}
|
||||
|
||||
return hp;
|
||||
}
|
||||
|
||||
export function hostPort(
|
||||
u: string,
|
||||
): { listen: string; hostname: string; port: number } {
|
||||
u = u.trim();
|
||||
// remove any protocol that may have been provided
|
||||
if (u.match(/^(.*:\/\/)(.*)/m)) {
|
||||
u = u.replace(/^(.*:\/\/)(.*)/gm, "$2");
|
||||
}
|
||||
// in web environments, URL may not be a living standard
|
||||
// that means that protocols other than HTTP/S are not
|
||||
// parsable correctly.
|
||||
|
||||
// the third complication is that we may have been given
|
||||
// an IPv6 or worse IPv6 mapping an Ipv4
|
||||
u = filterIpv6MappedToIpv4(u);
|
||||
|
||||
// we only wrap cases where they gave us a plain ipv6
|
||||
// and we are not already bracketed
|
||||
if (isIPV6(u) && u.indexOf("[") === -1) {
|
||||
u = `[${u}]`;
|
||||
}
|
||||
// if we have ipv6, we expect port after ']:' otherwise after ':'
|
||||
const op = isIPV6(u) ? u.match(/(]:)(\d+)/) : u.match(/(:)(\d+)/);
|
||||
const port = op && op.length === 3 && op[1] && op[2]
|
||||
? parseInt(op[2])
|
||||
: DEFAULT_PORT;
|
||||
|
||||
// the next complication is that new URL() may
|
||||
// eat ports which match the protocol - so for example
|
||||
// port 80 may be eliminated - so we flip the protocol
|
||||
// so that it always yields a value
|
||||
const protocol = port === 80 ? "https" : "http";
|
||||
const url = new URL(`${protocol}://${u}`);
|
||||
url.port = `${port}`;
|
||||
|
||||
let hostname = url.hostname;
|
||||
// if we are bracketed, we need to rip it out
|
||||
if (hostname.charAt(0) === "[") {
|
||||
hostname = hostname.substring(1, hostname.length - 1);
|
||||
}
|
||||
const listen = url.host;
|
||||
|
||||
return { listen, hostname, port };
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
export class ServerImpl implements Server {
|
||||
src: string;
|
||||
listen: string;
|
||||
hostname: string;
|
||||
port: number;
|
||||
didConnect: boolean;
|
||||
reconnects: number;
|
||||
lastConnect: number;
|
||||
gossiped: boolean;
|
||||
tlsName: string;
|
||||
resolves?: Server[];
|
||||
|
||||
constructor(u: string, gossiped = false) {
|
||||
this.src = u;
|
||||
this.tlsName = "";
|
||||
const v = hostPort(u);
|
||||
this.listen = v.listen;
|
||||
this.hostname = v.hostname;
|
||||
this.port = v.port;
|
||||
this.didConnect = false;
|
||||
this.reconnects = 0;
|
||||
this.lastConnect = 0;
|
||||
this.gossiped = gossiped;
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.listen;
|
||||
}
|
||||
|
||||
async resolve(
|
||||
opts: Partial<
|
||||
{
|
||||
fn: DnsResolveFn;
|
||||
randomize: boolean;
|
||||
resolve: boolean;
|
||||
debug?: boolean;
|
||||
}
|
||||
>,
|
||||
): Promise<Server[]> {
|
||||
if (!opts.fn) {
|
||||
// we cannot resolve - transport doesn't support it
|
||||
// don't add - to resolves or we get a circ reference
|
||||
return [this];
|
||||
}
|
||||
|
||||
const buf: Server[] = [];
|
||||
if (isIP(this.hostname)) {
|
||||
// don't add - to resolves or we get a circ reference
|
||||
return [this];
|
||||
} else {
|
||||
// resolve the hostname to ips
|
||||
const ips = await opts.fn(this.hostname);
|
||||
if (opts.debug) {
|
||||
console.log(`resolve ${this.hostname} = ${ips.join(",")}`);
|
||||
}
|
||||
|
||||
for (const ip of ips) {
|
||||
// letting URL handle the details of representing IPV6 ip with a port, etc
|
||||
// careful to make sure the protocol doesn't line with standard ports or they
|
||||
// get swallowed
|
||||
const proto = this.port === 80 ? "https" : "http";
|
||||
// ipv6 won't be bracketed here, because it came from resolve
|
||||
const url = new URL(`${proto}://${isIPV6(ip) ? "[" + ip + "]" : ip}`);
|
||||
url.port = `${this.port}`;
|
||||
const ss = new ServerImpl(url.host, false);
|
||||
ss.tlsName = this.hostname;
|
||||
buf.push(ss);
|
||||
}
|
||||
}
|
||||
if (opts.randomize) {
|
||||
shuffle(buf);
|
||||
}
|
||||
this.resolves = buf;
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @hidden
|
||||
*/
|
||||
export class Servers {
|
||||
private firstSelect: boolean;
|
||||
private readonly servers: ServerImpl[];
|
||||
private currentServer: ServerImpl;
|
||||
private tlsName: string;
|
||||
private randomize: boolean;
|
||||
|
||||
constructor(
|
||||
listens: string[] = [],
|
||||
opts: Partial<{ randomize: boolean }> = {},
|
||||
) {
|
||||
this.firstSelect = true;
|
||||
this.servers = [] as ServerImpl[];
|
||||
this.tlsName = "";
|
||||
this.randomize = opts.randomize || false;
|
||||
|
||||
const urlParseFn = getUrlParseFn();
|
||||
if (listens) {
|
||||
listens.forEach((hp) => {
|
||||
hp = urlParseFn ? urlParseFn(hp) : hp;
|
||||
this.servers.push(new ServerImpl(hp));
|
||||
});
|
||||
if (this.randomize) {
|
||||
this.servers = shuffle(this.servers);
|
||||
}
|
||||
}
|
||||
if (this.servers.length === 0) {
|
||||
this.addServer(`${DEFAULT_HOST}:${defaultPort()}`, false);
|
||||
}
|
||||
this.currentServer = this.servers[0];
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.servers.length = 0;
|
||||
}
|
||||
|
||||
updateTLSName(): void {
|
||||
const cs = this.getCurrentServer();
|
||||
if (!isIP(cs.hostname)) {
|
||||
this.tlsName = cs.hostname;
|
||||
this.servers.forEach((s) => {
|
||||
if (s.gossiped) {
|
||||
s.tlsName = this.tlsName;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getCurrentServer(): ServerImpl {
|
||||
return this.currentServer;
|
||||
}
|
||||
|
||||
addServer(u: string, implicit = false): void {
|
||||
const urlParseFn = getUrlParseFn();
|
||||
u = urlParseFn ? urlParseFn(u) : u;
|
||||
const s = new ServerImpl(u, implicit);
|
||||
if (isIP(s.hostname)) {
|
||||
s.tlsName = this.tlsName;
|
||||
}
|
||||
this.servers.push(s);
|
||||
}
|
||||
|
||||
selectServer(): ServerImpl | undefined {
|
||||
// allow using select without breaking the order of the servers
|
||||
if (this.firstSelect) {
|
||||
this.firstSelect = false;
|
||||
return this.currentServer;
|
||||
}
|
||||
const t = this.servers.shift();
|
||||
if (t) {
|
||||
this.servers.push(t);
|
||||
this.currentServer = t;
|
||||
}
|
||||
return t;
|
||||
}
|
||||
|
||||
removeCurrentServer(): void {
|
||||
this.removeServer(this.currentServer);
|
||||
}
|
||||
|
||||
removeServer(server: ServerImpl | undefined): void {
|
||||
if (server) {
|
||||
const index = this.servers.indexOf(server);
|
||||
this.servers.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
length(): number {
|
||||
return this.servers.length;
|
||||
}
|
||||
|
||||
next(): ServerImpl | undefined {
|
||||
return this.servers.length ? this.servers[0] : undefined;
|
||||
}
|
||||
|
||||
getServers(): ServerImpl[] {
|
||||
return this.servers;
|
||||
}
|
||||
|
||||
update(info: ServerInfo): ServersChanged {
|
||||
const added: string[] = [];
|
||||
let deleted: string[] = [];
|
||||
|
||||
const urlParseFn = getUrlParseFn();
|
||||
const discovered = new Map<string, ServerImpl>();
|
||||
if (info.connect_urls && info.connect_urls.length > 0) {
|
||||
info.connect_urls.forEach((hp) => {
|
||||
hp = urlParseFn ? urlParseFn(hp) : hp;
|
||||
const s = new ServerImpl(hp, true);
|
||||
discovered.set(hp, s);
|
||||
});
|
||||
}
|
||||
// remove gossiped servers that are no longer reported
|
||||
const toDelete: number[] = [];
|
||||
this.servers.forEach((s, index) => {
|
||||
const u = s.listen;
|
||||
if (
|
||||
s.gossiped && this.currentServer.listen !== u &&
|
||||
discovered.get(u) === undefined
|
||||
) {
|
||||
// server was removed
|
||||
toDelete.push(index);
|
||||
}
|
||||
// remove this entry from reported
|
||||
discovered.delete(u);
|
||||
});
|
||||
|
||||
// perform the deletion
|
||||
toDelete.reverse();
|
||||
toDelete.forEach((index) => {
|
||||
const removed = this.servers.splice(index, 1);
|
||||
deleted = deleted.concat(removed[0].listen);
|
||||
});
|
||||
|
||||
// remaining servers are new
|
||||
discovered.forEach((v, k) => {
|
||||
this.servers.push(v);
|
||||
added.push(k);
|
||||
});
|
||||
|
||||
return { added, deleted };
|
||||
}
|
||||
}
|
@ -0,0 +1,676 @@
|
||||
/*
|
||||
* Copyright 2022-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { Deferred, deferred } from "./util.ts";
|
||||
import { headers } from "./headers.ts";
|
||||
import { JSONCodec } from "./codec.ts";
|
||||
import { nuid } from "./nuid.ts";
|
||||
import { QueuedIteratorImpl } from "./queued_iterator.ts";
|
||||
import { nanos, validateName } from "../jetstream/jsutil.ts";
|
||||
import { parseSemVer } from "./semver.ts";
|
||||
import { Empty } from "./encoders.ts";
|
||||
import {
|
||||
Endpoint,
|
||||
EndpointInfo,
|
||||
EndpointOptions,
|
||||
Msg,
|
||||
MsgHdrs,
|
||||
NamedEndpointStats,
|
||||
Nanos,
|
||||
NatsConnection,
|
||||
NatsError,
|
||||
Payload,
|
||||
PublishOptions,
|
||||
QueuedIterator,
|
||||
ReviverFn,
|
||||
Service,
|
||||
ServiceConfig,
|
||||
ServiceError,
|
||||
ServiceErrorCodeHeader,
|
||||
ServiceErrorHeader,
|
||||
ServiceGroup,
|
||||
ServiceHandler,
|
||||
ServiceIdentity,
|
||||
ServiceInfo,
|
||||
ServiceMsg,
|
||||
ServiceResponseType,
|
||||
ServiceStats,
|
||||
ServiceVerb,
|
||||
Sub,
|
||||
} from "./core.ts";
|
||||
|
||||
/**
|
||||
* Services have common backplane subject pattern:
|
||||
*
|
||||
* `$SRV.PING|STATS|INFO` - pings or retrieves status for all services
|
||||
* `$SRV.PING|STATS|INFO.<name>` - pings or retrieves status for all services having the specified name
|
||||
* `$SRV.PING|STATS|INFO.<name>.<id>` - pings or retrieves status of a particular service
|
||||
*
|
||||
* Note that <name> and <id> are upper-cased.
|
||||
*/
|
||||
export const ServiceApiPrefix = "$SRV";
|
||||
|
||||
export class ServiceMsgImpl implements ServiceMsg {
|
||||
msg: Msg;
|
||||
constructor(msg: Msg) {
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
get data(): Uint8Array {
|
||||
return this.msg.data;
|
||||
}
|
||||
|
||||
get sid(): number {
|
||||
return this.msg.sid;
|
||||
}
|
||||
|
||||
get subject(): string {
|
||||
return this.msg.subject;
|
||||
}
|
||||
|
||||
get reply(): string {
|
||||
return this.msg.reply || "";
|
||||
}
|
||||
|
||||
get headers(): MsgHdrs | undefined {
|
||||
return this.msg.headers;
|
||||
}
|
||||
|
||||
respond(data?: Payload, opts?: PublishOptions): boolean {
|
||||
return this.msg.respond(data, opts);
|
||||
}
|
||||
|
||||
respondError(
|
||||
code: number,
|
||||
description: string,
|
||||
data?: Uint8Array,
|
||||
opts?: PublishOptions,
|
||||
): boolean {
|
||||
opts = opts || {};
|
||||
opts.headers = opts.headers || headers();
|
||||
opts.headers?.set(ServiceErrorCodeHeader, `${code}`);
|
||||
opts.headers?.set(ServiceErrorHeader, description);
|
||||
return this.msg.respond(data, opts);
|
||||
}
|
||||
|
||||
json<T = unknown>(reviver?: ReviverFn): T {
|
||||
return this.msg.json(reviver);
|
||||
}
|
||||
|
||||
string(): string {
|
||||
return this.msg.string();
|
||||
}
|
||||
}
|
||||
|
||||
export class ServiceGroupImpl implements ServiceGroup {
|
||||
subject: string;
|
||||
queue: string;
|
||||
srv: ServiceImpl;
|
||||
constructor(parent: ServiceGroup, name = "", queue = "") {
|
||||
if (name !== "") {
|
||||
validInternalToken("service group", name);
|
||||
}
|
||||
let root = "";
|
||||
if (parent instanceof ServiceImpl) {
|
||||
this.srv = parent as ServiceImpl;
|
||||
root = "";
|
||||
} else if (parent instanceof ServiceGroupImpl) {
|
||||
const sg = parent as ServiceGroupImpl;
|
||||
this.srv = sg.srv;
|
||||
if (queue === "" && sg.queue !== "") {
|
||||
queue = sg.queue;
|
||||
}
|
||||
root = sg.subject;
|
||||
} else {
|
||||
throw new Error("unknown ServiceGroup type");
|
||||
}
|
||||
this.subject = this.calcSubject(root, name);
|
||||
this.queue = queue;
|
||||
}
|
||||
|
||||
calcSubject(root: string, name = ""): string {
|
||||
if (name === "") {
|
||||
return root;
|
||||
}
|
||||
return root !== "" ? `${root}.${name}` : name;
|
||||
}
|
||||
addEndpoint(
|
||||
name = "",
|
||||
opts?: ServiceHandler | EndpointOptions,
|
||||
): QueuedIterator<ServiceMsg> {
|
||||
opts = opts || { subject: name } as EndpointOptions;
|
||||
const args: EndpointOptions = typeof opts === "function"
|
||||
? { handler: opts, subject: name }
|
||||
: opts;
|
||||
validateName("endpoint", name);
|
||||
let { subject, handler, metadata, queue } = args;
|
||||
subject = subject || name;
|
||||
queue = queue || this.queue;
|
||||
validSubjectName("endpoint subject", subject);
|
||||
subject = this.calcSubject(this.subject, subject);
|
||||
|
||||
const ne = { name, subject, queue, handler, metadata };
|
||||
return this.srv._addEndpoint(ne);
|
||||
}
|
||||
|
||||
addGroup(name = "", queue = ""): ServiceGroup {
|
||||
return new ServiceGroupImpl(this, name, queue);
|
||||
}
|
||||
}
|
||||
|
||||
function validSubjectName(context: string, subj: string) {
|
||||
if (subj === "") {
|
||||
throw new Error(`${context} cannot be empty`);
|
||||
}
|
||||
if (subj.indexOf(" ") !== -1) {
|
||||
throw new Error(`${context} cannot contain spaces: '${subj}'`);
|
||||
}
|
||||
const tokens = subj.split(".");
|
||||
tokens.forEach((v, idx) => {
|
||||
if (v === ">" && idx !== tokens.length - 1) {
|
||||
throw new Error(`${context} cannot have internal '>': '${subj}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function validInternalToken(context: string, subj: string) {
|
||||
if (subj.indexOf(" ") !== -1) {
|
||||
throw new Error(`${context} cannot contain spaces: '${subj}'`);
|
||||
}
|
||||
const tokens = subj.split(".");
|
||||
tokens.forEach((v) => {
|
||||
if (v === ">") {
|
||||
throw new Error(`${context} name cannot contain internal '>': '${subj}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
type NamedEndpoint = {
|
||||
name: string;
|
||||
} & Endpoint;
|
||||
|
||||
type ServiceSubscription<T = unknown> =
|
||||
& NamedEndpoint
|
||||
& {
|
||||
internal: boolean;
|
||||
sub: Sub<T>;
|
||||
qi?: QueuedIterator<T>;
|
||||
stats: NamedEndpointStatsImpl;
|
||||
metadata?: Record<string, string>;
|
||||
};
|
||||
|
||||
export class ServiceImpl implements Service {
|
||||
nc: NatsConnection;
|
||||
_id: string;
|
||||
config: ServiceConfig;
|
||||
handlers: ServiceSubscription[];
|
||||
internal: ServiceSubscription[];
|
||||
_stopped: boolean;
|
||||
_done: Deferred<Error | null>;
|
||||
started: string;
|
||||
|
||||
/**
|
||||
* @param verb
|
||||
* @param name
|
||||
* @param id
|
||||
* @param prefix - this is only supplied by tooling when building control subject that crosses an account
|
||||
*/
|
||||
static controlSubject(
|
||||
verb: ServiceVerb,
|
||||
name = "",
|
||||
id = "",
|
||||
prefix?: string,
|
||||
) {
|
||||
// the prefix is used as is, because it is an
|
||||
// account boundary permission
|
||||
const pre = prefix ?? ServiceApiPrefix;
|
||||
if (name === "" && id === "") {
|
||||
return `${pre}.${verb}`;
|
||||
}
|
||||
validateName("control subject name", name);
|
||||
if (id !== "") {
|
||||
validateName("control subject id", id);
|
||||
return `${pre}.${verb}.${name}.${id}`;
|
||||
}
|
||||
return `${pre}.${verb}.${name}`;
|
||||
}
|
||||
|
||||
constructor(
|
||||
nc: NatsConnection,
|
||||
config: ServiceConfig = { name: "", version: "" },
|
||||
) {
|
||||
this.nc = nc;
|
||||
this.config = Object.assign({}, config);
|
||||
if (!this.config.queue) {
|
||||
this.config.queue = "q";
|
||||
}
|
||||
|
||||
// this will throw if no name
|
||||
validateName("name", this.config.name);
|
||||
validateName("queue", this.config.queue);
|
||||
|
||||
// this will throw if not semver
|
||||
parseSemVer(this.config.version);
|
||||
this._id = nuid.next();
|
||||
this.internal = [] as ServiceSubscription[];
|
||||
this._done = deferred();
|
||||
this._stopped = false;
|
||||
this.handlers = [];
|
||||
this.started = new Date().toISOString();
|
||||
// initialize the stats
|
||||
this.reset();
|
||||
|
||||
// close if the connection closes
|
||||
this.nc.closed()
|
||||
.then(() => {
|
||||
this.close().catch();
|
||||
})
|
||||
.catch((err) => {
|
||||
this.close(err).catch();
|
||||
});
|
||||
}
|
||||
|
||||
get subjects(): string[] {
|
||||
return this.handlers.filter((s) => {
|
||||
return s.internal === false;
|
||||
}).map((s) => {
|
||||
return s.subject;
|
||||
});
|
||||
}
|
||||
|
||||
get id(): string {
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
return this.config.name;
|
||||
}
|
||||
|
||||
get description(): string {
|
||||
return this.config.description ?? "";
|
||||
}
|
||||
|
||||
get version(): string {
|
||||
return this.config.version;
|
||||
}
|
||||
|
||||
get metadata(): Record<string, string> | undefined {
|
||||
return this.config.metadata;
|
||||
}
|
||||
|
||||
errorToHeader(err: Error): MsgHdrs {
|
||||
const h = headers();
|
||||
if (err instanceof ServiceError) {
|
||||
const se = err as ServiceError;
|
||||
h.set(ServiceErrorHeader, se.message);
|
||||
h.set(ServiceErrorCodeHeader, `${se.code}`);
|
||||
} else {
|
||||
h.set(ServiceErrorHeader, err.message);
|
||||
h.set(ServiceErrorCodeHeader, "500");
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
setupHandler(
|
||||
h: NamedEndpoint,
|
||||
internal = false,
|
||||
): ServiceSubscription {
|
||||
// internals don't use a queue
|
||||
const queue = internal ? "" : (h.queue ? h.queue : this.config.queue);
|
||||
const { name, subject, handler } = h as NamedEndpoint;
|
||||
const sv = h as ServiceSubscription;
|
||||
sv.internal = internal;
|
||||
if (internal) {
|
||||
this.internal.push(sv);
|
||||
}
|
||||
sv.stats = new NamedEndpointStatsImpl(name, subject, queue);
|
||||
sv.queue = queue;
|
||||
|
||||
const callback = handler
|
||||
? (err: NatsError | null, msg: Msg) => {
|
||||
if (err) {
|
||||
this.close(err);
|
||||
return;
|
||||
}
|
||||
const start = Date.now();
|
||||
try {
|
||||
handler(err, new ServiceMsgImpl(msg));
|
||||
} catch (err) {
|
||||
sv.stats.countError(err);
|
||||
msg?.respond(Empty, { headers: this.errorToHeader(err) });
|
||||
} finally {
|
||||
sv.stats.countLatency(start);
|
||||
}
|
||||
}
|
||||
: undefined;
|
||||
|
||||
sv.sub = this.nc.subscribe(subject, {
|
||||
callback,
|
||||
queue,
|
||||
});
|
||||
|
||||
sv.sub.closed
|
||||
.then(() => {
|
||||
if (!this._stopped) {
|
||||
this.close(new Error(`required subscription ${h.subject} stopped`))
|
||||
.catch();
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
if (!this._stopped) {
|
||||
const ne = new Error(
|
||||
`required subscription ${h.subject} errored: ${err.message}`,
|
||||
);
|
||||
ne.stack = err.stack;
|
||||
this.close(ne).catch();
|
||||
}
|
||||
});
|
||||
return sv;
|
||||
}
|
||||
|
||||
info(): ServiceInfo {
|
||||
return {
|
||||
type: ServiceResponseType.INFO,
|
||||
name: this.name,
|
||||
id: this.id,
|
||||
version: this.version,
|
||||
description: this.description,
|
||||
metadata: this.metadata,
|
||||
endpoints: this.endpoints(),
|
||||
} as ServiceInfo;
|
||||
}
|
||||
|
||||
endpoints(): EndpointInfo[] {
|
||||
return this.handlers.map((v) => {
|
||||
const { subject, metadata, name, queue } = v;
|
||||
return { subject, metadata, name, queue_group: queue };
|
||||
});
|
||||
}
|
||||
|
||||
async stats(): Promise<ServiceStats> {
|
||||
const endpoints: NamedEndpointStats[] = [];
|
||||
for (const h of this.handlers) {
|
||||
if (typeof this.config.statsHandler === "function") {
|
||||
try {
|
||||
h.stats.data = await this.config.statsHandler(h);
|
||||
} catch (err) {
|
||||
h.stats.countError(err);
|
||||
}
|
||||
}
|
||||
endpoints.push(h.stats.stats(h.qi));
|
||||
}
|
||||
return {
|
||||
type: ServiceResponseType.STATS,
|
||||
name: this.name,
|
||||
id: this.id,
|
||||
version: this.version,
|
||||
started: this.started,
|
||||
metadata: this.metadata,
|
||||
endpoints,
|
||||
};
|
||||
}
|
||||
|
||||
addInternalHandler(
|
||||
verb: ServiceVerb,
|
||||
handler: (err: NatsError | null, msg: Msg) => Promise<void>,
|
||||
) {
|
||||
const v = `${verb}`.toUpperCase();
|
||||
this._doAddInternalHandler(`${v}-all`, verb, handler);
|
||||
this._doAddInternalHandler(`${v}-kind`, verb, handler, this.name);
|
||||
this._doAddInternalHandler(
|
||||
`${v}`,
|
||||
verb,
|
||||
handler,
|
||||
this.name,
|
||||
this.id,
|
||||
);
|
||||
}
|
||||
|
||||
_doAddInternalHandler(
|
||||
name: string,
|
||||
verb: ServiceVerb,
|
||||
handler: (err: NatsError | null, msg: Msg) => Promise<void>,
|
||||
kind = "",
|
||||
id = "",
|
||||
) {
|
||||
const endpoint = {} as NamedEndpoint;
|
||||
endpoint.name = name;
|
||||
endpoint.subject = ServiceImpl.controlSubject(verb, kind, id);
|
||||
endpoint.handler = handler;
|
||||
this.setupHandler(endpoint, true);
|
||||
}
|
||||
|
||||
start(): Promise<Service> {
|
||||
const jc = JSONCodec();
|
||||
const statsHandler = (err: Error | null, msg: Msg): Promise<void> => {
|
||||
if (err) {
|
||||
this.close(err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
return this.stats().then((s) => {
|
||||
msg?.respond(jc.encode(s));
|
||||
return Promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
const infoHandler = (err: Error | null, msg: Msg): Promise<void> => {
|
||||
if (err) {
|
||||
this.close(err);
|
||||
return Promise.reject(err);
|
||||
}
|
||||
msg?.respond(jc.encode(this.info()));
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const ping = jc.encode(this.ping());
|
||||
const pingHandler = (err: Error | null, msg: Msg): Promise<void> => {
|
||||
if (err) {
|
||||
this.close(err).then().catch();
|
||||
return Promise.reject(err);
|
||||
}
|
||||
msg.respond(ping);
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
this.addInternalHandler(ServiceVerb.PING, pingHandler);
|
||||
this.addInternalHandler(ServiceVerb.STATS, statsHandler);
|
||||
this.addInternalHandler(ServiceVerb.INFO, infoHandler);
|
||||
|
||||
// now the actual service
|
||||
this.handlers.forEach((h) => {
|
||||
const { subject } = h as Endpoint;
|
||||
if (typeof subject !== "string") {
|
||||
return;
|
||||
}
|
||||
// this is expected in cases where main subject is just
|
||||
// a root subject for multiple endpoints - user can disable
|
||||
// listening to the root endpoint, by specifying null
|
||||
if (h.handler === null) {
|
||||
return;
|
||||
}
|
||||
this.setupHandler(h as unknown as NamedEndpoint);
|
||||
});
|
||||
|
||||
return Promise.resolve(this);
|
||||
}
|
||||
|
||||
close(err?: Error): Promise<null | Error> {
|
||||
if (this._stopped) {
|
||||
return this._done;
|
||||
}
|
||||
this._stopped = true;
|
||||
let buf: Promise<void>[] = [];
|
||||
if (!this.nc.isClosed()) {
|
||||
buf = this.handlers.concat(this.internal).map((h) => {
|
||||
return h.sub.drain();
|
||||
});
|
||||
}
|
||||
Promise.allSettled(buf)
|
||||
.then(() => {
|
||||
this._done.resolve(err ? err : null);
|
||||
});
|
||||
return this._done;
|
||||
}
|
||||
|
||||
get stopped(): Promise<null | Error> {
|
||||
return this._done;
|
||||
}
|
||||
|
||||
get isStopped(): boolean {
|
||||
return this._stopped;
|
||||
}
|
||||
|
||||
stop(err?: Error): Promise<null | Error> {
|
||||
return this.close(err);
|
||||
}
|
||||
|
||||
ping(): ServiceIdentity {
|
||||
return {
|
||||
type: ServiceResponseType.PING,
|
||||
name: this.name,
|
||||
id: this.id,
|
||||
version: this.version,
|
||||
metadata: this.metadata,
|
||||
};
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
// pretend we restarted
|
||||
this.started = new Date().toISOString();
|
||||
if (this.handlers) {
|
||||
for (const h of this.handlers) {
|
||||
h.stats.reset(h.qi);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addGroup(name: string, queue?: string): ServiceGroup {
|
||||
return new ServiceGroupImpl(this, name, queue);
|
||||
}
|
||||
|
||||
addEndpoint(
|
||||
name: string,
|
||||
handler?: ServiceHandler | EndpointOptions,
|
||||
): QueuedIterator<ServiceMsg> {
|
||||
const sg = new ServiceGroupImpl(this);
|
||||
return sg.addEndpoint(name, handler);
|
||||
}
|
||||
|
||||
_addEndpoint(
|
||||
e: NamedEndpoint,
|
||||
): QueuedIterator<ServiceMsg> {
|
||||
const qi = new QueuedIteratorImpl<ServiceMsg>();
|
||||
qi.noIterator = typeof e.handler === "function";
|
||||
if (!qi.noIterator) {
|
||||
e.handler = (err, msg): void => {
|
||||
err ? this.stop(err).catch() : qi.push(new ServiceMsgImpl(msg));
|
||||
};
|
||||
// close the service if the iterator closes
|
||||
qi.iterClosed.then(() => {
|
||||
this.close().catch();
|
||||
});
|
||||
}
|
||||
// track the iterator for stats
|
||||
const ss = this.setupHandler(e, false);
|
||||
ss.qi = qi;
|
||||
this.handlers.push(ss);
|
||||
return qi;
|
||||
}
|
||||
}
|
||||
|
||||
class NamedEndpointStatsImpl implements NamedEndpointStats {
|
||||
name: string;
|
||||
subject: string;
|
||||
average_processing_time: Nanos;
|
||||
num_requests: number;
|
||||
processing_time: Nanos;
|
||||
num_errors: number;
|
||||
last_error?: string;
|
||||
data?: unknown;
|
||||
metadata?: Record<string, string>;
|
||||
queue: string;
|
||||
|
||||
constructor(name: string, subject: string, queue = "") {
|
||||
this.name = name;
|
||||
this.subject = subject;
|
||||
this.average_processing_time = 0;
|
||||
this.num_errors = 0;
|
||||
this.num_requests = 0;
|
||||
this.processing_time = 0;
|
||||
this.queue = queue;
|
||||
}
|
||||
reset(qi?: QueuedIterator<unknown>) {
|
||||
this.num_requests = 0;
|
||||
this.processing_time = 0;
|
||||
this.average_processing_time = 0;
|
||||
this.num_errors = 0;
|
||||
this.last_error = undefined;
|
||||
this.data = undefined;
|
||||
const qii = qi as QueuedIteratorImpl<unknown>;
|
||||
if (qii) {
|
||||
qii.time = 0;
|
||||
qii.processed = 0;
|
||||
}
|
||||
}
|
||||
countLatency(start: number) {
|
||||
this.num_requests++;
|
||||
this.processing_time += nanos(Date.now() - start);
|
||||
this.average_processing_time = Math.round(
|
||||
this.processing_time / this.num_requests,
|
||||
);
|
||||
}
|
||||
countError(err: Error): void {
|
||||
this.num_errors++;
|
||||
this.last_error = err.message;
|
||||
}
|
||||
|
||||
_stats(): NamedEndpointStats {
|
||||
const {
|
||||
name,
|
||||
subject,
|
||||
average_processing_time,
|
||||
num_errors,
|
||||
num_requests,
|
||||
processing_time,
|
||||
last_error,
|
||||
data,
|
||||
queue,
|
||||
} = this;
|
||||
return {
|
||||
name,
|
||||
subject,
|
||||
average_processing_time,
|
||||
num_errors,
|
||||
num_requests,
|
||||
processing_time,
|
||||
last_error,
|
||||
data,
|
||||
queue_group: queue,
|
||||
};
|
||||
}
|
||||
|
||||
stats(qi?: QueuedIterator<unknown>): NamedEndpointStats {
|
||||
const qii = qi as QueuedIteratorImpl<unknown>;
|
||||
if (qii?.noIterator === false) {
|
||||
// grab stats in the iterator
|
||||
this.processing_time = qii.time;
|
||||
this.num_requests = qii.processed;
|
||||
this.average_processing_time =
|
||||
this.processing_time > 0 && this.num_requests > 0
|
||||
? this.processing_time / this.num_requests
|
||||
: 0;
|
||||
}
|
||||
return this._stats();
|
||||
}
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
/*
|
||||
* Copyright 2022-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { Empty } from "./encoders.ts";
|
||||
import { JSONCodec } from "./codec.ts";
|
||||
import { QueuedIteratorImpl } from "./queued_iterator.ts";
|
||||
import {
|
||||
NatsConnection,
|
||||
RequestManyOptions,
|
||||
ServiceIdentity,
|
||||
ServiceInfo,
|
||||
ServiceStats,
|
||||
ServiceVerb,
|
||||
} from "./core.ts";
|
||||
import { ServiceImpl } from "./service.ts";
|
||||
|
||||
import { QueuedIterator, RequestStrategy, ServiceClient } from "./core.ts";
|
||||
|
||||
export class ServiceClientImpl implements ServiceClient {
|
||||
nc: NatsConnection;
|
||||
prefix: string | undefined;
|
||||
opts: RequestManyOptions;
|
||||
constructor(
|
||||
nc: NatsConnection,
|
||||
opts: RequestManyOptions = {
|
||||
strategy: RequestStrategy.JitterTimer,
|
||||
maxWait: 2000,
|
||||
},
|
||||
prefix?: string,
|
||||
) {
|
||||
this.nc = nc;
|
||||
this.prefix = prefix;
|
||||
this.opts = opts;
|
||||
}
|
||||
|
||||
ping(
|
||||
name = "",
|
||||
id = "",
|
||||
): Promise<QueuedIterator<ServiceIdentity>> {
|
||||
return this.q<ServiceIdentity>(ServiceVerb.PING, name, id);
|
||||
}
|
||||
|
||||
stats(
|
||||
name = "",
|
||||
id = "",
|
||||
): Promise<QueuedIterator<ServiceStats>> {
|
||||
return this.q<ServiceStats>(ServiceVerb.STATS, name, id);
|
||||
}
|
||||
|
||||
info(
|
||||
name = "",
|
||||
id = "",
|
||||
): Promise<QueuedIterator<ServiceInfo>> {
|
||||
return this.q<ServiceInfo>(ServiceVerb.INFO, name, id);
|
||||
}
|
||||
|
||||
async q<T>(
|
||||
v: ServiceVerb,
|
||||
name = "",
|
||||
id = "",
|
||||
): Promise<QueuedIterator<T>> {
|
||||
const iter = new QueuedIteratorImpl<T>();
|
||||
const jc = JSONCodec<T>();
|
||||
const subj = ServiceImpl.controlSubject(v, name, id, this.prefix);
|
||||
const responses = await this.nc.requestMany(subj, Empty, this.opts);
|
||||
(async () => {
|
||||
for await (const m of responses) {
|
||||
try {
|
||||
const s = jc.decode(m.data);
|
||||
iter.push(s);
|
||||
} catch (err) {
|
||||
// @ts-ignore: pushing fn
|
||||
iter.push(() => {
|
||||
iter.stop(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
//@ts-ignore: push a fn
|
||||
iter.push(() => {
|
||||
iter.stop();
|
||||
});
|
||||
})().catch((err) => {
|
||||
iter.stop(err);
|
||||
});
|
||||
return iter;
|
||||
}
|
||||
}
|
@ -0,0 +1,360 @@
|
||||
// deno-fmt-ignore-file
|
||||
// deno-lint-ignore-file
|
||||
// This code was bundled using `deno bundle` and it's not recommended to edit it manually
|
||||
|
||||
// deno bundle https://deno.land/x/sha256@v1.0.2/mod.ts
|
||||
|
||||
// The MIT License (MIT)
|
||||
//
|
||||
// Original work (c) Marco Paland (marco@paland.com) 2015-2018, PALANDesign Hannover, Germany
|
||||
//
|
||||
// Deno port Copyright (c) 2019 Noah Anabiik Schwarz
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included in
|
||||
// all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
// THE SOFTWARE.
|
||||
|
||||
function getLengths(b64) {
|
||||
const len = b64.length;
|
||||
let validLen = b64.indexOf("=");
|
||||
if (validLen === -1) {
|
||||
validLen = len;
|
||||
}
|
||||
const placeHoldersLen = validLen === len ? 0 : 4 - validLen % 4;
|
||||
return [
|
||||
validLen,
|
||||
placeHoldersLen
|
||||
];
|
||||
}
|
||||
function init(lookup, revLookup, urlsafe = false) {
|
||||
function _byteLength(validLen, placeHoldersLen) {
|
||||
return Math.floor((validLen + placeHoldersLen) * 3 / 4 - placeHoldersLen);
|
||||
}
|
||||
function tripletToBase64(num) {
|
||||
return lookup[num >> 18 & 0x3f] + lookup[num >> 12 & 0x3f] + lookup[num >> 6 & 0x3f] + lookup[num & 0x3f];
|
||||
}
|
||||
function encodeChunk(buf, start, end) {
|
||||
const out = new Array((end - start) / 3);
|
||||
for(let i = start, curTriplet = 0; i < end; i += 3){
|
||||
out[curTriplet++] = tripletToBase64((buf[i] << 16) + (buf[i + 1] << 8) + buf[i + 2]);
|
||||
}
|
||||
return out.join("");
|
||||
}
|
||||
return {
|
||||
byteLength (b64) {
|
||||
return _byteLength.apply(null, getLengths(b64));
|
||||
},
|
||||
toUint8Array (b64) {
|
||||
const [validLen, placeHoldersLen] = getLengths(b64);
|
||||
const buf = new Uint8Array(_byteLength(validLen, placeHoldersLen));
|
||||
const len = placeHoldersLen ? validLen - 4 : validLen;
|
||||
let tmp;
|
||||
let curByte = 0;
|
||||
let i;
|
||||
for(i = 0; i < len; i += 4){
|
||||
tmp = revLookup[b64.charCodeAt(i)] << 18 | revLookup[b64.charCodeAt(i + 1)] << 12 | revLookup[b64.charCodeAt(i + 2)] << 6 | revLookup[b64.charCodeAt(i + 3)];
|
||||
buf[curByte++] = tmp >> 16 & 0xff;
|
||||
buf[curByte++] = tmp >> 8 & 0xff;
|
||||
buf[curByte++] = tmp & 0xff;
|
||||
}
|
||||
if (placeHoldersLen === 2) {
|
||||
tmp = revLookup[b64.charCodeAt(i)] << 2 | revLookup[b64.charCodeAt(i + 1)] >> 4;
|
||||
buf[curByte++] = tmp & 0xff;
|
||||
} else if (placeHoldersLen === 1) {
|
||||
tmp = revLookup[b64.charCodeAt(i)] << 10 | revLookup[b64.charCodeAt(i + 1)] << 4 | revLookup[b64.charCodeAt(i + 2)] >> 2;
|
||||
buf[curByte++] = tmp >> 8 & 0xff;
|
||||
buf[curByte++] = tmp & 0xff;
|
||||
}
|
||||
return buf;
|
||||
},
|
||||
fromUint8Array (buf) {
|
||||
const maxChunkLength = 16383;
|
||||
const len = buf.length;
|
||||
const extraBytes = len % 3;
|
||||
const len2 = len - extraBytes;
|
||||
const parts = new Array(Math.ceil(len2 / 16383) + (extraBytes ? 1 : 0));
|
||||
let curChunk = 0;
|
||||
let chunkEnd;
|
||||
for(let i = 0; i < len2; i += maxChunkLength){
|
||||
chunkEnd = i + maxChunkLength;
|
||||
parts[curChunk++] = encodeChunk(buf, i, chunkEnd > len2 ? len2 : chunkEnd);
|
||||
}
|
||||
let tmp;
|
||||
if (extraBytes === 1) {
|
||||
tmp = buf[len2];
|
||||
parts[curChunk] = lookup[tmp >> 2] + lookup[tmp << 4 & 0x3f];
|
||||
if (!urlsafe) parts[curChunk] += "==";
|
||||
} else if (extraBytes === 2) {
|
||||
tmp = buf[len2] << 8 | buf[len2 + 1] & 0xff;
|
||||
parts[curChunk] = lookup[tmp >> 10] + lookup[tmp >> 4 & 0x3f] + lookup[tmp << 2 & 0x3f];
|
||||
if (!urlsafe) parts[curChunk] += "=";
|
||||
}
|
||||
return parts.join("");
|
||||
}
|
||||
};
|
||||
}
|
||||
const lookup = [];
|
||||
const revLookup = [];
|
||||
const code = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
||||
for(let i = 0, l = code.length; i < l; ++i){
|
||||
lookup[i] = code[i];
|
||||
revLookup[code.charCodeAt(i)] = i;
|
||||
}
|
||||
const { byteLength , toUint8Array , fromUint8Array } = init(lookup, revLookup, true);
|
||||
const decoder = new TextDecoder();
|
||||
const encoder = new TextEncoder();
|
||||
function toHexString(buf) {
|
||||
return buf.reduce((hex, __byte)=>`${hex}${__byte < 16 ? "0" : ""}${__byte.toString(16)}`, "");
|
||||
}
|
||||
function fromHexString(hex) {
|
||||
const len = hex.length;
|
||||
if (len % 2 || !/^[0-9a-fA-F]+$/.test(hex)) {
|
||||
throw new TypeError("Invalid hex string.");
|
||||
}
|
||||
hex = hex.toLowerCase();
|
||||
const buf = new Uint8Array(Math.floor(len / 2));
|
||||
const end = len / 2;
|
||||
for(let i = 0; i < end; ++i){
|
||||
buf[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
function decode(buf, encoding = "utf8") {
|
||||
if (/^utf-?8$/i.test(encoding)) {
|
||||
return decoder.decode(buf);
|
||||
} else if (/^base64$/i.test(encoding)) {
|
||||
return fromUint8Array(buf);
|
||||
} else if (/^hex(?:adecimal)?$/i.test(encoding)) {
|
||||
return toHexString(buf);
|
||||
} else {
|
||||
throw new TypeError("Unsupported string encoding.");
|
||||
}
|
||||
}
|
||||
function encode(str, encoding = "utf8") {
|
||||
if (/^utf-?8$/i.test(encoding)) {
|
||||
return encoder.encode(str);
|
||||
} else if (/^base64$/i.test(encoding)) {
|
||||
return toUint8Array(str);
|
||||
} else if (/^hex(?:adecimal)?$/i.test(encoding)) {
|
||||
return fromHexString(str);
|
||||
} else {
|
||||
throw new TypeError("Unsupported string encoding.");
|
||||
}
|
||||
}
|
||||
const BYTES = 32;
|
||||
class SHA256 {
|
||||
hashSize = 32;
|
||||
_buf;
|
||||
_bufIdx;
|
||||
_count;
|
||||
_K;
|
||||
_H;
|
||||
_finalized;
|
||||
constructor(){
|
||||
this._buf = new Uint8Array(64);
|
||||
this._K = new Uint32Array([
|
||||
0x428a2f98,
|
||||
0x71374491,
|
||||
0xb5c0fbcf,
|
||||
0xe9b5dba5,
|
||||
0x3956c25b,
|
||||
0x59f111f1,
|
||||
0x923f82a4,
|
||||
0xab1c5ed5,
|
||||
0xd807aa98,
|
||||
0x12835b01,
|
||||
0x243185be,
|
||||
0x550c7dc3,
|
||||
0x72be5d74,
|
||||
0x80deb1fe,
|
||||
0x9bdc06a7,
|
||||
0xc19bf174,
|
||||
0xe49b69c1,
|
||||
0xefbe4786,
|
||||
0x0fc19dc6,
|
||||
0x240ca1cc,
|
||||
0x2de92c6f,
|
||||
0x4a7484aa,
|
||||
0x5cb0a9dc,
|
||||
0x76f988da,
|
||||
0x983e5152,
|
||||
0xa831c66d,
|
||||
0xb00327c8,
|
||||
0xbf597fc7,
|
||||
0xc6e00bf3,
|
||||
0xd5a79147,
|
||||
0x06ca6351,
|
||||
0x14292967,
|
||||
0x27b70a85,
|
||||
0x2e1b2138,
|
||||
0x4d2c6dfc,
|
||||
0x53380d13,
|
||||
0x650a7354,
|
||||
0x766a0abb,
|
||||
0x81c2c92e,
|
||||
0x92722c85,
|
||||
0xa2bfe8a1,
|
||||
0xa81a664b,
|
||||
0xc24b8b70,
|
||||
0xc76c51a3,
|
||||
0xd192e819,
|
||||
0xd6990624,
|
||||
0xf40e3585,
|
||||
0x106aa070,
|
||||
0x19a4c116,
|
||||
0x1e376c08,
|
||||
0x2748774c,
|
||||
0x34b0bcb5,
|
||||
0x391c0cb3,
|
||||
0x4ed8aa4a,
|
||||
0x5b9cca4f,
|
||||
0x682e6ff3,
|
||||
0x748f82ee,
|
||||
0x78a5636f,
|
||||
0x84c87814,
|
||||
0x8cc70208,
|
||||
0x90befffa,
|
||||
0xa4506ceb,
|
||||
0xbef9a3f7,
|
||||
0xc67178f2
|
||||
]);
|
||||
this.init();
|
||||
}
|
||||
init() {
|
||||
this._H = new Uint32Array([
|
||||
0x6a09e667,
|
||||
0xbb67ae85,
|
||||
0x3c6ef372,
|
||||
0xa54ff53a,
|
||||
0x510e527f,
|
||||
0x9b05688c,
|
||||
0x1f83d9ab,
|
||||
0x5be0cd19
|
||||
]);
|
||||
this._bufIdx = 0;
|
||||
this._count = new Uint32Array(2);
|
||||
this._buf.fill(0);
|
||||
this._finalized = false;
|
||||
return this;
|
||||
}
|
||||
update(msg, inputEncoding) {
|
||||
if (msg === null) {
|
||||
throw new TypeError("msg must be a string or Uint8Array.");
|
||||
} else if (typeof msg === "string") {
|
||||
msg = encode(msg, inputEncoding);
|
||||
}
|
||||
for(let i = 0, len = msg.length; i < len; i++){
|
||||
this._buf[this._bufIdx++] = msg[i];
|
||||
if (this._bufIdx === 64) {
|
||||
this._transform();
|
||||
this._bufIdx = 0;
|
||||
}
|
||||
}
|
||||
const c = this._count;
|
||||
if ((c[0] += msg.length << 3) < msg.length << 3) {
|
||||
c[1]++;
|
||||
}
|
||||
c[1] += msg.length >>> 29;
|
||||
return this;
|
||||
}
|
||||
digest(outputEncoding) {
|
||||
if (this._finalized) {
|
||||
throw new Error("digest has already been called.");
|
||||
}
|
||||
this._finalized = true;
|
||||
const b = this._buf;
|
||||
let idx = this._bufIdx;
|
||||
b[idx++] = 0x80;
|
||||
while(idx !== 56){
|
||||
if (idx === 64) {
|
||||
this._transform();
|
||||
idx = 0;
|
||||
}
|
||||
b[idx++] = 0;
|
||||
}
|
||||
const c = this._count;
|
||||
b[56] = c[1] >>> 24 & 0xff;
|
||||
b[57] = c[1] >>> 16 & 0xff;
|
||||
b[58] = c[1] >>> 8 & 0xff;
|
||||
b[59] = c[1] >>> 0 & 0xff;
|
||||
b[60] = c[0] >>> 24 & 0xff;
|
||||
b[61] = c[0] >>> 16 & 0xff;
|
||||
b[62] = c[0] >>> 8 & 0xff;
|
||||
b[63] = c[0] >>> 0 & 0xff;
|
||||
this._transform();
|
||||
const hash = new Uint8Array(32);
|
||||
for(let i = 0; i < 8; i++){
|
||||
hash[(i << 2) + 0] = this._H[i] >>> 24 & 0xff;
|
||||
hash[(i << 2) + 1] = this._H[i] >>> 16 & 0xff;
|
||||
hash[(i << 2) + 2] = this._H[i] >>> 8 & 0xff;
|
||||
hash[(i << 2) + 3] = this._H[i] >>> 0 & 0xff;
|
||||
}
|
||||
this.init();
|
||||
return outputEncoding ? decode(hash, outputEncoding) : hash;
|
||||
}
|
||||
_transform() {
|
||||
const h = this._H;
|
||||
let h0 = h[0];
|
||||
let h1 = h[1];
|
||||
let h2 = h[2];
|
||||
let h3 = h[3];
|
||||
let h4 = h[4];
|
||||
let h5 = h[5];
|
||||
let h6 = h[6];
|
||||
let h7 = h[7];
|
||||
const w = new Uint32Array(16);
|
||||
let i;
|
||||
for(i = 0; i < 16; i++){
|
||||
w[i] = this._buf[(i << 2) + 3] | this._buf[(i << 2) + 2] << 8 | this._buf[(i << 2) + 1] << 16 | this._buf[i << 2] << 24;
|
||||
}
|
||||
for(i = 0; i < 64; i++){
|
||||
let tmp;
|
||||
if (i < 16) {
|
||||
tmp = w[i];
|
||||
} else {
|
||||
let a = w[i + 1 & 15];
|
||||
let b = w[i + 14 & 15];
|
||||
tmp = w[i & 15] = (a >>> 7 ^ a >>> 18 ^ a >>> 3 ^ a << 25 ^ a << 14) + (b >>> 17 ^ b >>> 19 ^ b >>> 10 ^ b << 15 ^ b << 13) + w[i & 15] + w[i + 9 & 15] | 0;
|
||||
}
|
||||
tmp = tmp + h7 + (h4 >>> 6 ^ h4 >>> 11 ^ h4 >>> 25 ^ h4 << 26 ^ h4 << 21 ^ h4 << 7) + (h6 ^ h4 & (h5 ^ h6)) + this._K[i] | 0;
|
||||
h7 = h6;
|
||||
h6 = h5;
|
||||
h5 = h4;
|
||||
h4 = h3 + tmp;
|
||||
h3 = h2;
|
||||
h2 = h1;
|
||||
h1 = h0;
|
||||
h0 = tmp + (h1 & h2 ^ h3 & (h1 ^ h2)) + (h1 >>> 2 ^ h1 >>> 13 ^ h1 >>> 22 ^ h1 << 30 ^ h1 << 19 ^ h1 << 10) | 0;
|
||||
}
|
||||
h[0] = h[0] + h0 | 0;
|
||||
h[1] = h[1] + h1 | 0;
|
||||
h[2] = h[2] + h2 | 0;
|
||||
h[3] = h[3] + h3 | 0;
|
||||
h[4] = h[4] + h4 | 0;
|
||||
h[5] = h[5] + h5 | 0;
|
||||
h[6] = h[6] + h6 | 0;
|
||||
h[7] = h[7] + h7 | 0;
|
||||
}
|
||||
}
|
||||
function sha256(msg, inputEncoding, outputEncoding) {
|
||||
return new SHA256().update(msg, inputEncoding).digest(outputEncoding);
|
||||
}
|
||||
export { BYTES as BYTES };
|
||||
export { SHA256 as SHA256 };
|
||||
export { sha256 as sha256 };
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
|
||||
|
||||
export { }
|
@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright 2020-2021 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { TD } from "./encoders";
|
||||
|
||||
import {
|
||||
ConnectionOptions,
|
||||
DEFAULT_PORT,
|
||||
DnsResolveFn,
|
||||
Server,
|
||||
URLParseFn,
|
||||
} from "./core";
|
||||
import { DataBuffer } from "./databuffer";
|
||||
|
||||
let transportConfig: TransportFactory;
|
||||
export function setTransportFactory(config: TransportFactory): void {
|
||||
transportConfig = config;
|
||||
}
|
||||
|
||||
export function defaultPort(): number {
|
||||
return transportConfig !== undefined &&
|
||||
transportConfig.defaultPort !== undefined
|
||||
? transportConfig.defaultPort
|
||||
: DEFAULT_PORT;
|
||||
}
|
||||
|
||||
export function getUrlParseFn(): URLParseFn | undefined {
|
||||
return transportConfig !== undefined && transportConfig.urlParseFn
|
||||
? transportConfig.urlParseFn
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function newTransport(): Transport {
|
||||
if (!transportConfig || typeof transportConfig.factory !== "function") {
|
||||
throw new Error("transport fn is not set");
|
||||
}
|
||||
return transportConfig.factory();
|
||||
}
|
||||
|
||||
export function getResolveFn(): DnsResolveFn | undefined {
|
||||
return transportConfig !== undefined && transportConfig.dnsResolveFn
|
||||
? transportConfig.dnsResolveFn
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export interface TransportFactory {
|
||||
factory?: () => Transport;
|
||||
defaultPort?: number;
|
||||
urlParseFn?: URLParseFn;
|
||||
dnsResolveFn?: DnsResolveFn;
|
||||
}
|
||||
|
||||
export interface Transport extends AsyncIterable<Uint8Array> {
|
||||
readonly isClosed: boolean;
|
||||
readonly lang: string;
|
||||
readonly version: string;
|
||||
readonly closeError?: Error;
|
||||
|
||||
connect(
|
||||
server: Server,
|
||||
opts: ConnectionOptions,
|
||||
): Promise<void>;
|
||||
|
||||
[Symbol.asyncIterator](): AsyncIterableIterator<Uint8Array>;
|
||||
|
||||
isEncrypted(): boolean;
|
||||
|
||||
send(frame: Uint8Array): void;
|
||||
|
||||
close(err?: Error): Promise<void>;
|
||||
|
||||
disconnect(): void;
|
||||
|
||||
closed(): Promise<void | Error>;
|
||||
|
||||
// this is here for websocket implementations as some implementations
|
||||
// (firefox) throttle connections that then resolve later
|
||||
discard(): void;
|
||||
}
|
||||
|
||||
export const CR_LF = "\r\n";
|
||||
export const CR_LF_LEN = CR_LF.length;
|
||||
export const CRLF = DataBuffer.fromAscii(CR_LF);
|
||||
export const CR = new Uint8Array(CRLF)[0]; // 13
|
||||
export const LF = new Uint8Array(CRLF)[1]; // 10
|
||||
export function protoLen(ba: Uint8Array): number {
|
||||
for (let i = 0; i < ba.length; i++) {
|
||||
const n = i + 1;
|
||||
if (ba.byteLength > n && ba[i] === CR && ba[n] === LF) {
|
||||
return n + 1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function extractProtocolMessage(a: Uint8Array): string {
|
||||
// protocol messages are ascii, so Uint8Array
|
||||
const len = protoLen(a);
|
||||
if (len > 0) {
|
||||
const ba = new Uint8Array(a);
|
||||
const out = ba.slice(0, len);
|
||||
return TD.decode(out);
|
||||
}
|
||||
return "";
|
||||
}
|
@ -0,0 +1,229 @@
|
||||
/*
|
||||
* Copyright 2021 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Deferred, deferred } from "./util.ts";
|
||||
import type {
|
||||
DispatchedFn,
|
||||
IngestionFilterFn,
|
||||
ProtocolFilterFn,
|
||||
} from "./queued_iterator.ts";
|
||||
import { QueuedIteratorImpl } from "./queued_iterator.ts";
|
||||
import {
|
||||
ErrorCode,
|
||||
Msg,
|
||||
NatsConnection,
|
||||
NatsError,
|
||||
Sub,
|
||||
SubOpts,
|
||||
Subscription,
|
||||
SubscriptionOptions,
|
||||
} from "./core.ts";
|
||||
import { SubscriptionImpl } from "./protocol.ts";
|
||||
|
||||
/**
|
||||
* Converts a NATS message into some other type. Implementers are expected to:
|
||||
* return [err, null] if the message callback is invoked with an error.
|
||||
* return [err, null] if converting the message yielded an error, note that
|
||||
* iterators will stop on the error, but callbacks will be presented with
|
||||
* the error.
|
||||
* return [null, T] if the conversion worked correctly
|
||||
*/
|
||||
export type MsgAdapter<T> = (
|
||||
err: NatsError | null,
|
||||
msg: Msg,
|
||||
) => [NatsError | null, T | null];
|
||||
|
||||
/**
|
||||
* Callback presented to the user with the converted type
|
||||
*/
|
||||
export type TypedCallback<T> = (err: NatsError | null, msg: T | null) => void;
|
||||
|
||||
export interface TypedSubscriptionOptions<T> extends SubOpts<T> {
|
||||
adapter: MsgAdapter<T>;
|
||||
callback?: TypedCallback<T>;
|
||||
ingestionFilterFn?: IngestionFilterFn<T>;
|
||||
protocolFilterFn?: ProtocolFilterFn<T>;
|
||||
dispatchedFn?: DispatchedFn<T>;
|
||||
cleanupFn?: (sub: Subscription, info?: unknown) => void;
|
||||
}
|
||||
|
||||
export function checkFn(fn: unknown, name: string, required = false) {
|
||||
if (required === true && !fn) {
|
||||
throw NatsError.errorForCode(
|
||||
ErrorCode.ApiError,
|
||||
new Error(`${name} is not a function`),
|
||||
);
|
||||
}
|
||||
if (fn && typeof fn !== "function") {
|
||||
throw NatsError.errorForCode(
|
||||
ErrorCode.ApiError,
|
||||
new Error(`${name} is not a function`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TypedSubscription wraps a subscription to provide payload specific
|
||||
* subscription semantics. That is messages are a transport
|
||||
* for user data, and the data is presented as application specific
|
||||
* data to the client.
|
||||
*/
|
||||
export class TypedSubscription<T> extends QueuedIteratorImpl<T>
|
||||
implements Sub<T> {
|
||||
sub: SubscriptionImpl;
|
||||
adapter: MsgAdapter<T>;
|
||||
subIterDone: Deferred<void>;
|
||||
|
||||
constructor(
|
||||
nc: NatsConnection,
|
||||
subject: string,
|
||||
opts: TypedSubscriptionOptions<T>,
|
||||
) {
|
||||
super();
|
||||
|
||||
checkFn(opts.adapter, "adapter", true);
|
||||
this.adapter = opts.adapter;
|
||||
|
||||
if (opts.callback) {
|
||||
checkFn(opts.callback, "callback");
|
||||
}
|
||||
this.noIterator = typeof opts.callback === "function";
|
||||
|
||||
if (opts.ingestionFilterFn) {
|
||||
checkFn(opts.ingestionFilterFn, "ingestionFilterFn");
|
||||
this.ingestionFilterFn = opts.ingestionFilterFn;
|
||||
}
|
||||
if (opts.protocolFilterFn) {
|
||||
checkFn(opts.protocolFilterFn, "protocolFilterFn");
|
||||
this.protocolFilterFn = opts.protocolFilterFn;
|
||||
}
|
||||
if (opts.dispatchedFn) {
|
||||
checkFn(opts.dispatchedFn, "dispatchedFn");
|
||||
this.dispatchedFn = opts.dispatchedFn;
|
||||
}
|
||||
if (opts.cleanupFn) {
|
||||
checkFn(opts.cleanupFn, "cleanupFn");
|
||||
}
|
||||
|
||||
let callback = (err: NatsError | null, msg: Msg) => {
|
||||
this.callback(err, msg);
|
||||
};
|
||||
if (opts.callback) {
|
||||
const uh = opts.callback;
|
||||
callback = (err: NatsError | null, msg: Msg) => {
|
||||
const [jer, tm] = this.adapter(err, msg);
|
||||
if (jer) {
|
||||
uh(jer, null);
|
||||
return;
|
||||
}
|
||||
const { ingest } = this.ingestionFilterFn
|
||||
? this.ingestionFilterFn(tm, this)
|
||||
: { ingest: true };
|
||||
if (ingest) {
|
||||
const ok = this.protocolFilterFn ? this.protocolFilterFn(tm) : true;
|
||||
if (ok) {
|
||||
uh(jer, tm);
|
||||
if (this.dispatchedFn && tm) {
|
||||
this.dispatchedFn(tm);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
const { max, queue, timeout } = opts;
|
||||
const sopts = { queue, timeout, callback } as SubscriptionOptions;
|
||||
if (max && max > 0) {
|
||||
sopts.max = max;
|
||||
}
|
||||
this.sub = nc.subscribe(subject, sopts) as SubscriptionImpl;
|
||||
if (opts.cleanupFn) {
|
||||
this.sub.cleanupFn = opts.cleanupFn;
|
||||
}
|
||||
|
||||
if (!this.noIterator) {
|
||||
this.iterClosed.then(() => {
|
||||
this.unsubscribe();
|
||||
});
|
||||
}
|
||||
|
||||
this.subIterDone = deferred<void>();
|
||||
Promise.all([this.sub.closed, this.iterClosed])
|
||||
.then(() => {
|
||||
this.subIterDone.resolve();
|
||||
})
|
||||
.catch(() => {
|
||||
this.subIterDone.resolve();
|
||||
});
|
||||
(async (s) => {
|
||||
await s.closed;
|
||||
this.stop();
|
||||
})(this.sub).then().catch();
|
||||
}
|
||||
|
||||
unsubscribe(max?: number): void {
|
||||
this.sub.unsubscribe(max);
|
||||
}
|
||||
|
||||
drain(): Promise<void> {
|
||||
return this.sub.drain();
|
||||
}
|
||||
|
||||
isDraining(): boolean {
|
||||
return this.sub.isDraining();
|
||||
}
|
||||
|
||||
isClosed(): boolean {
|
||||
return this.sub.isClosed();
|
||||
}
|
||||
|
||||
callback(e: NatsError | null, msg: Msg): void {
|
||||
this.sub.cancelTimeout();
|
||||
const [err, tm] = this.adapter(e, msg);
|
||||
if (err) {
|
||||
this.stop(err);
|
||||
}
|
||||
if (tm) {
|
||||
this.push(tm);
|
||||
}
|
||||
}
|
||||
|
||||
getSubject(): string {
|
||||
return this.sub.getSubject();
|
||||
}
|
||||
|
||||
getReceived(): number {
|
||||
return this.sub.getReceived();
|
||||
}
|
||||
|
||||
getProcessed(): number {
|
||||
return this.sub.getProcessed();
|
||||
}
|
||||
|
||||
getPending(): number {
|
||||
return this.sub.getPending();
|
||||
}
|
||||
|
||||
getID(): number {
|
||||
return this.sub.getID();
|
||||
}
|
||||
|
||||
getMax(): number | undefined {
|
||||
return this.sub.getMax();
|
||||
}
|
||||
|
||||
get closed(): Promise<void> {
|
||||
return this.sub.closed;
|
||||
}
|
||||
}
|
@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Copyright 2020-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
export type {
|
||||
ApiError,
|
||||
Dispatcher,
|
||||
MsgHdrs,
|
||||
QueuedIterator,
|
||||
ServiceClient,
|
||||
} from "./core.ts";
|
||||
export { NatsError } from "./core.ts";
|
||||
|
||||
export type { TypedSubscriptionOptions } from "./typedsub.ts";
|
||||
|
||||
export { Empty } from "./encoders.ts";
|
@ -0,0 +1,239 @@
|
||||
/*
|
||||
* Copyright 2018-2021 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { TD } from "./encoders";
|
||||
import { ErrorCode, NatsError } from "./core";
|
||||
|
||||
export type ValueResult<T> = {
|
||||
isError: false;
|
||||
value: T;
|
||||
};
|
||||
|
||||
export type ErrorResult = {
|
||||
isError: true;
|
||||
error: Error;
|
||||
};
|
||||
|
||||
/**
|
||||
* Result is a value that may have resulted in an error.
|
||||
*/
|
||||
export type Result<T> = ValueResult<T> | ErrorResult;
|
||||
|
||||
export function extend(a: any, ...b: any[]): any {
|
||||
for (let i = 0; i < b.length; i++) {
|
||||
const o = b[i];
|
||||
Object.keys(o).forEach(function(k) {
|
||||
a[k] = o[k];
|
||||
});
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
export interface Pending {
|
||||
pending: number;
|
||||
write: (c: number) => void;
|
||||
wrote: (c: number) => void;
|
||||
err: (err: Error) => void;
|
||||
close: () => void;
|
||||
promise: () => Promise<any>;
|
||||
resolved: boolean;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
export function render(frame: Uint8Array): string {
|
||||
const cr = "␍";
|
||||
const lf = "␊";
|
||||
return TD.decode(frame)
|
||||
.replace(/\n/g, lf)
|
||||
.replace(/\r/g, cr);
|
||||
}
|
||||
|
||||
export interface Timeout<T> extends Promise<T> {
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
export function timeout<T>(ms: number): Timeout<T> {
|
||||
// by generating the stack here to help identify what timed out
|
||||
const err = NatsError.errorForCode(ErrorCode.Timeout);
|
||||
let methods;
|
||||
let timer: number;
|
||||
const p = new Promise((_resolve, reject) => {
|
||||
const cancel = (): void => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
};
|
||||
methods = { cancel };
|
||||
// @ts-ignore: node is not a number
|
||||
timer = setTimeout(() => {
|
||||
reject(err);
|
||||
}, ms);
|
||||
});
|
||||
// noinspection JSUnusedAssignment
|
||||
return Object.assign(p, methods) as Timeout<T>;
|
||||
}
|
||||
|
||||
export function delay(ms = 0): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, ms);
|
||||
});
|
||||
}
|
||||
|
||||
export function deadline<T>(p: Promise<T>, millis = 1000): Promise<T> {
|
||||
const err = new Error(`deadline exceeded`);
|
||||
const d = deferred<never>();
|
||||
const timer = setTimeout(
|
||||
() => d.reject(err),
|
||||
millis,
|
||||
);
|
||||
return Promise.race([p, d]).finally(() => clearTimeout(timer));
|
||||
}
|
||||
|
||||
export interface Deferred<T> extends Promise<T> {
|
||||
/**
|
||||
* Resolves the Deferred to a value T
|
||||
* @param value
|
||||
*/
|
||||
resolve: (value?: T | PromiseLike<T>) => void;
|
||||
//@ts-ignore: tsc guard
|
||||
/**
|
||||
* Rejects the Deferred
|
||||
* @param reason
|
||||
*/
|
||||
reject: (reason?: any) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Promise that has a resolve/reject methods that can
|
||||
* be used to resolve and defer the Deferred.
|
||||
*/
|
||||
export function deferred<T>(): Deferred<T> {
|
||||
let methods = {};
|
||||
const p = new Promise<T>((resolve, reject): void => {
|
||||
methods = { resolve, reject };
|
||||
});
|
||||
return Object.assign(p, methods) as Deferred<T>;
|
||||
}
|
||||
|
||||
export function debugDeferred<T>(): Deferred<T> {
|
||||
let methods = {};
|
||||
const p = new Promise<T>((resolve, reject): void => {
|
||||
methods = {
|
||||
resolve: (v: T) => {
|
||||
console.trace("resolve", v);
|
||||
resolve(v);
|
||||
},
|
||||
reject: (err?: Error) => {
|
||||
console.trace("reject");
|
||||
reject(err);
|
||||
},
|
||||
};
|
||||
});
|
||||
return Object.assign(p, methods) as Deferred<T>;
|
||||
}
|
||||
|
||||
export function shuffle<T>(a: T[]): T[] {
|
||||
for (let i = a.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[a[i], a[j]] = [a[j], a[i]];
|
||||
}
|
||||
return a;
|
||||
}
|
||||
|
||||
export async function collect<T>(iter: AsyncIterable<T>): Promise<T[]> {
|
||||
const buf: T[] = [];
|
||||
for await (const v of iter) {
|
||||
buf.push(v);
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
export class Perf {
|
||||
timers: Map<string, number>;
|
||||
measures: Map<string, number>;
|
||||
|
||||
constructor() {
|
||||
this.timers = new Map();
|
||||
this.measures = new Map();
|
||||
}
|
||||
|
||||
mark(key: string) {
|
||||
this.timers.set(key, performance.now());
|
||||
}
|
||||
|
||||
measure(key: string, startKey: string, endKey: string) {
|
||||
const s = this.timers.get(startKey);
|
||||
if (s === undefined) {
|
||||
throw new Error(`${startKey} is not defined`);
|
||||
}
|
||||
const e = this.timers.get(endKey);
|
||||
if (e === undefined) {
|
||||
throw new Error(`${endKey} is not defined`);
|
||||
}
|
||||
this.measures.set(key, e - s);
|
||||
}
|
||||
|
||||
getEntries(): { name: string; duration: number }[] {
|
||||
const values: { name: string; duration: number }[] = [];
|
||||
this.measures.forEach((v, k) => {
|
||||
values.push({ name: k, duration: v });
|
||||
});
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
export class SimpleMutex {
|
||||
max: number;
|
||||
current: number;
|
||||
waiting: Deferred<void>[];
|
||||
|
||||
/**
|
||||
* @param max number of concurrent operations
|
||||
*/
|
||||
constructor(max = 1) {
|
||||
this.max = max;
|
||||
this.current = 0;
|
||||
this.waiting = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a promise that resolves when the mutex is acquired
|
||||
*/
|
||||
lock(): Promise<void> {
|
||||
// increment the count
|
||||
this.current++;
|
||||
// if we have runners, resolve it
|
||||
if (this.current <= this.max) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// otherwise defer it
|
||||
const d = deferred<void>();
|
||||
this.waiting.push(d);
|
||||
return d;
|
||||
}
|
||||
|
||||
/**
|
||||
* Release an acquired mutex - must be called
|
||||
*/
|
||||
unlock(): void {
|
||||
// decrement the count
|
||||
this.current--;
|
||||
// if we have deferred, resolve one
|
||||
const d = this.waiting.pop();
|
||||
d?.resolve();
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Copyright 2020-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import {
|
||||
ConnectionOptions,
|
||||
NatsConnection,
|
||||
NatsConnectionImpl,
|
||||
setTransportFactory,
|
||||
Transport,
|
||||
TransportFactory,
|
||||
} from "../nats-base-client/internal_mod";
|
||||
|
||||
import { WsTransport } from "./ws_transport";
|
||||
|
||||
export function wsUrlParseFn(u: string): string {
|
||||
const ut = /^(.*:\/\/)(.*)/;
|
||||
if (!ut.test(u)) {
|
||||
u = `https://${u}`;
|
||||
}
|
||||
let url = new URL(u);
|
||||
const srcProto = url.protocol.toLowerCase();
|
||||
if (srcProto !== "https:" && srcProto !== "http") {
|
||||
u = u.replace(/^(.*:\/\/)(.*)/gm, "$2");
|
||||
url = new URL(`http://${u}`);
|
||||
}
|
||||
|
||||
let protocol;
|
||||
let port;
|
||||
const host = url.hostname;
|
||||
const path = url.pathname;
|
||||
const search = url.search || "";
|
||||
|
||||
switch (srcProto) {
|
||||
case "http:":
|
||||
case "ws:":
|
||||
case "nats:":
|
||||
port = url.port || "80";
|
||||
protocol = "ws:";
|
||||
break;
|
||||
default:
|
||||
port = url.port || "443";
|
||||
protocol = "wss:";
|
||||
break;
|
||||
}
|
||||
return `${protocol}//${host}:${port}${path}${search}`;
|
||||
}
|
||||
|
||||
export function connect(opts: ConnectionOptions = {}): Promise<NatsConnection> {
|
||||
setTransportFactory({
|
||||
defaultPort: 443,
|
||||
urlParseFn: wsUrlParseFn,
|
||||
factory: (): Transport => {
|
||||
return new WsTransport();
|
||||
},
|
||||
} as TransportFactory);
|
||||
|
||||
return NatsConnectionImpl.connect(opts);
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2020-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
export * from "../nats-base-client/mod";
|
||||
export * from "../jetstream/mod";
|
||||
export { connect } from "./connect";
|
@ -0,0 +1,16 @@
|
||||
/*
|
||||
* Copyright 2020-2023 The NATS Authors
|
||||
* 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 import here to drive the build system
|
||||
export * from "../nats-base-client/internal_mod";
|
@ -0,0 +1,299 @@
|
||||
/*
|
||||
* Copyright 2020-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import type {
|
||||
ConnectionOptions,
|
||||
Deferred,
|
||||
Server,
|
||||
ServerInfo,
|
||||
Transport,
|
||||
} from "../nats-base-client/internal_mod";
|
||||
import {
|
||||
checkOptions,
|
||||
DataBuffer,
|
||||
deferred,
|
||||
delay,
|
||||
ErrorCode,
|
||||
extractProtocolMessage,
|
||||
INFO,
|
||||
NatsError,
|
||||
render,
|
||||
} from "../nats-base-client/internal_mod";
|
||||
|
||||
const VERSION = "1.18.0";
|
||||
const LANG = "nats.ws";
|
||||
|
||||
export type WsSocketFactory = (u: string, opts: ConnectionOptions) => Promise<{
|
||||
socket: WebSocket;
|
||||
encrypted: boolean;
|
||||
}>;
|
||||
interface WsConnectionOptions extends ConnectionOptions {
|
||||
wsFactory?: WsSocketFactory;
|
||||
}
|
||||
|
||||
export class WsTransport implements Transport {
|
||||
version: string;
|
||||
lang: string;
|
||||
closeError?: Error;
|
||||
connected: boolean;
|
||||
private done: boolean;
|
||||
// @ts-ignore: expecting global WebSocket
|
||||
private socket: WebSocket;
|
||||
private options!: WsConnectionOptions;
|
||||
socketClosed: boolean;
|
||||
encrypted: boolean;
|
||||
peeked: boolean;
|
||||
|
||||
yields: Uint8Array[];
|
||||
signal: Deferred<void>;
|
||||
closedNotification: Deferred<void | Error>;
|
||||
|
||||
constructor() {
|
||||
this.version = VERSION;
|
||||
this.lang = LANG;
|
||||
this.connected = false;
|
||||
this.done = false;
|
||||
this.socketClosed = false;
|
||||
this.encrypted = false;
|
||||
this.peeked = false;
|
||||
this.yields = [];
|
||||
this.signal = deferred();
|
||||
this.closedNotification = deferred();
|
||||
}
|
||||
|
||||
async connect(
|
||||
server: Server,
|
||||
options: WsConnectionOptions,
|
||||
): Promise<void> {
|
||||
const connected = false;
|
||||
const connLock = deferred<void>();
|
||||
|
||||
// ws client doesn't support TLS setting
|
||||
if (options.tls) {
|
||||
connLock.reject(new NatsError("tls", ErrorCode.InvalidOption));
|
||||
return connLock;
|
||||
}
|
||||
|
||||
this.options = options;
|
||||
const u = server.src;
|
||||
if (options.wsFactory) {
|
||||
const { socket, encrypted } = await options.wsFactory(
|
||||
server.src,
|
||||
options,
|
||||
);
|
||||
this.socket = socket;
|
||||
this.encrypted = encrypted;
|
||||
} else {
|
||||
this.encrypted = u.indexOf("wss://") === 0;
|
||||
this.socket = new WebSocket(u);
|
||||
}
|
||||
this.socket.binaryType = "arraybuffer";
|
||||
|
||||
this.socket.onopen = () => {
|
||||
if (this.isDiscarded()) {
|
||||
return;
|
||||
}
|
||||
// we don't do anything here...
|
||||
};
|
||||
|
||||
this.socket.onmessage = (me: MessageEvent) => {
|
||||
if (this.isDiscarded()) {
|
||||
return;
|
||||
}
|
||||
this.yields.push(new Uint8Array(me.data));
|
||||
if (this.peeked) {
|
||||
this.signal.resolve();
|
||||
return;
|
||||
}
|
||||
const t = DataBuffer.concat(...this.yields);
|
||||
const pm = extractProtocolMessage(t);
|
||||
if (pm !== "") {
|
||||
const m = INFO.exec(pm);
|
||||
if (!m) {
|
||||
if (options.debug) {
|
||||
console.error("!!!", render(t));
|
||||
}
|
||||
connLock.reject(new Error("unexpected response from server"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const info = JSON.parse(m[1]) as ServerInfo;
|
||||
checkOptions(info, this.options);
|
||||
this.peeked = true;
|
||||
this.connected = true;
|
||||
this.signal.resolve();
|
||||
connLock.resolve();
|
||||
} catch (err) {
|
||||
connLock.reject(err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// @ts-ignore: CloseEvent is provided in browsers
|
||||
this.socket.onclose = (evt: CloseEvent) => {
|
||||
if (this.isDiscarded()) {
|
||||
return;
|
||||
}
|
||||
this.socketClosed = true;
|
||||
let reason: Error | undefined;
|
||||
if (this.done) return;
|
||||
if (!evt.wasClean) {
|
||||
reason = new Error(evt.reason);
|
||||
}
|
||||
this._closed(reason);
|
||||
};
|
||||
|
||||
// @ts-ignore: signature can be any
|
||||
this.socket.onerror = (e: ErrorEvent | Event): void => {
|
||||
if (this.isDiscarded()) {
|
||||
return;
|
||||
}
|
||||
const evt = e as ErrorEvent;
|
||||
const err = new NatsError(
|
||||
evt.message,
|
||||
ErrorCode.Unknown,
|
||||
new Error(evt.error),
|
||||
);
|
||||
if (!connected) {
|
||||
connLock.reject(err);
|
||||
} else {
|
||||
this._closed(err);
|
||||
}
|
||||
};
|
||||
return connLock;
|
||||
}
|
||||
|
||||
disconnect(): void {
|
||||
this._closed(undefined, true);
|
||||
}
|
||||
|
||||
private async _closed(err?: Error, internal = true): Promise<void> {
|
||||
if (this.isDiscarded()) {
|
||||
return
|
||||
}
|
||||
if (!this.connected) return;
|
||||
if (this.done) return;
|
||||
this.closeError = err;
|
||||
if (!err) {
|
||||
while (!this.socketClosed && this.socket.bufferedAmount > 0) {
|
||||
await delay(100);
|
||||
}
|
||||
}
|
||||
this.done = true;
|
||||
try {
|
||||
// 1002 endpoint error, 1000 is clean
|
||||
this.socket.close(err ? 1002 : 1000, err ? err.message : undefined);
|
||||
} catch (err) {
|
||||
// ignore this
|
||||
}
|
||||
if (internal) {
|
||||
this.closedNotification.resolve(err);
|
||||
}
|
||||
}
|
||||
|
||||
get isClosed(): boolean {
|
||||
return this.done;
|
||||
}
|
||||
|
||||
[Symbol.asyncIterator]() {
|
||||
return this.iterate();
|
||||
}
|
||||
|
||||
async *iterate(): AsyncIterableIterator<Uint8Array> {
|
||||
while (true) {
|
||||
if (this.isDiscarded()) {
|
||||
return
|
||||
}
|
||||
if (this.yields.length === 0) {
|
||||
await this.signal;
|
||||
}
|
||||
const yields = this.yields;
|
||||
this.yields = [];
|
||||
for (let i = 0; i < yields.length; i++) {
|
||||
if (this.options.debug) {
|
||||
console.info(`> ${render(yields[i])}`);
|
||||
}
|
||||
yield yields[i];
|
||||
}
|
||||
// yielding could have paused and microtask
|
||||
// could have added messages. Prevent allocations
|
||||
// if possible
|
||||
if (this.done) {
|
||||
break;
|
||||
} else if (this.yields.length === 0) {
|
||||
yields.length = 0;
|
||||
this.yields = yields;
|
||||
this.signal = deferred();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isEncrypted(): boolean {
|
||||
return this.connected && this.encrypted;
|
||||
}
|
||||
|
||||
send(frame: Uint8Array): void {
|
||||
if (this.isDiscarded()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.socket.send(frame.buffer);
|
||||
if (this.options.debug) {
|
||||
console.info(`< ${render(frame)}`);
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
// we ignore write errors because client will
|
||||
// fail on a read or when the heartbeat timer
|
||||
// detects a stale connection
|
||||
if (this.options.debug) {
|
||||
console.error(`!!! ${render(frame)}: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(err?: Error | undefined): Promise<void> {
|
||||
return this._closed(err, false);
|
||||
}
|
||||
|
||||
closed(): Promise<void | Error> {
|
||||
return this.closedNotification;
|
||||
}
|
||||
|
||||
// check to see if we are discarded, as the connection
|
||||
// may not have been closed, we attempt it here as well.
|
||||
isDiscarded(): boolean {
|
||||
if (this.done) {
|
||||
this.discard();
|
||||
return true
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// this is to allow a force discard on a connection
|
||||
// if the connection fails during the handshake protocol.
|
||||
// Firefox for example, will keep connections going,
|
||||
// so eventually if it succeeds, the client will have
|
||||
// an additional transport running. With this
|
||||
discard() {
|
||||
this.done = true;
|
||||
try {
|
||||
this.socket?.close()
|
||||
} catch (_err) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
/*
|
||||
* Copyright 2018-2021 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// Fork of https://github.com/LinusU/base32-encode
|
||||
// and https://github.com/LinusU/base32-decode to support returning
|
||||
// buffers without padding.
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
const b32Alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export class base32 {
|
||||
static encode(src: Uint8Array): Uint8Array {
|
||||
let bits = 0;
|
||||
let value = 0;
|
||||
let a = new Uint8Array(src);
|
||||
let buf = new Uint8Array(src.byteLength * 2);
|
||||
let j = 0;
|
||||
for (let i = 0; i < a.byteLength; i++) {
|
||||
value = (value << 8) | a[i];
|
||||
bits += 8;
|
||||
while (bits >= 5) {
|
||||
let index = (value >>> (bits - 5)) & 31;
|
||||
buf[j++] = b32Alphabet.charAt(index).charCodeAt(0);
|
||||
bits -= 5;
|
||||
}
|
||||
}
|
||||
if (bits > 0) {
|
||||
let index = (value << (5 - bits)) & 31;
|
||||
buf[j++] = b32Alphabet.charAt(index).charCodeAt(0);
|
||||
}
|
||||
return buf.slice(0, j);
|
||||
}
|
||||
|
||||
static decode(src: Uint8Array): Uint8Array {
|
||||
let bits = 0;
|
||||
let byte = 0;
|
||||
let j = 0;
|
||||
let a = new Uint8Array(src);
|
||||
let out = new Uint8Array(a.byteLength * 5 / 8 | 0);
|
||||
for (let i = 0; i < a.byteLength; i++) {
|
||||
let v = String.fromCharCode(a[i]);
|
||||
let vv = b32Alphabet.indexOf(v);
|
||||
if (vv === -1) {
|
||||
throw new Error("Illegal Base32 character: " + a[i]);
|
||||
}
|
||||
byte = (byte << 5) | vv;
|
||||
bits += 5;
|
||||
if (bits >= 8) {
|
||||
out[j++] = (byte >>> (bits - 8)) & 255;
|
||||
bits -= 8;
|
||||
}
|
||||
}
|
||||
return out.slice(0, j);
|
||||
}
|
||||
}
|
@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2018-2020 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { crc16 } from "./crc16.ts";
|
||||
import { NKeysError, NKeysErrorCode, Prefix, Prefixes } from "./nkeys.ts";
|
||||
import { base32 } from "./base32.ts";
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface SeedDecode {
|
||||
prefix: Prefix;
|
||||
buf: Uint8Array;
|
||||
}
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export class Codec {
|
||||
static encode(prefix: Prefix, src: Uint8Array): Uint8Array {
|
||||
if (!src || !(src instanceof Uint8Array)) {
|
||||
throw new NKeysError(NKeysErrorCode.SerializationError);
|
||||
}
|
||||
|
||||
if (!Prefixes.isValidPrefix(prefix)) {
|
||||
throw new NKeysError(NKeysErrorCode.InvalidPrefixByte);
|
||||
}
|
||||
|
||||
return Codec._encode(false, prefix, src);
|
||||
}
|
||||
|
||||
static encodeSeed(role: Prefix, src: Uint8Array): Uint8Array {
|
||||
if (!src) {
|
||||
throw new NKeysError(NKeysErrorCode.ApiError);
|
||||
}
|
||||
|
||||
if (!Prefixes.isValidPublicPrefix(role)) {
|
||||
throw new NKeysError(NKeysErrorCode.InvalidPrefixByte);
|
||||
}
|
||||
|
||||
if (src.byteLength !== 32) {
|
||||
throw new NKeysError(NKeysErrorCode.InvalidSeedLen);
|
||||
}
|
||||
return Codec._encode(true, role, src);
|
||||
}
|
||||
|
||||
static decode(expected: Prefix, src: Uint8Array): Uint8Array {
|
||||
if (!Prefixes.isValidPrefix(expected)) {
|
||||
throw new NKeysError(NKeysErrorCode.InvalidPrefixByte);
|
||||
}
|
||||
const raw = Codec._decode(src);
|
||||
if (raw[0] !== expected) {
|
||||
throw new NKeysError(NKeysErrorCode.InvalidPrefixByte);
|
||||
}
|
||||
return raw.slice(1);
|
||||
}
|
||||
|
||||
static decodeSeed(src: Uint8Array): SeedDecode {
|
||||
const raw = Codec._decode(src);
|
||||
const prefix = Codec._decodePrefix(raw);
|
||||
if (prefix[0] != Prefix.Seed) {
|
||||
throw new NKeysError(NKeysErrorCode.InvalidSeed);
|
||||
}
|
||||
if (!Prefixes.isValidPublicPrefix(prefix[1])) {
|
||||
throw new NKeysError(NKeysErrorCode.InvalidPrefixByte);
|
||||
}
|
||||
return ({ buf: raw.slice(2), prefix: prefix[1] });
|
||||
}
|
||||
|
||||
// unsafe encode no prefix/role validation
|
||||
static _encode(seed: boolean, role: Prefix, payload: Uint8Array): Uint8Array {
|
||||
// offsets for this token
|
||||
const payloadOffset = seed ? 2 : 1;
|
||||
const payloadLen = payload.byteLength;
|
||||
const checkLen = 2;
|
||||
const cap = payloadOffset + payloadLen + checkLen;
|
||||
const checkOffset = payloadOffset + payloadLen;
|
||||
|
||||
const raw = new Uint8Array(cap);
|
||||
// make the prefixes human readable when encoded
|
||||
if (seed) {
|
||||
const encodedPrefix = Codec._encodePrefix(Prefix.Seed, role);
|
||||
raw.set(encodedPrefix);
|
||||
} else {
|
||||
raw[0] = role;
|
||||
}
|
||||
raw.set(payload, payloadOffset);
|
||||
|
||||
//calculate the checksum write it LE
|
||||
const checksum = crc16.checksum(raw.slice(0, checkOffset));
|
||||
const dv = new DataView(raw.buffer);
|
||||
dv.setUint16(checkOffset, checksum, true);
|
||||
|
||||
return base32.encode(raw);
|
||||
}
|
||||
|
||||
// unsafe decode - no prefix/role validation
|
||||
static _decode(src: Uint8Array): Uint8Array {
|
||||
if (src.byteLength < 4) {
|
||||
throw new NKeysError(NKeysErrorCode.InvalidEncoding);
|
||||
}
|
||||
let raw: Uint8Array;
|
||||
try {
|
||||
raw = base32.decode(src);
|
||||
} catch (ex) {
|
||||
throw new NKeysError(NKeysErrorCode.InvalidEncoding, ex);
|
||||
}
|
||||
|
||||
const checkOffset = raw.byteLength - 2;
|
||||
const dv = new DataView(raw.buffer);
|
||||
const checksum = dv.getUint16(checkOffset, true);
|
||||
|
||||
const payload = raw.slice(0, checkOffset);
|
||||
if (!crc16.validate(payload, checksum)) {
|
||||
throw new NKeysError(NKeysErrorCode.InvalidChecksum);
|
||||
}
|
||||
return payload;
|
||||
}
|
||||
|
||||
static _encodePrefix(kind: Prefix, role: Prefix): Uint8Array {
|
||||
// In order to make this human printable for both bytes, we need to do a little
|
||||
// bit manipulation to setup for base32 encoding which takes 5 bits at a time.
|
||||
const b1 = kind | (role >> 5);
|
||||
const b2 = (role & 31) << 3; // 31 = 00011111
|
||||
return new Uint8Array([b1, b2]);
|
||||
}
|
||||
|
||||
static _decodePrefix(raw: Uint8Array): Uint8Array {
|
||||
// Need to do the reverse from the printable representation to
|
||||
// get back to internal representation.
|
||||
const b1 = raw[0] & 248; // 248 = 11111000
|
||||
const b2 = (raw[0] & 7) << 5 | ((raw[1] & 248) >> 3); // 7 = 00000111
|
||||
return new Uint8Array([b1, b2]);
|
||||
}
|
||||
}
|
@ -0,0 +1,298 @@
|
||||
/*
|
||||
* Copyright 2018-2020 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// An implementation of crc16 according to CCITT standards for XMODEM.
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
const crc16tab = new Uint16Array([
|
||||
0x0000,
|
||||
0x1021,
|
||||
0x2042,
|
||||
0x3063,
|
||||
0x4084,
|
||||
0x50a5,
|
||||
0x60c6,
|
||||
0x70e7,
|
||||
0x8108,
|
||||
0x9129,
|
||||
0xa14a,
|
||||
0xb16b,
|
||||
0xc18c,
|
||||
0xd1ad,
|
||||
0xe1ce,
|
||||
0xf1ef,
|
||||
0x1231,
|
||||
0x0210,
|
||||
0x3273,
|
||||
0x2252,
|
||||
0x52b5,
|
||||
0x4294,
|
||||
0x72f7,
|
||||
0x62d6,
|
||||
0x9339,
|
||||
0x8318,
|
||||
0xb37b,
|
||||
0xa35a,
|
||||
0xd3bd,
|
||||
0xc39c,
|
||||
0xf3ff,
|
||||
0xe3de,
|
||||
0x2462,
|
||||
0x3443,
|
||||
0x0420,
|
||||
0x1401,
|
||||
0x64e6,
|
||||
0x74c7,
|
||||
0x44a4,
|
||||
0x5485,
|
||||
0xa56a,
|
||||
0xb54b,
|
||||
0x8528,
|
||||
0x9509,
|
||||
0xe5ee,
|
||||
0xf5cf,
|
||||
0xc5ac,
|
||||
0xd58d,
|
||||
0x3653,
|
||||
0x2672,
|
||||
0x1611,
|
||||
0x0630,
|
||||
0x76d7,
|
||||
0x66f6,
|
||||
0x5695,
|
||||
0x46b4,
|
||||
0xb75b,
|
||||
0xa77a,
|
||||
0x9719,
|
||||
0x8738,
|
||||
0xf7df,
|
||||
0xe7fe,
|
||||
0xd79d,
|
||||
0xc7bc,
|
||||
0x48c4,
|
||||
0x58e5,
|
||||
0x6886,
|
||||
0x78a7,
|
||||
0x0840,
|
||||
0x1861,
|
||||
0x2802,
|
||||
0x3823,
|
||||
0xc9cc,
|
||||
0xd9ed,
|
||||
0xe98e,
|
||||
0xf9af,
|
||||
0x8948,
|
||||
0x9969,
|
||||
0xa90a,
|
||||
0xb92b,
|
||||
0x5af5,
|
||||
0x4ad4,
|
||||
0x7ab7,
|
||||
0x6a96,
|
||||
0x1a71,
|
||||
0x0a50,
|
||||
0x3a33,
|
||||
0x2a12,
|
||||
0xdbfd,
|
||||
0xcbdc,
|
||||
0xfbbf,
|
||||
0xeb9e,
|
||||
0x9b79,
|
||||
0x8b58,
|
||||
0xbb3b,
|
||||
0xab1a,
|
||||
0x6ca6,
|
||||
0x7c87,
|
||||
0x4ce4,
|
||||
0x5cc5,
|
||||
0x2c22,
|
||||
0x3c03,
|
||||
0x0c60,
|
||||
0x1c41,
|
||||
0xedae,
|
||||
0xfd8f,
|
||||
0xcdec,
|
||||
0xddcd,
|
||||
0xad2a,
|
||||
0xbd0b,
|
||||
0x8d68,
|
||||
0x9d49,
|
||||
0x7e97,
|
||||
0x6eb6,
|
||||
0x5ed5,
|
||||
0x4ef4,
|
||||
0x3e13,
|
||||
0x2e32,
|
||||
0x1e51,
|
||||
0x0e70,
|
||||
0xff9f,
|
||||
0xefbe,
|
||||
0xdfdd,
|
||||
0xcffc,
|
||||
0xbf1b,
|
||||
0xaf3a,
|
||||
0x9f59,
|
||||
0x8f78,
|
||||
0x9188,
|
||||
0x81a9,
|
||||
0xb1ca,
|
||||
0xa1eb,
|
||||
0xd10c,
|
||||
0xc12d,
|
||||
0xf14e,
|
||||
0xe16f,
|
||||
0x1080,
|
||||
0x00a1,
|
||||
0x30c2,
|
||||
0x20e3,
|
||||
0x5004,
|
||||
0x4025,
|
||||
0x7046,
|
||||
0x6067,
|
||||
0x83b9,
|
||||
0x9398,
|
||||
0xa3fb,
|
||||
0xb3da,
|
||||
0xc33d,
|
||||
0xd31c,
|
||||
0xe37f,
|
||||
0xf35e,
|
||||
0x02b1,
|
||||
0x1290,
|
||||
0x22f3,
|
||||
0x32d2,
|
||||
0x4235,
|
||||
0x5214,
|
||||
0x6277,
|
||||
0x7256,
|
||||
0xb5ea,
|
||||
0xa5cb,
|
||||
0x95a8,
|
||||
0x8589,
|
||||
0xf56e,
|
||||
0xe54f,
|
||||
0xd52c,
|
||||
0xc50d,
|
||||
0x34e2,
|
||||
0x24c3,
|
||||
0x14a0,
|
||||
0x0481,
|
||||
0x7466,
|
||||
0x6447,
|
||||
0x5424,
|
||||
0x4405,
|
||||
0xa7db,
|
||||
0xb7fa,
|
||||
0x8799,
|
||||
0x97b8,
|
||||
0xe75f,
|
||||
0xf77e,
|
||||
0xc71d,
|
||||
0xd73c,
|
||||
0x26d3,
|
||||
0x36f2,
|
||||
0x0691,
|
||||
0x16b0,
|
||||
0x6657,
|
||||
0x7676,
|
||||
0x4615,
|
||||
0x5634,
|
||||
0xd94c,
|
||||
0xc96d,
|
||||
0xf90e,
|
||||
0xe92f,
|
||||
0x99c8,
|
||||
0x89e9,
|
||||
0xb98a,
|
||||
0xa9ab,
|
||||
0x5844,
|
||||
0x4865,
|
||||
0x7806,
|
||||
0x6827,
|
||||
0x18c0,
|
||||
0x08e1,
|
||||
0x3882,
|
||||
0x28a3,
|
||||
0xcb7d,
|
||||
0xdb5c,
|
||||
0xeb3f,
|
||||
0xfb1e,
|
||||
0x8bf9,
|
||||
0x9bd8,
|
||||
0xabbb,
|
||||
0xbb9a,
|
||||
0x4a75,
|
||||
0x5a54,
|
||||
0x6a37,
|
||||
0x7a16,
|
||||
0x0af1,
|
||||
0x1ad0,
|
||||
0x2ab3,
|
||||
0x3a92,
|
||||
0xfd2e,
|
||||
0xed0f,
|
||||
0xdd6c,
|
||||
0xcd4d,
|
||||
0xbdaa,
|
||||
0xad8b,
|
||||
0x9de8,
|
||||
0x8dc9,
|
||||
0x7c26,
|
||||
0x6c07,
|
||||
0x5c64,
|
||||
0x4c45,
|
||||
0x3ca2,
|
||||
0x2c83,
|
||||
0x1ce0,
|
||||
0x0cc1,
|
||||
0xef1f,
|
||||
0xff3e,
|
||||
0xcf5d,
|
||||
0xdf7c,
|
||||
0xaf9b,
|
||||
0xbfba,
|
||||
0x8fd9,
|
||||
0x9ff8,
|
||||
0x6e17,
|
||||
0x7e36,
|
||||
0x4e55,
|
||||
0x5e74,
|
||||
0x2e93,
|
||||
0x3eb2,
|
||||
0x0ed1,
|
||||
0x1ef0,
|
||||
]);
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export class crc16 {
|
||||
// crc16 returns the crc for the data provided.
|
||||
static checksum(data: Uint8Array): number {
|
||||
let crc: number = 0;
|
||||
for (let i = 0; i < data.byteLength; i++) {
|
||||
let b = data[i];
|
||||
crc = ((crc << 8) & 0xffff) ^ crc16tab[((crc >> 8) ^ (b)) & 0x00FF];
|
||||
}
|
||||
return crc;
|
||||
}
|
||||
|
||||
// validate will check the calculated crc16 checksum for data against the expected.
|
||||
static validate(data: Uint8Array, expected: number): boolean {
|
||||
let ba = crc16.checksum(data);
|
||||
return ba == expected;
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2020 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
interface SignPair {
|
||||
publicKey: Uint8Array;
|
||||
secretKey: Uint8Array;
|
||||
}
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export interface Ed25519Helper {
|
||||
fromSeed(seed: Uint8Array): SignPair;
|
||||
sign(data: Uint8Array, key: Uint8Array): Uint8Array;
|
||||
verify(data: Uint8Array, sig: Uint8Array, pub: Uint8Array): boolean;
|
||||
randomBytes(len: number): Uint8Array;
|
||||
}
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
let helper: Ed25519Helper;
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export function setEd25519Helper(lib: Ed25519Helper) {
|
||||
helper = lib;
|
||||
}
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export function getEd25519Helper() {
|
||||
return helper;
|
||||
}
|
@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2018-2020 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Codec } from "./codec.ts";
|
||||
import { KeyPair, NKeysError, NKeysErrorCode, Prefix } from "./nkeys.ts";
|
||||
import { getEd25519Helper } from "./helper.ts";
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export class KP implements KeyPair {
|
||||
seed?: Uint8Array;
|
||||
constructor(seed: Uint8Array) {
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
getRawSeed(): Uint8Array {
|
||||
if (!this.seed) {
|
||||
throw new NKeysError(NKeysErrorCode.ClearedPair);
|
||||
}
|
||||
let sd = Codec.decodeSeed(this.seed);
|
||||
return sd.buf;
|
||||
}
|
||||
|
||||
getSeed(): Uint8Array {
|
||||
if (!this.seed) {
|
||||
throw new NKeysError(NKeysErrorCode.ClearedPair);
|
||||
}
|
||||
return this.seed;
|
||||
}
|
||||
|
||||
getPublicKey(): string {
|
||||
if (!this.seed) {
|
||||
throw new NKeysError(NKeysErrorCode.ClearedPair);
|
||||
}
|
||||
const sd = Codec.decodeSeed(this.seed);
|
||||
const kp = getEd25519Helper().fromSeed(this.getRawSeed());
|
||||
const buf = Codec.encode(sd.prefix, kp.publicKey);
|
||||
return new TextDecoder().decode(buf);
|
||||
}
|
||||
|
||||
getPrivateKey(): Uint8Array {
|
||||
if (!this.seed) {
|
||||
throw new NKeysError(NKeysErrorCode.ClearedPair);
|
||||
}
|
||||
const kp = getEd25519Helper().fromSeed(this.getRawSeed());
|
||||
return Codec.encode(Prefix.Private, kp.secretKey);
|
||||
}
|
||||
|
||||
sign(input: Uint8Array): Uint8Array {
|
||||
if (!this.seed) {
|
||||
throw new NKeysError(NKeysErrorCode.ClearedPair);
|
||||
}
|
||||
const kp = getEd25519Helper().fromSeed(this.getRawSeed());
|
||||
return getEd25519Helper().sign(input, kp.secretKey);
|
||||
}
|
||||
|
||||
verify(input: Uint8Array, sig: Uint8Array): boolean {
|
||||
if (!this.seed) {
|
||||
throw new NKeysError(NKeysErrorCode.ClearedPair);
|
||||
}
|
||||
const kp = getEd25519Helper().fromSeed(this.getRawSeed());
|
||||
return getEd25519Helper().verify(input, sig, kp.publicKey);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
if (!this.seed) {
|
||||
return;
|
||||
}
|
||||
this.seed.fill(0);
|
||||
this.seed = undefined;
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright 2020-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
export type { KeyPair } from "./nkeys.ts";
|
||||
export {
|
||||
createAccount,
|
||||
createCluster,
|
||||
createOperator,
|
||||
createPair,
|
||||
createServer,
|
||||
createUser,
|
||||
fromPublic,
|
||||
fromSeed,
|
||||
NKeysError,
|
||||
NKeysErrorCode,
|
||||
Prefix,
|
||||
} from "./nkeys.ts";
|
||||
|
||||
export { decode, encode } from "./util.ts";
|
@ -0,0 +1,250 @@
|
||||
/*
|
||||
* Copyright 2018-2023 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
import { KP } from "./kp.ts";
|
||||
import { PublicKey } from "./public.ts";
|
||||
import { Codec } from "./codec.ts";
|
||||
import { getEd25519Helper } from "./helper.ts";
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export function createPair(prefix: Prefix): KeyPair {
|
||||
const rawSeed = getEd25519Helper().randomBytes(32);
|
||||
let str = Codec.encodeSeed(prefix, new Uint8Array(rawSeed));
|
||||
return new KP(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a KeyPair with an operator prefix
|
||||
* @returns {KeyPair} Returns the created KeyPair.
|
||||
*/
|
||||
export function createOperator(): KeyPair {
|
||||
return createPair(Prefix.Operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a KeyPair with an account prefix
|
||||
* @returns {KeyPair} Returns the created KeyPair.
|
||||
*/
|
||||
export function createAccount(): KeyPair {
|
||||
return createPair(Prefix.Account);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a KeyPair with a user prefix
|
||||
* @returns {KeyPair} Returns the created KeyPair.
|
||||
*/
|
||||
export function createUser(): KeyPair {
|
||||
return createPair(Prefix.User);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export function createCluster(): KeyPair {
|
||||
return createPair(Prefix.Cluster);
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export function createServer(): KeyPair {
|
||||
return createPair(Prefix.Server);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a KeyPair from a specified public key
|
||||
* @param {string} src of the public key in string format.
|
||||
* @returns {KeyPair} Returns the created KeyPair.
|
||||
* @see KeyPair#getPublicKey
|
||||
*/
|
||||
export function fromPublic(src: string): KeyPair {
|
||||
const ba = new TextEncoder().encode(src);
|
||||
const raw = Codec._decode(ba);
|
||||
const prefix = Prefixes.parsePrefix(raw[0]);
|
||||
if (Prefixes.isValidPublicPrefix(prefix)) {
|
||||
return new PublicKey(ba);
|
||||
}
|
||||
throw new NKeysError(NKeysErrorCode.InvalidPublicKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a KeyPair from a specified seed.
|
||||
* @param {Uint8Array} src of the seed key as Uint8Array
|
||||
* @returns {KeyPair} Returns the created KeyPair.
|
||||
* @see KeyPair#getSeed
|
||||
*/
|
||||
export function fromSeed(src: Uint8Array): KeyPair {
|
||||
Codec.decodeSeed(src);
|
||||
// if we are here it decoded
|
||||
return new KP(src);
|
||||
}
|
||||
|
||||
export interface KeyPair {
|
||||
/**
|
||||
* Returns the public key associated with the KeyPair
|
||||
* @returns {string}
|
||||
* @throws NKeysError
|
||||
*/
|
||||
getPublicKey(): string;
|
||||
|
||||
/**
|
||||
* Returns the private key associated with the KeyPair
|
||||
* @returns Uint8Array
|
||||
* @throws NKeysError
|
||||
*/
|
||||
getPrivateKey(): Uint8Array;
|
||||
|
||||
/**
|
||||
* Returns the PrivateKey's seed.
|
||||
* @returns Uint8Array
|
||||
* @throws NKeysError
|
||||
*/
|
||||
getSeed(): Uint8Array;
|
||||
|
||||
/**
|
||||
* Returns the digital signature of signing the input with the
|
||||
* the KeyPair's private key.
|
||||
* @param {Uint8Array} input
|
||||
* @returns Uint8Array
|
||||
* @throws NKeysError
|
||||
*/
|
||||
sign(input: Uint8Array): Uint8Array;
|
||||
|
||||
/**
|
||||
* Returns true if the signature can be verified with the KeyPair
|
||||
* @param {Uint8Array} input
|
||||
* @param {Uint8Array} sig
|
||||
* @returns {boolean}
|
||||
* @throws NKeysError
|
||||
*/
|
||||
verify(input: Uint8Array, sig: Uint8Array): boolean;
|
||||
|
||||
/**
|
||||
* Clears the secret stored in the keypair. After clearing
|
||||
* a keypair cannot be used or recovered.
|
||||
*/
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export enum Prefix {
|
||||
//Seed is the version byte used for encoded NATS Seeds
|
||||
Seed = 18 << 3, // Base32-encodes to 'S...'
|
||||
|
||||
//PrefixBytePrivate is the version byte used for encoded NATS Private keys
|
||||
Private = 15 << 3, // Base32-encodes to 'P...'
|
||||
|
||||
//PrefixByteOperator is the version byte used for encoded NATS Operators
|
||||
Operator = 14 << 3, // Base32-encodes to 'O...'
|
||||
|
||||
//PrefixByteServer is the version byte used for encoded NATS Servers
|
||||
Server = 13 << 3, // Base32-encodes to 'N...'
|
||||
|
||||
//PrefixByteCluster is the version byte used for encoded NATS Clusters
|
||||
Cluster = 2 << 3, // Base32-encodes to 'C...'
|
||||
|
||||
//PrefixByteAccount is the version byte used for encoded NATS Accounts
|
||||
Account = 0, // Base32-encodes to 'A...'
|
||||
|
||||
//PrefixByteUser is the version byte used for encoded NATS Users
|
||||
User = 20 << 3, // Base32-encodes to 'U...'
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
export class Prefixes {
|
||||
static isValidPublicPrefix(prefix: Prefix): boolean {
|
||||
return prefix == Prefix.Server ||
|
||||
prefix == Prefix.Operator ||
|
||||
prefix == Prefix.Cluster ||
|
||||
prefix == Prefix.Account ||
|
||||
prefix == Prefix.User;
|
||||
}
|
||||
|
||||
static startsWithValidPrefix(s: string) {
|
||||
let c = s[0];
|
||||
return c == "S" || c == "P" || c == "O" || c == "N" || c == "C" ||
|
||||
c == "A" || c == "U";
|
||||
}
|
||||
|
||||
static isValidPrefix(prefix: Prefix): boolean {
|
||||
let v = this.parsePrefix(prefix);
|
||||
return v != -1;
|
||||
}
|
||||
|
||||
static parsePrefix(v: number): Prefix {
|
||||
switch (v) {
|
||||
case Prefix.Seed:
|
||||
return Prefix.Seed;
|
||||
case Prefix.Private:
|
||||
return Prefix.Private;
|
||||
case Prefix.Operator:
|
||||
return Prefix.Operator;
|
||||
case Prefix.Server:
|
||||
return Prefix.Server;
|
||||
case Prefix.Cluster:
|
||||
return Prefix.Cluster;
|
||||
case Prefix.Account:
|
||||
return Prefix.Account;
|
||||
case Prefix.User:
|
||||
return Prefix.User;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Possible error codes on exceptions thrown by the library.
|
||||
*/
|
||||
export enum NKeysErrorCode {
|
||||
InvalidPrefixByte = "nkeys: invalid prefix byte",
|
||||
InvalidKey = "nkeys: invalid key",
|
||||
InvalidPublicKey = "nkeys: invalid public key",
|
||||
InvalidSeedLen = "nkeys: invalid seed length",
|
||||
InvalidSeed = "nkeys: invalid seed",
|
||||
InvalidEncoding = "nkeys: invalid encoded key",
|
||||
InvalidSignature = "nkeys: signature verification failed",
|
||||
CannotSign = "nkeys: cannot sign, no private key available",
|
||||
PublicKeyOnly = "nkeys: no seed or private key available",
|
||||
InvalidChecksum = "nkeys: invalid checksum",
|
||||
SerializationError = "nkeys: serialization error",
|
||||
ApiError = "nkeys: api error",
|
||||
ClearedPair = "nkeys: pair is cleared",
|
||||
}
|
||||
|
||||
export class NKeysError extends Error {
|
||||
name: string;
|
||||
code: string;
|
||||
chainedError?: Error;
|
||||
|
||||
/**
|
||||
* @param {NKeysErrorCode} code
|
||||
* @param {Error} [chainedError]
|
||||
* @constructor
|
||||
*
|
||||
* @api private
|
||||
*/
|
||||
constructor(code: NKeysErrorCode, chainedError?: Error) {
|
||||
super(code);
|
||||
this.name = "NKeysError";
|
||||
this.code = code;
|
||||
this.chainedError = chainedError;
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright 2018-2020 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { Codec } from "./codec.ts";
|
||||
import { KeyPair, NKeysError, NKeysErrorCode } from "./nkeys.ts";
|
||||
import { getEd25519Helper } from "./helper.ts";
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export class PublicKey implements KeyPair {
|
||||
publicKey?: Uint8Array;
|
||||
|
||||
constructor(publicKey: Uint8Array) {
|
||||
this.publicKey = publicKey;
|
||||
}
|
||||
|
||||
getPublicKey(): string {
|
||||
if (!this.publicKey) {
|
||||
throw new NKeysError(NKeysErrorCode.ClearedPair);
|
||||
}
|
||||
return new TextDecoder().decode(this.publicKey);
|
||||
}
|
||||
|
||||
getPrivateKey(): Uint8Array {
|
||||
if (!this.publicKey) {
|
||||
throw new NKeysError(NKeysErrorCode.ClearedPair);
|
||||
}
|
||||
throw new NKeysError(NKeysErrorCode.PublicKeyOnly);
|
||||
}
|
||||
|
||||
getSeed(): Uint8Array {
|
||||
if (!this.publicKey) {
|
||||
throw new NKeysError(NKeysErrorCode.ClearedPair);
|
||||
}
|
||||
throw new NKeysError(NKeysErrorCode.PublicKeyOnly);
|
||||
}
|
||||
|
||||
sign(_: Uint8Array): Uint8Array {
|
||||
if (!this.publicKey) {
|
||||
throw new NKeysError(NKeysErrorCode.ClearedPair);
|
||||
}
|
||||
throw new NKeysError(NKeysErrorCode.CannotSign);
|
||||
}
|
||||
|
||||
verify(input: Uint8Array, sig: Uint8Array): boolean {
|
||||
if (!this.publicKey) {
|
||||
throw new NKeysError(NKeysErrorCode.ClearedPair);
|
||||
}
|
||||
let buf = Codec._decode(this.publicKey);
|
||||
return getEd25519Helper().verify(input, sig, buf.slice(1));
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
if (!this.publicKey) {
|
||||
return;
|
||||
}
|
||||
this.publicKey.fill(0);
|
||||
this.publicKey = undefined;
|
||||
}
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2018-2020 The NATS Authors
|
||||
* 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.
|
||||
*/
|
||||
/**
|
||||
* Encode binary data to a base64 string
|
||||
* @param {Uint8Array} bytes to encode to base64
|
||||
*/
|
||||
export function encode(bytes: Uint8Array): string {
|
||||
return btoa(String.fromCharCode(...bytes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a base64 encoded string to a binary Uint8Array
|
||||
* @param {string} b64str encoded string
|
||||
*/
|
||||
export function decode(b64str: string): Uint8Array {
|
||||
const bin = atob(b64str);
|
||||
const bytes = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) {
|
||||
bytes[i] = bin.charCodeAt(i);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
*/
|
||||
export function dump(buf: Uint8Array, msg?: string): void {
|
||||
if (msg) {
|
||||
console.log(msg);
|
||||
}
|
||||
let a: string[] = [];
|
||||
for (let i = 0; i < buf.byteLength; i++) {
|
||||
if (i % 8 === 0) {
|
||||
a.push("\n");
|
||||
}
|
||||
let v = buf[i].toString(16);
|
||||
if (v.length === 1) {
|
||||
v = "0" + v;
|
||||
}
|
||||
a.push(v);
|
||||
}
|
||||
console.log(a.join(" "));
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
# nats cfg
|
||||
#
|
||||
host: 127.0.0.1
|
||||
port: 4222
|
||||
|
||||
# 监控端口
|
||||
http_port: 8222
|
||||
|
||||
jetstream: {
|
||||
}
|
||||
|
||||
authorization: {
|
||||
users: [
|
||||
{ nkey: UCXFAAVMCPTATZUZX6H24YF6FI3NKPQBPLM6BNN2EDFPNSUUEZPNFKEL}
|
||||
]
|
||||
}
|
||||
|
||||
websocket: {
|
||||
listen: '0.0.0.0:4221'
|
||||
no_tls: true
|
||||
}
|
Loading…
Reference in New Issue