@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.
@@ -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 };
@@ -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
  });
@@ -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.0.0",
4
- "description": "Feature flag and experimentation SDK",
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
- "templates/"
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
  }