@feathq/js-sdk 0.2.0 → 0.3.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/dist/index.cjs CHANGED
@@ -145,7 +145,7 @@ var SseParser = class {
145
145
  };
146
146
 
147
147
  // src/version.ts
148
- var SDK_VERSION = "0.2.0";
148
+ var SDK_VERSION = "0.3.0";
149
149
 
150
150
  // src/client.ts
151
151
  var MIN_POLL_INTERVAL_MS = 5e3;
@@ -292,7 +292,16 @@ var FeatClient = class {
292
292
  this.scheduleNextPoll();
293
293
  }
294
294
  handleFrame(frame) {
295
- if (frame.event !== "put") return;
295
+ if (frame.event === "put") {
296
+ this.handlePutFrame(frame);
297
+ return;
298
+ }
299
+ if (frame.event === "patch") {
300
+ this.handlePatchFrame(frame);
301
+ return;
302
+ }
303
+ }
304
+ handlePutFrame(frame) {
296
305
  let next;
297
306
  try {
298
307
  next = JSON.parse(frame.data);
@@ -302,6 +311,49 @@ var FeatClient = class {
302
311
  }
303
312
  this.adoptDatafile(next);
304
313
  }
314
+ // Apply an incremental `patch` frame in place. The patch is gated on the
315
+ // base `from` version: it only applies when it lines up exactly with the
316
+ // datafile we currently hold. On any gap (or before we have bootstrapped a
317
+ // datafile at all) the patch is dropped; the reconnect reseed (a full `put`)
318
+ // and the safety-net poll keep the client correct.
319
+ handlePatchFrame(frame) {
320
+ let patch;
321
+ try {
322
+ patch = parseDatafilePatch(JSON.parse(frame.data));
323
+ } catch {
324
+ warn("ignoring stream frame with invalid patch JSON");
325
+ return;
326
+ }
327
+ if (!patch) {
328
+ warn("ignoring malformed datafile patch");
329
+ return;
330
+ }
331
+ this.applyPatch(patch);
332
+ }
333
+ // Merge a version-matched delta atomically: build the next datafile in full,
334
+ // then swap it in so a partially-applied patch is never observable. Returns
335
+ // true if applied. Updates the ETag the conditional poll sends so the
336
+ // safety-net poll 304s instead of re-downloading the whole datafile.
337
+ applyPatch(patch) {
338
+ const current = this.datafile;
339
+ if (!current || current.version !== patch.from) return false;
340
+ const flags = { ...current.flags };
341
+ for (const [key, flag] of Object.entries(patch.flags)) flags[key] = flag;
342
+ for (const key of patch.removedFlags) delete flags[key];
343
+ const segments = { ...current.segments };
344
+ for (const [key, segment] of Object.entries(patch.segments)) segments[key] = segment;
345
+ for (const key of patch.removedSegments) delete segments[key];
346
+ this.datafile = {
347
+ ...current,
348
+ flags,
349
+ segments,
350
+ version: patch.to,
351
+ etag: patch.etag,
352
+ generatedAt: patch.generatedAt
353
+ };
354
+ this.etag = patch.etag;
355
+ return true;
356
+ }
305
357
  // Adopt a datafile only if its version is strictly newer than what we
306
358
  // hold. Equal or older versions are ignored so out-of-order pushes or a
307
359
  // stale poll can never roll the datafile backwards. Returns true if
@@ -340,6 +392,40 @@ var FeatClient = class {
340
392
  return adopted;
341
393
  }
342
394
  };
395
+ function parseDatafilePatch(raw) {
396
+ if (typeof raw !== "object" || raw === null) return null;
397
+ const p = raw;
398
+ if (typeof p.from !== "number" || typeof p.to !== "number") return null;
399
+ if (!Number.isInteger(p.from) || !Number.isInteger(p.to) || p.to <= p.from) return null;
400
+ if (typeof p.etag !== "string" || typeof p.generatedAt !== "string") return null;
401
+ const flags = asRecord(p.flags);
402
+ const segments = asRecord(p.segments);
403
+ const removedFlags = asStringArray(p.removedFlags);
404
+ const removedSegments = asStringArray(p.removedSegments);
405
+ if (flags === null || segments === null) return null;
406
+ if (removedFlags === null || removedSegments === null) return null;
407
+ return {
408
+ from: p.from,
409
+ to: p.to,
410
+ etag: p.etag,
411
+ generatedAt: p.generatedAt,
412
+ flags,
413
+ removedFlags,
414
+ segments,
415
+ removedSegments
416
+ };
417
+ }
418
+ function asRecord(value) {
419
+ if (value === void 0) return {};
420
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
421
+ return value;
422
+ }
423
+ function asStringArray(value) {
424
+ if (value === void 0) return [];
425
+ if (!Array.isArray(value)) return null;
426
+ if (value.some((item) => typeof item !== "string")) return null;
427
+ return value;
428
+ }
343
429
  function warn(message, err) {
344
430
  if (err === void 0) {
345
431
  console.warn(`feat: ${message}`);
package/dist/index.d.cts CHANGED
@@ -54,6 +54,9 @@ declare class FeatClient {
54
54
  private runStreamLoop;
55
55
  private setStreamConnected;
56
56
  private handleFrame;
57
+ private handlePutFrame;
58
+ private handlePatchFrame;
59
+ private applyPatch;
57
60
  private adoptDatafile;
58
61
  private fetchDatafile;
59
62
  }
package/dist/index.d.ts CHANGED
@@ -54,6 +54,9 @@ declare class FeatClient {
54
54
  private runStreamLoop;
55
55
  private setStreamConnected;
56
56
  private handleFrame;
57
+ private handlePutFrame;
58
+ private handlePatchFrame;
59
+ private applyPatch;
57
60
  private adoptDatafile;
58
61
  private fetchDatafile;
59
62
  }
package/dist/index.js CHANGED
@@ -144,7 +144,7 @@ var SseParser = class {
144
144
  };
145
145
 
146
146
  // src/version.ts
147
- var SDK_VERSION = "0.2.0";
147
+ var SDK_VERSION = "0.3.0";
148
148
 
149
149
  // src/client.ts
150
150
  var MIN_POLL_INTERVAL_MS = 5e3;
@@ -291,7 +291,16 @@ var FeatClient = class {
291
291
  this.scheduleNextPoll();
292
292
  }
293
293
  handleFrame(frame) {
294
- if (frame.event !== "put") return;
294
+ if (frame.event === "put") {
295
+ this.handlePutFrame(frame);
296
+ return;
297
+ }
298
+ if (frame.event === "patch") {
299
+ this.handlePatchFrame(frame);
300
+ return;
301
+ }
302
+ }
303
+ handlePutFrame(frame) {
295
304
  let next;
296
305
  try {
297
306
  next = JSON.parse(frame.data);
@@ -301,6 +310,49 @@ var FeatClient = class {
301
310
  }
302
311
  this.adoptDatafile(next);
303
312
  }
313
+ // Apply an incremental `patch` frame in place. The patch is gated on the
314
+ // base `from` version: it only applies when it lines up exactly with the
315
+ // datafile we currently hold. On any gap (or before we have bootstrapped a
316
+ // datafile at all) the patch is dropped; the reconnect reseed (a full `put`)
317
+ // and the safety-net poll keep the client correct.
318
+ handlePatchFrame(frame) {
319
+ let patch;
320
+ try {
321
+ patch = parseDatafilePatch(JSON.parse(frame.data));
322
+ } catch {
323
+ warn("ignoring stream frame with invalid patch JSON");
324
+ return;
325
+ }
326
+ if (!patch) {
327
+ warn("ignoring malformed datafile patch");
328
+ return;
329
+ }
330
+ this.applyPatch(patch);
331
+ }
332
+ // Merge a version-matched delta atomically: build the next datafile in full,
333
+ // then swap it in so a partially-applied patch is never observable. Returns
334
+ // true if applied. Updates the ETag the conditional poll sends so the
335
+ // safety-net poll 304s instead of re-downloading the whole datafile.
336
+ applyPatch(patch) {
337
+ const current = this.datafile;
338
+ if (!current || current.version !== patch.from) return false;
339
+ const flags = { ...current.flags };
340
+ for (const [key, flag] of Object.entries(patch.flags)) flags[key] = flag;
341
+ for (const key of patch.removedFlags) delete flags[key];
342
+ const segments = { ...current.segments };
343
+ for (const [key, segment] of Object.entries(patch.segments)) segments[key] = segment;
344
+ for (const key of patch.removedSegments) delete segments[key];
345
+ this.datafile = {
346
+ ...current,
347
+ flags,
348
+ segments,
349
+ version: patch.to,
350
+ etag: patch.etag,
351
+ generatedAt: patch.generatedAt
352
+ };
353
+ this.etag = patch.etag;
354
+ return true;
355
+ }
304
356
  // Adopt a datafile only if its version is strictly newer than what we
305
357
  // hold. Equal or older versions are ignored so out-of-order pushes or a
306
358
  // stale poll can never roll the datafile backwards. Returns true if
@@ -339,6 +391,40 @@ var FeatClient = class {
339
391
  return adopted;
340
392
  }
341
393
  };
394
+ function parseDatafilePatch(raw) {
395
+ if (typeof raw !== "object" || raw === null) return null;
396
+ const p = raw;
397
+ if (typeof p.from !== "number" || typeof p.to !== "number") return null;
398
+ if (!Number.isInteger(p.from) || !Number.isInteger(p.to) || p.to <= p.from) return null;
399
+ if (typeof p.etag !== "string" || typeof p.generatedAt !== "string") return null;
400
+ const flags = asRecord(p.flags);
401
+ const segments = asRecord(p.segments);
402
+ const removedFlags = asStringArray(p.removedFlags);
403
+ const removedSegments = asStringArray(p.removedSegments);
404
+ if (flags === null || segments === null) return null;
405
+ if (removedFlags === null || removedSegments === null) return null;
406
+ return {
407
+ from: p.from,
408
+ to: p.to,
409
+ etag: p.etag,
410
+ generatedAt: p.generatedAt,
411
+ flags,
412
+ removedFlags,
413
+ segments,
414
+ removedSegments
415
+ };
416
+ }
417
+ function asRecord(value) {
418
+ if (value === void 0) return {};
419
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
420
+ return value;
421
+ }
422
+ function asStringArray(value) {
423
+ if (value === void 0) return [];
424
+ if (!Array.isArray(value)) return null;
425
+ if (value.some((item) => typeof item !== "string")) return null;
426
+ return value;
427
+ }
342
428
  function warn(message, err) {
343
429
  if (err === void 0) {
344
430
  console.warn(`feat: ${message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@feathq/js-sdk",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "feat feature-flag SDK for JavaScript and TypeScript (server-side, OpenFeature provider)",
5
5
  "keywords": [
6
6
  "feature-flags",