@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 +88 -2
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +88 -2
- package/package.json +1 -1
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.
|
|
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
|
|
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
package/dist/index.d.ts
CHANGED
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.
|
|
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
|
|
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}`);
|