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
1 change: 1 addition & 0 deletions packages/core/src/service-worker/ServiceWorkerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export class ServiceWorkerClient {
config: {
channelName: this.channelName,
dbName: this.dbName,
deliveryUrl: this.config.deliveryUrl,
},
});

Expand Down
49 changes: 25 additions & 24 deletions packages/core/src/service-worker/tabmesh-sw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,38 +136,39 @@ async function drainPendingEvents(): Promise<void> {
// Sort by priority (desc), then createdAt (asc)
pending.sort((a, b) => b.priority - a.priority || a.createdAt - b.createdAt);

// Without a delivery endpoint the SW has nothing to send to. Leave
// pending entries in the outbox so the next Hub session picks them up;
// we still GC TTL-expired entries below.
const canDeliver = Boolean(config.deliveryUrl);

for (const entry of pending) {
// Filter expired events
if (entry.expiresAt !== undefined && entry.expiresAt <= now) {
expiredIds.push(entry.id);
continue;
}

// Attempt delivery via fetch if deliveryUrl is configured
if (config.deliveryUrl) {
try {
const response = await fetch(config.deliveryUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: entry.type,
payload: entry.payload,
id: entry.id,
sourceTabId: entry.sourceTabId,
}),
});

if (response.ok) {
deliveredIds.push(entry.id);
}
// Non-OK response: leave as pending for next sync
} catch {
// Network failure: leave as pending for next sync
if (!canDeliver) continue;

// Attempt delivery via fetch.
try {
const response = await fetch(config.deliveryUrl as string, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: entry.type,
payload: entry.payload,
id: entry.id,
sourceTabId: entry.sourceTabId,
}),
});

if (response.ok) {
deliveredIds.push(entry.id);
}
} else {
// No delivery URL: mark as delivered (events will be picked up
// by the next Hub that starts)
deliveredIds.push(entry.id);
// Non-OK response: leave as pending for next sync
} catch {
// Network failure: leave as pending for next sync
}
}

Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ export interface ServiceWorkerConfig {
enabled: boolean;
/** Service Worker script URL. @default '/tabmesh-sw.js' */
scriptUrl: string;
/**
* HTTP endpoint the Service Worker POSTs pending events to during
* Background Sync. Required for the SW handoff to actually deliver
* events. When omitted, the SW leaves pending events in the outbox
* for the next Hub session to drain — it does NOT silently mark them
* delivered.
*/
deliveryUrl?: string;
}

/**
Expand Down
19 changes: 19 additions & 0 deletions packages/core/tests/ServiceWorkerClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,25 @@ describe('ServiceWorkerClient', () => {
config: {
channelName: 'test-app',
dbName: 'tabmesh:test-app',
deliveryUrl: undefined,
},
});
});

it('forwards deliveryUrl to the service worker config', async () => {
const client = new ServiceWorkerClient('test-app', {
enabled: true,
deliveryUrl: 'https://api.example.com/events',
});

await client.register();

expect(mockRegistration.active?.postMessage).toHaveBeenCalledWith({
kind: 'tabmesh-sw-config',
config: {
channelName: 'test-app',
dbName: 'tabmesh:test-app',
deliveryUrl: 'https://api.example.com/events',
},
});
});
Expand Down
Loading