Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,9 @@ The client distributes as a **single JS file**. The server is optional — omit
| `data-room-id` | Activates P2P mode (UUID recommended) | absent = no P2P |
| `data-relay-url` | Override hosted relay for P2P signaling | project's hosted relay |
| `data-admin-id` | UUID of the designated admin browser; that browser's Settings → Data section is shown in all sync modes | absent = first-annotator heuristic |
| `data-sync-ms` | Server sync poll interval in milliseconds (Mode 3 only); invalid values fall back to 30 000 with a console warning | `30000` |

`data-sync-url` and `data-room-id` are mutually exclusive — pick one mode per embed.
`data-sync-url` and `data-room-id` are mutually exclusive — the library warns and suppresses P2P if both are set.

Everything (IndexedDB layer, anchor serialization, BroadcastChannel, P2P sync, server sync, UI, CSS) is inlined into the IIFE.
Injects a collapsible right sidebar + floating comment button. All CSS embedded via `document.createElement('style')`.
Expand Down Expand Up @@ -615,8 +616,10 @@ Three demo pages available after `npm start`:
- **IIFE pattern** — all client code scoped in `(function() { ... })()` to avoid globals; esbuild wraps this in an outer IIFE via `--format=iife`, making it a nested IIFE (harmless)
- **`siteId`** — read via `document.currentScript.dataset.siteId`; IDB namespace + server/P2P scope
- **`syncUrl`** — read via `document.currentScript.dataset.syncUrl`; `null` = sync disabled
- **`roomId`** — read via `document.currentScript.dataset.roomId`; `null` = P2P disabled; activates `initP2P()` in boot sequence
- **`roomId`** — read via `document.currentScript.dataset.roomId`; `null` = P2P disabled; activates `initP2P()` in boot sequence only when `_syncUrl` is absent (mutual exclusivity enforced)
- **`relayUrl`** — read via `document.currentScript.dataset.relayUrl`; defaults to `'wss://relay.annotate-js.workers.dev'`; overridden by `data-relay-url` for self-hosted relay
- **`_pollMs`** — read from `data-sync-ms` via `document.currentScript.dataset.syncMs`; validated via `parseInt > 0`; falls back to `30000` with a `console.warn` if the attribute is present but invalid (non-numeric, zero, or negative); replaces the hardcoded `30000` in the `setInterval` for server sync; absent attribute is the same as `30000` (no warning)
- **Mutual exclusivity guard** — if both `_syncUrl` and `_roomId` are set, `console.warn` fires at IIFE init and `initP2P()` is not called (`if (_roomId && !_syncUrl)`); server sync takes precedence
- **BroadcastChannel** — `_bc` created for same-origin multi-tab sync; posts `THREAD_UPDATE` / `ACTIVITY_UPDATE` messages; handler in `_bc.onmessage` applies last-write-wins before re-rendering; gracefully absent (`null`) in environments without BroadcastChannel support
- **BroadcastChannel `>=` not `>` for updatedAt** — IDB is shared across all same-origin tabs in the same browser; when Tab 1 calls `dbSaveThread` and then (inside `.then()`) posts the BC message, the write has already committed to shared IDB by the time Tab 2's handler calls `dbGetThread`; so `existing.updatedAt === msg.thread.updatedAt` for brand-new threads, and a strict `>` comparison would skip the re-render entirely; the condition must use `>=` so equal timestamps still trigger `_rerenderAfterPull`
- **P2P tiered signaling** — `initP2P()` tries the relay first with a 5 s fallback timer; `_initRelayP2P(url, onConnected, onFailure)` calls `onConnected()` on `ws.onopen` (clears timer); `onFailure()` fires on `ws.onerror`; both paths lead to `_initNostrP2P()` as fallback
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,23 @@ In all cases, annotations are stored in IndexedDB and survive page reloads.

---

## Script tag API

All configuration is done via `data-*` attributes on the `<script>` tag. These are the stable public interface of Annotate.js — all six attributes will be preserved without breaking changes from v1 onwards.

| Attribute | Type | Default | Notes |
|---|---|---|---|
| `data-site-id` | string | `"default"` | Namespaces annotations — IDB, server, and P2P scope. Set a unique value per site. |
| `data-sync-url` | URL | — | Activates server sync (Mode 3). **Mutually exclusive with `data-room-id`**. |
| `data-room-id` | UUID | — | Activates P2P sync (Mode 4). **Mutually exclusive with `data-sync-url`**. Requires `annotate.min.js`. |
| `data-relay-url` | WSS URL | hosted relay | Override the hosted signaling relay with a self-hosted one (P2P mode only). |
| `data-admin-id` | UUID | — | Grants the admin Data panel to the browser whose `localStorage.annotate_author_id` matches. If absent, the first annotator becomes admin. |
| `data-sync-ms` | integer (ms) | `30000` | Server sync poll interval (Mode 3 only). Values ≤ 0 or non-numeric fall back to 30 s with a console warning. |

> `data-sync-url` and `data-room-id` are mutually exclusive. If both are set the library logs a console warning and ignores `data-room-id`.

---

## Multi-user sync

### 1. Check Node.js version
Expand Down Expand Up @@ -597,6 +614,9 @@ Point `src` at your deployed server and you're done:
- [ ] Live re-render of the Resolved tab on inbound peer updates (currently re-renders only on tab switch)

**Shipped:**
- [x] `data-sync-ms` — configurable server sync poll interval; defaults to 30 s; invalid values fall back with a console warning
- [x] `data-sync-url` + `data-room-id` mutual exclusivity enforced at runtime — console warning + P2P suppressed when both set
- [x] Stable `data-*` attribute API documented as public interface ahead of v1
- [x] Ownership-based access control — Edit/Delete gated per browser; Resolve open to all; offline mode unrestricted
- [x] Frozen Resolved tab — Edit / Delete / Reply hidden on resolved threads; **Un-Resolve** toggle open to anyone, reseeds the Threads tab locally and across peers without reload
- [x] About panel in Settings — app name, version (injected at build time from `package.json`), sync mode chip, mode-aware privacy note, GitHub link
Expand Down
Loading
Loading