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
3 changes: 3 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,12 @@ export {
platformInfo,
serviceVersion,
signal,
sourceResync,
sourceResyncStatus,
} from './root/functions';
export type {
PlacePlatformInfo,
PlaceSourceResyncStatus,
PlaceVersion,
SignalOptions,
} from './root/interfaces';
Expand Down
23 changes: 22 additions & 1 deletion src/root/functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { apiEndpoint } from '../auth/functions';
import { get, post } from '../http/functions';
import { toQueryString } from '../utilities/api';
import { HashMap } from '../utilities/types';
import { PlacePlatformInfo, PlaceVersion, SignalOptions } from './interfaces';
import {
PlacePlatformInfo,
PlaceSourceResyncStatus,
PlaceVersion,
SignalOptions,
} from './interfaces';

/** Check API health */
export function healthCheck(): Promise<void> {
Expand All @@ -29,6 +34,22 @@ export function apiScopes(): Promise<string[]> {
return get(`${apiEndpoint()}/scopes`) as any;
}

/**
* Publish every module's state and every model's metadata to the MQTT brokers
* again. With `clear`, everything PlaceOS publishes is removed from the brokers
* first, for when what they hold has drifted. Runs in the background; follow
* it with `sourceResyncStatus`. Rejects with a 409 while one is running.
* Needs a system admin.
*/
export function sourceResync(clear = false): Promise<PlaceSourceResyncStatus> {
return post(`${apiEndpoint()}/source/resync?clear=${clear}`, {}) as any;
}

/** Where the last resync of the MQTT brokers got to. Needs a system admin */
export function sourceResyncStatus(): Promise<PlaceSourceResyncStatus> {
return get(`${apiEndpoint()}/source/resync`) as any;
}

/** Signal a channel in a similar manner to a webhook for drivers */
export function signal(channel: string, body: HashMap = {}): Promise<void> {
const q = toQueryString({ channel } as SignalOptions);
Expand Down
14 changes: 14 additions & 0 deletions src/root/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,17 @@ export interface PlaceVersion {
export interface SignalOptions {
channel: string;
}

/** Where a resync of the MQTT brokers got to */
export interface PlaceSourceResyncStatus {
state: 'idle' | 'clearing' | 'resyncing' | 'complete' | 'failed';
/** Whether everything source publishes was removed from the brokers first */
clear: boolean;
started_at?: string;
finished_at?: string;
/** Retained topics removed, when clearing */
cleared: number;
/** Models whose metadata was published again */
metadata: number;
error?: string;
}
45 changes: 45 additions & 0 deletions test/root/functions.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { afterEach, describe, expect, test, vi } from 'vitest';
import * as Auth from '../../src/auth/functions';
import * as Http from '../../src/http/functions';
import * as SERVICE from '../../src/root/functions';

describe('Root API', () => {
afterEach(() => vi.restoreAllMocks());

const status = {
state: 'clearing',
clear: true,
started_at: '2026-10-15T00:00:00Z',
cleared: 0,
metadata: 0,
};

test('should allow starting a resync of the MQTT brokers', async () => {
vi.spyOn(Auth, 'apiEndpoint').mockReturnValue('/api/engine/v2');
const http = vi
.spyOn(Http, 'post')
.mockImplementation(() => Promise.resolve(status) as any);

expect(await SERVICE.sourceResync(true)).toEqual(status);
expect(http).toHaveBeenCalledWith(
'/api/engine/v2/source/resync?clear=true',
{},
);

await SERVICE.sourceResync();
expect(http).toHaveBeenLastCalledWith(
'/api/engine/v2/source/resync?clear=false',
{},
);
});

test('should allow reading where the resync got to', async () => {
vi.spyOn(Auth, 'apiEndpoint').mockReturnValue('/api/engine/v2');
const http = vi
.spyOn(Http, 'get')
.mockImplementation(() => Promise.resolve(status) as any);

expect(await SERVICE.sourceResyncStatus()).toEqual(status);
expect(http).toHaveBeenCalledWith('/api/engine/v2/source/resync');
});
});
Loading