@datalayer/core
Version:
[](https://datalayer.io)
164 lines (163 loc) • 5.94 kB
JavaScript
/*
* Copyright (c) 2023-2025 Datalayer, Inc.
* Distributed under the terms of the Modified BSD License.
*/
import { WebsocketProvider } from 'y-websocket';
import { URLExt } from '@jupyterlab/coreutils';
import { Signal } from '@lumino/signaling';
// Import CollaborationStatus enum separately since it's exported
var CollaborationStatus;
(function (CollaborationStatus) {
CollaborationStatus["Disconnected"] = "disconnected";
CollaborationStatus["Connecting"] = "connecting";
CollaborationStatus["Connected"] = "connected";
CollaborationStatus["Error"] = "error";
})(CollaborationStatus || (CollaborationStatus = {}));
import { requestDatalayerCollaborationSessionId } from './DatalayerCollaboration';
/**
* Datalayer collaboration provider
*
* This provider connects to Datalayer's collaboration service using WebSockets.
*/
export class DatalayerCollaborationProvider {
type = 'datalayer';
_status = CollaborationStatus.Disconnected;
_provider = null;
_sharedModel = null;
_statusChanged = new Signal(this);
_errorOccurred = new Signal(this);
_syncStateChanged = new Signal(this);
_isDisposed = false;
_config;
_onSync = null;
_onConnectionClose = null;
constructor(config) {
this._config = config;
}
get status() {
return this._status;
}
get isConnected() {
return this._status === CollaborationStatus.Connected;
}
get isDisposed() {
return this._isDisposed;
}
get events() {
return {
statusChanged: this._statusChanged,
errorOccurred: this._errorOccurred,
syncStateChanged: this._syncStateChanged,
};
}
setStatus(status) {
if (this._status !== status) {
this._status = status;
this._statusChanged.emit(status);
}
}
async connect(sharedModel, documentId, options) {
if (this.isConnected) {
console.warn('Already connected to Datalayer collaboration service');
return;
}
this.setStatus(CollaborationStatus.Connecting);
try {
// Use ONLY explicitly provided configuration
// NO fallback to coreStore - if config is wrong, we want to fail fast
// This is critical for VS Code extension where config must come from settings
const runUrl = this._config.runUrl;
const token = this._config.token;
if (!runUrl) {
throw new Error('Datalayer runUrl is not configured - must be explicitly provided in DatalayerCollaborationProvider config');
}
if (!token) {
throw new Error('Datalayer token is not configured - must be explicitly provided in DatalayerCollaborationProvider config');
}
const { ydoc, awareness } = sharedModel;
// Build URLs.
const documentURL = URLExt.join(runUrl, '/api/spacer/v1/documents');
const wsUrl = URLExt.join(runUrl, '/api/spacer/v1/documents/ws').replace(/^http/, 'ws');
// Request collaboration session from Datalayer
const sessionId = await requestDatalayerCollaborationSessionId({
url: URLExt.join(documentURL, documentId),
token,
fetchFn: this._config.fetchFn,
});
// Create WebSocket provider
this._provider = new WebsocketProvider(wsUrl, documentId, ydoc, {
disableBc: true,
params: {
sessionId,
token,
},
awareness,
...options,
});
this._sharedModel = sharedModel;
// Set up event handlers
this._onSync = (isSynced) => {
this.handleSync(isSynced);
};
this._onConnectionClose = (event) => {
if (event) {
this.handleConnectionClose(event);
}
};
this._provider.on('sync', this._onSync);
this._provider.on('connection-close', this._onConnectionClose);
console.log('Connected to Datalayer collaboration service');
}
catch (error) {
this.setStatus(CollaborationStatus.Error);
this._errorOccurred.emit(error);
throw error;
}
}
disconnect() {
if (this._provider) {
if (this._onSync) {
this._provider.off('sync', this._onSync);
}
if (this._onConnectionClose) {
this._provider.off('connection-close', this._onConnectionClose);
}
this._provider.disconnect();
this._provider = null;
}
this._sharedModel = null;
this.setStatus(CollaborationStatus.Disconnected);
}
getProvider() {
return this._provider;
}
getSharedModel() {
return this._sharedModel;
}
handleConnectionClose(event) {
console.warn('Collaboration connection closed:', event);
this.setStatus(CollaborationStatus.Disconnected);
// Handle session expiration (code 4002)
if (event.code === 4002) {
console.warn('Datalayer collaboration session expired');
// Attempt to reconnect could be implemented here
}
}
handleSync(isSynced) {
this._syncStateChanged.emit(isSynced);
if (isSynced) {
this.setStatus(CollaborationStatus.Connected);
}
}
dispose() {
if (this._isDisposed) {
return;
}
this.disconnect();
// Signals don't need explicit disposal in Lumino
// They are cleaned up when the object is garbage collected
this._isDisposed = true;
}
}
// Export the provider for direct instantiation
export default DatalayerCollaborationProvider;