npm package: @nanolink/mirrors
Description: GraphQL subscription client + mirror synchronization utilities.
Author: Nanolink ApS, Mogens Bak Nielsen
Latest version: 1.1.18

# @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 `SubscriptionClient` around `graphql-ws` with explicit connect, controlled reconnect, and small event surface.
* Dual version support in `MirrorSync` (`version` numeric + optional `opVersion` string) 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; `loaded` promise 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).
* `Connection` helper 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 `ws` implementation; otherwise uses existing global WebSocket (browser / Node >=18) or falls back to `ws`.
* Minimal dependencies; event system via `eventemitter3`.

## Install
```bash
npm install @nanolink/mirrors
```

Optional (proxy via global-agent for generic HTTP(S) requests—WebSocket selection is still handled automatically as described):
```js
// enableProxy.js
import 'global-agent/bootstrap';
process.env.GLOBAL_AGENT_HTTP_PROXY = 'http://proxy:3128';
```
Run with:
```bash
node -r global-agent/bootstrap app.js
```

## Usage
### Quick (multi‑mirror connection)
```ts
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()`
```ts
// 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.

```ts
// 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.

```ts
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 by `getMirror()` / `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 `webSocketImpl` manually if bundling for environments without a global WebSocket and you do NOT want `ws` as fallback.
* When proxy env vars are set in Node, `SubscriptionClient` prefers `ws` (allowing external agent configuration); browsers ignore these env vars.

## Build & Publish
TypeScript sources compile to `dist/`.

Scripts:
```bash
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.ts` now re-exports the assembled `Subscriptions`, `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.json` lists `dist` and `dist-compat` in the `files` field, so the raw TypeScript source files under `src/` (including `src/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)
----------------------------------
1) Build the project:

```powershell
npm run build
```

2) Inspect the compiled `dist` tree to see the compiled outputs for the subscription modules:

```powershell
Get-ChildItem -Recurse .\dist | Select-Object FullName
```

3) See exactly what would be published (dry-run):

```powershell
npm pack --dry-run
```

If 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:
1. Generate an npm token on https://www.npmjs.com/ under your account settings (Access Tokens -> Automation).
2. In the GitHub repository, go to Settings → Secrets → Actions and add a new secret named `NPM_TOKEN` with 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