Skip to main content

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 OnOneSecondTick for log flushing, connection checks, or lightweight status updates.
  • Moderate polling: subscribe to OnTenSecondTick for checking for updates, syncing small data sets.
  • Heavy polling: subscribe to OnOneMinuteTick for 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

ObservableFrequencyGated by EnableTypical use
OnRefreshTick~250 msYesUI refresh, animations, real-time displays.
OnOneSecondTick1 secondNoLog flushing, lightweight status checks.
OnTenSecondTick10 secondsNoModerate polling, sync checks.
OnOneMinuteTick1 minuteNoSession checks, heavy polling, maintenance.

Enable / disable

MemberDescription
Enableboolean 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

  • OnRefreshTick fires 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 finally block: if your disable/enable is wrapped around an async operation, use try/finally to ensure re-enabling even on errors.
  • Use descriptive source names: ClockEnable('myFeature-export', false) is better than ClockEnable('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

MethodSignatureDescription
emit<T>(event, data)(event: string, data: T): voidPublishes 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 eventbusService is a root singleton, subscriptions persist across component lifecycles. Always unsubscribe in ngOnDestroy.
  • 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> and on<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.OnOnline emits.
  • Startup delay: the queue does not start processing until stateService.IsReady is true, ensuring the service worker and session are fully initialised.
import { preloadService } from '@unpispas/upp-base';

constructor(private preload: preloadService) {}

Methods

MethodSignatureDescription
Enqueue(url, onload)(url: string, onload: (safeUrl: SafeUrl | null) => void): voidAdds 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 | nullReturns 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:cache or data: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 in OnDestroy(). The service's OnDestroy() is called when the service is destroyed (which, for a root singleton, is when the application shuts down).
  • SafeUrl is required for template binding: Angular's security sanitizer blocks raw object URLs. The service uses DomSanitizer.bypassSecurityTrustUrl() to create SafeUrl instances 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

PropertyTypeDescription
IsActivebooleanWhether a logging campaign is currently running.
Campaignstring | nullThe campaign name. Persisted in localStorage under upp-logger-campaignkey. Setting it to null removes the key from storage.

Methods

MethodSignatureDescription
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): voidManually 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:

  1. Calls the original console method (log, error, warn, info, debug, trace) so the browser's dev tools still work normally.
  2. 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

CommandBodyResponseDescription
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 logsService is constructed, all console.* 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 LOG command with errorcode 9 or 10, the service automatically sends a new INI to get a fresh token and resends the logs.
  • Each logsService instance generates a unique session UUID: this is not the same as stateService.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 a console.* call and the log appearing on the server.
  • The Stop() method clears IsActive but does not clear the queue: any remaining queued messages are lost when the campaign is stopped.
ServiceRelationship
clockServicelogsService uses OnOneSecondTick for periodic flushing. preloadService does not use the clock but respects stateService.IsReady.
adhocServicelogsService uses it to send log entries to the server.
viewServicelogsService checks viewService.IsOnline before flushing. preloadService subscribes to viewService.OnOnline for offline handling.
stateServicepreloadService waits for stateService.IsReady before starting.
httpServicepreloadService does not use httpService -- it uses fetch() directly for image loading and the Cache API for caching.