propline 0.44.0 → 0.46.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/README.md +70 -0
- package/dist/index.cjs +134 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +119 -2
- package/dist/index.d.ts +119 -2
- package/dist/index.js +134 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -805,6 +805,7 @@ Each POST carries these headers:
|
|
|
805
805
|
| `X-PropLine-Timestamp` | Unix seconds |
|
|
806
806
|
| `X-PropLine-Signature` | HMAC-SHA256 over `${timestamp}.` + body |
|
|
807
807
|
| `X-PropLine-Delivery` | Stable delivery id (use for idempotency) |
|
|
808
|
+
| `X-PropLine-Sequence` | Your subscription's own event counter (use for replay) |
|
|
808
809
|
|
|
809
810
|
```ts
|
|
810
811
|
import express from "express";
|
|
@@ -936,6 +937,75 @@ await client.listWebhookDeliveries(whId, { limit: 200, beforeId: 123456 });
|
|
|
936
937
|
await client.deleteWebhook(whId);
|
|
937
938
|
```
|
|
938
939
|
|
|
940
|
+
### Catching up after an outage
|
|
941
|
+
|
|
942
|
+
Every delivery carries `X-PropLine-Sequence` — a counter monotonic *within your
|
|
943
|
+
subscription*. Store the highest one you processed, then read forward from it.
|
|
944
|
+
Do not use `X-PropLine-Delivery` as the cursor: that id is global across all
|
|
945
|
+
subscriptions, so its gaps are other customers' traffic.
|
|
946
|
+
|
|
947
|
+
```ts
|
|
948
|
+
let cursor = await loadMyCursor(); // highest X-PropLine-Sequence processed
|
|
949
|
+
|
|
950
|
+
for (;;) {
|
|
951
|
+
const page = await client.replayWebhookEvents(whId, { sinceSeq: cursor, limit: 100 });
|
|
952
|
+
|
|
953
|
+
if (page.truncated) {
|
|
954
|
+
// Events after your cursor aged out of retention and are gone.
|
|
955
|
+
// Resync from the REST endpoints rather than assume you are current.
|
|
956
|
+
await resyncFromRest();
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
for (const ev of page.events) { // oldest first
|
|
960
|
+
await handle(ev.event_type, ev.data);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
cursor = page.next_seq;
|
|
964
|
+
await saveMyCursor(cursor);
|
|
965
|
+
if (!page.has_more) {
|
|
966
|
+
console.log(`behind by ${page.latest_seq - cursor} events`);
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
```
|
|
971
|
+
|
|
972
|
+
### Websocket streaming
|
|
973
|
+
|
|
974
|
+
If your stack already speaks websockets — or you can't host a public HTTPS
|
|
975
|
+
endpoint — connect a socket instead of receiving POSTs. Same events, same
|
|
976
|
+
filters, same `seq`: a stream and a webhook are the **same subscription with a
|
|
977
|
+
different transport**.
|
|
978
|
+
|
|
979
|
+
```ts
|
|
980
|
+
const wh = await client.createWebhook({
|
|
981
|
+
transport: "websocket", // no url — there is nowhere to POST
|
|
982
|
+
events: ["line_movement"],
|
|
983
|
+
filterSportKey: "baseball_mlb",
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
for await (const ev of client.stream({ webhookId: wh.id, sinceSeq: myCursor })) {
|
|
987
|
+
await handle(ev.event_type, ev.data);
|
|
988
|
+
myCursor = ev.seq; // persist it; this is your resume point
|
|
989
|
+
}
|
|
990
|
+
```
|
|
991
|
+
|
|
992
|
+
Reconnects and resumes from the last `seq` automatically, so a dropped
|
|
993
|
+
connection is not a gap in your data. `onTruncated` fires when events after
|
|
994
|
+
your cursor aged out of retention — the one case streaming cannot make you
|
|
995
|
+
whole, where you should resync from REST. Zero dependencies: it uses Node's
|
|
996
|
+
built-in `WebSocket` (Node 22+).
|
|
997
|
+
|
|
998
|
+
Concurrent connections are capped per plan (Streaming Lite 2, Streaming 5).
|
|
999
|
+
Delivered events are **not** metered.
|
|
1000
|
+
|
|
1001
|
+
Replay is bounded by delivery retention: 2 days, and at most 5,000 deliveries
|
|
1002
|
+
per subscription. `latest_seq` is **not** subject to retention, so
|
|
1003
|
+
`latest_seq - next_seq` stays honest even after the rows are pruned. Sequence
|
|
1004
|
+
numbers always increase and never repeat but are not guaranteed to be dense —
|
|
1005
|
+
treat a skipped number as normal, and read `truncated` for real loss. Neither
|
|
1006
|
+
`replayWebhookEvents` nor `listWebhookDeliveries` counts against your daily
|
|
1007
|
+
quota.
|
|
1008
|
+
|
|
939
1009
|
## Error handling
|
|
940
1010
|
|
|
941
1011
|
```ts
|
package/dist/index.cjs
CHANGED
|
@@ -818,6 +818,139 @@ var PropLine = class {
|
|
|
818
818
|
{ params: { limit: options.limit ?? 50, before_id: options.beforeId } }
|
|
819
819
|
);
|
|
820
820
|
}
|
|
821
|
+
/**
|
|
822
|
+
* Re-read this subscription's events in order, from a cursor.
|
|
823
|
+
*
|
|
824
|
+
* Answers "my endpoint was down — what did I miss?". Every delivery carries
|
|
825
|
+
* an `X-PropLine-Sequence` header: a counter monotonic *within your
|
|
826
|
+
* subscription*. Store the highest one you processed and pass it as
|
|
827
|
+
* `sinceSeq`.
|
|
828
|
+
*
|
|
829
|
+
* Do NOT use `X-PropLine-Delivery` as the cursor — that id is global across
|
|
830
|
+
* every subscription, so its gaps are other customers' traffic.
|
|
831
|
+
*
|
|
832
|
+
* Events come back oldest-first (the opposite of `listWebhookDeliveries`,
|
|
833
|
+
* which is a newest-first debugging log). Page by passing `next_seq` back
|
|
834
|
+
* as `sinceSeq` while `has_more` is true.
|
|
835
|
+
*
|
|
836
|
+
* **Check `truncated`.** True means events after your cursor have aged out
|
|
837
|
+
* of retention (2 days, max 5,000 deliveries per subscription) and are gone
|
|
838
|
+
* — resync from the REST endpoints instead of assuming you are current.
|
|
839
|
+
*
|
|
840
|
+
* Does not count against your daily request quota.
|
|
841
|
+
*/
|
|
842
|
+
replayWebhookEvents(webhookId, options = {}) {
|
|
843
|
+
return this._request(
|
|
844
|
+
"GET",
|
|
845
|
+
`/webhooks/${webhookId}/replay`,
|
|
846
|
+
{ params: { since_seq: options.sinceSeq ?? 0, limit: options.limit ?? 100 } }
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Stream a websocket subscription as an async iterable.
|
|
851
|
+
*
|
|
852
|
+
* ```ts
|
|
853
|
+
* for await (const ev of client.stream({ webhookId: 12, sinceSeq: 4180 })) {
|
|
854
|
+
* console.log(ev.seq, ev.event_type, ev.data);
|
|
855
|
+
* }
|
|
856
|
+
* ```
|
|
857
|
+
*
|
|
858
|
+
* The subscription must have been created with `transport: "websocket"`.
|
|
859
|
+
* Same events, same filters, same `seq` as an HTTP webhook — one
|
|
860
|
+
* subscription, different transport.
|
|
861
|
+
*
|
|
862
|
+
* **Reconnects automatically and resumes from the last `seq` it saw**, which
|
|
863
|
+
* is the whole point of the sequence: a dropped connection does not become a
|
|
864
|
+
* gap in your data. Set `reconnect: false` to get a single connection that
|
|
865
|
+
* ends when the socket closes.
|
|
866
|
+
*
|
|
867
|
+
* If the server reports `truncated` — events after your cursor aged out of
|
|
868
|
+
* retention and are gone — `onTruncated` fires. Handle it: that is the one
|
|
869
|
+
* case where the stream cannot make you whole and you should resync from the
|
|
870
|
+
* REST endpoints.
|
|
871
|
+
*/
|
|
872
|
+
async *stream(options) {
|
|
873
|
+
const wsBase = (options.wsUrl ?? this.baseUrl).replace(/^http:/, "ws:").replace(/^https:/, "wss:").replace(/\/v1\/?$/, "");
|
|
874
|
+
const url = `${wsBase}/v1/stream`;
|
|
875
|
+
let cursor = options.sinceSeq ?? 0;
|
|
876
|
+
let attempt = 0;
|
|
877
|
+
for (; ; ) {
|
|
878
|
+
const queue = [];
|
|
879
|
+
let notify = null;
|
|
880
|
+
let closed = null;
|
|
881
|
+
let opened = false;
|
|
882
|
+
const ws = new WebSocket(url);
|
|
883
|
+
const wake = () => {
|
|
884
|
+
const n = notify;
|
|
885
|
+
notify = null;
|
|
886
|
+
n?.();
|
|
887
|
+
};
|
|
888
|
+
ws.addEventListener("open", () => {
|
|
889
|
+
opened = true;
|
|
890
|
+
ws.send(JSON.stringify({
|
|
891
|
+
type: "auth",
|
|
892
|
+
api_key: this.apiKey,
|
|
893
|
+
webhook_id: options.webhookId,
|
|
894
|
+
since_seq: cursor
|
|
895
|
+
}));
|
|
896
|
+
});
|
|
897
|
+
ws.addEventListener("message", (e) => {
|
|
898
|
+
let msg;
|
|
899
|
+
try {
|
|
900
|
+
msg = JSON.parse(String(e.data));
|
|
901
|
+
} catch {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
if (msg.type === "ready") {
|
|
905
|
+
attempt = 0;
|
|
906
|
+
if (msg.truncated) options.onTruncated?.(msg);
|
|
907
|
+
options.onReady?.(msg);
|
|
908
|
+
} else if (msg.type === "event") {
|
|
909
|
+
queue.push(msg);
|
|
910
|
+
wake();
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
ws.addEventListener("close", (e) => {
|
|
914
|
+
const terminal = [4400, 4401, 4403, 4404].includes(e.code);
|
|
915
|
+
closed = new PropLineError(
|
|
916
|
+
e.code,
|
|
917
|
+
`stream closed${e.reason ? `: ${e.reason}` : ""}`
|
|
918
|
+
);
|
|
919
|
+
closed.terminal = terminal;
|
|
920
|
+
wake();
|
|
921
|
+
});
|
|
922
|
+
ws.addEventListener("error", () => {
|
|
923
|
+
if (!closed) closed = new PropLineError(0, "stream connection error");
|
|
924
|
+
wake();
|
|
925
|
+
});
|
|
926
|
+
try {
|
|
927
|
+
for (; ; ) {
|
|
928
|
+
while (queue.length) {
|
|
929
|
+
const ev = queue.shift();
|
|
930
|
+
cursor = ev.seq;
|
|
931
|
+
yield ev;
|
|
932
|
+
}
|
|
933
|
+
if (closed) break;
|
|
934
|
+
await new Promise((r) => {
|
|
935
|
+
notify = r;
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
} finally {
|
|
939
|
+
try {
|
|
940
|
+
ws.close();
|
|
941
|
+
} catch {
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const err = closed;
|
|
945
|
+
if (err?.terminal) throw err;
|
|
946
|
+
if (options.reconnect === false) {
|
|
947
|
+
if (err && !opened) throw err;
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
const delayMs = Math.min(3e4, 500 * 2 ** attempt++);
|
|
951
|
+
await new Promise((r) => setTimeout(r, delayMs));
|
|
952
|
+
}
|
|
953
|
+
}
|
|
821
954
|
/**
|
|
822
955
|
* Grade placed bets against their closing lines (CLV).
|
|
823
956
|
*
|
|
@@ -958,7 +1091,7 @@ var Bookmakers = {
|
|
|
958
1091
|
POLYMARKET: "polymarket",
|
|
959
1092
|
PRIZEPICKS: "prizepicks"
|
|
960
1093
|
};
|
|
961
|
-
var VERSION = "0.
|
|
1094
|
+
var VERSION = "0.46.0";
|
|
962
1095
|
// Annotate the CommonJS export names for ESM import in node:
|
|
963
1096
|
0 && (module.exports = {
|
|
964
1097
|
AuthError,
|