Skip to content

Server-Side Store

Overview

KittoX maintains data stores in server memory across HTTP requests, tied to the user's session. When a form is opened, the store (with its records and field values) is registered in the session and remains alive until the form is closed. Subsequent requests — such as loading a blob image or saving changes — access the same store instance without re-querying the database.

This architecture enables three key capabilities:

  1. Blob lazy-loading — Image fields are loaded from the database only when actually displayed, not when the record is first loaded
  2. Record state tracking — Each record knows whether it is New, Modified, Deleted, or Clean
  3. Transactional master-detail saving — A master record and all its detail records can be saved in a single database transaction

How It Works

Every browser session is identified by a kx_session cookie. The server maintains a TKWebSession object for each active session, which holds authentication data, open controllers, and — crucially — a dictionary of active stores keyed by view name.

Browser                          Server (TKWebSession)
  │                                │
  │  Cookie: kx_session=abc123     │
  │ ──────────────────────────────>│
  │                                │  FStores['Customers'] → TKViewTableStore
  │                                │  FStores['UserProfile'] → TKViewTableStore
  │                                │  ...

Lifecycle of a Store

1. Form Opens

When a form is opened (either as a modal dialog from a grid, or as a standalone form in a tab), the server:

  1. Creates a TKViewTableStore for the view's main table
  2. Loads the record(s) from the database
  3. Registers the store in the session: TKWebSession.Current.RegisterStore(ViewName, Store)
  4. Renders the form HTML with field values from the loaded record

The store remains in memory — it is not freed at the end of the HTTP request.

2. Blob Request

When the browser requests a blob field (e.g., an image thumbnail), the URL is:

GET kx/view/{ViewName}/blob/{FieldName}?key=...

The server:

  1. Looks up the store from the session: TKWebSession.Current.FindStore(ViewName)
  2. If found: accesses the record's blob field directly via AsBytes
  3. The blob data is lazy-loaded from the database at this point — only the blob column is fetched, not the entire record

This avoids a full SELECT * query for each blob field, which is especially important when a form has multiple image fields or when images are large.

Lazy Loading

Delphi's TBlobField loads blob data on demand. When the store is first loaded, blob columns contain only a placeholder. The actual binary data is fetched from the database only when AsBytes (or AsStream) is accessed. This is a built-in optimization of FireDAC and other Delphi database frameworks.

If no session store is found (e.g., the session expired), the server falls back to a direct database query with properly qualified key fields to avoid ambiguity in JOINs.

3. Save

When the user saves the form, the server:

  1. Processes the POST data and updates the record fields
  2. Persists to the database via Model.SaveRecord
  3. Releases the store from the session: TKWebSession.Current.UnregisterStore(ViewName)

4. Cancel or Close

When the user cancels the form or closes the tab, the client sends a lightweight fire-and-forget request:

POST kx/view/{ViewName}/form-close

The server simply releases the store: TKWebSession.Current.UnregisterStore(ViewName). No database operation is performed.

5. Session Timeout

If the user abandons the browser without closing forms, the stores remain in memory until the session expires. The session's store dictionary uses TObjectDictionary with doOwnsValues, so all stores are automatically freed when the session is destroyed.

Store Release Summary

ScenarioStore ReleasedMechanism
Save + closeYesUnregisterStore after successful save
Save & CloneNo (correct)Form stays open for next entry
Save & KeepOpenNo (correct)Form stays open for editing
Cancel (modal)Yesform-close from kxForm.cancel
Cancel (tab)Yesform-close from kxForm.cancel
Close tab with XYesform-close from kxTabs.close
Session timeoutYesTObjectDictionary frees all stores

Record State Tracking

Every record in a store has a state that tracks what happened to it since it was loaded:

pascal
TKRecordState = (rsNew, rsClean, rsDirty, rsDeleted)
StateMeaningSQL Operation on Save
rsNewRecord created in memory, not yet in DBINSERT
rsCleanRecord loaded from DB, not modifiedNo operation
rsDirtyRecord loaded from DB, then modifiedUPDATE (only modified fields)
rsDeletedRecord marked for deletionDELETE

State transitions are managed by methods on TKRecord:

  • MarkAsNew — called when a record is created for insertion
  • MarkAsModified — called automatically when a field value changes (only if not already New or Deleted)
  • MarkAsDeleted — called when the user deletes a record
  • MarkAsClean — called after a successful save

Optimized Updates

The UPDATE SQL statement only includes fields that were actually modified (IsModified = True). Unchanged fields are not sent to the database, reducing network traffic and avoiding unnecessary trigger executions.

Transactional Master-Detail Saving

The Problem

In a master-detail form (e.g., an Invoice with Line Items), the user may:

  • Modify the invoice header (master)
  • Add new line items (detail — rsNew)
  • Modify existing line items (detail — rsDirty)
  • Delete line items (detail — rsDeleted)

All these changes must be saved atomically: either everything succeeds, or nothing is written to the database. An invoice must never be saved without its line items, and line items must never be orphaned.

The Solution

KittoX uses the PersistRecord method which:

  1. BEGIN TRANSACTION
  2. Persists the master record (INSERT, UPDATE, or DELETE based on its state)
  3. Calls PersistDetailStores — iterates all detail stores attached to the master record
  4. For each detail store: saves each record according to its state
  5. COMMIT on success, ROLLBACK on any error
  6. Marks all saved records as rsClean
Master Record (rsDirty → UPDATE)
  ├── Detail Store: Line Items
  │     ├── Record 1 (rsNew → INSERT)
  │     ├── Record 2 (rsDirty → UPDATE)
  │     ├── Record 3 (rsClean → skip)
  │     └── Record 4 (rsDeleted → DELETE)
  └── Detail Store: Attachments
        └── Record 1 (rsNew → INSERT)

All in a single database transaction.

Even if the master record is rsClean (not modified), its detail stores are still checked and saved. This allows the user to modify only detail records while leaving the master unchanged.

Detail Records Don't Persist Immediately

When editing a detail record (e.g., a line item in an invoice form), changes are stored in memory only. The detail record's state changes to rsDirty or rsNew, but no SQL is executed until the master form is saved.

This is determined structurally: ViewTable.IsDetail returns True when the view table is a child of another view table in the YAML hierarchy. The save logic uses:

pascal
Model.SaveRecord(ARecord, APersist and not ViewTable.IsDetail);

For detail tables, APersist is forced to False — changes accumulate in the server-side store until the master triggers the final transactional save.

API Reference

TKWebSession Methods

pascal
/// Registers a store for a view. The session becomes owner.
procedure RegisterStore(const AViewName: string; AStore: TKViewTableStore);

/// Returns the store for a view, or nil if none is registered.
function FindStore(const AViewName: string): TKViewTableStore;

/// Removes and frees the store for a view.
procedure UnregisterStore(const AViewName: string);

HTTP Endpoints

EndpointMethodPurpose
kx/view/{ViewName}/formGETOpens form, registers store in session
kx/view/{ViewName}/blob/{Field}?key=...GETReads blob from session store (lazy-load)
kx/view/{ViewName}/savePOSTSaves record, releases store
kx/view/{ViewName}/form-closePOSTReleases store without saving (cancel/close)

TKRecordState

pascal
TKRecordState = (rsNew, rsClean, rsDirty, rsDeleted);
MethodEffect
MarkAsNewSets state to rsNew
MarkAsModifiedSets state to rsDirty (no-op if already rsNew or rsDeleted)
MarkAsDeletedSets state to rsDeleted
MarkAsCleanSets state to rsClean (called after successful save)

Thread Safety

Each HTTP request is handled by a thread from the pool. The thread sets TKWebSession.Current (a class threadvar) at the beginning of the request. Since stores are accessed only through the current session, and each session is accessed by one thread at a time, no additional locking is needed.

Implementation Status

PhaseStatusDescription
Phase 1CompleteSession store cache, blob lazy-load, store lifecycle management
Phase 2PlannedTransactional master-detail saving using PersistRecord with cascading
Phase 3PlannedIncremental client-to-server field synchronization
Phase 4PlannedOptimizations (store timeout, concurrency locks, blob cache)

See KittoXServerStore.md in the repository root for detailed implementation notes.

Released under Apache License, Version 2.0.