From 6ef866feb3140299a24c79da18ff549ed9f90d4d Mon Sep 17 00:00:00 2001 From: ennmichael Date: Tue, 28 Feb 2023 22:47:08 +0100 Subject: [PATCH 1/5] handle all errors in error callbacks --- packages/nostr/src/client/conn.ts | 31 ++++++++++++++++--------- packages/nostr/src/client/index.ts | 36 ++++++++++++++++++------------ 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/packages/nostr/src/client/conn.ts b/packages/nostr/src/client/conn.ts index 78c1a10..3019710 100644 --- a/packages/nostr/src/client/conn.ts +++ b/packages/nostr/src/client/conn.ts @@ -47,14 +47,7 @@ export class Conn { const msg = await parseIncomingMessage(value) this.#msgCallback?.(msg) } catch (err) { - if (err instanceof ProtocolError) { - this.#errorCallback?.(err) - } else { - // TODO Not sure if this is the best idea. - // Investigate what WebSocket does if the callback throws? - // Either way it seems like the best idea is to have `onError` called on all types of errors - throw err - } + this.#errorCallback?.(err) } }) @@ -65,6 +58,10 @@ export class Conn { } this.#pending = [] }) + + this.#socket.addEventListener("error", (err) => { + this.#errorCallback?.(err) + }) } on(on: "message", cb: IncomingMessageCallback): void @@ -84,11 +81,23 @@ export class Conn { this.#pending.push(msg) return } - this.#socket.send(serializeOutgoingMessage(msg)) + try { + this.#socket.send(serializeOutgoingMessage(msg), (err) => { + if (err !== undefined && err !== null) { + this.#errorCallback?.(err) + } + }) + } catch (err) { + this.#errorCallback?.(err) + } } close(): void { - this.#socket.close() + try { + this.#socket.close() + } catch (err) { + this.#errorCallback?.(err) + } } } @@ -160,7 +169,7 @@ export interface OutgoingCloseSubscription { } type IncomingMessageCallback = (message: IncomingMessage) => unknown -type ErrorCallback = (error: ProtocolError) => unknown +type ErrorCallback = (error: unknown) => unknown interface RawFilters { ids?: string[] diff --git a/packages/nostr/src/client/index.ts b/packages/nostr/src/client/index.ts index 7ec88d8..158cf04 100644 --- a/packages/nostr/src/client/index.ts +++ b/packages/nostr/src/client/index.ts @@ -81,19 +81,27 @@ export class Nostr { // Handle messages on this connection. conn.on("message", async (msg) => { - if (msg.kind === IncomingKind.Event) { - this.#eventCallback?.( - { - signed: msg.signed, - subscriptionId: msg.subscriptionId, - raw: msg.raw, - }, - this - ) - } else if (msg.kind === IncomingKind.Notice) { - this.#noticeCallback?.(msg.notice, this) - } else { - const err = new ProtocolError(`invalid message ${msg}`) + try { + if (msg.kind === IncomingKind.Event) { + this.#eventCallback?.( + { + signed: msg.signed, + subscriptionId: msg.subscriptionId, + raw: msg.raw, + }, + this + ) + } else if (msg.kind === IncomingKind.Notice) { + this.#noticeCallback?.(msg.notice, this) + } else { + const err = new ProtocolError(`invalid message ${msg}`) + try { + this.#errorCallback?.(err, this) + } catch (err) { + // Don't propagate errors from the error callback. + } + } + } catch (err) { this.#errorCallback?.(err, this) } }) @@ -317,7 +325,7 @@ export interface Filters { export type EventCallback = (params: EventParams, nostr: Nostr) => unknown export type NoticeCallback = (notice: string, nostr: Nostr) => unknown -export type ErrorCallback = (error: ProtocolError, nostr: Nostr) => unknown +export type ErrorCallback = (error: unknown, nostr: Nostr) => unknown export interface EventParams { signed: SignedEvent From a32d80653261548c73f4ab25a39f50420c51f3b1 Mon Sep 17 00:00:00 2001 From: ennmichael Date: Wed, 1 Mar 2023 00:10:53 +0100 Subject: [PATCH 2/5] use EventEmitter --- packages/nostr/package.json | 2 + packages/nostr/src/client/emitter.ts | 109 +++++++++++++++++++++++++++ packages/nostr/src/client/index.ts | 61 +++------------ packages/nostr/tsconfig.json | 3 +- yarn.lock | 7 +- 5 files changed, 131 insertions(+), 51 deletions(-) create mode 100644 packages/nostr/src/client/emitter.ts diff --git a/packages/nostr/package.json b/packages/nostr/package.json index 7282da4..46bbf09 100644 --- a/packages/nostr/package.json +++ b/packages/nostr/package.json @@ -10,6 +10,7 @@ "lint": "eslint ." }, "devDependencies": { + "@types/events": "^3.0.0", "@types/expect": "^24.3.0", "@types/mocha": "^10.0.1", "@typescript-eslint/eslint-plugin": "^5.53.0", @@ -25,6 +26,7 @@ "dependencies": { "@noble/secp256k1": "^1.7.1", "bech32": "^2.0.0", + "events": "^3.3.0", "isomorphic-ws": "^5.0.0", "ws": "^8.12.1" } diff --git a/packages/nostr/src/client/emitter.ts b/packages/nostr/src/client/emitter.ts new file mode 100644 index 0000000..d99215d --- /dev/null +++ b/packages/nostr/src/client/emitter.ts @@ -0,0 +1,109 @@ +import Base from "events" +import { Nostr, SubscriptionId } from "." +import { RawEvent, SignedEvent } from "../event" + +/** + * Overrides providing better types for EventEmitter methods. + */ +export class EventEmitter extends Base { + override addListener(eventName: "event", listener: EventListener): this + override addListener(eventName: "notice", listener: NoticeListener): this + override addListener(eventName: "error", listener: ErrorListener): this + override addListener(eventName: EventName, listener: Listener): this { + return super.addListener(eventName, listener) + } + + override emit(eventName: "event", params: EventParams, nostr: Nostr): boolean + override emit(eventName: "notice", notice: string, nostr: Nostr): boolean + override emit(eventName: "error", err: unknown, nostr: Nostr): boolean + override emit(eventName: EventName, ...args: unknown[]): boolean { + return super.emit(eventName, ...args) + } + + override eventNames(): EventName[] { + return super.eventNames() as EventName[] + } + + override listeners(eventName: "event"): EventListener[] + override listeners(eventName: "notice"): NoticeListener[] + override listeners(eventName: "error"): ErrorListener[] + override listeners(eventName: EventName): Listener[] { + return super.listeners(eventName) as Listener[] + } + + override off(eventName: "event", listener: EventListener): this + override off(eventName: "notice", listener: NoticeListener): this + override off(eventName: "error", listener: ErrorListener): this + override off(eventName: string, listener: Listener): this { + return super.off(eventName, listener) + } + + override on(eventName: "event", listener: EventListener): this + override on(eventName: "notice", listener: NoticeListener): this + override on(eventName: "error", listener: ErrorListener): this + override on(eventName: EventName, listener: Listener): this { + return super.on(eventName, listener) + } + + override once(eventName: "event", listener: EventListener): this + override once(eventName: "notice", listener: NoticeListener): this + override once(eventName: "error", listener: ErrorListener): this + override once(eventName: EventName, listener: Listener): this { + return super.once(eventName, listener) + } + + override prependListener(eventName: "event", listener: EventListener): this + override prependListener(eventName: "notice", listener: NoticeListener): this + override prependListener(eventName: "error", listener: ErrorListener): this + override prependListener(eventName: EventName, listener: Listener): this { + return super.prependListener(eventName, listener) + } + + override prependOnceListener( + eventName: "event", + listener: EventListener + ): this + override prependOnceListener( + eventName: "notice", + listener: NoticeListener + ): this + override prependOnceListener( + eventName: "error", + listener: ErrorListener + ): this + override prependOnceListener(eventName: EventName, listener: Listener): this { + return super.prependOnceListener(eventName, listener) + } + + override removeAllListeners(event?: EventName): this { + return super.removeAllListeners(event) + } + + override removeListener(eventName: "event", listener: EventListener): this + override removeListener(eventName: "notice", listener: NoticeListener): this + override removeListener(eventName: "error", listener: ErrorListener): this + override removeListener(eventName: EventName, listener: Listener): this { + return super.removeListener(eventName, listener) + } + + override rawListeners(eventName: EventName): Listener[] { + return super.rawListeners(eventName) as Listener[] + } + + // TODO + // emitter[Symbol.for('nodejs.rejection')](err, eventName[, ...args]) shenanigans? +} + +// TODO Also add on: ("subscribed", subscriptionId) which checks "OK"/"NOTICE" and makes a callback? +// TODO Also add on: ("ok", boolean, eventId) which checks "OK"/"NOTICE" and makes a callback? +type EventName = "event" | "notice" | "error" +type EventListener = (params: EventParams, nostr: Nostr) => void +type NoticeListener = (notice: string, nostr: Nostr) => void +type ErrorListener = (error: unknown, nostr: Nostr) => void +type Listener = EventListener | NoticeListener | ErrorListener + +interface EventParams { + signed: SignedEvent + subscriptionId: SubscriptionId + raw: RawEvent +} diff --git a/packages/nostr/src/client/index.ts b/packages/nostr/src/client/index.ts index 158cf04..ae712ab 100644 --- a/packages/nostr/src/client/index.ts +++ b/packages/nostr/src/client/index.ts @@ -3,11 +3,15 @@ import { EventId, Event, EventKind, SignedEvent, RawEvent } from "../event" import { PrivateKey, PublicKey } from "../keypair" import { Conn, IncomingKind, OutgoingKind } from "./conn" import * as secp from "@noble/secp256k1" +import { EventEmitter } from "./emitter" /** * A nostr client. + * + * TODO Document the events here + * TODO Move all this to that file and add the newListener and removeListener events there as well */ -export class Nostr { +export class Nostr extends EventEmitter { // TODO NIP-44 AUTH, leave this for later /** * Open connections to relays. @@ -19,39 +23,6 @@ export class Nostr { */ readonly #subscriptions: Map = new Map() - #eventCallback?: EventCallback - #noticeCallback?: NoticeCallback - #errorCallback?: ErrorCallback - - /** - * Add a new callback for received events. - */ - on(on: "event", cb: EventCallback | undefined | null): void - /** - * Add a new callback for received notices. - */ - on(on: "notice", cb: NoticeCallback | undefined | null): void - /** - * Add a new callback for errors. - */ - on(on: "error", cb: ErrorCallback | undefined | null): void - // TODO Also add on: ("subscribed", subscriptionId) which checks "OK"/"NOTICE" and makes a callback? - // TODO Also add on: ("sent", eventId) which checks "OK"/"NOTICE" and makes a callback? - on( - on: "event" | "notice" | "error", - cb: EventCallback | NoticeCallback | ErrorCallback | undefined | null - ) { - if (on === "event") { - this.#eventCallback = (cb as EventCallback) ?? undefined - } else if (on === "notice") { - this.#noticeCallback = (cb as NoticeCallback) ?? undefined - } else if (on === "error") { - this.#errorCallback = (cb as ErrorCallback) ?? undefined - } else { - throw new Error(`unexpected input: ${on}`) - } - } - /** * Open a connection and start communicating with a relay. This method recreates all existing * subscriptions on the new relay as well. If there is already an existing connection, @@ -83,7 +54,8 @@ export class Nostr { conn.on("message", async (msg) => { try { if (msg.kind === IncomingKind.Event) { - this.#eventCallback?.( + this.emit( + "event", { signed: msg.signed, subscriptionId: msg.subscriptionId, @@ -92,23 +64,23 @@ export class Nostr { this ) } else if (msg.kind === IncomingKind.Notice) { - this.#noticeCallback?.(msg.notice, this) + this.emit("notice", msg.notice, this) } else { const err = new ProtocolError(`invalid message ${msg}`) try { - this.#errorCallback?.(err, this) + this.emit("error", err, this) } catch (err) { // Don't propagate errors from the error callback. } } } catch (err) { - this.#errorCallback?.(err, this) + this.emit("error", err, this) } }) // Forward connection errors to the error callbacks. conn.on("error", (err) => { - this.#errorCallback?.(err, this) + this.emit("error", err, this) }) // Resend existing subscriptions to this connection. @@ -286,6 +258,7 @@ export class SubscriptionId { } } +// TODO Rethink this type. Maybe it's not very idiomatic. /** * A prefix filter. These filters match events which have the appropriate prefix. * This also means that exact matches pass the filters. No special syntax is required. @@ -322,13 +295,3 @@ export interface Filters { until?: Date limit?: number } - -export type EventCallback = (params: EventParams, nostr: Nostr) => unknown -export type NoticeCallback = (notice: string, nostr: Nostr) => unknown -export type ErrorCallback = (error: unknown, nostr: Nostr) => unknown - -export interface EventParams { - signed: SignedEvent - subscriptionId: SubscriptionId - raw: RawEvent -} diff --git a/packages/nostr/tsconfig.json b/packages/nostr/tsconfig.json index f6cff62..ae65c78 100644 --- a/packages/nostr/tsconfig.json +++ b/packages/nostr/tsconfig.json @@ -8,7 +8,8 @@ "outDir": "dist", "moduleResolution": "node", "esModuleInterop": true, - "skipLibCheck": true + "skipLibCheck": true, + "noImplicitOverride": true }, "include": ["src"] } diff --git a/yarn.lock b/yarn.lock index 3ff2f76..0128f4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2072,6 +2072,11 @@ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== +"@types/events@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" + integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g== + "@types/expect@^24.3.0": version "24.3.0" resolved "https://registry.yarnpkg.com/@types/expect/-/expect-24.3.0.tgz#d7cab8b3c10c2d92a0cbb31981feceb81d3486f1" @@ -4763,7 +4768,7 @@ eventemitter3@^4.0.0: resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== -events@^3.2.0: +events@^3.2.0, events@^3.3.0: version "3.3.0" resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== From c20dea1dbaec98e06bb7a15e8d1a5ee5d0684aab Mon Sep 17 00:00:00 2001 From: ennmichael Date: Thu, 2 Mar 2023 21:03:50 +0100 Subject: [PATCH 3/5] add newEvent and removeEvent --- packages/nostr/src/client/emitter.ts | 49 ++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/packages/nostr/src/client/emitter.ts b/packages/nostr/src/client/emitter.ts index d99215d..e0cc67d 100644 --- a/packages/nostr/src/client/emitter.ts +++ b/packages/nostr/src/client/emitter.ts @@ -6,13 +6,20 @@ import { RawEvent, SignedEvent } from "../event" * Overrides providing better types for EventEmitter methods. */ export class EventEmitter extends Base { - override addListener(eventName: "event", listener: EventListener): this + override addListener(eventName: "newListener", listener: NewListener): this + override addListener( + eventName: "removeListener", + listener: RemoveListener + ): this override addListener(eventName: "notice", listener: NoticeListener): this override addListener(eventName: "error", listener: ErrorListener): this + override addListener(eventName: "newListener", listener: ErrorListener): this override addListener(eventName: EventName, listener: Listener): this { return super.addListener(eventName, listener) } + override emit(eventName: "newListener", listener: NewListener): boolean + override emit(eventName: "removeListener", listener: RemoveListener): boolean override emit(eventName: "event", params: EventParams, nostr: Nostr): boolean override emit(eventName: "notice", notice: string, nostr: Nostr): boolean override emit(eventName: "error", err: unknown, nostr: Nostr): boolean @@ -24,6 +31,8 @@ export class EventEmitter extends Base { return super.eventNames() as EventName[] } + override listeners(eventName: "newListener"): EventListener[] + override listeners(eventName: "removeListener"): EventListener[] override listeners(eventName: "event"): EventListener[] override listeners(eventName: "notice"): NoticeListener[] override listeners(eventName: "error"): ErrorListener[] @@ -31,6 +40,8 @@ export class EventEmitter extends Base { return super.listeners(eventName) as Listener[] } + override off(eventName: "newListener", listener: NewListener): this + override off(eventName: "removeListener", listener: RemoveListener): this override off(eventName: "event", listener: EventListener): this override off(eventName: "notice", listener: NoticeListener): this override off(eventName: "error", listener: ErrorListener): this @@ -38,6 +49,8 @@ export class EventEmitter extends Base { return super.off(eventName, listener) } + override on(eventName: "newListener", listener: NewListener): this + override on(eventName: "removeListener", listener: RemoveListener): this override on(eventName: "event", listener: EventListener): this override on(eventName: "notice", listener: NoticeListener): this override on(eventName: "error", listener: ErrorListener): this @@ -45,6 +58,8 @@ export class EventEmitter extends Base { return super.on(eventName, listener) } + override once(eventName: "newListener", listener: NewListener): this + override once(eventName: "removeListener", listener: RemoveListener): this override once(eventName: "event", listener: EventListener): this override once(eventName: "notice", listener: NoticeListener): this override once(eventName: "error", listener: ErrorListener): this @@ -52,6 +67,14 @@ export class EventEmitter extends Base { return super.once(eventName, listener) } + override prependListener( + eventName: "newListener", + listener: NewListener + ): this + override prependListener( + eventName: "removeListener", + listener: RemoveListener + ): this override prependListener(eventName: "event", listener: EventListener): this override prependListener(eventName: "notice", listener: NoticeListener): this override prependListener(eventName: "error", listener: ErrorListener): this @@ -59,6 +82,14 @@ export class EventEmitter extends Base { return super.prependListener(eventName, listener) } + override prependOnceListener( + eventName: "newListener", + listener: NewListener + ): this + override prependOnceListener( + eventName: "removeListener", + listener: RemoveListener + ): this override prependOnceListener( eventName: "event", listener: EventListener @@ -79,6 +110,11 @@ export class EventEmitter extends Base { return super.removeAllListeners(event) } + override removeListener(eventName: "newListener", listener: NewListener): this + override removeListener( + eventName: "removeListener", + listener: RemoveListener + ): this override removeListener(eventName: "event", listener: EventListener): this override removeListener(eventName: "notice", listener: NoticeListener): this override removeListener(eventName: "error", listener: ErrorListener): this @@ -96,11 +132,18 @@ export class EventEmitter extends Base { // TODO Also add on: ("subscribed", subscriptionId) which checks "OK"/"NOTICE" and makes a callback? // TODO Also add on: ("ok", boolean, eventId) which checks "OK"/"NOTICE" and makes a callback? -type EventName = "event" | "notice" | "error" +type EventName = "newListener" | "removeListener" | "event" | "notice" | "error" +type NewListener = (eventName: EventName, listener: Listener) => void +type RemoveListener = (eventName: EventName, listener: Listener) => void type EventListener = (params: EventParams, nostr: Nostr) => void type NoticeListener = (notice: string, nostr: Nostr) => void type ErrorListener = (error: unknown, nostr: Nostr) => void -type Listener = EventListener | NoticeListener | ErrorListener +type Listener = + | NewListener + | RemoveListener + | EventListener + | NoticeListener + | ErrorListener interface EventParams { signed: SignedEvent From 6daa75b21cac5a5ad0b9281fbd892f0369dc436f Mon Sep 17 00:00:00 2001 From: ennmichael Date: Thu, 2 Mar 2023 21:13:57 +0100 Subject: [PATCH 4/5] fix the test --- packages/nostr/src/client/emitter.ts | 9 +-------- packages/nostr/src/client/index.ts | 7 +++++++ packages/nostr/test/simple-communication.ts | 10 ++++++---- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/nostr/src/client/emitter.ts b/packages/nostr/src/client/emitter.ts index e0cc67d..3c98021 100644 --- a/packages/nostr/src/client/emitter.ts +++ b/packages/nostr/src/client/emitter.ts @@ -1,6 +1,5 @@ import Base from "events" -import { Nostr, SubscriptionId } from "." -import { RawEvent, SignedEvent } from "../event" +import { EventParams, Nostr } from "." /** * Overrides providing better types for EventEmitter methods. @@ -144,9 +143,3 @@ type Listener = | EventListener | NoticeListener | ErrorListener - -interface EventParams { - signed: SignedEvent - subscriptionId: SubscriptionId - raw: RawEvent -} diff --git a/packages/nostr/src/client/index.ts b/packages/nostr/src/client/index.ts index ae712ab..49a7957 100644 --- a/packages/nostr/src/client/index.ts +++ b/packages/nostr/src/client/index.ts @@ -295,3 +295,10 @@ export interface Filters { until?: Date limit?: number } + +// TODO Document this +export interface EventParams { + signed: SignedEvent + subscriptionId: SubscriptionId + raw: RawEvent +} diff --git a/packages/nostr/test/simple-communication.ts b/packages/nostr/test/simple-communication.ts index af4041c..86c6618 100644 --- a/packages/nostr/test/simple-communication.ts +++ b/packages/nostr/test/simple-communication.ts @@ -1,4 +1,4 @@ -import { Nostr } from "../src/client" +import { EventParams, Nostr } from "../src/client" import { EventKind } from "../src/event" import { PrivateKey } from "../src/keypair" import assert from "assert" @@ -20,7 +20,7 @@ describe("single event communication", function () { const subscriber = new Nostr() subscriber.open("ws://localhost:12648") - subscriber.on("event", ({ signed: { event } }) => { + function listener({ signed: { event } }: EventParams) { assert.equal(event.kind, EventKind.TextNote) assert.equal(event.pubkey.toString(), pubkey.toString()) assert.equal(event.createdAt.toString(), timestamp.toString()) @@ -32,13 +32,15 @@ describe("single event communication", function () { // subscribe happen at the same time, the same event might end up being broadcast twice. // To prevent reacting to the same event and calling done() twice, remove the callback // for future events. - subscriber.on("event", null) + subscriber.off("event", listener) publisher.close() subscriber.close() done() - }) + } + + subscriber.on("event", listener) subscriber.subscribe([]) From a34e6960d286f904a2b1f277a2c9d7f78649a17d Mon Sep 17 00:00:00 2001 From: ennmichael Date: Thu, 2 Mar 2023 21:23:00 +0100 Subject: [PATCH 5/5] tiny cleanup --- packages/nostr/src/client/index.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/nostr/src/client/index.ts b/packages/nostr/src/client/index.ts index 49a7957..48be246 100644 --- a/packages/nostr/src/client/index.ts +++ b/packages/nostr/src/client/index.ts @@ -9,7 +9,6 @@ import { EventEmitter } from "./emitter" * A nostr client. * * TODO Document the events here - * TODO Move all this to that file and add the newListener and removeListener events there as well */ export class Nostr extends EventEmitter { // TODO NIP-44 AUTH, leave this for later @@ -66,12 +65,7 @@ export class Nostr extends EventEmitter { } else if (msg.kind === IncomingKind.Notice) { this.emit("notice", msg.notice, this) } else { - const err = new ProtocolError(`invalid message ${msg}`) - try { - this.emit("error", err, this) - } catch (err) { - // Don't propagate errors from the error callback. - } + throw new ProtocolError(`invalid message ${msg}`) } } catch (err) { this.emit("error", err, this)