@decaf-ts/for-nano
Version:
decaf-ts persistence adapter for CouchDB via nano
241 lines • 9.45 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.NanoDispatch = void 0;
const core_1 = require("@decaf-ts/core");
const db_decorators_1 = require("@decaf-ts/db-decorators");
const for_couchdb_1 = require("@decaf-ts/for-couchdb");
const decorator_validation_1 = require("@decaf-ts/decorator-validation");
/**
* @description Dispatcher for Nano database change events
* @summary Handles the subscription to and processing of database change events from a Nano database,
* notifying observers when documents are created, updated, or deleted
* @template DocumentScope - The Nano document scope type
* @param {number} [timeout=5000] - Timeout in milliseconds for change feed requests
* @class NanoDispatch
* @example
* ```typescript
* // Create a dispatcher for a Nano database
* const db = server.db.use('my_database');
* const adapter = new NanoAdapter(db);
* const dispatch = new NanoDispatch();
*
* // The dispatcher will automatically subscribe to changes
* // and notify observers when documents change
* ```
* @mermaid
* classDiagram
* class Dispatch {
* +initialize()
* +updateObservers()
* }
* class NanoDispatch {
* -observerLastUpdate?: string
* -attemptCounter: number
* -timeout: number
* +constructor(timeout)
* #changeHandler()
* #initialize()
* }
* Dispatch <|-- NanoDispatch
*/
class NanoDispatch extends core_1.Dispatch {
constructor(timeout = 5000) {
super();
this.timeout = timeout;
this.attemptCounter = 0;
this.active = false;
}
/**
* @description Closes the dispatcher
* @summary Stops the dispatcher and cleans up any active subscriptions or resources
* @return {Promise<void>} A promise that resolves when the dispatcher has been closed
*/
close(...args) {
this.active = false;
this.attemptCounter = 0;
this.disposeChangeFeed();
return super.close(...args);
}
/**
* @description Processes database change events
* @summary Handles the response from the Nano changes feed, processes the changes,
* and notifies observers about document changes
* @param {RequestError | null} error - Error object if the request failed
* @param response - The changes response from Nano
* @param {any} [headers] - Response headers (unused)
* @return {Promise<void>} A promise that resolves when all changes have been processed
* @mermaid
* sequenceDiagram
* participant D as NanoDispatch
* participant L as Logger
* participant O as Observers
* Note over D: Receive changes from Nano
* alt Error in response
* D->>L: Log error
* D-->>D: Return early
* end
* alt Response is string
* D->>D: Parse JSON from string
* end
* D->>D: Process changes
* D->>D: Group changes by table and operation
* loop For each table
* loop For each operation
* D->>O: updateObservers(table, operation, ids)
* D->>D: Update observerLastUpdate
* D->>L: Log successful dispatch
* end
* end
*/
async changeHandler(error, response, headers, ctxArg) {
const ctx = (ctxArg ||
this.adapter.context(db_decorators_1.OperationKeys.UPDATE, {}, decorator_validation_1.Model));
const log = ctx.logger.for(this.changeHandler);
if (error)
return log.error(`Error in change request: ${error}`);
try {
response = (typeof response === "string"
? response
.split("\n")
.filter((r) => !!r)
.map((r) => JSON.parse(r))
: response);
}
catch (e) {
return log.error(`Error parsing couchdb change feed: ${e}`);
}
const count = response.length;
if (count > 0) {
log.debug(`Received ${count} changes. processing...`);
const changes = response
.map((rec, i) => {
if (i === count - 1) {
if (this.observerLastUpdate ===
rec.last_seq)
log.error(`Invalid last update check: ${this.observerLastUpdate} !== ${rec.last_seq}`);
return;
}
const r = rec;
const [table, id] = r.id.split(for_couchdb_1.CouchDBKeys.SEPARATOR);
return {
table: table,
id: id,
operation: r.deleted
? db_decorators_1.OperationKeys.DELETE
: r.changes[r.changes.length - 1].rev.split("-")[0] === "1"
? db_decorators_1.OperationKeys.CREATE
: db_decorators_1.OperationKeys.UPDATE,
step: r.changes[r.changes.length - 1].rev,
};
})
.reduce((accum, r) => {
if (!r)
return accum;
const { table, id, operation, step } = r;
if (!accum[table])
accum[table] = {};
if (!accum[table][operation])
accum[table][operation] = { ids: new Set(), step: step };
accum[table][operation].ids.add(id);
accum[table][operation].step = step;
return accum;
}, {});
for (const table of Object.keys(changes)) {
for (const op of Object.keys(changes[table])) {
try {
await this.updateObservers(table, op, [...changes[table][op].ids.values()], ctx);
this.observerLastUpdate = changes[table][op].step;
log.verbose(`Observer refresh dispatched by ${op} for ${table}`);
log.debug(`pks: ${Array.from(changes[table][op].ids.values())}`);
}
catch (e) {
log.error(`Failed to dispatch observer refresh for ${table}, op ${op}: ${e}`);
}
}
}
}
}
/**
* @description Initializes the dispatcher and subscribes to database changes
* @summary Sets up the continuous changes feed subscription to the Nano database
* and handles reconnection attempts if the connection fails
* @return {Promise<void>} A promise that resolves when the subscription is established
* @mermaid
* sequenceDiagram
* participant D as NanoDispatch
* participant S as subscribeToCouch
* participant DB as Nano Database
* participant L as Logger
* D->>S: Call subscribeToCouch
* S->>S: Check adapter and native
* alt No adapter or native
* S-->>S: throw InternalError
* end
* S->>DB: changes(options, changeHandler)
* alt Success
* DB-->>S: Subscription established
* S-->>D: Promise resolves
* D->>L: Log successful subscription
* else Error
* DB-->>S: Error
* S->>S: Increment attemptCounter
* alt attemptCounter > 3
* S->>L: Log error
* S-->>D: Promise rejects
* else attemptCounter <= 3
* S->>L: Log retry
* S->>S: Wait timeout
* S->>S: Recursive call to subscribeToCouch
* end
* end
*/
async initialize() {
const log = this.log.for(this.initialize);
const subLog = log.for(subscribeToCouch);
async function subscribeToCouch() {
if (!this.adapter)
throw new db_decorators_1.InternalError(`No adapter/native observed for dispatch`);
if (this.active)
return;
try {
const feed = this.adapter.client.changes({
feed: "continuous",
include_docs: false,
since: this.observerLastUpdate || "now",
timeout: this.timeout,
}, this.changeHandler.bind(this));
this.changeFeed = feed;
}
catch (e) {
if (++this.attemptCounter > 3)
return subLog.error(`Failed to subscribe to couchdb changes: ${e}`);
subLog.info(`Failed to subscribe to couchdb changes: ${e}. Retrying in 5 seconds...`);
if (!this.active)
return;
await new Promise((resolve) => setTimeout(resolve, this.timeout));
return subscribeToCouch.call(this);
}
}
this.active = true;
subscribeToCouch
.call(this)
.then(() => {
this.log.info(`Subscribed to couchdb changes`);
})
.catch((e) => {
throw new db_decorators_1.InternalError(`Failed to subscribe to couchdb changes: ${e}`);
});
}
disposeChangeFeed() {
if (!this.changeFeed)
return;
this.changeFeed.cancel?.();
this.changeFeed.close?.();
this.changeFeed.destroy?.();
this.changeFeed.removeAllListeners?.();
this.changeFeed = undefined;
}
}
exports.NanoDispatch = NanoDispatch;
//# sourceMappingURL=NanoDispatch.js.map
//# sourceMappingURL=NanoDispatch.cjs.map