cozy-search
Version:
UI components about search bar and IA assistant
278 lines (236 loc) • 7.86 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.StreamBridge = void 0;
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
/**
* StreamBridge bridges WebSocket events from cozy-realtime to async iterables
* that can be consumed by assistant-ui's ChatModelAdapter.
*
* The bridge receives push-based events from WebSocket and converts them
* to a pull-based async iterator pattern.
*/
var StreamBridge = /*#__PURE__*/function () {
function StreamBridge() {
(0, _classCallCheck2.default)(this, StreamBridge);
(0, _defineProperty2.default)(this, "streams", new Map());
(0, _defineProperty2.default)(this, "cleanupCallback", null);
(0, _defineProperty2.default)(this, "positionBuffers", new Map());
(0, _defineProperty2.default)(this, "nextPositions", new Map());
(0, _defineProperty2.default)(this, "sourcesMap", new Map());
}
(0, _createClass2.default)(StreamBridge, [{
key: "setCleanupCallback",
value:
/**
* Sets a callback to be invoked when cleanup is called.
* This allows the provider to mark the current message as cancelled.
*/
function setCleanupCallback(callback) {
this.cleanupCallback = callback;
}
/**
* Creates a new async iterable stream for a conversation.
* The stream will yield string chunks as they arrive via onDelta().
*/
}, {
key: "createStream",
value: function createStream(conversationId) {
this.cleanup(conversationId);
var queue = [];
var resolveNext = null;
var rejectNext = null;
var isDone = false;
var _error = null;
var controller = {
push: function push(chunk) {
if (isDone) return;
if (resolveNext) {
resolveNext({
value: chunk,
done: false
});
resolveNext = null;
rejectNext = null;
} else {
queue.push(chunk);
}
},
complete: function complete() {
if (isDone) return;
isDone = true;
if (resolveNext) {
resolveNext({
value: undefined,
done: true
});
resolveNext = null;
rejectNext = null;
}
},
error: function error(err) {
if (isDone) return;
isDone = true;
_error = err;
if (rejectNext) {
rejectNext(err);
resolveNext = null;
rejectNext = null;
}
},
iterator: (0, _defineProperty2.default)({
next: function next() {
return new Promise(function (resolve, reject) {
if (_error) {
reject(_error);
return;
}
if (queue.length > 0) {
resolve({
value: queue.shift(),
done: false
});
} else if (isDone) {
resolve({
value: undefined,
done: true
});
} else {
if (resolveNext) {
reject(new Error('StreamBridge: concurrent next() calls are not supported'));
return;
}
resolveNext = resolve;
rejectNext = reject;
}
});
}
}, Symbol.asyncIterator, function () {
return this;
})
};
this.streams.set(conversationId, controller);
return controller.iterator;
}
/**
* Called when a delta event is received from WebSocket.
* When a position is provided, chunks are buffered and flushed in order.
* Without a position, chunks are pushed directly in arrival order.
*/
}, {
key: "onDelta",
value: function onDelta(conversationId, content, position) {
var _this$nextPositions$g;
var stream = this.streams.get(conversationId);
if (!stream) return;
if (position === undefined) {
stream.push(content);
return;
} // Fast path: chunk arrived in order, no buffering needed
var nextExpected = (_this$nextPositions$g = this.nextPositions.get(conversationId)) !== null && _this$nextPositions$g !== void 0 ? _this$nextPositions$g : 0;
if (position === nextExpected) {
stream.push(content);
var next = nextExpected + 1; // Flush any previously buffered chunks that are now contiguous
var _buffer = this.positionBuffers.get(conversationId);
if (_buffer && _buffer.size > 0) {
while (_buffer.has(next)) {
stream.push(_buffer.get(next));
_buffer.delete(next);
next++;
}
if (_buffer.size === 0) {
this.positionBuffers.delete(conversationId);
}
}
this.nextPositions.set(conversationId, next);
return;
} // Out-of-order: buffer until the gap is filled
var buffer = this.positionBuffers.get(conversationId);
if (!buffer) {
buffer = new Map();
this.positionBuffers.set(conversationId, buffer);
}
buffer.set(position, content);
}
/**
* Called when a 'done' event is received from WebSocket.
* Completes the stream for the conversation.
*/
}, {
key: "onDone",
value: function onDone(conversationId) {
var stream = this.streams.get(conversationId);
if (stream) {
stream.complete();
this.positionBuffers.delete(conversationId);
this.nextPositions.delete(conversationId);
}
}
/**
* Called when a 'sources' event is received from WebSocket.
* Stores sources for the conversation to be retrieved by the adapter.
*/
}, {
key: "onSources",
value: function onSources(conversationId, sources) {
if (!this.streams.has(conversationId)) return;
this.sourcesMap.set(conversationId, sources);
}
/**
* Returns stored sources for a conversation.
*/
}, {
key: "getSources",
value: function getSources(conversationId) {
return this.sourcesMap.get(conversationId);
}
/**
* Called when an error occurs.
* Errors the stream for the conversation.
*/
}, {
key: "onError",
value: function onError(conversationId, error) {
var stream = this.streams.get(conversationId);
if (stream) {
stream.error(error);
}
this.positionBuffers.delete(conversationId);
this.nextPositions.delete(conversationId);
this.sourcesMap.delete(conversationId);
}
/**
* Cleans up the stream for a conversation.
* Should be called when navigating away or on unmount.
*/
}, {
key: "cleanup",
value: function cleanup(conversationId) {
var stream = this.streams.get(conversationId);
if (stream) {
if (this.cleanupCallback) {
this.cleanupCallback();
}
stream.complete();
this.streams.delete(conversationId);
}
this.positionBuffers.delete(conversationId);
this.nextPositions.delete(conversationId);
this.sourcesMap.delete(conversationId);
}
/**
* Checks if there's an active stream for a conversation.
*/
}, {
key: "hasStream",
value: function hasStream(conversationId) {
return this.streams.has(conversationId);
}
}]);
return StreamBridge;
}();
exports.StreamBridge = StreamBridge;