@nanolink/mirrors
v1.1.8
Published
GraphQL subscription client + mirror synchronization utilities.
Readme
@nanolink/mirrors
GraphQL subscription client + in‑memory mirror synchronization utilities. Optimized for incremental change streams that send START / UPDATED / DELETED / DONE / VERSION_ERROR frames.
Features
- Lightweight
SubscriptionClientaroundgraphql-wswith explicit connect, controlled reconnect, and small event surface. - Dual version support in
MirrorSync(versionnumeric + optionalopVersionstring) for hybrid sequence + causality ordering (either dimension can drive resync logic when present). - Stale delete & update guards: ignores events older in either version dimension to prevent resurrecting removed or outdated entities.
- Efficient updates: UPDATED replaces item wholesale only when newer; no deep merge overhead.
- Full sync cycle handling via START/DONE gates;
loadedpromise resolves after first DONE and re-arms on VERSION_ERROR. - Automatic resubscribe after reconnect using last known versions (no duplicate inserts).
- Read‑only delegated map interface for consumers (prevents accidental mutation of internal state).
Connectionhelper manages multiple mirrors, re‑emitting namespaced events (mirror:start,mirror:updated, ...).- Proxy-aware WebSocket resolution: if proxy env vars are present (ALL_PROXY / HTTPS_PROXY / HTTP_PROXY / GLOBAL_AGENT_HTTP_PROXY) the client prefers the Node
wsimplementation; otherwise uses existing global WebSocket (browser / Node >=18) or falls back tows. - Minimal dependencies; event system via
eventemitter3.
Install
npm install @nanolink/mirrorsOptional (proxy via global-agent for generic HTTP(S) requests—WebSocket selection is still handled automatically as described):
// enableProxy.js
import 'global-agent/bootstrap';
process.env.GLOBAL_AGENT_HTTP_PROXY = 'http://proxy:3128';Run with:
node -r global-agent/bootstrap app.jsUsage
Quick (multi‑mirror connection)
import { Connection } from '@nanolink/mirrors';
const conn = new Connection('https://api.example.com', 'TOKEN');
conn.connect();
conn.on('connected', () => console.log('socket up'));
conn.on('mirror:updated', e => console.log('updated', e.mirrorName, e.item.id));
async function main() {
const users = await conn.getMirror('users', /* GraphQL subscription */ `
subscription Users($version: Long, $opVersion: String) {
users(version: $version, opVersion: $opVersion) {
type
total
deleteId
deleteVersion
deleteOpVersion
data { id version opVersion name }
}
}
`, {});
console.log('Initial size', users.size);
for (const user of users.values()) {
console.log(user);
}
}
main();Recommended: use getPredefinedMirror()
// preferred helper — uses predefined subscription templates from `src/definitions`
const devices = await conn.getPredefinedMirror('trackers');
await devices.loaded;
console.log('Trackers count', devices.size);
devices.on('updated', e => console.log('tracker updated', e.item.id));Direct low‑level client
Numeric‑only subscriptions (most common)
Most Nanolink GraphQL subscription fields expose only a numeric version and omit opVersion. MirrorSync handles this seamlessly: pass just $version in the query and the server responses won’t include opVersion / deleteOpVersion fields.
// Numeric-only example
const products = await conn.getMirror('products', `
subscription Products($version: Long) {
products(version: $version) {
type
total
deleteId
deleteVersion
data { id version title }
}
}
`, {});
await products.loaded; // after first DONE
console.log('Products count', products.size);When opVersion fields are absent they are simply ignored; ordering & stale protections rely on numeric version only.
import { SubscriptionClient } from '@nanolink/mirrors';
const sc = new SubscriptionClient({ url: 'https://api.example.com', maxReconnectAttempts: 10 });
sc.connect();
sc.on('connected', () => {
const dispose = sc.subscribe({
query: 'subscription Ping { ping }'
}, {
next: (msg) => console.log(msg),
error: (e) => console.error('err', e),
complete: () => console.log('done')
});
});Events
SubscriptionClient emits:
- connecting
- connected (first successful connect)
- reconnected (subsequent successful connect after a disconnect)
- disconnected ({ code, reason, wasClean })
- retry ({ attempt }) before a reconnect attempt delay
- error (network/protocol)
Connection re‑emits mirror events as mirror:<event> with payload { mirrorName, ... }:
- start
- updated (only when a newer item actually replaced stored data)
- deleted
- done (end of full sync batch)
- versionError (triggered resync)
- resubscribe (automatic after reconnect)
- error
- removed (mirror explicitly removed)
- cleared (mirror internal state cleared)
API Surface
SubscriptionClient– low level websocket subscription wrapper.MirrorSync– single mirror controller (dual version tracking).Connection– manages multiple mirrors + namespaced events.ReadonlyMapView– immutable view returned bygetMirror()/MirrorSync.load().
Note about mirror helpers
getMirror() is available for custom ad-hoc mirrors but is seldom used in most integrations. The more commonly used helper is getPredefinedMirror() which returns mirrors for known server-side definitions (IDs and field payload shapes) and avoids having to supply the raw GraphQL subscription yourself. Check src/definitions for available predefined mirror names and subscription fragments.
Notes
- Always call
connect()explicitly; no implicit lazy connect. - VERSION_ERROR triggers automatic full resync (re-arms
loaded). - First top-level field in GraphQL subscription payload is treated as the sync envelope.
- Provide
webSocketImplmanually if bundling for environments without a global WebSocket and you do NOT wantwsas fallback. - When proxy env vars are set in Node,
SubscriptionClientprefersws(allowing external agent configuration); browsers ignore these env vars.
Build & Publish
TypeScript sources compile to dist/.
Scripts:
npm run build # compile
npm run publish:dry # preview publish contents
npm run release # build + publish (public)Developer notes (recent refactor)
This repository recently split GraphQL subscription template literals into per-property modules to make maintenance easier:
- Subscription templates:
src/definitions/subscriptions/*.ts— one file per subscription property. - Shared fragments:
src/definitions/fragments.ts— common fragment string constants used by the subscription files. - Compatibility surface:
src/definitions/mirrors.tsnow re-exports the assembledSubscriptions,RequiredMirrors,TempSubscriptions, and the fragment constants to preserve the original API.
Packaging and what is published
- The npm package only ships the compiled build output.
package.jsonlistsdistanddist-compatin thefilesfield, so the raw TypeScript source files undersrc/(includingsrc/definitions/subscriptions/*.ts) are not included in the published package by default. - The TypeScript compiler emits JavaScript (to
dist) and declaration files (.d.ts) when you run the build; those compiled artifacts are what go into the package.
How to verify locally (PowerShell)
- Build the project:
npm run build- Inspect the compiled
disttree to see the compiled outputs for the subscription modules:
Get-ChildItem -Recurse .\dist | Select-Object FullName- See exactly what would be published (dry-run):
npm pack --dry-runIf you want .ts sources included in the published package, add src to the files array in package.json or add a copy step that places sources in dist/dist-compat before publishing; then verify with npm pack --dry-run.
CI / GitHub Actions
This repository includes a workflow that publishes the package to npm when changes are pushed to main and when a GitHub Release is published. The workflow expects a repository secret named NPM_TOKEN containing a valid npm automation token.
To create and add the secret:
- Generate an npm token on https://www.npmjs.com/ under your account settings (Access Tokens -> Automation).
- In the GitHub repository, go to Settings → Secrets → Actions and add a new secret named
NPM_TOKENwith the token value.
The workflow builds both dist and dist-compat and then runs npm publish --access public. If you need to restrict publishing (for example, to skip on regular pushes), adjust the workflow triggers in .github/workflows/publish.yml.
License
MIT
