UNPKG

@datalayer/core

Version:

[![Datalayer](https://assets.datalayer.tech/datalayer-25.svg)](https://datalayer.io)

341 lines (340 loc) 14.8 kB
/* * Copyright (c) 2023-2025 Datalayer, Inc. * Distributed under the terms of the Modified BSD License. */ /** * Spacer mixin for managing workspaces, notebooks, and content. * @module client/mixins/SpacerMixin */ import * as spaces from '../../api/spacer/spaces'; import * as notebooks from '../../api/spacer/notebooks'; import * as users from '../../api/spacer/users'; import * as lexicals from '../../api/spacer/lexicals'; import * as documents from '../../api/spacer/documents'; import * as items from '../../api/spacer/items'; import { NotebookDTO } from '../../models/NotebookDTO'; import { LexicalDTO } from '../../models/LexicalDTO'; import { SpaceDTO } from '../../models/SpaceDTO'; import { HealthCheck } from '../../models/HealthCheck'; import { convertSpaceItemsToModels } from '../utils/spacerUtils'; /** Spacer mixin providing workspace and content management. */ export function SpacerMixin(Base) { return class extends Base { // ======================================================================== // User // ======================================================================== /** * Get all workspaces for the authenticated user. * @returns Array of Space instances */ async getMySpaces() { const token = this.getToken(); const spacerRunUrl = this.getSpacerRunUrl(); const response = await users.getMySpaces(token, spacerRunUrl); return response.spaces.map(s => new SpaceDTO(s, this)); } // ======================================================================== // Spaces // ======================================================================== /** * Create a new workspace. * @param name - Space name * @param description - Space description * @param variant - Space variant type * @param spaceHandle - Unique handle for the space * @param organizationId - Organization ID * @param seedSpaceId - Seed space ID for initialization * @param isPublic - Whether the space is public * @returns Created Space instance */ async createSpace(name, description, variant, spaceHandle, organizationId, seedSpaceId, isPublic) { const token = this.getToken(); const spacerRunUrl = this.getSpacerRunUrl(); const data = { name, description, variant, spaceHandle, organizationId, seedSpaceId, public: isPublic, }; const response = await spaces.createSpace(token, data, spacerRunUrl); if (!response.space) { throw new Error('Failed to create space: no space returned'); } return new SpaceDTO(response.space, this); } // ======================================================================== // Notebooks // ======================================================================== /** * Create a new notebook. * @param spaceId - ID of the space to create the notebook in * @param name - Name of the notebook * @param description - Description of the notebook * @param file - Optional file for notebook content * @returns Created Notebook instance */ async createNotebook(spaceId, name, description, file) { // Get the Space model instance const spaces = await this.getMySpaces(); const spaceModel = spaces.find((s) => s.uid === spaceId); if (!spaceModel) { throw new Error(`Space with ID '${spaceId}' not found`); } // Use the Space model's createNotebook method return await spaceModel.createNotebook({ name, description, file, }); } /** * Get a notebook by ID. * @param id - Notebook ID * @returns Notebook instance */ async getNotebook(id) { const spacerRunUrl = this.getSpacerRunUrl(); const token = this.getToken(); const response = await notebooks.getNotebook(token, id, spacerRunUrl); if (!response.notebook) { throw new Error(`Notebook with ID '${id}' not found`); } return new NotebookDTO(response.notebook, this); } /** * Update a notebook. * @param id - Notebook ID * @param name - Optional new name for the notebook * @param description - Optional new description for the notebook * @returns Updated Notebook instance */ async updateNotebook(id, name, description) { const spacerRunUrl = this.getSpacerRunUrl(); const token = this.getToken(); const data = {}; if (name !== undefined) data.name = name; if (description !== undefined) data.description = description; const response = await notebooks.updateNotebook(token, id, data, spacerRunUrl); if (!response.notebook) { throw new Error('Failed to update notebook: no notebook returned'); } return new NotebookDTO(response.notebook, this); } // ======================================================================== // Lexicals // ======================================================================== /** * Create a new lexical document. * @param spaceId - ID of the space to create the lexical document in * @param name - Name of the lexical document * @param description - Description of the lexical document * @param file - Optional file for document content * @returns Created Lexical instance */ async createLexical(spaceId, name, description, file) { // Get the Space model instance const spaces = await this.getMySpaces(); const spaceModel = spaces.find((s) => s.uid === spaceId); if (!spaceModel) { throw new Error(`Space with ID '${spaceId}' not found`); } // Use the Space model's createLexical method return await spaceModel.createLexical({ name, description, file, }); } /** * Get a lexical document by ID. * @param id - Document ID * @returns Lexical instance */ async getLexical(id) { const spacerRunUrl = this.getSpacerRunUrl(); const token = this.getToken(); const response = await lexicals.getLexical(token, id, spacerRunUrl); if (!response.document) { throw new Error(`Lexical document with ID '${id}' not found`); } return new LexicalDTO(response.document, this); } /** * Update a lexical document. * @param id - Document ID * @param name - Optional new name for the lexical document * @param description - Optional new description for the lexical document * @returns Updated Lexical instance */ async updateLexical(id, name, description) { const spacerRunUrl = this.getSpacerRunUrl(); const token = this.getToken(); const data = {}; if (name !== undefined) data.name = name; if (description !== undefined) data.description = description; const response = await lexicals.updateLexical(token, id, data, spacerRunUrl); return new LexicalDTO(response.document, this); } // ======================================================================== // Items // ======================================================================== /** * Get the items of a space as model instances. * @param spaceId - Space ID * @returns Array of Notebook and Lexical model instances */ async getSpaceItems(spaceId) { const spacerRunUrl = this.getSpacerRunUrl(); const token = this.getToken(); const response = await items.getSpaceItems(token, spaceId, spacerRunUrl); // Use shared utility function to convert items to model instances return convertSpaceItemsToModels(response.items, this); } /** * Get a single item from a space. * @param itemId - Item ID to retrieve * @returns Notebook or Lexical model instance * @throws Error if item not found */ async getSpaceItem(itemId) { const spacerRunUrl = this.getSpacerRunUrl(); const token = this.getToken(); const response = await items.getItem(token, itemId, spacerRunUrl); if (!response.success || !response.item) { throw new Error(`Space item '${itemId}' not found`); } // Determine item type and create appropriate model const item = response.item; if (item.type_s === 'notebook' || item.notebook_name_s !== undefined) { return new NotebookDTO(item, this); } else if (item.type_s === 'lexical' || item.document_name_s !== undefined) { return new LexicalDTO(item, this); } else { throw new Error(`Unknown item type for item '${itemId}'`); } } /** * Delete an item from a space. * @param itemId - Item ID to delete * @throws Error if deletion fails */ async deleteSpaceItem(itemId) { const spacerRunUrl = this.getSpacerRunUrl(); const token = this.getToken(); // First, check if the item exists try { const getResponse = await items.getItem(token, itemId, spacerRunUrl); if (!getResponse.success || !getResponse.item) { throw new Error(`Space item '${itemId}' not found`); } } catch (error) { // If getItem throws (e.g., 404), wrap in descriptive error if (error.message?.includes('404') || error.message?.includes('not found')) { throw new Error(`Failed to delete space item '${itemId}': Item not found`); } throw new Error(`Failed to delete space item '${itemId}': ${error.message}`); } // Item exists, proceed with deletion const response = await items.deleteItem(token, itemId, spacerRunUrl); if (!response.success) { throw new Error(`Failed to delete space item '${itemId}': ${response.message}`); } // Success - return void } // ======================================================================== // Content Loading with CDN Support // ======================================================================== async getContent(itemId) { const spacerRunUrl = this.getSpacerRunUrl(); const token = this.getToken(); // First, get the item to check for CDN URL const response = await items.getItem(token, itemId, spacerRunUrl); if (!response.success || !response.item) { throw new Error(`Space item '${itemId}' not found`); } const item = response.item; const cdnUrl = item.cdn_url_s; if (cdnUrl) { // Load content from CDN const cdnResponse = await fetch(cdnUrl); if (!cdnResponse.ok) { throw new Error(`Failed to load content from CDN: ${cdnResponse.statusText}`); } return await cdnResponse.json(); } // No CDN URL, return content from item return item.content; } // ======================================================================== // Service Health Checks // ======================================================================== /** * Check the health status of the Spacer service. * Performs a lightweight check to verify service accessibility. * * @returns Health check result with status and response time */ async checkSpacerHealth() { const startTime = Date.now(); const errors = []; let status = 'unknown'; let healthy = false; try { // Test basic connectivity by getting user spaces (lightweight operation) const spaces = await this.getMySpaces(); const responseTime = Date.now() - startTime; if (Array.isArray(spaces)) { healthy = true; status = 'operational'; } else { status = 'degraded'; errors.push('Unexpected response format from spaces endpoint'); } return new HealthCheck({ healthy, status, responseTime, errors, timestamp: new Date(), }, this); } catch (error) { const responseTime = Date.now() - startTime; status = 'down'; errors.push(`Service unreachable: ${error}`); return new HealthCheck({ healthy: false, status, responseTime, errors, timestamp: new Date(), }, this); } } /** * Get collaboration session ID for a document */ async getCollaborationSessionId(documentId) { const token = this.getToken(); const spacerRunUrl = this.getSpacerRunUrl(); const response = await documents.getCollaborationSessionId(token, documentId, spacerRunUrl); if (!response.success || !response.sessionId) { throw new Error(`Failed to get collaboration session ID for document '${documentId}': ${response.error || 'Unknown error'}`); } return response.sessionId; } }; }