feat(store): live-updates plugin (notify_push subscription transport for useObjectStore)#144
Merged
Merged
Conversation
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
This was referenced May 9, 2026
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.
Contributor
|
🎉 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.
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.
Summary
Frontend half of the live-updates capability. Extends
useObjectStorewith anotify_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 contributingsubscribe(type, id?, opts?)/unsubscribe(handle)actions plusliveStatus/liveSubscriptions/liveLastEventAtdiagnostic state.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.src/store/liveUpdates/pollingTransport.js) — coalescedsetIntervalper(type, paramsHash); visibility-gated viauseDocumentVisibility(paused when tab hidden); default 30s collections / 60s objects, configurable.src/store/liveUpdates/transport.js) —getLiveUpdates()returns one instance per browser tab. HoldsMap<eventKey, Set<callback>>for refcounted listeners; last unsubscribe tears down the underlying socket listener for that key.src/store/liveUpdates/eventKeys.js) — mirrors PHPOCA\OpenRegister\Push\PushEvents; key buildersbuildObjectKey(uuid)/buildCollectionKey(registerSlug, schemaSlug).fetchObjectandfetchCollectionin the plugin'ssetup()hook so 5 concurrent subscribers triggering the same key result in one HTTP fetch, not 5.registerObjectTypeextension — back-compat 4th arg{ registerSlug, schemaSlug }. Existing 3-arg call sites continue to work; slugs lazy-resolve viafetchRegister/fetchSchemaon firstsubscribe().Spec coverage
REQ-ST-001:registerObjectTypeaccepts optional slugs.Quality gates
npm run lint— 0 errors, 209 pre-existing JSDoc warnings (no warnings in new code).npm test— 646 / 646 passing across 37 suites (33 pre-existing + 4 new:eventKeys.spec.js,liveUpdates.spec.js, updateduseObjectStore.spec.js, mocks).Test plan
feature/add-live-updatesbranch, installnotify_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 refreshnotify_pushuninstalled → polling fallback kicks in (visible viastore.liveStatus === 'polling'in devtools)Out of scope (separate changes)
migrate-kpi-cards-to-aggregation-endpointfollow-up will use this when it adopts subscriptions; other apps similar)subscribe()API surface can host it later)Decisions documented
@vueuse/coreis 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.tryOnScopeDisposeworks inside Vue 2.7'ssetup()but silently no-ops in Options APImounted()without composition-scope tracking. Consumers usingmounted()MUST manually callunsubscribe(handle)inbeforeDestroy. Documented in the plugin's JSDoc.Companion change