@faynosync/sdk-js 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.ts DELETED
@@ -1,264 +0,0 @@
1
- import type { CheckOptions, PackageUpdateURL, UpdateResponse, UpdateSource } from './types';
2
- import {
3
- CheckError,
4
- EndpointError,
5
- ErrInvalidBaseURL,
6
- ErrInvalidEdgeURL,
7
- ErrMissingAppName,
8
- ErrMissingBaseURL,
9
- ErrMissingOwner,
10
- ErrMissingVersion,
11
- ValidationError,
12
- } from './errors';
13
-
14
- const DEFAULT_TIMEOUT_MS = 30_000;
15
- const USER_AGENT = 'faynosync-js/1.0';
16
-
17
- export interface Config {
18
- readonly baseURL: string;
19
- readonly edgeURL?: string;
20
- readonly fetch?: typeof globalThis.fetch;
21
- readonly timeoutMs?: number;
22
- }
23
-
24
- interface RawUpdateResponse {
25
- update_available?: boolean;
26
- update_url?: string;
27
- changelog?: string;
28
- critical?: boolean;
29
- is_intermediate_required?: boolean;
30
- possible_rollback?: boolean;
31
- [key: string]: unknown;
32
- }
33
-
34
- export class Client {
35
- private readonly baseURL: string;
36
- private readonly edgeURL: string;
37
- private readonly fetchFn: typeof globalThis.fetch;
38
- private readonly timeoutMs: number;
39
-
40
- constructor(cfg: Config) {
41
- this.baseURL = cfg.baseURL;
42
- this.edgeURL = cfg.edgeURL ?? '';
43
- this.fetchFn = cfg.fetch ?? globalThis.fetch.bind(globalThis);
44
- this.timeoutMs = cfg.timeoutMs ?? DEFAULT_TIMEOUT_MS;
45
- }
46
-
47
- async checkForUpdates(opts: CheckOptions, signal?: AbortSignal): Promise<UpdateResponse> {
48
- this.validateConfig();
49
- validateCheckOptions(opts);
50
-
51
- const sig = signal ?? null;
52
- let edgeError: Error | undefined;
53
-
54
- if (this.edgeURL !== '') {
55
- try {
56
- const resp = await this.checkEdge(opts, sig);
57
- if (opts.deviceId) {
58
- try {
59
- await this.sendEdgeTelemetryBeacon(opts, !resp.updateAvailable, sig);
60
- } catch {
61
- // telemetry failures don't affect the update check result
62
- }
63
- }
64
- return { ...resp, source: 'edge' };
65
- } catch (err) {
66
- edgeError = err as Error;
67
- if (signal?.aborted === true) {
68
- throw new CheckError(edgeError);
69
- }
70
- }
71
- }
72
-
73
- try {
74
- const resp = await this.checkAPI(opts, sig);
75
- return { ...resp, source: 'api' };
76
- } catch (apiError) {
77
- throw new CheckError(edgeError, apiError as Error);
78
- }
79
- }
80
-
81
- private validateConfig(): void {
82
- if (this.baseURL.trim() === '') {
83
- throw ErrMissingBaseURL;
84
- }
85
- parseAbsoluteURL(this.baseURL, ErrInvalidBaseURL);
86
- }
87
-
88
- private async checkEdge(opts: CheckOptions, signal: AbortSignal | null): Promise<UpdateResponse> {
89
- const url = this.buildEdgeCheckURL(opts);
90
- return this.doUpdateRequest(url, opts.deviceId, 'edge', signal);
91
- }
92
-
93
- private async checkAPI(opts: CheckOptions, signal: AbortSignal | null): Promise<UpdateResponse> {
94
- const url = this.buildAPICheckURL(opts);
95
- return this.doUpdateRequest(url, opts.deviceId, 'api', signal);
96
- }
97
-
98
- private async doUpdateRequest(
99
- url: string,
100
- deviceId: string | undefined,
101
- source: UpdateSource,
102
- signal: AbortSignal | null,
103
- ): Promise<UpdateResponse> {
104
- const headers: Record<string, string> = {
105
- Accept: 'application/json',
106
- 'User-Agent': USER_AGENT,
107
- };
108
- if (deviceId) {
109
- headers['X-Device-ID'] = deviceId;
110
- }
111
-
112
- const reqSignal = createRequestSignal(signal, this.timeoutMs);
113
-
114
- let res: Response;
115
- try {
116
- res = await this.fetchFn(url, { headers, signal: reqSignal });
117
- } catch (err) {
118
- throw new EndpointError(source, url, undefined, err as Error);
119
- }
120
-
121
- if (res.status !== 200) {
122
- await res.body?.cancel();
123
- throw new EndpointError(source, url, res.status);
124
- }
125
-
126
- let raw: RawUpdateResponse;
127
- try {
128
- raw = (await res.json()) as RawUpdateResponse;
129
- } catch (err) {
130
- throw new EndpointError(source, url, undefined, err as Error);
131
- }
132
-
133
- return parseUpdateResponse(raw);
134
- }
135
-
136
- private buildAPICheckURL(opts: CheckOptions): string {
137
- const u = parseAbsoluteURL(this.baseURL, ErrInvalidBaseURL);
138
- u.pathname = joinURLPath(u.pathname, 'checkVersion');
139
- u.search = new URLSearchParams({
140
- app_name: opts.appName,
141
- version: opts.version,
142
- channel: opts.channel ?? '',
143
- platform: opts.platform ?? '',
144
- arch: opts.arch ?? '',
145
- owner: opts.owner,
146
- }).toString();
147
- return u.toString();
148
- }
149
-
150
- private buildEdgeCheckURL(opts: CheckOptions): string {
151
- const u = parseAbsoluteURL(this.edgeURL, ErrInvalidEdgeURL);
152
- const segments = [
153
- 'responses',
154
- opts.owner,
155
- opts.appName,
156
- opts.channel ?? '',
157
- opts.platform ?? '',
158
- opts.arch ?? '',
159
- `${opts.version}.json`,
160
- ];
161
- const base = (u.origin + u.pathname).replace(/\/+$/, '');
162
- return `${base}/${segments.map(encodeURIComponent).join('/')}`;
163
- }
164
-
165
- private buildEdgeTelemetryBeaconURL(opts: CheckOptions, isLatest: boolean): string {
166
- const u = parseAbsoluteURL(this.baseURL, ErrInvalidBaseURL);
167
- u.pathname = joinURLPath(u.pathname, 'telemetry/beacon');
168
- u.search = new URLSearchParams({
169
- app_name: opts.appName,
170
- version: opts.version,
171
- channel: opts.channel ?? '',
172
- platform: opts.platform ?? '',
173
- arch: opts.arch ?? '',
174
- owner: opts.owner,
175
- is_latest: String(isLatest),
176
- }).toString();
177
- return u.toString();
178
- }
179
-
180
- private async sendEdgeTelemetryBeacon(
181
- opts: CheckOptions,
182
- isLatest: boolean,
183
- signal: AbortSignal | null,
184
- ): Promise<void> {
185
- const url = this.buildEdgeTelemetryBeaconURL(opts, isLatest);
186
- const headers: Record<string, string> = { 'User-Agent': USER_AGENT };
187
- if (opts.deviceId) {
188
- headers['X-Device-ID'] = opts.deviceId;
189
- }
190
-
191
- const reqSignal = createRequestSignal(signal, this.timeoutMs);
192
- const res = await this.fetchFn(url, { headers, signal: reqSignal });
193
- await res.body?.cancel();
194
- if (res.status !== 200) {
195
- throw new EndpointError('edge', url, res.status);
196
- }
197
- }
198
- }
199
-
200
- function validateCheckOptions(opts: CheckOptions): void {
201
- if (!opts.owner) throw ErrMissingOwner;
202
- if (!opts.appName) throw ErrMissingAppName;
203
- if (!opts.version) throw ErrMissingVersion;
204
- }
205
-
206
- function parseAbsoluteURL(raw: string, sentinel: ValidationError): URL {
207
- let u: URL;
208
- try {
209
- u = new URL(raw);
210
- } catch {
211
- throw sentinel;
212
- }
213
- if (!u.protocol || !u.hostname) {
214
- throw sentinel;
215
- }
216
- return u;
217
- }
218
-
219
- function joinURLPath(basePath: string, segment: string): string {
220
- if (basePath === '' || basePath === '/') {
221
- return `/${segment}`;
222
- }
223
- return `${basePath.replace(/\/+$/, '')}/${segment}`;
224
- }
225
-
226
- function createRequestSignal(userSignal: AbortSignal | null, timeoutMs: number): AbortSignal {
227
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
228
- if (userSignal === null) return timeoutSignal;
229
-
230
- if (userSignal.aborted) {
231
- const c = new AbortController();
232
- c.abort(userSignal.reason);
233
- return c.signal;
234
- }
235
-
236
- const controller = new AbortController();
237
- const abort = (reason: unknown) => controller.abort(reason);
238
- userSignal.addEventListener('abort', () => abort(userSignal.reason), { once: true });
239
- timeoutSignal.addEventListener('abort', () => abort(timeoutSignal.reason), { once: true });
240
- return controller.signal;
241
- }
242
-
243
- function parseUpdateResponse(raw: RawUpdateResponse): UpdateResponse {
244
- const packageUrls: PackageUpdateURL[] = [];
245
-
246
- for (const [key, value] of Object.entries(raw)) {
247
- if (key.startsWith('update_url_') && typeof value === 'string') {
248
- packageUrls.push({ package: key.slice('update_url_'.length), url: value });
249
- }
250
- }
251
-
252
- packageUrls.sort((a, b) => a.package.localeCompare(b.package));
253
-
254
- return {
255
- updateAvailable: raw.update_available ?? false,
256
- updateUrl: raw.update_url ?? '',
257
- changelog: raw.changelog ?? '',
258
- critical: raw.critical ?? false,
259
- isIntermediateRequired: raw.is_intermediate_required ?? false,
260
- possibleRollback: raw.possible_rollback ?? false,
261
- packageUrls,
262
- source: 'unknown',
263
- };
264
- }
package/src/errors.ts DELETED
@@ -1,72 +0,0 @@
1
- import type { UpdateSource } from './types';
2
-
3
- export class FaynoSyncError extends Error {
4
- constructor(message: string) {
5
- super(message);
6
- this.name = 'FaynoSyncError';
7
- }
8
- }
9
-
10
- export class ValidationError extends FaynoSyncError {
11
- constructor(message: string) {
12
- super(message);
13
- this.name = 'ValidationError';
14
- }
15
- }
16
-
17
- export class RequestFailedError extends FaynoSyncError {
18
- constructor(message: string) {
19
- super(message);
20
- this.name = 'RequestFailedError';
21
- }
22
- }
23
-
24
- export class EndpointError extends RequestFailedError {
25
- constructor(
26
- public readonly source: UpdateSource,
27
- public readonly endpointUrl: string,
28
- public readonly statusCode?: number,
29
- cause?: Error,
30
- ) {
31
- let msg: string;
32
- if (statusCode !== undefined) {
33
- msg = `faynosync: request failed: ${endpointUrl} returned HTTP ${statusCode}`;
34
- } else if (cause !== undefined) {
35
- msg = `faynosync: request failed: ${endpointUrl}: ${cause.message}`;
36
- } else {
37
- msg = `faynosync: request failed: ${endpointUrl}`;
38
- }
39
- super(msg);
40
- this.name = 'EndpointError';
41
- if (cause !== undefined) {
42
- this.cause = cause;
43
- }
44
- }
45
- }
46
-
47
- export class CheckError extends RequestFailedError {
48
- constructor(
49
- public readonly edgeError?: Error,
50
- public readonly apiError?: Error,
51
- ) {
52
- let msg: string;
53
- if (edgeError !== undefined && apiError !== undefined) {
54
- msg = `faynosync: request failed: edge failed: ${edgeError.message}; api failed: ${apiError.message}`;
55
- } else if (edgeError !== undefined) {
56
- msg = `faynosync: request failed: edge failed: ${edgeError.message}`;
57
- } else if (apiError !== undefined) {
58
- msg = `faynosync: request failed: api failed: ${apiError.message}`;
59
- } else {
60
- msg = 'faynosync: request failed';
61
- }
62
- super(msg);
63
- this.name = 'CheckError';
64
- }
65
- }
66
-
67
- export const ErrMissingBaseURL = new ValidationError('faynosync: missing base URL');
68
- export const ErrInvalidBaseURL = new ValidationError('faynosync: invalid base URL');
69
- export const ErrInvalidEdgeURL = new ValidationError('faynosync: invalid edge URL');
70
- export const ErrMissingOwner = new ValidationError('faynosync: missing owner');
71
- export const ErrMissingAppName = new ValidationError('faynosync: missing app name');
72
- export const ErrMissingVersion = new ValidationError('faynosync: missing version');
package/src/index.ts DELETED
@@ -1,17 +0,0 @@
1
- export { Client } from './client';
2
- export type { Config } from './client';
3
- export type { CheckOptions, PackageUpdateURL, UpdateResponse, UpdateSource } from './types';
4
- export {
5
- CheckError,
6
- EndpointError,
7
- ErrInvalidBaseURL,
8
- ErrInvalidEdgeURL,
9
- ErrMissingAppName,
10
- ErrMissingBaseURL,
11
- ErrMissingOwner,
12
- ErrMissingVersion,
13
- FaynoSyncError,
14
- RequestFailedError,
15
- ValidationError,
16
- } from './errors';
17
- export { systemArch, systemPlatform } from './system';
package/src/system.ts DELETED
@@ -1,7 +0,0 @@
1
- export function systemPlatform(): string {
2
- return process.platform;
3
- }
4
-
5
- export function systemArch(): string {
6
- return process.arch;
7
- }
package/src/types.ts DELETED
@@ -1,27 +0,0 @@
1
- export type UpdateSource = 'unknown' | 'edge' | 'api';
2
-
3
- export interface PackageUpdateURL {
4
- readonly package: string;
5
- readonly url: string;
6
- }
7
-
8
- export interface CheckOptions {
9
- readonly owner: string;
10
- readonly appName: string;
11
- readonly version: string;
12
- readonly channel?: string;
13
- readonly platform?: string;
14
- readonly arch?: string;
15
- readonly deviceId?: string;
16
- }
17
-
18
- export interface UpdateResponse {
19
- readonly updateAvailable: boolean;
20
- readonly updateUrl: string;
21
- readonly changelog: string;
22
- readonly critical: boolean;
23
- readonly isIntermediateRequired: boolean;
24
- readonly possibleRollback: boolean;
25
- readonly packageUrls: readonly PackageUpdateURL[];
26
- readonly source: UpdateSource;
27
- }