Skip to content

feat(store): live-updates plugin (notify_push subscription transport for useObjectStore)#144

Merged
rubenvdlinde merged 12 commits into
betafrom
feature/add-live-updates-plugin
May 9, 2026
Merged

feat(store): live-updates plugin (notify_push subscription transport for useObjectStore)#144
rubenvdlinde merged 12 commits into
betafrom
feature/add-live-updates-plugin

Conversation

@rubenvdlinde
Copy link
Copy Markdown
Contributor

Summary

Frontend half of the live-updates capability. Extends useObjectStore with a notify_push-based subscription transport, an automatic polling fallback, in-flight fetch deduplication, and refcounted listener fan-in. The OpenRegister backend (NotifyPushListener, soft-fail, dedup, batch mode, slug-based event keys) ships in openregister#1453 and is currently in test against this branch ahead of Monday's review.

What ships

  • liveUpdatesPlugin (src/store/plugins/liveUpdates.js) — Pinia plugin contributing subscribe(type, id?, opts?) / unsubscribe(handle) actions plus liveStatus / liveSubscriptions / liveLastEventAt diagnostic state.
  • WebSocket transport (src/store/liveUpdates/websocketTransport.js) — wraps @nextcloud/notify_push; reconnect with exponential backoff + jitter (1s base, 30s cap, ×2). After 5 consecutive failures, drops to polling.
  • Polling transport (src/store/liveUpdates/pollingTransport.js) — coalesced setInterval per (type, paramsHash); visibility-gated via useDocumentVisibility (paused when tab hidden); default 30s collections / 60s objects, configurable.
  • Transport singleton (src/store/liveUpdates/transport.js) — getLiveUpdates() returns one instance per browser tab. Holds Map<eventKey, Set<callback>> for refcounted listeners; last unsubscribe tears down the underlying socket listener for that key.
  • Event-key constants (src/store/liveUpdates/eventKeys.js) — mirrors PHP OCA\OpenRegister\Push\PushEvents; key builders buildObjectKey(uuid) / buildCollectionKey(registerSlug, schemaSlug).
  • In-flight fetch dedup — patches fetchObject and fetchCollection in the plugin's setup() hook so 5 concurrent subscribers triggering the same key result in one HTTP fetch, not 5.
  • registerObjectType extension — back-compat 4th arg { registerSlug, schemaSlug }. Existing 3-arg call sites continue to work; slugs lazy-resolve via fetchRegister / fetchSchema on first subscribe().

Spec coverage

  • MODIFIED REQ-ST-001: registerObjectType accepts optional slugs.
  • ADDED REQ-ST-LU-001 (singleton plugin), -002 (subscribe action), -003 (refcounted fan-in), -004 (in-flight dedup), -005 (polling fallback), -006 (reconnect+jitter), -007 (refetch on event), -008 (diagnostic state), -009 (lazy slug resolution + failure handling).

Quality gates

  • npm run lint — 0 errors, 209 pre-existing JSDoc warnings (no warnings in new code).
  • npm test646 / 646 passing across 37 suites (33 pre-existing + 4 new: eventKeys.spec.js, liveUpdates.spec.js, updated useObjectStore.spec.js, mocks).

Test plan

  • CI lint + jest pass
  • Local: install OR with feature/add-live-updates branch, install notify_push, link this branch into a consumer app (e.g. procest, pipelinq), open detail page in two browser tabs as different users, edit object in tab A → tab B updates without manual refresh
  • Local: same flow with notify_push uninstalled → polling fallback kicks in (visible via store.liveStatus === 'polling' in devtools)
  • Local: 5 widgets on the same UUID → confirm exactly 1 fetch in network tab when an event fires
  • Local: hidden-tab handling — minimise the tab, verify polling pauses (no network requests in devtools); restore tab → polling resumes

Out of scope (separate changes)

  • Per-app frontend migration to the subscription API (procest's migrate-kpi-cards-to-aggregation-endpoint follow-up will use this when it adopts subscriptions; other apps similar)
  • GraphQL subscription transport (the same subscribe() API surface can host it later)
  • Server-Sent Events transport (same)

Decisions documented

  • @vueuse/core is a direct dependency, not a peer dep — 5 of 7 consumer apps don't bundle it today; tree-shaking keeps cost ~400 bytes gzipped.
  • @nextcloud/notify_push ^1.0.0 (latest 1.4.0) — npm registry has no 2.x; the API is stable across the 1.x series.
  • Vue 2 Options API caveattryOnScopeDispose works inside Vue 2.7's setup() but silently no-ops in Options API mounted() without composition-scope tracking. Consumers using mounted() MUST manually call unsubscribe(handle) in beforeDestroy. Documented in the plugin's JSDoc.

Companion change

Frontend half of the live-updates capability. Extends useObjectStore
with a notify_push subscription transport, polling fallback, in-flight
fetch dedup, and refcounted listener fan-in. Backend (OR notify_push)
ships in openregister PR #1453.

Spec deltas:
- MODIFIED REQ-ST-001: registerObjectType accepts optional registerSlug
  + schemaSlug so collection-event keys can be derived without a lookup
  (back-compat: existing 3-arg form still works, slugs lazy-resolve).
- ADDED REQ-ST-LU-001..009: useLiveUpdates singleton plugin, subscribe
  /unsubscribe actions, refcounted fan-in, in-flight fetch dedup,
  polling fallback, reconnect with jitter, refetch on event, diagnostic
  state, lazy slug resolution.

Decisions:
- @vueuse/core as direct dep (not peer): 5 of 7 consumer apps don't
  bundle it today; tree-shaking keeps cost ~400 bytes gzipped.
- @nextcloud/notify_push as direct dep, mirroring other @nextcloud/*
  packages (capabilities, dialogs).
Adds @nextcloud/notify_push ^1.0.0 as a direct dependency (WebSocket
transport) and @vueuse/core ^10.0.0 as a peerDependency (tryOnScopeDispose
for auto-cleanup in Vue 2.7 composition scopes).
Adds src/store/liveUpdates/eventKeys.js with OR_OBJECT_PREFIX and
OR_COLLECTION_PREFIX constants mirroring PushEvents.php from the OR backend,
plus buildObjectKey() and buildCollectionKey() builder helpers.
All other modules must import from this file — no hardcoded strings elsewhere.
Adds src/store/liveUpdates/websocketTransport.js wrapping
@nextcloud/notify_push listen() API. Features refcounted fan-in per event
key, exponential backoff + jitter reconnect (base 1s, cap 30s, x2 multiplier),
and status transitions (offline/connecting/live/reconnecting/polling).
After 5 consecutive reconnect failures emits polling status.
Adds src/store/liveUpdates/pollingTransport.js with refcounted coalesced
setInterval per event key (one interval regardless of subscriber count),
document visibilitychange gating (pauses when tab hidden, fires immediately
on restore then resets interval), and configurable poll intervals.
Adds src/store/liveUpdates/transport.js implementing getLiveUpdates() module-level
factory (singleton). Selects websocket transport when notify_push returns true,
falls back to polling otherwise. On websocket emitting polling status, switches
all active subscriptions to polling transport transparently. Exports resetLiveUpdates()
for test isolation.
Extends registerObjectType(slug, schemaId, registerId, opts?) to accept an
optional fourth argument { registerSlug, schemaSlug }. Omitting the fourth
arg is fully back-compatible — slugs default to null. Stored in
objectTypeRegistry[slug] alongside the UUIDs so liveUpdatesPlugin can use
them without a lazy fetch when known at registration time.
…g resolution

Adds src/store/plugins/liveUpdates.js. Plugin contributes liveStatus,
liveSubscriptions, liveLastEventAt state and matching getters, plus
subscribe(type, id?) and unsubscribe(handle) actions.

Lazy slug resolution (REQ-ST-LU-009): when collection subscribe is called
and registerSlug/schemaSlug are null, fetches via fetchRegister/fetchSchema,
caches result, fails clearly if fetch fails (no malformed event keys).

In-flight dedup (REQ-ST-LU-004): setup() hook patches fetchObject and
fetchCollection with plain Map dedup caches (non-reactive) so concurrent
calls for the same key share one HTTP request; caches cleared on settle.

setup() also uses store.$onAction to stash last fetchCollection params per
type for use in collection event refetch callbacks.
Re-exports liveUpdatesPlugin through the barrel chain:
src/store/plugins/index.js -> src/store/index.js -> src/index.js
Consumers can import { liveUpdatesPlugin } from '@conduction/nextcloud-vue'.
Also adds logsPlugin and registerMappingPlugin to store/index.js barrel
to match plugins/index.js completeness.
Adds tests/store/liveUpdates/eventKeys.spec.js (6 tests) covering constant
values and key builders. Adds tests/store/plugins/liveUpdates.spec.js
(27 tests) covering subscribe/unsubscribe, dedup, status transitions, lazy
slug resolution success/failure paths, and 3-arg/4-arg registerObjectType
back-compat. Updates useObjectStore.spec.js for new registerSlug/schemaSlug
fields. Adds jest mocks for @nextcloud/notify_push and @vueuse/core.
Original commit landed @vueuse/core in peerDependencies (matching the
repo's convention for framework libs like vue/pinia/bootstrap-vue).
Reverted to direct dependency per the resolved DQ-1: 5 of 7 consumer
apps (procest, pipelinq, docudesk, mydash, larpingapp) do not bundle
VueUse today, so peer-dep would force them all to add it manually.
Tree-shaking keeps the bundle cost ~400 bytes gzipped (only
tryOnScopeDispose and useDocumentVisibility ship).
…tes-plugin

# Conflicts:
#	package-lock.json
#	package.json
@rubenvdlinde rubenvdlinde merged commit c01a5e9 into beta May 9, 2026
0 of 2 checks passed
@rubenvdlinde rubenvdlinde deleted the feature/add-live-updates-plugin branch May 9, 2026 11:48
rubenvdlinde added a commit that referenced this pull request May 9, 2026
…#151)

PR #144's merge resolution accidentally dropped this devDependency along
with @semantic-release/git. @nextcloud/eslint-config@8.4.2's index.js
extends from @vue/eslint-config-typescript/recommended at runtime, so
without it npm run lint fails with 'ESLint couldn't find the config'
— blocking the release workflow.

Adding it back to devDependencies restores both local and CI lint.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 9, 2026

🎉 This PR is included in version 1.0.0-beta.4 🎉

The release is available on:

Your semantic-release bot 📦🚀

rubenvdlinde added a commit that referenced this pull request May 9, 2026
PR #144 added the `liveUpdatesPlugin` export to src/index.js but did
not include the corresponding doc page that scripts/check-docs.js
requires for every public store-plugin export. CI's docs-coverage gate
on every PR is now blocked on this missing page.

The doc follows the same shape as the other store-plugin docs
(audit-trails.md, lifecycle.md, etc.): usage example, plugin options
table, contributed state/getters/actions, cleanup notes (with an
explicit Vue 2.7 Options API caveat), in-flight dedup explanation, and
a transport section covering the @nextcloud/notify_push → polling
fallback ladder.

Verified locally with `npm run check:docs` — all 110 exports
documented, all accuracy checks pass.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant