@maximem/synap-js-sdk 0.4.8 → 0.5.2
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/anticipation-cache-LRE3TLK4.cjs.map +1 -1
- package/dist/{chunk-OCMM2Z5P.js → chunk-BWMPKLFG.js} +40 -2
- package/dist/chunk-BWMPKLFG.js.map +1 -0
- package/dist/{chunk-T5QQBJ6T.js → chunk-DJPYUPSV.js} +530 -7
- package/dist/chunk-DJPYUPSV.js.map +1 -0
- package/dist/{chunk-2TVL5IPQ.cjs → chunk-T372OL3Q.cjs} +41 -3
- package/dist/chunk-T372OL3Q.cjs.map +1 -0
- package/dist/chunk-T6U5PVAD.cjs.map +1 -1
- package/dist/{chunk-7CWBRHUO.cjs → chunk-WWTGJZA7.cjs} +542 -19
- package/dist/chunk-WWTGJZA7.cjs.map +1 -0
- package/dist/chunk-ZKOJDUN4.cjs.map +1 -1
- package/dist/env-55ULHNJZ.cjs.map +1 -1
- package/dist/errors-Bl3vY1eb.d.cts +382 -0
- package/dist/errors-Bl3vY1eb.d.ts +382 -0
- package/dist/grpc/index.cjs +7 -7
- package/dist/grpc/index.cjs.map +1 -1
- package/dist/grpc/index.d.cts +50 -4
- package/dist/grpc/index.d.ts +50 -4
- package/dist/grpc/index.js +5 -5
- package/dist/index.cjs +278 -161
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -148
- package/dist/index.d.ts +97 -148
- package/dist/index.js +171 -54
- package/dist/index.js.map +1 -1
- package/dist/stream-client-5CGJNTH4.js +16 -0
- package/dist/stream-client-GJM4YSXW.cjs +16 -0
- package/dist/stream-client-GJM4YSXW.cjs.map +1 -0
- package/package.json +1 -1
- package/dist/anticipation-cache-C1rUEpaW.d.cts +0 -211
- package/dist/anticipation-cache-C1rUEpaW.d.ts +0 -211
- package/dist/chunk-2TVL5IPQ.cjs.map +0 -1
- package/dist/chunk-7CWBRHUO.cjs.map +0 -1
- package/dist/chunk-OCMM2Z5P.js.map +0 -1
- package/dist/chunk-T5QQBJ6T.js.map +0 -1
- package/dist/stream-client-2C5KIOWP.js +0 -7
- package/dist/stream-client-GPU37E6F.cjs +0 -7
- package/dist/stream-client-GPU37E6F.cjs.map +0 -1
- /package/dist/{stream-client-2C5KIOWP.js.map → stream-client-5CGJNTH4.js.map} +0 -0
|
@@ -1,5 +1,11 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
-
|
|
1
|
+
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { newObj[key] = obj[key]; } } } newObj.default = obj; return newObj; } } function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
var _chunkT372OL3Qcjs = require('./chunk-T372OL3Q.cjs');
|
|
6
|
+
|
|
7
|
+
// src/grpc/descriptor.ts
|
|
8
|
+
var PROTO_SHA = "c1d834043608ad4f";
|
|
3
9
|
var SYNAP_PROTO_DESCRIPTOR = {
|
|
4
10
|
"nested": {
|
|
5
11
|
"synap": {
|
|
@@ -202,6 +208,14 @@ var SYNAP_PROTO_DESCRIPTOR = {
|
|
|
202
208
|
"type": "string",
|
|
203
209
|
"id": 11
|
|
204
210
|
},
|
|
211
|
+
"tool_result_json": {
|
|
212
|
+
"type": "string",
|
|
213
|
+
"id": 15
|
|
214
|
+
},
|
|
215
|
+
"tool_call_id": {
|
|
216
|
+
"type": "string",
|
|
217
|
+
"id": 16
|
|
218
|
+
},
|
|
205
219
|
"search_queries": {
|
|
206
220
|
"rule": "repeated",
|
|
207
221
|
"type": "string",
|
|
@@ -216,6 +230,14 @@ var SYNAP_PROTO_DESCRIPTOR = {
|
|
|
216
230
|
"keyType": "string",
|
|
217
231
|
"type": "string",
|
|
218
232
|
"id": 14
|
|
233
|
+
},
|
|
234
|
+
"event_id": {
|
|
235
|
+
"type": "string",
|
|
236
|
+
"id": 17
|
|
237
|
+
},
|
|
238
|
+
"sent_at_ms": {
|
|
239
|
+
"type": "int64",
|
|
240
|
+
"id": 18
|
|
219
241
|
}
|
|
220
242
|
}
|
|
221
243
|
},
|
|
@@ -257,7 +279,8 @@ var SYNAP_PROTO_DESCRIPTOR = {
|
|
|
257
279
|
"oneof": [
|
|
258
280
|
"context_bundle",
|
|
259
281
|
"heartbeat_pong",
|
|
260
|
-
"signal"
|
|
282
|
+
"signal",
|
|
283
|
+
"event_ack"
|
|
261
284
|
]
|
|
262
285
|
}
|
|
263
286
|
},
|
|
@@ -273,6 +296,23 @@ var SYNAP_PROTO_DESCRIPTOR = {
|
|
|
273
296
|
"signal": {
|
|
274
297
|
"type": "StreamSignal",
|
|
275
298
|
"id": 3
|
|
299
|
+
},
|
|
300
|
+
"event_ack": {
|
|
301
|
+
"type": "EventAck",
|
|
302
|
+
"id": 4
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
},
|
|
306
|
+
"EventAck": {
|
|
307
|
+
"fields": {
|
|
308
|
+
"event_ids": {
|
|
309
|
+
"rule": "repeated",
|
|
310
|
+
"type": "string",
|
|
311
|
+
"id": 1
|
|
312
|
+
},
|
|
313
|
+
"timestamp_ms": {
|
|
314
|
+
"type": "int64",
|
|
315
|
+
"id": 2
|
|
276
316
|
}
|
|
277
317
|
}
|
|
278
318
|
},
|
|
@@ -616,6 +656,192 @@ var SYNAP_PROTO_DESCRIPTOR = {
|
|
|
616
656
|
}
|
|
617
657
|
};
|
|
618
658
|
|
|
659
|
+
// src/grpc/outbox-journal.ts
|
|
660
|
+
var OP_QUEUED = "q";
|
|
661
|
+
var OP_UNACKED = "u";
|
|
662
|
+
var OP_ACKED = "a";
|
|
663
|
+
var OP_DROPPED = "d";
|
|
664
|
+
var COMPACT_AT_LINES = 500;
|
|
665
|
+
var _fs = null;
|
|
666
|
+
var _fsResolved = false;
|
|
667
|
+
var _fsPending = null;
|
|
668
|
+
async function resolveFs() {
|
|
669
|
+
if (_fsResolved) return;
|
|
670
|
+
if (_fsPending === null) {
|
|
671
|
+
_fsPending = (async () => {
|
|
672
|
+
try {
|
|
673
|
+
_fs = await Promise.resolve().then(() => _interopRequireWildcard(require("fs")));
|
|
674
|
+
} catch (e2) {
|
|
675
|
+
_fs = null;
|
|
676
|
+
}
|
|
677
|
+
_fsResolved = true;
|
|
678
|
+
})();
|
|
679
|
+
}
|
|
680
|
+
await _fsPending;
|
|
681
|
+
}
|
|
682
|
+
function defaultOutboxRoot() {
|
|
683
|
+
try {
|
|
684
|
+
const env = _optionalChain([globalThis, 'access', _ => _.process, 'optionalAccess', _2 => _2.env]);
|
|
685
|
+
const home = _nullishCoalesce(_nullishCoalesce(_optionalChain([env, 'optionalAccess', _3 => _3["SYNAP_STORAGE_PATH"]]), () => ( _optionalChain([env, 'optionalAccess', _4 => _4["HOME"]]))), () => ( _optionalChain([env, 'optionalAccess', _5 => _5["USERPROFILE"]])));
|
|
686
|
+
if (home !== void 0 && home !== "") {
|
|
687
|
+
return _optionalChain([env, 'optionalAccess', _6 => _6["SYNAP_STORAGE_PATH"]]) !== void 0 && env["SYNAP_STORAGE_PATH"] !== "" ? env["SYNAP_STORAGE_PATH"] : `${home}/.synap`;
|
|
688
|
+
}
|
|
689
|
+
} catch (e3) {
|
|
690
|
+
}
|
|
691
|
+
return "";
|
|
692
|
+
}
|
|
693
|
+
var OutboxJournal = class {
|
|
694
|
+
|
|
695
|
+
#fs;
|
|
696
|
+
#lines = 0;
|
|
697
|
+
#disabled = false;
|
|
698
|
+
/**
|
|
699
|
+
* @param root Directory the journal lives under. One file per instance id,
|
|
700
|
+
* because two clients on one instance share a stream in the
|
|
701
|
+
* registry and so the events are the instance's, not the
|
|
702
|
+
* object's.
|
|
703
|
+
*/
|
|
704
|
+
/**
|
|
705
|
+
* Resolve `node:fs` once for the process. The stream client awaits this in
|
|
706
|
+
* `connect()` before any event can flow, so by the time anything is written
|
|
707
|
+
* the module is in hand and every append is a plain synchronous call. An
|
|
708
|
+
* async write still pending when the process dies is a write that did not
|
|
709
|
+
* happen, and this file exists for exactly that case.
|
|
710
|
+
*/
|
|
711
|
+
static async init() {
|
|
712
|
+
await resolveFs();
|
|
713
|
+
}
|
|
714
|
+
constructor(root, instanceId, fsImpl) {
|
|
715
|
+
this.path = root === "" ? "" : `${root}/outbox/${instanceId}.jsonl`;
|
|
716
|
+
this.#fs = fsImpl !== void 0 ? fsImpl : _fs;
|
|
717
|
+
if (this.#fs === null || this.path === "") this.#disabled = true;
|
|
718
|
+
}
|
|
719
|
+
get enabled() {
|
|
720
|
+
return !this.#disabled;
|
|
721
|
+
}
|
|
722
|
+
// ------------------------------------------------------------------ writes
|
|
723
|
+
recordQueued(event) {
|
|
724
|
+
this.#append({ op: OP_QUEUED, e: event });
|
|
725
|
+
}
|
|
726
|
+
recordUnacked(event) {
|
|
727
|
+
this.#append({ op: OP_UNACKED, e: event });
|
|
728
|
+
}
|
|
729
|
+
recordAcked(id) {
|
|
730
|
+
this.#append({ op: OP_ACKED, id });
|
|
731
|
+
}
|
|
732
|
+
/** An event the buffers gave up on. It does not come back on restart either:
|
|
733
|
+
* replaying what the depth or age bound deliberately evicted would quietly
|
|
734
|
+
* undo the bound that evicted it. */
|
|
735
|
+
recordDropped(id) {
|
|
736
|
+
if (id === void 0 || id === "") return;
|
|
737
|
+
this.#append({ op: OP_DROPPED, id });
|
|
738
|
+
}
|
|
739
|
+
// ------------------------------------------------------------------- reads
|
|
740
|
+
/**
|
|
741
|
+
* Replay the file into `{queued, unacked}`, oldest first.
|
|
742
|
+
*
|
|
743
|
+
* A partial last line from a kill mid-append stops the read there. What came
|
|
744
|
+
* before it is the prefix that was durably written; a torn line is an event
|
|
745
|
+
* the caller was never told had been accepted.
|
|
746
|
+
*/
|
|
747
|
+
load() {
|
|
748
|
+
const empty = { queued: [], unacked: [] };
|
|
749
|
+
if (this.#disabled || this.#fs === null) return empty;
|
|
750
|
+
let text;
|
|
751
|
+
try {
|
|
752
|
+
if (!this.#fs.existsSync(this.path)) return empty;
|
|
753
|
+
text = this.#fs.readFileSync(this.path, "utf8");
|
|
754
|
+
} catch (e4) {
|
|
755
|
+
return empty;
|
|
756
|
+
}
|
|
757
|
+
const queued = /* @__PURE__ */ new Map();
|
|
758
|
+
const unacked = /* @__PURE__ */ new Map();
|
|
759
|
+
let lines = 0;
|
|
760
|
+
for (const raw of text.split("\n")) {
|
|
761
|
+
const line = raw.trim();
|
|
762
|
+
if (line === "") continue;
|
|
763
|
+
let rec;
|
|
764
|
+
try {
|
|
765
|
+
rec = JSON.parse(line);
|
|
766
|
+
} catch (e5) {
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
lines += 1;
|
|
770
|
+
const id = rec.op === OP_QUEUED || rec.op === OP_UNACKED ? _optionalChain([rec, 'access', _7 => _7.e, 'optionalAccess', _8 => _8.event_id]) : rec.id;
|
|
771
|
+
if (id === void 0 || id === "") continue;
|
|
772
|
+
if (rec.op === OP_QUEUED) {
|
|
773
|
+
queued.set(id, rec.e);
|
|
774
|
+
} else if (rec.op === OP_UNACKED) {
|
|
775
|
+
queued.delete(id);
|
|
776
|
+
unacked.set(id, rec.e);
|
|
777
|
+
} else if (rec.op === OP_ACKED || rec.op === OP_DROPPED) {
|
|
778
|
+
queued.delete(id);
|
|
779
|
+
unacked.delete(id);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
this.#lines = lines;
|
|
783
|
+
return { queued: [...queued.values()], unacked: [...unacked.values()] };
|
|
784
|
+
}
|
|
785
|
+
// ------------------------------------------------------------- maintenance
|
|
786
|
+
/**
|
|
787
|
+
* Rewrite the file to hold exactly what the buffers hold now.
|
|
788
|
+
*
|
|
789
|
+
* Written beside the target and renamed over it, because rename is atomic: a
|
|
790
|
+
* kill during a compaction leaves either the whole old file or the whole new
|
|
791
|
+
* one, never a half-written mix.
|
|
792
|
+
*/
|
|
793
|
+
compact(queued, unacked) {
|
|
794
|
+
if (this.#disabled || this.#fs === null) return;
|
|
795
|
+
if (queued.length === 0 && unacked.length === 0) {
|
|
796
|
+
this.clear();
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
this.#mkdir();
|
|
801
|
+
const body = [
|
|
802
|
+
...unacked.map((e) => JSON.stringify({ op: OP_UNACKED, e })),
|
|
803
|
+
...queued.map((e) => JSON.stringify({ op: OP_QUEUED, e }))
|
|
804
|
+
].join("\n") + "\n";
|
|
805
|
+
const tmp = `${this.path}.${Date.now()}.tmp`;
|
|
806
|
+
this.#fs.writeFileSync(tmp, body, "utf8");
|
|
807
|
+
this.#fs.renameSync(tmp, this.path);
|
|
808
|
+
this.#lines = queued.length + unacked.length;
|
|
809
|
+
} catch (e6) {
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
/** Nothing is outstanding, so there is nothing to replay. */
|
|
813
|
+
clear() {
|
|
814
|
+
if (this.#disabled || this.#fs === null) return;
|
|
815
|
+
try {
|
|
816
|
+
this.#fs.unlinkSync(this.path);
|
|
817
|
+
} catch (e7) {
|
|
818
|
+
}
|
|
819
|
+
this.#lines = 0;
|
|
820
|
+
}
|
|
821
|
+
get needsCompaction() {
|
|
822
|
+
return this.#lines >= COMPACT_AT_LINES;
|
|
823
|
+
}
|
|
824
|
+
// ---------------------------------------------------------------- internal
|
|
825
|
+
#mkdir() {
|
|
826
|
+
const dir = this.path.slice(0, this.path.lastIndexOf("/"));
|
|
827
|
+
_optionalChain([this, 'access', _9 => _9.#fs, 'optionalAccess', _10 => _10.mkdirSync, 'call', _11 => _11(dir, { recursive: true })]);
|
|
828
|
+
}
|
|
829
|
+
#append(record) {
|
|
830
|
+
if (this.#disabled || this.#fs === null) return;
|
|
831
|
+
try {
|
|
832
|
+
this.#mkdir();
|
|
833
|
+
this.#fs.appendFileSync(this.path, `${JSON.stringify(record)}
|
|
834
|
+
`, "utf8");
|
|
835
|
+
this.#lines += 1;
|
|
836
|
+
} catch (err) {
|
|
837
|
+
console.warn(
|
|
838
|
+
`[synap] outbox journal write failed (${String(err)}); buffered events will not survive a restart from here on`
|
|
839
|
+
);
|
|
840
|
+
this.#disabled = true;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
};
|
|
844
|
+
|
|
619
845
|
// src/grpc/stream-client.ts
|
|
620
846
|
var UNCACHEABLE_BUNDLE_TYPE = "reactive";
|
|
621
847
|
var MAX_RECONNECT_ATTEMPTS = 10;
|
|
@@ -624,7 +850,66 @@ var BACKOFF_MAX_MS = 3e4;
|
|
|
624
850
|
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
625
851
|
var HEARTBEAT_TIMEOUT_MS = 1e4;
|
|
626
852
|
var MAX_MISSED_HEARTBEATS = 3;
|
|
853
|
+
var SEND_QUEUE_MAX_DEPTH = 100;
|
|
854
|
+
var CLOSE_GRACE_MS = 2e3;
|
|
855
|
+
var SEND_QUEUE_MAX_AGE_MS = 5 * 60 * 1e3;
|
|
627
856
|
var MIN_TTL_SECONDS = 60;
|
|
857
|
+
var GRPC_RESOURCE_EXHAUSTED = 8;
|
|
858
|
+
var CREDIT_ABORT_DETAILS = "insufficient_credits";
|
|
859
|
+
var CREDIT_STOP_REASONS = /* @__PURE__ */ new Set([
|
|
860
|
+
"overages_disabled",
|
|
861
|
+
"trial_limit_reached",
|
|
862
|
+
"subscription_inactive"
|
|
863
|
+
]);
|
|
864
|
+
function metadataValue(metadata, key) {
|
|
865
|
+
if (metadata === null || typeof metadata !== "object") return null;
|
|
866
|
+
const getter = metadata.get;
|
|
867
|
+
const raw = typeof getter === "function" ? getter.call(metadata, key) : metadata[key];
|
|
868
|
+
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
869
|
+
if (value === void 0 || value === null) return null;
|
|
870
|
+
const text = typeof value === "string" ? value : String(value);
|
|
871
|
+
return text === "" ? null : text;
|
|
872
|
+
}
|
|
873
|
+
function numberOrNull(value) {
|
|
874
|
+
if (value === null) return null;
|
|
875
|
+
const parsed = Number(value);
|
|
876
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
877
|
+
}
|
|
878
|
+
function creditErrorFromRpc(error, correlationId = null) {
|
|
879
|
+
if (error === null || typeof error !== "object") return null;
|
|
880
|
+
if (error.code !== GRPC_RESOURCE_EXHAUSTED) return null;
|
|
881
|
+
const metadata = error.metadata;
|
|
882
|
+
const reason = metadataValue(metadata, "credit-reason");
|
|
883
|
+
if (reason === null || !CREDIT_STOP_REASONS.has(reason)) return null;
|
|
884
|
+
const rawDetails = error.details;
|
|
885
|
+
const details = typeof rawDetails === "string" && rawDetails !== "" ? rawDetails : CREDIT_ABORT_DETAILS;
|
|
886
|
+
const requestId = _nullishCoalesce(metadataValue(metadata, "request-id"), () => ( correlationId));
|
|
887
|
+
const manageUrl = metadataValue(metadata, "credit-manage-url");
|
|
888
|
+
if (reason === "overages_disabled") {
|
|
889
|
+
return new (0, _chunkT372OL3Qcjs.InsufficientCreditsError)(details, {
|
|
890
|
+
correlationId: requestId,
|
|
891
|
+
balanceCredits: numberOrNull(metadataValue(metadata, "credit-balance")),
|
|
892
|
+
requiredCredits: numberOrNull(metadataValue(metadata, "credit-minimum-required")),
|
|
893
|
+
recoveryUrl: metadataValue(metadata, "credit-recovery-url"),
|
|
894
|
+
redeemUrl: metadataValue(metadata, "credit-redeem-url"),
|
|
895
|
+
reason,
|
|
896
|
+
manageUrl
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
return new (0, _chunkT372OL3Qcjs.RateLimitError)(details, {
|
|
900
|
+
correlationId: requestId,
|
|
901
|
+
reason,
|
|
902
|
+
upgradeUrl: metadataValue(metadata, "credit-upgrade-url"),
|
|
903
|
+
manageUrl
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
function isCreditStop(error) {
|
|
907
|
+
if (!(error instanceof _chunkT372OL3Qcjs.InsufficientCreditsError) && !(error instanceof _chunkT372OL3Qcjs.RateLimitError)) {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
const reason = error.reason;
|
|
911
|
+
return typeof reason === "string" && CREDIT_STOP_REASONS.has(reason);
|
|
912
|
+
}
|
|
628
913
|
var GrpcStreamClient = class {
|
|
629
914
|
|
|
630
915
|
|
|
@@ -636,6 +921,19 @@ var GrpcStreamClient = class {
|
|
|
636
921
|
#missedHeartbeats = 0;
|
|
637
922
|
#heartbeatTimer = null;
|
|
638
923
|
#shutdownRequested = false;
|
|
924
|
+
// Events that could not be written because there was no stream.
|
|
925
|
+
#sendQueue = [];
|
|
926
|
+
// Events written to a live stream that the server has not acknowledged. The
|
|
927
|
+
// queue above covers "the stream was down"; this covers the half that
|
|
928
|
+
// actually loses sessions — the write succeeded, the stream broke before the
|
|
929
|
+
// server processed it, and nobody knows.
|
|
930
|
+
#unacked = /* @__PURE__ */ new Map();
|
|
931
|
+
// Why the stream stopped, when it stopped for a reason the caller can act on
|
|
932
|
+
// (today: a credit refusal). Mirrors Python's `last_error`.
|
|
933
|
+
#lastError = null;
|
|
934
|
+
// Disk backing for the two buffers above. They survive a dropped
|
|
935
|
+
// connection and not the process; this is what the next start reads.
|
|
936
|
+
#journal;
|
|
639
937
|
#grpc = null;
|
|
640
938
|
#ServiceStub = null;
|
|
641
939
|
#credentials;
|
|
@@ -652,6 +950,10 @@ var GrpcStreamClient = class {
|
|
|
652
950
|
this.useTls = _nullishCoalesce(options.useTls, () => ( true));
|
|
653
951
|
this.#sleep = _nullishCoalesce(options.sleep, () => ( ((ms) => new Promise((r) => setTimeout(r, ms)))));
|
|
654
952
|
this.#connectTimeoutMs = _nullishCoalesce(options.connectTimeoutMs, () => ( 1e4));
|
|
953
|
+
this.#journal = new OutboxJournal(
|
|
954
|
+
_nullishCoalesce(options.outboxPath, () => ( defaultOutboxRoot())),
|
|
955
|
+
credentials.instanceId
|
|
956
|
+
);
|
|
655
957
|
}
|
|
656
958
|
get currentState() {
|
|
657
959
|
return this.#state;
|
|
@@ -659,21 +961,190 @@ var GrpcStreamClient = class {
|
|
|
659
961
|
get isConnected() {
|
|
660
962
|
return this.#state === "connected";
|
|
661
963
|
}
|
|
964
|
+
/**
|
|
965
|
+
* The error that ended the stream, if one did.
|
|
966
|
+
*
|
|
967
|
+
* Set when the server refused the stream for credits, so an `onDisconnect`
|
|
968
|
+
* handler can read the balance, the reason and the URL to fix it. Null for an
|
|
969
|
+
* ordinary disconnect.
|
|
970
|
+
*/
|
|
971
|
+
get lastError() {
|
|
972
|
+
return this.#lastError;
|
|
973
|
+
}
|
|
662
974
|
async connect() {
|
|
663
975
|
await this.#loadGrpc();
|
|
976
|
+
await OutboxJournal.init();
|
|
664
977
|
this.#shutdownRequested = false;
|
|
978
|
+
this.#lastError = null;
|
|
979
|
+
this.#restoreFromJournal();
|
|
665
980
|
await this.#establish();
|
|
666
981
|
this.#startHeartbeat();
|
|
667
982
|
}
|
|
983
|
+
/**
|
|
984
|
+
* Pull a previous process's unsent events back into the send queue.
|
|
985
|
+
*
|
|
986
|
+
* Everything lands in the send queue, including what was unacknowledged last
|
|
987
|
+
* time. The stream those events were written to is gone, so "the server may
|
|
988
|
+
* already have it" is not a state we can resume; it has to be written again.
|
|
989
|
+
* That is safe because each event carries the id it was first sent with and
|
|
990
|
+
* the server drops a second sighting of it before dispatch, so an event it
|
|
991
|
+
* DID process is deduplicated there rather than doubling the turn.
|
|
992
|
+
*/
|
|
993
|
+
#restoreFromJournal() {
|
|
994
|
+
const { queued, unacked } = this.#journal.load();
|
|
995
|
+
if (queued.length === 0 && unacked.length === 0) return;
|
|
996
|
+
const now = Date.now();
|
|
997
|
+
for (const event of [...unacked, ...queued]) {
|
|
998
|
+
this.#sendQueue.push({
|
|
999
|
+
queuedAt: now,
|
|
1000
|
+
event
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
console.warn(
|
|
1004
|
+
`[synap] restored ${unacked.length + queued.length} event(s) from the previous process (${unacked.length} unacknowledged, ${queued.length} queued); they go out on this stream`
|
|
1005
|
+
);
|
|
1006
|
+
this.#compactJournal();
|
|
1007
|
+
}
|
|
1008
|
+
#compactJournal() {
|
|
1009
|
+
this.#journal.compact(
|
|
1010
|
+
this.#sendQueue.map((h) => h.event),
|
|
1011
|
+
[...this.#unacked.values()].map(
|
|
1012
|
+
(h) => h.event
|
|
1013
|
+
)
|
|
1014
|
+
);
|
|
1015
|
+
}
|
|
668
1016
|
async disconnect() {
|
|
669
1017
|
this.#shutdownRequested = true;
|
|
670
1018
|
this.#stopHeartbeat();
|
|
671
|
-
this.#
|
|
1019
|
+
this.#flushBeforeClose();
|
|
1020
|
+
const call = this.#call;
|
|
1021
|
+
if (call !== null) {
|
|
1022
|
+
try {
|
|
1023
|
+
call.end();
|
|
1024
|
+
} catch (e8) {
|
|
1025
|
+
}
|
|
1026
|
+
await new Promise((resolve) => {
|
|
1027
|
+
let settled = false;
|
|
1028
|
+
const done = () => {
|
|
1029
|
+
if (!settled) {
|
|
1030
|
+
settled = true;
|
|
1031
|
+
resolve();
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
const timer = setTimeout(() => {
|
|
1035
|
+
const left = this.undelivered();
|
|
1036
|
+
if (left.unacknowledged > 0 || left.queued > 0) {
|
|
1037
|
+
console.warn(
|
|
1038
|
+
`[synap] closing with ${left.queued} queued and ${left.unacknowledged} unacknowledged event(s) the server did not confirm within ${CLOSE_GRACE_MS}ms`
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
done();
|
|
1042
|
+
}, CLOSE_GRACE_MS);
|
|
1043
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
1044
|
+
call.on("end", () => {
|
|
1045
|
+
clearTimeout(timer);
|
|
1046
|
+
done();
|
|
1047
|
+
});
|
|
1048
|
+
call.on("status", () => {
|
|
1049
|
+
clearTimeout(timer);
|
|
1050
|
+
done();
|
|
1051
|
+
});
|
|
1052
|
+
call.on("error", () => {
|
|
1053
|
+
clearTimeout(timer);
|
|
1054
|
+
done();
|
|
1055
|
+
});
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
this.#call = null;
|
|
1059
|
+
this.#compactJournal();
|
|
672
1060
|
this.#setState("closed");
|
|
673
1061
|
}
|
|
674
|
-
/**
|
|
1062
|
+
/**
|
|
1063
|
+
* What this client could not get to the server.
|
|
1064
|
+
*
|
|
1065
|
+
* Every send path returns void, so a caller had no way to tell a delivered
|
|
1066
|
+
* turn from a lost one. Mirrors Python's `undelivered()`.
|
|
1067
|
+
*/
|
|
1068
|
+
undelivered() {
|
|
1069
|
+
return { queued: this.#sendQueue.length, unacknowledged: this.#unacked.size };
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Never fails the caller's turn, and no longer loses it either.
|
|
1073
|
+
*
|
|
1074
|
+
* A write that cannot go out is buffered and replayed on reconnect, and a
|
|
1075
|
+
* write that does go out is held until the server acknowledges it. Before
|
|
1076
|
+
* this, both cases ended at a silent `return`.
|
|
1077
|
+
*/
|
|
675
1078
|
sendConversationEvent(event) {
|
|
676
|
-
|
|
1079
|
+
const stamped = {
|
|
1080
|
+
...event,
|
|
1081
|
+
event_id: event.event_id && event.event_id !== "" ? event.event_id : _chunkT372OL3Qcjs.newCorrelationId.call(void 0, ),
|
|
1082
|
+
sent_at_ms: _nullishCoalesce(event.sent_at_ms, () => ( Date.now()))
|
|
1083
|
+
};
|
|
1084
|
+
if (!this.isConnected || this.#call === null) {
|
|
1085
|
+
this.#enqueue(stamped);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
this.#write({ conversation_event: { ...stamped } });
|
|
1089
|
+
this.#trackUnacked(stamped);
|
|
1090
|
+
}
|
|
1091
|
+
#enqueue(event) {
|
|
1092
|
+
this.#pruneQueue();
|
|
1093
|
+
if (this.#sendQueue.length >= SEND_QUEUE_MAX_DEPTH) {
|
|
1094
|
+
const evicted = this.#sendQueue.shift();
|
|
1095
|
+
this.#journal.recordDropped(_optionalChain([evicted, 'optionalAccess', _12 => _12.event, 'access', _13 => _13.event_id]));
|
|
1096
|
+
console.warn(
|
|
1097
|
+
`[synap] send queue full (max=${SEND_QUEUE_MAX_DEPTH}); dropping oldest event (event_type=${_optionalChain([evicted, 'optionalAccess', _14 => _14.event, 'access', _15 => _15.event_type])})`
|
|
1098
|
+
);
|
|
1099
|
+
}
|
|
1100
|
+
this.#sendQueue.push({ queuedAt: Date.now(), event });
|
|
1101
|
+
this.#journal.recordQueued(event);
|
|
1102
|
+
}
|
|
1103
|
+
#pruneQueue() {
|
|
1104
|
+
const cutoff = Date.now() - SEND_QUEUE_MAX_AGE_MS;
|
|
1105
|
+
while (this.#sendQueue.length > 0 && (_nullishCoalesce(_optionalChain([this, 'access', _16 => _16.#sendQueue, 'access', _17 => _17[0], 'optionalAccess', _18 => _18.queuedAt]), () => ( 0))) < cutoff) {
|
|
1106
|
+
const expired = this.#sendQueue.shift();
|
|
1107
|
+
this.#journal.recordDropped(_optionalChain([expired, 'optionalAccess', _19 => _19.event, 'access', _20 => _20.event_id]));
|
|
1108
|
+
console.warn(
|
|
1109
|
+
`[synap] queued event expired (age>${SEND_QUEUE_MAX_AGE_MS}ms); dropping (event_type=${_optionalChain([expired, 'optionalAccess', _21 => _21.event, 'access', _22 => _22.event_type])})`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
#trackUnacked(event) {
|
|
1114
|
+
const id = event.event_id;
|
|
1115
|
+
if (id === void 0 || id === "") return;
|
|
1116
|
+
const cutoff = Date.now() - SEND_QUEUE_MAX_AGE_MS;
|
|
1117
|
+
for (const [oldestId, held] of this.#unacked) {
|
|
1118
|
+
if (held.queuedAt >= cutoff && this.#unacked.size < SEND_QUEUE_MAX_DEPTH) break;
|
|
1119
|
+
this.#unacked.delete(oldestId);
|
|
1120
|
+
this.#journal.recordDropped(oldestId);
|
|
1121
|
+
console.warn(
|
|
1122
|
+
`[synap] unacknowledged event dropped: the server never confirmed it (event_type=${held.event.event_type})`
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
this.#unacked.set(id, { queuedAt: Date.now(), event });
|
|
1126
|
+
this.#journal.recordUnacked(event);
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* Replay what the server never confirmed, then what never went out.
|
|
1130
|
+
*
|
|
1131
|
+
* That order is the order things happened in: an unacked event was written
|
|
1132
|
+
* to the previous stream, so it is older than anything that piled up while
|
|
1133
|
+
* there was no stream at all. Safe to repeat — every event carries the id it
|
|
1134
|
+
* was first sent with and the server drops a second sighting of it.
|
|
1135
|
+
*/
|
|
1136
|
+
#replayBuffered() {
|
|
1137
|
+
const unacked = [...this.#unacked.values()];
|
|
1138
|
+
const queued = this.#sendQueue;
|
|
1139
|
+
this.#sendQueue = [];
|
|
1140
|
+
for (const held of [...unacked, ...queued]) {
|
|
1141
|
+
this.#write({ conversation_event: { ...held.event } });
|
|
1142
|
+
}
|
|
1143
|
+
if (unacked.length > 0 || queued.length > 0) {
|
|
1144
|
+
console.warn(
|
|
1145
|
+
`[synap] replayed ${unacked.length} unacknowledged and ${queued.length} queued event(s) after reconnect`
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
677
1148
|
}
|
|
678
1149
|
sendSessionControl(input) {
|
|
679
1150
|
this.#write({ session_control: { ...input } });
|
|
@@ -684,11 +1155,26 @@ var GrpcStreamClient = class {
|
|
|
684
1155
|
sendContextAssembled(event) {
|
|
685
1156
|
this.#write({ context_assembled: { ...event } });
|
|
686
1157
|
}
|
|
1158
|
+
#flushBeforeClose() {
|
|
1159
|
+
if (!this.isConnected || this.#call === null) {
|
|
1160
|
+
if (this.#sendQueue.length > 0 || this.#unacked.size > 0) {
|
|
1161
|
+
console.warn(
|
|
1162
|
+
`[synap] closing with ${this.#sendQueue.length} queued and ${this.#unacked.size} unacknowledged event(s) and no stream to send them on; they are lost`
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const queued = this.#sendQueue;
|
|
1168
|
+
this.#sendQueue = [];
|
|
1169
|
+
for (const held of queued) {
|
|
1170
|
+
this.#write({ conversation_event: { ...held.event } });
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
687
1173
|
#write(msg) {
|
|
688
1174
|
if (!this.isConnected || this.#call === null) return;
|
|
689
1175
|
try {
|
|
690
1176
|
this.#call.write(msg);
|
|
691
|
-
} catch (
|
|
1177
|
+
} catch (e9) {
|
|
692
1178
|
}
|
|
693
1179
|
}
|
|
694
1180
|
async #loadGrpc() {
|
|
@@ -741,6 +1227,11 @@ var GrpcStreamClient = class {
|
|
|
741
1227
|
const call = stub["Listen"](metadata);
|
|
742
1228
|
call.on("data", (msg) => this.#handleMessage(msg));
|
|
743
1229
|
call.on("error", (err) => {
|
|
1230
|
+
const creditError = creditErrorFromRpc(err);
|
|
1231
|
+
if (creditError !== null) {
|
|
1232
|
+
this.#handleCreditStop(creditError);
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
744
1235
|
void this.#handleDisconnect(`error: ${String(err)}`);
|
|
745
1236
|
});
|
|
746
1237
|
call.on("end", () => {
|
|
@@ -750,8 +1241,18 @@ var GrpcStreamClient = class {
|
|
|
750
1241
|
this.#setState("connected");
|
|
751
1242
|
this.#reconnectAttempts = 0;
|
|
752
1243
|
this.#lastPongAt = Date.now();
|
|
1244
|
+
this.#replayBuffered();
|
|
753
1245
|
}
|
|
754
1246
|
#handleMessage(msg) {
|
|
1247
|
+
const ack = msg["event_ack"];
|
|
1248
|
+
if (ack) {
|
|
1249
|
+
for (const id of _nullishCoalesce(ack.event_ids, () => ( []))) {
|
|
1250
|
+
this.#unacked.delete(id);
|
|
1251
|
+
this.#journal.recordAcked(id);
|
|
1252
|
+
}
|
|
1253
|
+
if (this.#journal.needsCompaction) this.#compactJournal();
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
755
1256
|
if (msg["context_bundle"]) {
|
|
756
1257
|
this.#handleBundle(msg["context_bundle"]);
|
|
757
1258
|
return;
|
|
@@ -762,7 +1263,7 @@ var GrpcStreamClient = class {
|
|
|
762
1263
|
return;
|
|
763
1264
|
}
|
|
764
1265
|
const signal = msg["signal"];
|
|
765
|
-
if (_optionalChain([signal, 'optionalAccess',
|
|
1266
|
+
if (_optionalChain([signal, 'optionalAccess', _23 => _23.signal_type]) === "closing") {
|
|
766
1267
|
void this.#handleDisconnect("server_signal_closing");
|
|
767
1268
|
}
|
|
768
1269
|
}
|
|
@@ -771,14 +1272,14 @@ var GrpcStreamClient = class {
|
|
|
771
1272
|
const conversationId = String(_nullishCoalesce(bundle["anticipation_conversation_id"], () => ( "")));
|
|
772
1273
|
if (bundleType !== UNCACHEABLE_BUNDLE_TYPE) this.#store(bundle, bundleType, conversationId);
|
|
773
1274
|
if (bundleType === "compaction_update" && conversationId !== "") {
|
|
774
|
-
_optionalChain([this, 'access',
|
|
1275
|
+
_optionalChain([this, 'access', _24 => _24.#options, 'access', _25 => _25.onCompactionUpdate, 'optionalCall', _26 => _26(conversationId, bundle)]);
|
|
775
1276
|
}
|
|
776
|
-
_optionalChain([this, 'access',
|
|
1277
|
+
_optionalChain([this, 'access', _27 => _27.#options, 'access', _28 => _28.onContext, 'optionalCall', _29 => _29(bundle)]);
|
|
777
1278
|
}
|
|
778
1279
|
#store(bundle, bundleType, conversationId) {
|
|
779
1280
|
const itemsByType = {};
|
|
780
1281
|
for (const [type, list] of Object.entries(_nullishCoalesce(bundle["items_by_type"], () => ( {})))) {
|
|
781
|
-
const items = _optionalChain([list, 'optionalAccess',
|
|
1282
|
+
const items = _optionalChain([list, 'optionalAccess', _30 => _30.items]);
|
|
782
1283
|
itemsByType[type] = _nullishCoalesce(items, () => ( []));
|
|
783
1284
|
}
|
|
784
1285
|
const ttlHint = Number(_nullishCoalesce(bundle["ttl_hint_seconds"], () => ( 0)));
|
|
@@ -803,6 +1304,20 @@ var GrpcStreamClient = class {
|
|
|
803
1304
|
]
|
|
804
1305
|
});
|
|
805
1306
|
}
|
|
1307
|
+
/**
|
|
1308
|
+
* End the stream because the server refused it for credits.
|
|
1309
|
+
*
|
|
1310
|
+
* Reconnecting cannot clear a credit refusal, so the stream closes and the
|
|
1311
|
+
* typed error is kept on `lastError` for the application to read. Mirrors
|
|
1312
|
+
* Python's `_handle_credit_stop`.
|
|
1313
|
+
*/
|
|
1314
|
+
#handleCreditStop(error) {
|
|
1315
|
+
this.#lastError = error;
|
|
1316
|
+
this.#shutdownRequested = true;
|
|
1317
|
+
this.#stopHeartbeat();
|
|
1318
|
+
this.#closeCall();
|
|
1319
|
+
this.#setState("disconnected");
|
|
1320
|
+
}
|
|
806
1321
|
async #handleDisconnect(_reason) {
|
|
807
1322
|
if (this.#shutdownRequested) return;
|
|
808
1323
|
this.#closeCall();
|
|
@@ -820,7 +1335,11 @@ var GrpcStreamClient = class {
|
|
|
820
1335
|
if (this.#shutdownRequested) return;
|
|
821
1336
|
try {
|
|
822
1337
|
await this.#establish();
|
|
823
|
-
} catch (
|
|
1338
|
+
} catch (error) {
|
|
1339
|
+
if (isCreditStop(error)) {
|
|
1340
|
+
this.#handleCreditStop(error);
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
824
1343
|
void this.#handleDisconnect("reconnect_failed");
|
|
825
1344
|
}
|
|
826
1345
|
}
|
|
@@ -837,11 +1356,11 @@ var GrpcStreamClient = class {
|
|
|
837
1356
|
}
|
|
838
1357
|
try {
|
|
839
1358
|
this.#call.write({ heartbeat_ping: { timestamp_ms: now } });
|
|
840
|
-
} catch (
|
|
1359
|
+
} catch (e10) {
|
|
841
1360
|
void this.#handleDisconnect("heartbeat_write_error");
|
|
842
1361
|
}
|
|
843
1362
|
}, HEARTBEAT_INTERVAL_MS);
|
|
844
|
-
_optionalChain([this, 'access',
|
|
1363
|
+
_optionalChain([this, 'access', _31 => _31.#heartbeatTimer, 'access', _32 => _32.unref, 'optionalCall', _33 => _33()]);
|
|
845
1364
|
}
|
|
846
1365
|
#stopHeartbeat() {
|
|
847
1366
|
if (this.#heartbeatTimer !== null) {
|
|
@@ -851,19 +1370,23 @@ var GrpcStreamClient = class {
|
|
|
851
1370
|
}
|
|
852
1371
|
#closeCall() {
|
|
853
1372
|
try {
|
|
854
|
-
_optionalChain([this, 'access',
|
|
855
|
-
} catch (
|
|
1373
|
+
_optionalChain([this, 'access', _34 => _34.#call, 'optionalAccess', _35 => _35.end, 'call', _36 => _36()]);
|
|
1374
|
+
} catch (e11) {
|
|
856
1375
|
}
|
|
857
1376
|
this.#call = null;
|
|
858
1377
|
}
|
|
859
1378
|
#setState(state) {
|
|
860
1379
|
this.#state = state;
|
|
861
|
-
_optionalChain([this, 'access',
|
|
1380
|
+
_optionalChain([this, 'access', _37 => _37.#options, 'access', _38 => _38.onStateChange, 'optionalCall', _39 => _39(state)]);
|
|
862
1381
|
}
|
|
863
1382
|
};
|
|
864
1383
|
|
|
865
1384
|
|
|
866
1385
|
|
|
867
1386
|
|
|
868
|
-
|
|
869
|
-
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
exports.PROTO_SHA = PROTO_SHA; exports.CREDIT_ABORT_DETAILS = CREDIT_ABORT_DETAILS; exports.CREDIT_STOP_REASONS = CREDIT_STOP_REASONS; exports.creditErrorFromRpc = creditErrorFromRpc; exports.isCreditStop = isCreditStop; exports.GrpcStreamClient = GrpcStreamClient;
|
|
1392
|
+
//# sourceMappingURL=chunk-WWTGJZA7.cjs.map
|