services
See also: Architecture overview · Components · Tools · common
The core/services/ subsystem holds small, mostly client-side runtime objects that package a piece of shared interface/logic (file upload, rich-text editing, autocomplete search, …) so that many components, sections and tools can reuse it without re-implementing it.
A service is not an external service
"Service" on this page means a reusable client-side interface module. It is unrelated to an external record service — the server-side subsystem that resolves a record from a third-party catalogue — which has its own ontology configuration, its own outbound controls and no client module at all. The two words collide by history; the pages do not.
This page is the subsystem reference for core/services/. A service is not a
section or a component — it does not live in the ontology and it has no tipo.
It is a reusable helper instanced by a component, section or tool and wired to
its caller. Read Components first for the
component/datum model these services plug into.
Services are client modules; their server side is the generic API
Services are vanilla-JS client modules, living at
client/dedalo/core/services/. Their server side is whatever generic API
action they call, served by the dispatch registry
(src/core/api/dispatch.ts) — there is no service-specific endpoint. The
paths written core/services/<service>/… below are client-relative.
Role
A service is a self-contained interface-plus-logic module that is shared
between unrelated callers. Where a component is the
abstraction of a field and a tool
is a discrete task, a service is the shared building block underneath them:
the same upload machinery powers component_3d, tool_upload and
tool_import_dedalo_csv; the same CKEditor wrapper powers component_text_area;
the same autocomplete search powers every relational component.
Services are entirely client-side (ES6 modules under <service>/js/). They are
not ontology nodes and have no server model of their own: their server side
is whatever generic API action they call — get_system_info, upload,
join_chunked_files_uploaded, the read API — each a handler in the dispatch
registry (src/core/api/dispatch.ts).
A client service object reuses the shared common JS prototype the same way a
component does — it borrows common.prototype.render / destroy / refresh /
init — but it is instanced through the same factory with a service_-prefixed
model, not declared in the ontology:
// instances.js resolves the module path from the model prefix:
// tool_* -> ../../../tools/<model>/js/<model>.js
// service_* -> ../../../core/services/<model>/js/<model>.js <-- services
// (default) -> ../../../core/<model>/js/<model>.js
A service is not a component
Services have no ontology tipo, no properties, no record data and no
typed-JSONB column. They carry a caller reference and operate on the
caller's behalf — reading the caller's context/request_config, calling
the caller's save_value(), or publishing events the caller subscribes to.
Two services (service_autocomplete, service_ckeditor) do not even use
common.prototype.init; they set their own minimal state.
Responsibilities
- Package shared UI + logic that more than one component/section/tool needs, so it is implemented and maintained once.
- Bind to a caller. Every service keeps a
self.callerand acts on it — there is no standalone service;service_uploadeven logs an error if no caller is set. - Talk to a generic server action, never to a service-specific endpoint:
service_uploadcalls theget_system_info,uploadandjoin_chunked_files_uploadedactions ondd_utils_api(backed bysrc/core/api/handlers/system_info.tsandsrc/core/media/ingest/upload.ts);service_autocomplete/service_time_machinebuild an RQO or SQO and call the read API through the caller. - Stay out of the ontology. No node, no
tipo, no permission row. The caller's permissions and the server action's own checks govern access — theuploadhandler, for instance, asserts write permission on the target section when atipois supplied. - Reuse the client lifecycle (
init→build→render→destroy) via the sharedcommonprototype so the caller can hold them in itsar_instancesand tear them down uniformly.
Key concepts
The caller relationship
A service is always created by something and given that thing as
options.caller. The service then reaches back into the caller:
service_autocompletereadscaller.context.request_configand callscaller.build_rqo_search(...)to construct the search request.service_ckeditorcallscaller.save_value(key, value)andcaller.update_changed_data(...), and asks the caller to render its tag/ reference modals (caller.render_reference(...)).service_uploadresolves its target directory fromcaller.context.features.key_dir(walking upcaller.callerif nested) and publishesupload_file_done_<caller.id>when finished.
Lifecycle & instancing
Services that use the standard lifecycle are instanced through the shared
factory get_instance({model:'service_*', caller, …}) (from
core/common/js/instances.js), which resolves the module path from the
service_ prefix, news up the exported function (named exactly as the model),
sets id/id_base, then runs init(). The caller usually then calls
build() and render(), and pushes the service into its own ar_instances
so destroy() cascades. Two services are instead imported directly as ES6
modules by a single caller:
| how it is obtained | services |
|---|---|
get_instance({model:'service_*'}) (factory, by model prefix) |
service_autocomplete, service_time_machine, service_tmp_section, service_subtitles |
direct ES6 import by one component |
service_upload (the upload() fn, used by component_3d), service_ckeditor (used by component_text_area) |
service_upload is consumed two ways
Tools (tool_upload, tool_import_dedalo_csv, tool_dev_template) instance
the object via get_instance({model:'service_upload'}) and call its
upload_file() method. component_3d instead imports the standalone
upload() function directly and calls it. Both end up at the same
dd_utils_api actions.
Files & structure
core/services/
├── service_autocomplete/ # relational search-as-you-type
│ ├── css/service_autocomplete.less
│ └── js/
│ ├── service_autocomplete.js
│ └── view_default_autocomplete.js
├── service_ckeditor/ # rich-text editor wrapper (component_text_area)
│ ├── css/service_ckeditor.less (+ dist/)
│ ├── js/
│ │ ├── service_ckeditor.js
│ │ └── render_text_editor.js
│ └── plug-ins/reference/ # custom CKEditor "reference" plug-in (src + theme)
├── service_subtitles/ # subtitle text generation (transcription tools)
│ └── js/service_subtitles.js # client shell only (server path not yet ported)
├── service_time_machine/ # dd15 history list (time machine)
│ ├── css/service_time_machine.less
│ └── js/
│ ├── service_time_machine.js
│ ├── render_service_time_machine_list.js
│ └── view_*_time_machine_list.js
├── service_tmp_section/ # ephemeral in-memory section (import preview)
│ ├── css/service_tmp_section.less
│ ├── img/icon.svg
│ └── js/
│ ├── service_tmp_section.js
│ └── render_edit_service_tmp_section.js
└── service_upload/ # chunked upload, single-file AND multi-file
├── css/service_upload.less
├── img/icon.svg
└── js/
├── service_upload.js
├── render_edit_service_upload.js # single-file form
├── upload_transport.js # DOM-free wire core (the only XHR)
├── upload_queue.js # DOM-free multi-file state model
├── dropped_files.js # recursive directory-drop traversal
└── render_edit_service_upload_queue.js # multi-file queue renderer
The file nomenclature mirrors components: the main
class is <service>/js/<service>.js, render helpers are
render_*_<service>.js, views are view_*_<service>.js, and styles are
<service>/css/<service>.less.
The available services
service_upload
Chunked upload to the server, with progress, retry and concurrency control, in
two modes on one model: the default single-file form, and the multi-file
drag-and-drop queue selected by the multiple:true init option. The main entry
points are the standalone upload() function and the
service_upload.prototype.upload_file() method.
- Validates extension (
allowed_extensions) and size (max_size_bytes, fetched fromdd_utils_api::get_system_info). - Slices the file into
DEDALO_UPLOAD_SERVICE_CHUNK_FILES-MB chunks, queues them, and uploads at mostDEDALO_UPLOAD_SERVICE_MAX_CONCURRENT(default 50) at a time; falls back to a singlesend()when chunking is disabled. - Each chunk is a
POSTtoDEDALO_API_URLcarrying aContent-Rangeheader,X-File-Name, the per-session CSRF token (headerX-Dedalo-Csrf-Tokenplus acsrf_tokenform-field fallback, SEC-008), and the chunk in aFormData. - On network error a chunk retries up to 3 times (5 s backoff).
- When all chunks land it calls
dd_utils_api::join_chunked_files_uploadedserver-side, then publishesupload_file_done_<caller.id>and reports progress throughupload_file_status_<id>events.
Consumed by: component_3d (direct upload() import), tool_upload,
tool_import_dedalo_csv, tool_dev_template. The media components
(component_image, component_av, component_pdf, component_svg, component_3d)
are the canonical upload-service consumers — see
Components → media components.
Multi-file mode (multiple: true)
init({multiple:true}) swaps the single-file renderer for
render_edit_service_upload_queue, a drag-and-drop queue that accepts whole
directories (recursive traversal in dropped_files.js) and uploads through
the same transport, chunking and CSRF path as the single-file mode — one wire,
two renderers. It also restores a queue across a page reload from
dd_utils_api::list_uploaded_files, and removes a staged row through
dd_utils_api::delete_uploaded_file.
Consumed by the import tools: tool_import_files, tool_import_marc21,
tool_import_zotero.
service_autocomplete
Search-as-you-type for relational components. It does not use
common.prototype.init; it builds a small context, resolves the main
request_config object, and drives a pluggable search engine
(dedalo_engine over the internal read API, or external_engine, which asks
the Dédalo server to search the bound external service — the browser never
contacts a third party, and never needs to know which service it is), re-combining user input + fixed filters into an SQO via
rebuild_search_query_object(). Consumed by component_portal (and through
it the other relational components — component_relation_parent,
component_relation_children, component_relation_related); the consumer
passes caller: self so the service can call caller.build_rqo_search().
service_ckeditor
Wraps a custom-built CKEditor (lib/ckeditor). It builds either a ddEditor
(limited custom toolbar, used by component_text_area) or an InlineEditor
(full toolbar). It owns the Dédalo tag model — index in/out, geo, timecode,
draw, SVG, person, note, lang and the custom reference plug-in
(plug-ins/reference/) — converting between CKEditor model nodes and Dédalo
tags, and tracking an is_dirty flag that drives save() →
caller.save_value(key, value). Consumed by component_text_area
(self.service_text_editor = service_ckeditor).
service_time_machine
The data/render logic of the Time Machine
history list (section dd15). It hard-codes tipo/section_tipo to dd15,
takes the caller's section_tipo/section_id as the record whose history to
show, borrows common.prototype.build_rqo_show and a paginator, and renders the
versions list. Consumed by tool_time_machine, the inspector,
section_record list view, component_text_area note view and the shared
component_common event subscriptions.
service_tmp_section
An ephemeral, in-memory section used to render and collect data without a
persisted record (the import-preview pattern). init() takes a ddo_map;
build() instances each mapped element via the JS get_instance factory;
get_components_data() harvests their current values back out. Consumed by
the import tools (e.g. tool_import_marc21) to stage parsed rows before commit.
service_subtitles
The client lifecycle shell for subtitle work, consumed by tool_subtitles and
tool_tr_print.
Subtitle file generation is a tool action, not a service endpoint:
build_subtitles_file (tools/tool_transcription/server/index.ts) builds the
WEBVTT file from a transcription's text and timecodes. See
Creating tools for how a tool action
is registered and permission-gated.
Public API / Key methods
Client services share four borrowed common prototype methods —
render(), destroy(), refresh(), and (for most) init(). Below are the
service-specific public methods, verified from the source, grouped by
service. (All are instance methods on the JS prototype unless noted.)
service_upload (core/services/service_upload/js/service_upload.js)
| method | exported fn? | purpose |
|---|---|---|
init(options) |
Set caller, allowed_extensions, key_dir, max_concurrent, multiple (strict true selects the queue renderer); subscribe the upload_file_status_<id> progress handler. |
|
build(autoload=false) |
Fetch server limits via dd_utils_api::get_system_info (max size, tmp dir, chunk size, OCR engine). |
|
upload_file(options) |
Resolve key_dir from the caller, run upload(), then publish upload_file_done_<caller.id>. |
|
join_chunked_files(options) |
Call dd_utils_api::join_chunked_files_uploaded to reassemble chunks server-side. |
|
upload(options) |
✓ (module export) | The standalone uploader: validate, chunk/queue/concurrency, XHR with CSRF + retry; resolves the API response. Imported directly by component_3d. |
service_autocomplete (core/services/service_autocomplete/js/service_autocomplete.js)
| method | purpose |
|---|---|
init(options) |
Build the minimal service context; store caller, request_config, lang. |
build(options={}) |
Resolve the main dedalo request_config, build rqo_search via the caller, map columns. |
render(options={}) |
Delegate to view_default_autocomplete.render. |
autocomplete_search() |
Dispatch to the configured <engine>_engine. |
dedalo_engine() |
Internal read-API search (list mode, skip_projects_filter). |
external_engine(options) |
External-service search THROUGH the engine (dd_external_api::search). zenon_engine is a stable alias of it. |
rebuild_search_query_object(options) |
Merge user input + fixed/list filters into the SQO. |
service_autocomplete_keys(e) |
Keyboard navigation (Up/Down/Enter) over the datalist. |
split_q(q) |
Split a multi-field query on |. |
get_total() |
Paginator-compatibility shim (returns limit). |
show() / hide() |
(from view_default_autocomplete) toggle the datalist. |
destroy(...) |
Remove the keydown listener, then call the parent destroy. |
service_ckeditor (core/services/service_ckeditor/js/service_ckeditor.js)
| method | purpose |
|---|---|
init(options) |
Load CKEditor + lang JSON, create the editor (ddEditor/InlineEditor). |
create_ddEditor(cfg) / create_InlineEditor(cfg) |
Build the two editor flavours and wire toolbar/events. |
save() |
If dirty, caller.save_value(key, editor.getData()). |
get_value() |
Editor content as raw HTML string. |
set_content(tag_obj) |
Insert a Dédalo tag node at the caret. |
delete_tag(tag_obj) / update_tag(options) |
Remove / edit a tag (by type+tag_id) in the model. |
get_selection() / set_selection_from_tag(tag_obj) |
Read / set the current selection. |
wrap_selection_with_tags(in,out) |
Wrap a selection with paired in/out tags. |
set_reference(options) / remove_reference() / get_selected_reference_element() |
Manage the custom reference plug-in tags. |
get_view_tag / get_view_tag_node / get_view_tag_attributes / get_last_tag_id |
Tag lookup helpers over the editor model/view. |
setup_events(cfg) / setup_button_reference() / build_toolbar(cfg) / factory_events_for_buttons(btn) |
Wire editor events and the custom toolbar buttons. |
set_dirty(value) / init_status_changes() |
Track and propagate the dirty state to the caller. |
focus() / scroll_to_selection() / destroy() |
Focus, scroll, teardown. |
service_time_machine (core/services/service_time_machine/js/service_time_machine.js)
| method | purpose |
|---|---|
init(options) |
Pin tipo/section_tipo to dd15; store caller record + columns map. |
build(autoload=false) |
Prepare the request and paginator for the history list. |
build_request_config() |
Compose the request_config for the dd15 list. |
list() / tm() |
(from render_service_time_machine_list) render the versions list. |
build_rqo_show() |
(from common) build the show RQO. |
get_total() |
Total versions, for the paginator. |
service_tmp_section (core/services/service_tmp_section/js/service_tmp_section.js)
| method | purpose |
|---|---|
init(options) |
Store the ddo_map. |
build(autoload=false) |
Instance each ddo_map element via the JS factory. |
get_components_data() |
Harvest the current values from the instanced components. |
edit() |
(from render_edit_service_tmp_section) render the editable inputs. |
service_subtitles — server side
service_subtitles.js is a client shell. The server work — building the WEBVTT
file from a transcription — is the build_subtitles_file action of
tool_transcription, not a service endpoint.
How it fits with the rest of Dédalo
- Components — media components (
component_image,component_av,component_pdf,component_svg,component_3d) consume the upload service;component_text_areaconsumes ckeditor; the relational components (viacomponent_portal) consume autocomplete. See Components → index, component_text_area, component_portal, component_3d. - Tools — upload/import tools consume upload (single-file or the
multi-file queue) and tmp_section;
tool_time_machineconsumes time_machine;tool_subtitles/tool_transcriptionconsume subtitles. See Creating tools. - The shared client prototype — every client service borrows
render/destroy/refresh(and most borrowinit), so it lives in the caller'sar_instancesand tears down with it. - The instance factory —
core/common/js/instances.jsresolvesservice_*models tocore/services/<model>/js/<model>.js; factory-instanced services share the same caching and keying as components and tools. - The server API — services never expose a bespoke endpoint; they call the
generic actions in the dispatch registry (
src/core/api/dispatch.ts):get_system_info(src/core/api/handlers/system_info.ts), the multipartuploadplusjoin_chunked_files_uploaded/list_uploaded_files(src/core/media/ingest/upload.ts), and the read API. Permission enforcement is the caller's and the API action's responsibility — the upload handler asserts write permission on the target section when atipois supplied. - Search —
service_autocompletebuilds an SQO through the caller and runs it over the internal read API. See SQO and RQO.
Examples
A tool instancing the upload service (factory)
import { get_instance } from '../../../core/common/js/instances.js'
// inside a tool's build():
self.service_upload = await get_instance({
model : 'service_upload', // resolves to core/services/service_upload/js/service_upload.js
caller : self, // MANDATORY — the service acts on the caller
allowed_extensions : ['jpg','jpeg','png','tiff'],
key_dir : { type:'dedalo_config', value:'DEDALO_TOOL_UPLOAD_FOLDER_PATH' }
})
self.ar_instances.push(self.service_upload) // tear down with the tool
await self.service_upload.build() // fetch server limits (get_system_info)
// later, on a chosen file:
const api_response = await self.service_upload.upload_file({ file })
A component importing the standalone upload() function
// component_3d/js/component_3d.js
import { upload } from '../../services/service_upload/js/service_upload.js'
const api_response = await upload({
self : self,
id : self.id, // used for upload_file_status_<id> progress events
file : file, // { name:'model.glb', size:12345678 }
key_dir : 'image',
allowed_extensions : self.allowed_extensions,
max_size_bytes : self.max_size_bytes,
tipo : self.tipo,
max_concurrent : self.max_concurrent
})
component_portal wiring the autocomplete service
self.autocomplete = await get_instance({
model : 'service_autocomplete',
caller : self, // service calls caller.build_rqo_search()
tipo : self.tipo,
section_tipo : self.section_tipo,
request_config : self.context.request_config,
properties : self.context.properties.service_autocomplete || null,
id_variant : (self.id_variant || '') + '_' + Date.now() // avoid instance cache collisions
})
await self.autocomplete.build()
const service_node = await self.autocomplete.render()
Related
- Architecture overview — where the work-system client/server split sits.
- Components — the field abstraction services plug into; component_text_area (ckeditor), component_portal (autocomplete), media components (upload).
- Creating tools — the task abstraction that consumes upload/tmp_section/time_machine/subtitles.
- common — the shared object machinery services borrow on the client.
- RQO · SQO — the request/query objects
service_autocompletebuilds. - dd_object (ddo) — the datum services render and collect.