Conversation
- native ES modules: type=module, exports map, explicit .js import extensions, engines node >=22.12.0 - TypeScript 7 (tsgo) with es2024 target, nodenext resolution, verbatimModuleSyntax and explicit types: [node] - tests migrated from mocha/chai/sinon/mock-require/nyc to node:test: module mocks (pg, constants) preloaded via --import, sinon replaced with a small spy shim over node:test mock that patches own properties (so spying inherited methods does not leak through prototypes) - shutdown specs made deterministic: keep-alive timer for async signal delivery (Node 22 cancellation), exit timers drained into the stub - LISTEN/UNLISTEN/NOTIFY specs now really assert (they were no-op expect() calls before) and scan all query calls - ambient event declarations imported as type-only (required at runtime under ESM); farmhash imported via default import (native CJS addon) - lint/format switched from eslint to oxlint + oxfmt; devDeps reduced - version bumped to 3.0.0
- version back to the published 2.0.0: the migration commit had pre-set 3.0.0, which would make 'npm version major' skip to 4.0.0 - uuid -> crypto.randomUUID for the pg client application name - farmhash -> one-shot node:crypto hash truncated to 64 bits (16 hex chars) in signature(): same key space as fingerprint64 without the native addon; note signatures change value, so mixed-version instances sharing an execution lock disagree until fully rolled out - runtime deps are now just pg and pg-format - lint warnings resolved: bare catch for unused bindings, side-effect spies without dead bindings, underscore-prefixed unused mock param, mock logger printing via console.info
pg ^8.22.0, @types/pg ^8.20.0, @types/node ^24.13.3, oxlint 1.73.0, oxfmt 0.58.0. @types/node stays on the 24.x line: types newer than the supported runtime floor would let unavailable APIs compile.
Runs test-lcov (with comment-line stripping) and renders the report with genhtml into coverage/html.
Reconnect (was broken with the real driver): - pg clients are single-use; reconnect now recreates the client, rewires handlers and re-creates channel locks against the new connection - a single connection loss fires both 'error' and 'end': reconnect arming is now guarded, so two client recreations cannot race - close() cancels a pending reconnect timer (it would resurrect the instance after destroy) Exactly-once execution locks (duplicate processing was possible): - processed messages leave their unique lock row in place as a marker; deleting it right after processing let a slower competing listener re-insert and reprocess the same message (caught by the integration suite: first=[0] second=[1,2,3,4,0]) - markers expire by TTL (UNIQUE_LOCK_TTL, 1h), cleanup rides along the next unique-lock acquisition round-trip; unique lock table gains a created_at column (auto-recreated when the column is missing) - release() never deletes a row the instance does not hold Resource ownership: - destroy() is instance-scoped: it no longer sweeps the process-global lock registry killing locks of other PgPubSub instances - unlistenAll()/release() dispose local lock resources (acquire timers, notification listeners) - they leaked before and kept the event loop alive; PgIpLock.destroy() no longer corrupts the instance registry when called twice (splice(-1) removed the wrong lock) - notification handlers guard against racing destroy() and no longer create locks for messages they are about to skip API-visible changes: - signal handling is opt-in: enableGracefulShutdown() or handleSignals: true (importing the package no longer installs process-wide SIGINT/SIGTERM/SIGABRT handlers that call process.exit) - 'error' events carry the actual error; without an 'error' listener errors go to the logger instead of crashing the process; appName and DDL bootstrap failures are logged loudly instead of swallowed - notify() rejects payloads above the 8000-byte postgres limit with a clear RangeError instead of a server error Testing: - new test:integration script runs flow tests against a real postgres (PG_TEST_DSN): pub/sub delivery, single-listener exclusivity and handover, server-side termination -> reconnect -> delivery, competing execution-lock listeners exactly-once, destroy isolation and payload limit; CI gains a postgres-service integration job (Node 22/24) - unit specs updated for instance-scoped destroy and client recreation (mock behavior now patched on the prototype)
- timers/events imports now use the node: prefix
- test mock registration no longer uses import * as: the pg mock forwards
a { Client } object (the only runtime export consumed from pg), and the
constants mock namespace is captured with a dynamic import (its module
re-exports the real constants via export *, so enumerating members by
hand would silently drop future additions)
The CI integration run flaked on the exactly-once exec-lock test: with a cold database, listen() in execution-lock mode does not create a lock, so the schema bootstrap ran lazily inside per-message handlers. A burst of concurrent handlers across fresh connections raced the DDL - including a destructive DROP TABLE on a column-mismatch check - and an INSERT could hit 'relation does not exist', so a message was skipped by every listener and lost. - schema bootstrap is now single-flight per connection: concurrent locks on the same client await one shared bootstrap (WeakMap keyed by client, so a recreated client on reconnect bootstraps afresh) instead of each running the ddl - bootstrap retries transient concurrent-catalog failures so the idempotent IF NOT EXISTS ddl converges between competing connections; a persistent failure (e.g. missing ddl privileges) is logged and rethrown, and the exec-lock handler now skips the message via emitError instead of raising an unhandled rejection - lock table creation is non-destructive: the DROP TABLE migration blocks are gone. The dedicated unique schema only needs an idempotent ALTER TABLE ADD COLUMN IF NOT EXISTS created_at; the channel schema never holds a foreign shape (separate _unique suffix), so a plain CREATE TABLE IF NOT EXISTS is safe under concurrency Verified: exactly-once holds across 3 cold-start listeners for 50 concurrently-published messages (no loss, no duplicates), and the integration suite passes 5x against a freshly-dropped schema.
The published tarball was shipping the oxlint/oxfmt configs and the coverage post-processing script; exclude scripts/ and the tool configs so it contains only LICENSE, README, package.json and the compiled output, matching the other @imqueue packages.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.