Clock, Event Bus, Preload & Logs
These four services provide cross-cutting infrastructure: clockService drives periodic tasks, eventbusService enables decoupled communication, preloadService manages background image loading, and logsService captures and forwards client-side logs to the server. They are the "plumbing" that other services and features build on.
clockService
Introduction
clockService is the heartbeat of the application. It provides periodic tick observables at four frequencies (250 ms, 1 second, 10 seconds, 1 minute) derived from a single setInterval. Rather than having each component or service set up its own timers, everything that needs periodic execution subscribes to the appropriate clockService observable. This centralisation makes it easy to pause all periodic activity (e.g. during heavy computation) and ensures consistent timing across the application.
When to use
- UI refresh: subscribe to
OnRefreshTick(250 ms) for smooth, continuous updates like progress bars or real-time displays. - Light polling: subscribe to
OnOneSecondTickfor log flushing, connection checks, or lightweight status updates. - Moderate polling: subscribe to
OnTenSecondTickfor checking for updates, syncing small data sets. - Heavy polling: subscribe to
OnOneMinuteTickfor session validity checks, data synchronisation, or background maintenance. - Pausing refresh: use
ClockEnable(source, false)to temporarily stop the refresh tick during heavy operations.
How it works
setInterval(250ms)
│
▼
_tick()
│
├── every tick (250ms): OnRefreshTick (only if clock is enabled)
│
├── every 4 ticks (1s): OnOneSecondTick
│
├── every 40 ticks (10s): OnTenSecondTick
│
└── every 240 ticks (1m): OnOneMinuteTick
The service uses a single setInterval at 250 ms and derives all frequencies using counters. The 1s/10s/1min ticks always fire regardless of the enable state. Only OnRefreshTick is gated by the enable flag. This ensures that critical periodic tasks (like log flushing) continue even when the UI refresh is paused.
The enable/disable mechanism uses a Set<string> to track which "sources" have disabled the clock. The clock is only enabled when the set is empty -- meaning every source that disabled it must also re-enable it. This prevents one source from accidentally re-enabling the clock while another source still needs it disabled.
import { clockService } from '@unpispas/upp-base';
constructor(private clock: clockService) {}
Tick intervals
| Observable | Frequency | Gated by Enable | Typical use |
|---|---|---|---|
OnRefreshTick | ~250 ms | Yes | UI refresh, animations, real-time displays. |
OnOneSecondTick | 1 second | No | Log flushing, lightweight status checks. |
OnTenSecondTick | 10 seconds | No | Moderate polling, sync checks. |
OnOneMinuteTick | 1 minute | No | Session checks, heavy polling, maintenance. |
Enable / disable
| Member | Description |
|---|---|
Enable | boolean getter -- returns true if no source has disabled the clock (the disabling set is empty). |
ClockEnable(source, value) | Enables (true) or disables (false) the clock from a named source. When disabling, the source string is added to the set. When enabling, it is removed. If the set becomes empty, a tick is immediately fired. |
Usage examples
Subscribe to periodic updates
// Check for new data every 10 seconds
this.clock.OnTenSecondTick.subscribe(() => {
this.checkForUpdates();
});
// Update a live clock display every second
this.clock.OnOneSecondTick.subscribe(() => {
this.currentTime = new Date();
});
// Check session validity every minute
this.clock.OnOneMinuteTick.subscribe(() => {
this.validateSession();
});
Temporarily disable refresh ticks during heavy work
// Pause refresh ticks during a bulk import
this.clock.ClockEnable('bulk-import', false);
try {
await this.importLargeDataSet();
} finally {
this.clock.ClockEnable('bulk-import', true);
}
Multiple sources can disable independently
// Source A disables for a sync operation
this.clock.ClockEnable('sync', false);
// Source B also disables for a heavy computation
this.clock.ClockEnable('computation', false);
// Source A finishes -- clock is still disabled because 'computation' is active
this.clock.ClockEnable('sync', true);
// Source B finishes -- now the clock resumes
this.clock.ClockEnable('computation', true);
Gotchas and tips
OnRefreshTickfires very frequently (250 ms): keep subscriber handlers extremely lightweight. Avoid DOM manipulation, HTTP calls, or heavy computation in refresh tick handlers.- 1s/10s/1min ticks are never paused: they always fire regardless of
ClockEnable. This is by design -- log flushing and session checks must continue. - Always re-enable in a
finallyblock: if your disable/enable is wrapped around an async operation, usetry/finallyto ensure re-enabling even on errors. - Use descriptive source names:
ClockEnable('myFeature-export', false)is better thanClockEnable('x', false)for debugging.
eventbusService
Introduction
eventbusService is a lightweight publish/subscribe event bus that allows decoupled communication between components and services. It solves the problem of components needing to react to events from distant parts of the application without establishing direct dependencies. Instead of injecting ServiceA into ComponentB just to listen for an event, both can use the event bus with a shared event name.
When to use
- Cross-component communication: notify components in different parts of the component tree about state changes.
- Decoupled notifications: emit events from services that components can optionally subscribe to.
- Application-wide signals: user logged in, data refreshed, theme changed (when the built-in observables on specific services are insufficient).
For communication within a single service's domain, prefer the service's own observables (e.g. stateService.OnSession). Use eventbusService when the publisher and subscriber have no natural shared dependency.
How it works
Internally, each event name maps to a Subject<T> created on first access (either emit or on). The Map<string, Subject> grows as new event names are used and is never cleaned up -- this is acceptable because the number of distinct event names in the application is small and bounded.
import { eventbusService } from '@unpispas/upp-base';
constructor(private events: eventbusService) {}
Methods
| Method | Signature | Description |
|---|---|---|
emit<T>(event, data) | (event: string, data: T): void | Publishes an event with associated data. All current subscribers receive it synchronously. If there are no subscribers, the event is silently dropped. |
on<T>(event) | (event: string): Observable<T> | Returns an Observable that emits whenever the named event is published. Subscribe to listen; unsubscribe to stop. |
Usage examples
Publish an event when a ticket is created
// In the ticket creation service
this.events.emit('ticketCreated', { ticketId: newTicket.objid, place: this.state.place });
Subscribe to ticket creation events in a dashboard component
ngOnInit() {
this.ticketSub = this.events.on<{ ticketId: string; place: string }>('ticketCreated')
.subscribe(data => {
this.refreshTicketCount();
});
}
ngOnDestroy() {
this.ticketSub.unsubscribe();
}
Global refresh signal
// Trigger a global refresh from anywhere
this.events.emit('globalRefresh', null);
// Listen in multiple components
this.events.on('globalRefresh').subscribe(() => {
this.loadData();
});
Gotchas and tips
- Always unsubscribe: since
eventbusServiceis a root singleton, subscriptions persist across component lifecycles. Always unsubscribe inngOnDestroy. - Events are not replayed: if you subscribe after an event was emitted, you will not receive it. Use
BehaviorSubject-backed state services if you need last-value semantics. - Type safety is opt-in: the generic type parameter on
emit<T>andon<T>is for TypeScript type checking only. There is no runtime validation. - Use consistent event name constants: define event names as string constants (e.g.
const TICKET_CREATED = 'ticketCreated') to avoid typos and enable refactoring.
preloadService
Introduction
preloadService manages a background queue for preloading images. In the application, product listings and catalogs can contain hundreds of images. Loading them all at once would overwhelm the network and cause visible jank. preloadService loads images sequentially in the background, caches them using the browser Cache API, and creates safe object URLs via DomSanitizer for template binding.
When to use
- Product images: enqueue product image URLs when building a product grid. As each image loads, the callback updates the component.
- Catalog images: preload category icons and banners.
- Any image that should be cached for offline use: the service puts images into the service worker cache.
How it works
Enqueue(url, callback)
│
▼
Already in _ready Map? ──yes──► callback(safeUrl) immediately
│ no
▼
Add to _queue array
│
▼
_LoadImage() processes queue sequentially:
│
├── Is app offline? ──yes──► pause, wait for OnOnline
│
├── Already loaded? ──yes──► callback(safeUrl), shift queue, next
│
└── no ──► _addToCache(url)
│
├── base64 data URI? ──► return as-is
│
├── Check service worker cache
│ ├── 'assets:assets:cache' for /assets URLs
│ └── 'data:images:cache' for other URLs
│
├── Found in cache? ──► blob → objectURL → SafeUrl
│
└── Not in cache? ──► fetch → put in cache → blob → objectURL → SafeUrl
│
▼
callback(safeUrl), shift queue, next
Key design decisions:
- Sequential loading: images are loaded one at a time, not in parallel. This prevents network congestion and ensures the most recently enqueued images do not starve earlier ones.
- Offline handling: if the app goes offline mid-queue, processing pauses and resumes when
viewService.OnOnlineemits. - Startup delay: the queue does not start processing until
stateService.IsReadyistrue, ensuring the service worker and session are fully initialised.
import { preloadService } from '@unpispas/upp-base';
constructor(private preload: preloadService) {}
Methods
| Method | Signature | Description |
|---|---|---|
Enqueue(url, onload) | (url: string, onload: (safeUrl: SafeUrl | null) => void): void | Adds an image URL to the preload queue. If already cached in the _ready map, the callback fires immediately with the cached SafeUrl. Duplicate URLs are ignored (the queue checks for existing entries). |
Recover(url) | (url: string): SafeUrl | null | Returns the cached SafeUrl for a previously loaded image, or null if not yet loaded. Synchronous -- useful for template bindings where you cannot await. |
Usage examples
Preload a product image and bind it in the template
// In the component
productImage: SafeUrl | null = null;
ngOnInit() {
this.preload.Enqueue(this.product.imageUrl, (safeUrl) => {
this.productImage = safeUrl;
this.cdr.markForCheck(); // trigger change detection if using OnPush
});
}
<img [src]="productImage" *ngIf="productImage" />
<div class="placeholder" *ngIf="!productImage">Loading...</div>
Recover a previously loaded image
// When navigating back to a previously viewed product
const cached = this.preload.Recover(this.product.imageUrl);
if (cached) {
this.productImage = cached;
} else {
this.preload.Enqueue(this.product.imageUrl, (safeUrl) => {
this.productImage = safeUrl;
});
}
Preload a batch of images
for (const product of this.products) {
this.preload.Enqueue(product.imageUrl, (safeUrl) => {
product.safeImage = safeUrl;
});
}
// Images will load sequentially in the background
Gotchas and tips
- Images load sequentially: if you enqueue 100 images, they load one by one. The callback for image #100 fires after all 99 previous images have loaded. Put the most important images first.
- Base64 data URIs bypass caching: URLs starting with
data:image/...;base64,...are returned as-is without going through the Cache API. - The service worker cache must exist: the service looks for caches named
assets:assets:cacheordata:images:cache. If these caches do not exist (e.g. the service worker is not registered), a warning is logged once and the image is not cached. - Object URLs must be revoked: the service creates object URLs via
URL.createObjectURL(). These are revoked inOnDestroy(). The service'sOnDestroy()is called when the service is destroyed (which, for a root singleton, is when the application shuts down). SafeUrlis required for template binding: Angular's security sanitizer blocks raw object URLs. The service usesDomSanitizer.bypassSecurityTrustUrl()to createSafeUrlinstances that can be bound to[src].
logsService
Introduction
logsService captures client-side console output and periodically flushes it to the server. It works by overriding the global window.console object with a proxy that intercepts all log, error, warn, info, debug, and trace calls, queues them, and sends them to the server every second. This is invaluable for debugging issues in production -- a support person can start a "logging campaign" on a user's device, reproduce the issue, and then retrieve the logs from the server.
When to use
- Production debugging: start a logging campaign when a user reports an issue.
- Remote diagnostics: the campaign name and auth token are persisted in
localStorage, so the campaign survives page reloads. - Development: useful during development to capture logs from a device that is not connected to dev tools.
You do not need to call logsService methods directly to log -- all standard console.* calls are automatically intercepted when a campaign is active.
How it works
constructor()
│
▼
window.console = OverrideConsole()
│ creates a proxy that:
│ 1. calls the original console method
│ 2. queues the message for server submission
│
▼
Check localStorage for active campaign
│ found? ──► Start(campaign)
│
▼
Start(campaign)
│
├── POST logger/writer.php { command: 'INI', session, source }
│ └── receives authtok
│
├── Subscribe to clockService.OnOneSecondTick
│ └── calls Flush() every second
│
▼
Flush()
│
├── offline? ──► skip
│
├── queue empty? ──► skip
│
└── POST logger/writer.php { command: 'LOG', session, authtok, logs: [...] }
│
├── success ──► clear queue
│
└── authtok expired (errorcode 9 or 10) ──► re-INI and resend
import { logsService } from '@unpispas/upp-base';
constructor(private logs: logsService) {}
Log levels
enum LogsLevel {
Error = "Error",
Warn = "Warn",
Info = "Info",
Debug = "Debug",
Trace = "Trace"
}
Properties
| Property | Type | Description |
|---|---|---|
IsActive | boolean | Whether a logging campaign is currently running. |
Campaign | string | null | The campaign name. Persisted in localStorage under upp-logger-campaignkey. Setting it to null removes the key from storage. |
Methods
| Method | Signature | Description |
|---|---|---|
Start(campaign) | (campaign: string): Promise<void> | Starts a logging campaign. Sends an INI command to the server to obtain an auth token. Subscribes to clockService.OnOneSecondTick for periodic flushing. The campaign name is persisted to localStorage so it survives page reloads. |
Stop() | (): Promise<void> | Stops logging. Sends an END command to the server and clears the auth token. Unsubscribes from the clock tick. |
Queue(level, message, extras) | (level: LogsLevel, message: string, extras: any): void | Manually queues a log entry. This is called internally by the console override -- you rarely need to call it directly. |
Console override
On construction, logsService replaces window.console with a proxy that:
- Calls the original console method (
log,error,warn,info,debug,trace) so the browser's dev tools still work normally. - Queues the message with its level and stringified arguments for server submission.
This override happens immediately on construction, even before a campaign is started. However, queued messages are only flushed when a campaign is active.
Server protocol
| Command | Body | Response | Description |
|---|---|---|---|
INI | { command: 'INI', session, source } | { errorcode: 0, authtok } | Starts a campaign. Returns an auth token. |
LOG | { command: 'LOG', session, authtok, logs } | { errorcode: 0 } | Sends queued log entries. On auth-token expiry (errorcode 9 or 10), automatically re-authenticates. |
END | { command: 'END', session, authtok } | { errorcode: 0 } | Ends the campaign. |
Usage examples
Start a logging campaign for debugging
// In a settings or debug panel
async onStartLogging() {
await this.logs.Start('debug-session-' + Date.now());
this.toast.ShowAlert('primary', 'Logging started');
}
async onStopLogging() {
await this.logs.Stop();
this.toast.ShowAlert('primary', 'Logging stopped');
}
All console calls are now captured
// These are automatically intercepted -- no special code needed
console.info('User logged in', { userId: 42 });
console.warn('Slow network detected', { latency: 2500 });
console.error('Payment failed', { orderId: 'ABC', error: 'timeout' });
Check if logging is active
if (this.logs.IsActive) {
this.showLoggingIndicator = true;
}
Common patterns
Auto-resume on page reload
The campaign name is stored in localStorage. When logsService is constructed (on app startup), it checks for an active campaign and resumes automatically:
// In the constructor
const _active = this.Campaign;
if (_active) {
this.Start(_active);
}
This means the user does not need to restart logging after a page refresh.
Gotchas and tips
- Console override is global: once
logsServiceis constructed, allconsole.*calls in the entire application (including third-party libraries) are intercepted. The original methods still work -- the proxy calls them first, then queues. - Logs are queued even when offline: the queue grows in memory while offline. When connectivity returns, the next flush sends all accumulated logs.
- Auth token expiry is handled transparently: if the server rejects a
LOGcommand with errorcode 9 or 10, the service automatically sends a newINIto get a fresh token and resends the logs. - Each
logsServiceinstance generates a unique session UUID: this is not the same asstateService.session. It is a random UUID used to correlate log entries on the server. - Flush frequency is 1 second: logs are batched and sent every second via
clockService.OnOneSecondTick. This means there is up to a 1-second delay between aconsole.*call and the log appearing on the server. - The
Stop()method clearsIsActivebut does not clear the queue: any remaining queued messages are lost when the campaign is stopped.
Related services
| Service | Relationship |
|---|---|
clockService | logsService uses OnOneSecondTick for periodic flushing. preloadService does not use the clock but respects stateService.IsReady. |
adhocService | logsService uses it to send log entries to the server. |
viewService | logsService checks viewService.IsOnline before flushing. preloadService subscribes to viewService.OnOnline for offline handling. |
stateService | preloadService waits for stateService.IsReady before starting. |
httpService | preloadService does not use httpService -- it uses fetch() directly for image loading and the Cache API for caching. |