@shipeasy/sdk 1.0.0 → 1.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/LICENSE +40 -0
- package/README.md +94 -0
- package/dist/client/index.d.mts +143 -7
- package/dist/client/index.d.ts +143 -7
- package/dist/client/index.js +509 -25
- package/dist/client/index.mjs +492 -25
- package/dist/server/index.d.mts +38 -1
- package/dist/server/index.d.ts +38 -1
- package/dist/server/index.js +94 -1
- package/dist/server/index.mjs +89 -1
- package/package.json +34 -6
package/dist/server/index.d.ts
CHANGED
|
@@ -9,13 +9,17 @@ interface ExperimentResult<P> {
|
|
|
9
9
|
group: string;
|
|
10
10
|
params: P;
|
|
11
11
|
}
|
|
12
|
+
type FlagsClientEnv = "dev" | "staging" | "prod";
|
|
12
13
|
interface FlagsClientOptions {
|
|
13
14
|
apiKey: string;
|
|
14
15
|
baseUrl?: string;
|
|
16
|
+
/** Which published env to read values from. Defaults to "prod". */
|
|
17
|
+
env?: FlagsClientEnv;
|
|
15
18
|
}
|
|
16
19
|
declare class FlagsClient {
|
|
17
20
|
private readonly apiKey;
|
|
18
21
|
private readonly baseUrl;
|
|
22
|
+
private readonly env;
|
|
19
23
|
private flagsBlob;
|
|
20
24
|
private expsBlob;
|
|
21
25
|
private flagsEtag;
|
|
@@ -36,5 +40,38 @@ declare class FlagsClient {
|
|
|
36
40
|
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
37
41
|
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
38
42
|
}
|
|
43
|
+
interface LabelFile {
|
|
44
|
+
v: number;
|
|
45
|
+
profile: string;
|
|
46
|
+
chunk: string;
|
|
47
|
+
strings: Record<string, string>;
|
|
48
|
+
}
|
|
49
|
+
interface FetchLabelsOptions {
|
|
50
|
+
key: string;
|
|
51
|
+
profile: string;
|
|
52
|
+
chunk?: string;
|
|
53
|
+
cdnBaseUrl?: string;
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
}
|
|
56
|
+
declare function fetchLabelsForSSR(opts: FetchLabelsOptions): Promise<LabelFile | null>;
|
|
57
|
+
declare function configureShipeasyServer(opts: FlagsClientOptions): FlagsClient;
|
|
58
|
+
declare function getShipeasyServerClient(): FlagsClient | null;
|
|
59
|
+
declare function _resetShipeasyServerForTests(): void;
|
|
60
|
+
declare const flags: {
|
|
61
|
+
configure(opts: FlagsClientOptions): void;
|
|
62
|
+
/**
|
|
63
|
+
* Long-running server: starts the background poll. Call once at app boot.
|
|
64
|
+
* Throws if the initial fetch fails (caller decides whether to crash or degrade).
|
|
65
|
+
*/
|
|
66
|
+
init(): Promise<void>;
|
|
67
|
+
/** Serverless / edge: fetch rules once, no background timer. */
|
|
68
|
+
initOnce(): Promise<void>;
|
|
69
|
+
/** Stop background timers. Safe to call repeatedly. */
|
|
70
|
+
destroy(): void;
|
|
71
|
+
get(name: string, user: User): boolean;
|
|
72
|
+
getConfig<T = unknown>(name: string, decode?: (raw: unknown) => T): T | undefined;
|
|
73
|
+
getExperiment<P extends Record<string, unknown>>(name: string, user: User, defaultParams: P, decode?: (raw: unknown) => P): ExperimentResult<P>;
|
|
74
|
+
track(userId: string, eventName: string, props?: Record<string, unknown>): void;
|
|
75
|
+
};
|
|
39
76
|
|
|
40
|
-
export { type ExperimentResult, FlagsClient, type FlagsClientOptions, type User, version };
|
|
77
|
+
export { type ExperimentResult, type FetchLabelsOptions, FlagsClient, type FlagsClientEnv, type FlagsClientOptions, type LabelFile, type User, _resetShipeasyServerForTests, configureShipeasyServer, fetchLabelsForSSR, flags, getShipeasyServerClient, version };
|
package/dist/server/index.js
CHANGED
|
@@ -21,6 +21,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var server_exports = {};
|
|
22
22
|
__export(server_exports, {
|
|
23
23
|
FlagsClient: () => FlagsClient,
|
|
24
|
+
_resetShipeasyServerForTests: () => _resetShipeasyServerForTests,
|
|
25
|
+
configureShipeasyServer: () => configureShipeasyServer,
|
|
26
|
+
fetchLabelsForSSR: () => fetchLabelsForSSR,
|
|
27
|
+
flags: () => flags,
|
|
28
|
+
getShipeasyServerClient: () => getShipeasyServerClient,
|
|
24
29
|
version: () => version
|
|
25
30
|
});
|
|
26
31
|
module.exports = __toCommonJS(server_exports);
|
|
@@ -127,6 +132,7 @@ function evalGateInternal(gate, user) {
|
|
|
127
132
|
var FlagsClient = class {
|
|
128
133
|
apiKey;
|
|
129
134
|
baseUrl;
|
|
135
|
+
env;
|
|
130
136
|
flagsBlob = null;
|
|
131
137
|
expsBlob = null;
|
|
132
138
|
flagsEtag = null;
|
|
@@ -137,6 +143,7 @@ var FlagsClient = class {
|
|
|
137
143
|
constructor(opts) {
|
|
138
144
|
this.apiKey = opts.apiKey;
|
|
139
145
|
this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
|
|
146
|
+
this.env = opts.env ?? "prod";
|
|
140
147
|
}
|
|
141
148
|
async init() {
|
|
142
149
|
await this.fetchAll();
|
|
@@ -174,7 +181,7 @@ var FlagsClient = class {
|
|
|
174
181
|
async fetchFlags() {
|
|
175
182
|
const headers = { "X-SDK-Key": this.apiKey };
|
|
176
183
|
if (this.flagsEtag) headers["If-None-Match"] = this.flagsEtag;
|
|
177
|
-
const res = await globalThis.fetch(`${this.baseUrl}/sdk/flags`, { headers });
|
|
184
|
+
const res = await globalThis.fetch(`${this.baseUrl}/sdk/flags?env=${this.env}`, { headers });
|
|
178
185
|
const interval = Number(res.headers.get("X-Poll-Interval") ?? "30") || 30;
|
|
179
186
|
if (res.status === 304) return interval;
|
|
180
187
|
if (!res.ok) throw new Error(`/sdk/flags returned ${res.status}`);
|
|
@@ -265,8 +272,94 @@ var FlagsClient = class {
|
|
|
265
272
|
}).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
|
|
266
273
|
}
|
|
267
274
|
};
|
|
275
|
+
var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
|
|
276
|
+
async function fetchJson(url, timeoutMs = 2e3) {
|
|
277
|
+
const controller = new AbortController();
|
|
278
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
279
|
+
try {
|
|
280
|
+
const res = await fetch(url, {
|
|
281
|
+
signal: controller.signal,
|
|
282
|
+
next: { revalidate: 60 }
|
|
283
|
+
});
|
|
284
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
285
|
+
return res.json();
|
|
286
|
+
} finally {
|
|
287
|
+
clearTimeout(timer);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function fetchLabelsForSSR(opts) {
|
|
291
|
+
const cdn = opts.cdnBaseUrl ?? DEFAULT_I18N_CDN;
|
|
292
|
+
const chunk = opts.chunk ?? "index";
|
|
293
|
+
try {
|
|
294
|
+
const manifest = await fetchJson(
|
|
295
|
+
`${cdn}/labels/${opts.key}/${opts.profile}/manifest.json`,
|
|
296
|
+
opts.timeoutMs
|
|
297
|
+
);
|
|
298
|
+
const fileUrl = manifest[chunk];
|
|
299
|
+
if (!fileUrl) return null;
|
|
300
|
+
return await fetchJson(fileUrl, opts.timeoutMs);
|
|
301
|
+
} catch {
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
var _server = null;
|
|
306
|
+
function configureShipeasyServer(opts) {
|
|
307
|
+
if (_server) return _server;
|
|
308
|
+
_server = new FlagsClient(opts);
|
|
309
|
+
return _server;
|
|
310
|
+
}
|
|
311
|
+
function getShipeasyServerClient() {
|
|
312
|
+
return _server;
|
|
313
|
+
}
|
|
314
|
+
function _resetShipeasyServerForTests() {
|
|
315
|
+
_server?.destroy();
|
|
316
|
+
_server = null;
|
|
317
|
+
}
|
|
318
|
+
var flags = {
|
|
319
|
+
configure(opts) {
|
|
320
|
+
configureShipeasyServer(opts);
|
|
321
|
+
},
|
|
322
|
+
/**
|
|
323
|
+
* Long-running server: starts the background poll. Call once at app boot.
|
|
324
|
+
* Throws if the initial fetch fails (caller decides whether to crash or degrade).
|
|
325
|
+
*/
|
|
326
|
+
init() {
|
|
327
|
+
if (!_server) throw new Error("[shipeasy] flags.init called before configure()");
|
|
328
|
+
return _server.init();
|
|
329
|
+
},
|
|
330
|
+
/** Serverless / edge: fetch rules once, no background timer. */
|
|
331
|
+
initOnce() {
|
|
332
|
+
if (!_server) throw new Error("[shipeasy] flags.initOnce called before configure()");
|
|
333
|
+
return _server.initOnce();
|
|
334
|
+
},
|
|
335
|
+
/** Stop background timers. Safe to call repeatedly. */
|
|
336
|
+
destroy() {
|
|
337
|
+
_server?.destroy();
|
|
338
|
+
},
|
|
339
|
+
get(name, user) {
|
|
340
|
+
return _server?.getFlag(name, user) ?? false;
|
|
341
|
+
},
|
|
342
|
+
getConfig(name, decode) {
|
|
343
|
+
return _server?.getConfig(name, decode);
|
|
344
|
+
},
|
|
345
|
+
getExperiment(name, user, defaultParams, decode) {
|
|
346
|
+
return _server?.getExperiment(name, user, defaultParams, decode) ?? {
|
|
347
|
+
inExperiment: false,
|
|
348
|
+
group: "control",
|
|
349
|
+
params: defaultParams
|
|
350
|
+
};
|
|
351
|
+
},
|
|
352
|
+
track(userId, eventName, props) {
|
|
353
|
+
_server?.track(userId, eventName, props);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
268
356
|
// Annotate the CommonJS export names for ESM import in node:
|
|
269
357
|
0 && (module.exports = {
|
|
270
358
|
FlagsClient,
|
|
359
|
+
_resetShipeasyServerForTests,
|
|
360
|
+
configureShipeasyServer,
|
|
361
|
+
fetchLabelsForSSR,
|
|
362
|
+
flags,
|
|
363
|
+
getShipeasyServerClient,
|
|
271
364
|
version
|
|
272
365
|
});
|
package/dist/server/index.mjs
CHANGED
|
@@ -102,6 +102,7 @@ function evalGateInternal(gate, user) {
|
|
|
102
102
|
var FlagsClient = class {
|
|
103
103
|
apiKey;
|
|
104
104
|
baseUrl;
|
|
105
|
+
env;
|
|
105
106
|
flagsBlob = null;
|
|
106
107
|
expsBlob = null;
|
|
107
108
|
flagsEtag = null;
|
|
@@ -112,6 +113,7 @@ var FlagsClient = class {
|
|
|
112
113
|
constructor(opts) {
|
|
113
114
|
this.apiKey = opts.apiKey;
|
|
114
115
|
this.baseUrl = (opts.baseUrl ?? "https://edge.shipeasy.dev").replace(/\/$/, "");
|
|
116
|
+
this.env = opts.env ?? "prod";
|
|
115
117
|
}
|
|
116
118
|
async init() {
|
|
117
119
|
await this.fetchAll();
|
|
@@ -149,7 +151,7 @@ var FlagsClient = class {
|
|
|
149
151
|
async fetchFlags() {
|
|
150
152
|
const headers = { "X-SDK-Key": this.apiKey };
|
|
151
153
|
if (this.flagsEtag) headers["If-None-Match"] = this.flagsEtag;
|
|
152
|
-
const res = await globalThis.fetch(`${this.baseUrl}/sdk/flags`, { headers });
|
|
154
|
+
const res = await globalThis.fetch(`${this.baseUrl}/sdk/flags?env=${this.env}`, { headers });
|
|
153
155
|
const interval = Number(res.headers.get("X-Poll-Interval") ?? "30") || 30;
|
|
154
156
|
if (res.status === 304) return interval;
|
|
155
157
|
if (!res.ok) throw new Error(`/sdk/flags returned ${res.status}`);
|
|
@@ -240,7 +242,93 @@ var FlagsClient = class {
|
|
|
240
242
|
}).catch((err) => console.warn("[shipeasy] track failed:", String(err)));
|
|
241
243
|
}
|
|
242
244
|
};
|
|
245
|
+
var DEFAULT_I18N_CDN = "https://cdn.i18n.shipeasy.ai";
|
|
246
|
+
async function fetchJson(url, timeoutMs = 2e3) {
|
|
247
|
+
const controller = new AbortController();
|
|
248
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
249
|
+
try {
|
|
250
|
+
const res = await fetch(url, {
|
|
251
|
+
signal: controller.signal,
|
|
252
|
+
next: { revalidate: 60 }
|
|
253
|
+
});
|
|
254
|
+
if (!res.ok) throw new Error(`HTTP ${res.status} fetching ${url}`);
|
|
255
|
+
return res.json();
|
|
256
|
+
} finally {
|
|
257
|
+
clearTimeout(timer);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function fetchLabelsForSSR(opts) {
|
|
261
|
+
const cdn = opts.cdnBaseUrl ?? DEFAULT_I18N_CDN;
|
|
262
|
+
const chunk = opts.chunk ?? "index";
|
|
263
|
+
try {
|
|
264
|
+
const manifest = await fetchJson(
|
|
265
|
+
`${cdn}/labels/${opts.key}/${opts.profile}/manifest.json`,
|
|
266
|
+
opts.timeoutMs
|
|
267
|
+
);
|
|
268
|
+
const fileUrl = manifest[chunk];
|
|
269
|
+
if (!fileUrl) return null;
|
|
270
|
+
return await fetchJson(fileUrl, opts.timeoutMs);
|
|
271
|
+
} catch {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
var _server = null;
|
|
276
|
+
function configureShipeasyServer(opts) {
|
|
277
|
+
if (_server) return _server;
|
|
278
|
+
_server = new FlagsClient(opts);
|
|
279
|
+
return _server;
|
|
280
|
+
}
|
|
281
|
+
function getShipeasyServerClient() {
|
|
282
|
+
return _server;
|
|
283
|
+
}
|
|
284
|
+
function _resetShipeasyServerForTests() {
|
|
285
|
+
_server?.destroy();
|
|
286
|
+
_server = null;
|
|
287
|
+
}
|
|
288
|
+
var flags = {
|
|
289
|
+
configure(opts) {
|
|
290
|
+
configureShipeasyServer(opts);
|
|
291
|
+
},
|
|
292
|
+
/**
|
|
293
|
+
* Long-running server: starts the background poll. Call once at app boot.
|
|
294
|
+
* Throws if the initial fetch fails (caller decides whether to crash or degrade).
|
|
295
|
+
*/
|
|
296
|
+
init() {
|
|
297
|
+
if (!_server) throw new Error("[shipeasy] flags.init called before configure()");
|
|
298
|
+
return _server.init();
|
|
299
|
+
},
|
|
300
|
+
/** Serverless / edge: fetch rules once, no background timer. */
|
|
301
|
+
initOnce() {
|
|
302
|
+
if (!_server) throw new Error("[shipeasy] flags.initOnce called before configure()");
|
|
303
|
+
return _server.initOnce();
|
|
304
|
+
},
|
|
305
|
+
/** Stop background timers. Safe to call repeatedly. */
|
|
306
|
+
destroy() {
|
|
307
|
+
_server?.destroy();
|
|
308
|
+
},
|
|
309
|
+
get(name, user) {
|
|
310
|
+
return _server?.getFlag(name, user) ?? false;
|
|
311
|
+
},
|
|
312
|
+
getConfig(name, decode) {
|
|
313
|
+
return _server?.getConfig(name, decode);
|
|
314
|
+
},
|
|
315
|
+
getExperiment(name, user, defaultParams, decode) {
|
|
316
|
+
return _server?.getExperiment(name, user, defaultParams, decode) ?? {
|
|
317
|
+
inExperiment: false,
|
|
318
|
+
group: "control",
|
|
319
|
+
params: defaultParams
|
|
320
|
+
};
|
|
321
|
+
},
|
|
322
|
+
track(userId, eventName, props) {
|
|
323
|
+
_server?.track(userId, eventName, props);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
243
326
|
export {
|
|
244
327
|
FlagsClient,
|
|
328
|
+
_resetShipeasyServerForTests,
|
|
329
|
+
configureShipeasyServer,
|
|
330
|
+
fetchLabelsForSSR,
|
|
331
|
+
flags,
|
|
332
|
+
getShipeasyServerClient,
|
|
245
333
|
version
|
|
246
334
|
};
|
package/package.json
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipeasy/sdk",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Shipeasy SDK — feature gates, runtime configs, experiments, and metrics for the Shipeasy hosted service.",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"homepage": "https://shipeasy.ai",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/shipeasy-ai/sdk.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/shipeasy-ai/sdk/issues"
|
|
13
|
+
},
|
|
5
14
|
"main": "./dist/server/index.js",
|
|
6
15
|
"module": "./dist/server/index.mjs",
|
|
7
16
|
"browser": "./dist/client/index.js",
|
|
17
|
+
"typesVersions": {
|
|
18
|
+
"*": {
|
|
19
|
+
"client": [
|
|
20
|
+
"./dist/client/index.d.ts"
|
|
21
|
+
],
|
|
22
|
+
"server": [
|
|
23
|
+
"./dist/server/index.d.ts"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
8
27
|
"exports": {
|
|
9
28
|
".": {
|
|
10
29
|
"node": "./dist/server/index.js",
|
|
@@ -23,13 +42,18 @@
|
|
|
23
42
|
"./templates/*": "./templates/*.js"
|
|
24
43
|
},
|
|
25
44
|
"files": [
|
|
26
|
-
"dist/",
|
|
27
|
-
"
|
|
45
|
+
"dist/server/",
|
|
46
|
+
"dist/client/",
|
|
47
|
+
"templates/",
|
|
48
|
+
"LICENSE",
|
|
49
|
+
"README.md"
|
|
28
50
|
],
|
|
29
51
|
"scripts": {
|
|
30
52
|
"build": "tsup",
|
|
31
53
|
"type-check": "tsc --noEmit",
|
|
32
|
-
"test": "vitest"
|
|
54
|
+
"test": "vitest run",
|
|
55
|
+
"test:watch": "vitest",
|
|
56
|
+
"publish-loader": "wrangler r2 object put shipeasy-sdk/loader-v$npm_package_version.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=31536000, immutable' --remote && wrangler r2 object put shipeasy-sdk/loader.js --file=dist/loader/loader.global.js --content-type 'application/javascript; charset=utf-8' --cache-control 'public, max-age=300' --remote"
|
|
33
57
|
},
|
|
34
58
|
"dependencies": {
|
|
35
59
|
"murmurhash-js": "^1.0.0"
|
|
@@ -38,7 +62,8 @@
|
|
|
38
62
|
"@types/murmurhash-js": "^1.0.6",
|
|
39
63
|
"tsup": "^8.3.0",
|
|
40
64
|
"typescript": "^5.7.4",
|
|
41
|
-
"vitest": "^2.1.0"
|
|
65
|
+
"vitest": "^2.1.0",
|
|
66
|
+
"wrangler": "^4.83.0"
|
|
42
67
|
},
|
|
43
68
|
"peerDependencies": {
|
|
44
69
|
"zod": "^4.0.0"
|
|
@@ -50,5 +75,8 @@
|
|
|
50
75
|
},
|
|
51
76
|
"publishConfig": {
|
|
52
77
|
"access": "public"
|
|
78
|
+
},
|
|
79
|
+
"engines": {
|
|
80
|
+
"node": ">=20"
|
|
53
81
|
}
|
|
54
82
|
}
|