Skip to content

Services support#1528

Merged
vadimpiven merged 41 commits intomainfrom
feat/services
Mar 31, 2026
Merged

Services support#1528
vadimpiven merged 41 commits intomainfrom
feat/services

Conversation

@vadimpiven
Copy link
Copy Markdown
Member

@vadimpiven vadimpiven commented Mar 23, 2026

Greptile Summary

This PR introduces a services subsystem for Platforma V3 blocks, providing typed access to WASM services (e.g., PFrameSpec via SpecDriver) and node services (e.g., PFrame proxied over IPC). The implementation is well-architected across several layers: service declaration (service_declarations.ts), a shared type and registry layer (pl-model-common), QuickJS VM injection for model scripts (service_injectors.ts, computable_context.ts), and UI-side lazy service composition (service_bridge.ts, createAppV3.ts).

Key design decisions implemented cleanly relative to the previously-reviewed plan:

  • Single source of truth: Services object in service_declarations.ts drives all auto-derived types (ServiceRequireFlags, SERVICE_CAPABILITY_FLAGS, SERVICE_METHOD_MAP), eliminating the string-literal duplication risks noted in plan-phase reviews.
  • Type-safe injector maps: Both ServiceInjectorMap (model/QuickJS side) and UiServiceInjectorMap (UI side) are compile-time exhaustive — missing a service after adding it to Services is a type error.
  • Correct caching: ServiceRegistryBase.getById() uses Map.has() (not === undefined) to cache singleton instances, and the UI-side buildServices uses an explicit fetched flag set before the factory call to prevent retry on error.
  • Getter-safe introspection: getMethodNames uses Object.getOwnPropertyDescriptor to walk the prototype chain without triggering getters.
  • satisfies used correctly: BLOCK_SERVICE_FLAGS uses satisfies rather than a direct annotation, preserving the literal type needed by ResolveModelServices.

One issue found:

  • resolveUiInjector in lib/model/common/src/services/service_injectors.ts returns undefined instead of the declared null for WASM services (see inline comment).

Confidence Score: 5/5

Safe to merge — implementation is solid, well-tested, and all plan-phase concerns have been addressed in the actual code.

The only finding is a P2 type-contract mismatch in resolveUiInjector (returns undefined for WASM services instead of null). All prior P0/P1 concerns from the design review have been correctly addressed: getters are not triggered during introspection, Map.has() is used for singleton caching, satisfies is used instead of widening annotations, and the injector maps are compile-time exhaustive. No runtime bugs or data-integrity issues remain.

lib/model/common/src/services/service_injectors.ts — resolveUiInjector return type mismatch for WASM services.

Important Files Changed

Filename Overview
lib/model/common/src/services/service_injectors.ts SERVICE_METHOD_MAP eagerly computed at module load via stub DriverKit. resolveUiInjector has a return-type mismatch for WASM services — returns undefined rather than the declared null.
lib/model/common/src/services/service_types.ts Introduces service registration system via IIFE-scoped typeMap; service() enforces unique names with pattern validation; isNodeService() closure is clean. ServiceDispatch interface and factory map types are well-typed.
lib/model/common/src/services/service_registry.ts ServiceRegistryBase with lazy factory instantiation and Map.has()-based caching; dispose() correctly chains async/sync dispose; ServiceNotRegisteredError thrown for unknown IDs.
lib/model/common/src/services/service_capabilities.ts Auto-derives SERVICE_CAPABILITY_FLAGS from Services keys; getMethodNames uses Object.getOwnPropertyDescriptor to avoid triggering getters; resolveRequiredServices handles undefined flags gracefully.
lib/node/pl-middle-layer/src/js_render/service_injectors.ts Model-side VM injectors with complete type-checked ServiceInjectorMap; PFrameSpec injector correctly guards for null driver; PoolEntryGuard usage with idempotent unref is correct.
lib/node/pl-middle-layer/src/js_render/computable_context.ts Service injection into QuickJS VM via serviceFunctions Map; getServiceMethods prefix-filter correctly uses colon separator preventing cross-service ID prefix collisions; callServiceMethod dispatches correctly.
lib/node/pl-middle-layer/src/service_factories.ts Correct ModelServiceRegistry factory: PFrameSpec gets lazy SpecDriver factory, PFrame gets null (desktop provides it); lazy instantiation on first use.
sdk/model/src/services/service_bridge.ts buildServices uses fetched flag (set before registry.get) to cache failures permanently — intentional per JSDoc; createNodeServiceProxy wraps all methods as async IPC calls correctly.
sdk/model/src/services/service_resolve.ts Type-level service resolution with compile-time assertions; ResolveModelServices correctly maps feature flags to typed service maps; @ts-expect-error guards verify non-default services are excluded.
sdk/ui-vue/src/internal/createAppV3.ts createUiServiceRegistry and buildServices wired correctly; services exposed as markRaw on app and per-plugin states; uiRegistry.dispose() called on beforeunload.

Comments Outside Diff (3)

  1. sdk/model/src/columns/column_collection_builder.ts, line 232-243 (link)

    P1 Spec frames created but never disposed — WASM resource leak

    ColumnCollectionImpl calls this.specDriver.createSpecFrame(...) directly on the PFrameSpecDriver. The old ComputableContextHelper.createSpecFrame() wrapper registered an addOnDestroy cleanup callback; that wrapper is removed. Every createPlDataTableV3 call accumulates a PFrameWasmV2 in the long-lived SpecDriver.frames Map without ever freeing it.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: sdk/model/src/columns/column_collection_builder.ts
    Line: 232-243
    
    Comment:
    **Spec frames created but never disposed — WASM resource leak**
    
    `ColumnCollectionImpl` calls `this.specDriver.createSpecFrame(...)` directly on the `PFrameSpecDriver`. The old `ComputableContextHelper.createSpecFrame()` wrapper registered an `addOnDestroy` cleanup callback; that wrapper is removed. Every `createPlDataTableV3` call accumulates a `PFrameWasmV2` in the long-lived `SpecDriver.frames` Map without ever freeing it.
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. lib/node/ts-helpers/src/ref_count_pool.ts, line 123-135 (link)

    dispose() iterates envelopes, never disposes inner resources

    this.resources.values() yields RefCountEnvelope<R> objects ({ refCount, resource }), not the actual R instances. Both isDisposable(resource) and isAsyncDisposable(resource) check the envelope, which never has Symbol.dispose or Symbol.asyncDispose, so neither branch is ever taken. Every resource managed by the pool silently leaks on disposal.

  3. lib/node/ts-helpers/src/ref_count_manual_pool.ts, line 70-81 (link)

    dispose() checks the envelope, not the inner resource

    this.resources.values() yields RefCountEnvelope<R> objects. The variable name resource is misleading — it is the envelope ({ refCount, resource }), not the contained resource. isDisposable(resource) will never be true because envelopes don't implement Symbol.dispose, so all managed resources leak on pool disposal.

    The release() method gets this right by doing const resource = envelope.resource before checking; dispose() should follow the same pattern:

Prompt To Fix All With AI
This is a comment left during a code review.
Path: lib/model/common/src/services/service_injectors.ts
Line: 48-58

Comment:
**`resolveUiInjector` returns `undefined` for WASM services instead of `null`**

For WASM services (e.g., `Services.PFrameSpec`), `Object.keys(Services).find(...)` successfully returns `"PFrameSpec"`, so the `if (!key) return null` early exit is never taken. The function then returns `injectors[key]`, but `UiServiceInjectorMap` only has entries for node services (`NodeServiceKeys = "PFrame"`). Accessing `injectors["PFrameSpec"]` evaluates to `undefined` at runtime, not `null`.

The declared return type is `Record<string, Function> | null`, but the function actually returns `undefined` for any WASM service. Any caller that checks `result === null` to detect "no injector" will silently receive `undefined` and may pass it to code expecting a service object.

Consider adding an early guard with `isNodeService(serviceId)` — WASM services have no UI injector and should return `null` immediately — and using `?? null` on the final return to ensure the contract is always met.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (14): Last reviewed commit: "Add coverage to oxfmt ignorePatterns acr..." | Re-trigger Greptile

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Mar 23, 2026

🦋 Changeset detected

Latest commit: 6b68910

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 35 packages
Name Type
@milaboratories/pl-model-common Minor
@milaboratories/pf-spec-driver Minor
@milaboratories/pf-driver Minor
@milaboratories/ts-helpers Minor
@milaboratories/pl-middle-layer Minor
@platforma-sdk/model Minor
@platforma-sdk/ui-vue Minor
@platforma-sdk/test Minor
@milaboratories/pl-model-middle-layer Patch
@milaboratories/pl-client Patch
@milaboratories/pl-drivers Patch
@milaboratories/pl-deployments Patch
@platforma-open/milaboratories.software-ptabler.schema Patch
@platforma-sdk/block-tools Patch
@milaboratories/ts-helpers-oclif Patch
@milaboratories/ts-helpers-winston Patch
@milaboratories/pl-errors Patch
@milaboratories/computable Patch
@milaboratories/pl-tree Patch
@milaboratories/pl-config Patch
@platforma-sdk/tengo-builder Patch
@platforma-sdk/pl-cli Patch
@milaboratories/uikit Patch
@milaboratories/milaboratories.monetization-test.model Patch
@milaboratories/milaboratories.monetization-test.ui Patch
@milaboratories/milaboratories.ui-examples.model Patch
@milaboratories/milaboratories.ui-examples.ui Patch
@milaboratories/milaboratories.ui-examples Patch
@milaboratories/milaboratories.pool-explorer.model Patch
@milaboratories/milaboratories.pool-explorer Patch
@milaboratories/milaboratories.pool-explorer.ui Patch
@milaboratories/pl-model-backend Patch
@platforma-sdk/bootstrap Patch
@milaboratories/ptabler-expression-js Patch
@milaboratories/milaboratories.monetization-test Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request presents a detailed architectural plan for implementing a unified service provider within the application. The plan introduces a declarative system for defining and consuming versioned services, allowing blocks and plugins to specify their dependencies explicitly. It outlines the creation of dedicated service registries for both model and UI components, incorporating lazy loading for UI services and a robust IPC mechanism for remote Node.js services. The design also includes comprehensive compatibility checks for service availability and a clear migration path, ensuring a smooth transition from the current unconditional driver injection model to a more modular and maintainable service-oriented architecture.

Highlights

  • Unified Service Provider Architecture: Introduced a comprehensive plan for a new system to define, declare, and provide versioned services to blocks and plugins, moving away from unconditional driver injection.
  • Service Definition and Declaration: Defined ServiceDef and defineService for creating type-safe service identifiers, and established a mechanism for blocks and plugins to explicitly declare their required services using a new .service() method.
  • Model and UI Service Registries: Designed separate service registries for model-side and UI-side service factories, enabling lazy instantiation for UI services and efficient management of multiple service versions.
  • Service Router for IPC: Planned a new IPC service router to handle remote calls for Node.js-based services, ensuring explicit method registration and immediate failure for missing channels.
  • Backward Compatibility Strategy: Included a detailed strategy to ensure older block versions continue to function without modification while new blocks adopt the service-oriented approach, using requiresModelAPIVersion for detection.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive design plan for a unified service provider, aiming to replace the current unconditional driver injection with a structured, versioned service definition and registry system. The plan details how services will be defined, declared as requirements by blocks and plugins, provisioned on both model and UI sides (with lazy loading for UI services), and how compatibility will be handled. Key feedback includes suggestions to improve the robustness of the _registeredIds set against HMR, enhance the lazyMap proxy with additional traps for standard object operations, clarify the wrapResult function in the service router, provide a concrete implementation sketch for ServiceRegistry.get, and detail the mechanism for tracking and rejecting in-flight IPC calls from evicted UI components.

Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
@vadimpiven
Copy link
Copy Markdown
Member Author

@greptileai

Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
@vadimpiven
Copy link
Copy Markdown
Member Author

@greptileai

Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
@vadimpiven
Copy link
Copy Markdown
Member Author

@greptileai

Comment thread plan.md Outdated
Comment thread plan.md Outdated
@vadimpiven
Copy link
Copy Markdown
Member Author

@greptileai

Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
@vadimpiven
Copy link
Copy Markdown
Member Author

@greptileai

Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
@vadimpiven
Copy link
Copy Markdown
Member Author

@greptileai

1 similar comment
@vadimpiven
Copy link
Copy Markdown
Member Author

@greptileai

Comment thread plan.md Outdated
Comment thread plan.md Outdated
@vadimpiven
Copy link
Copy Markdown
Member Author

@greptileai

@vadimpiven vadimpiven changed the title Plan for services Services support Mar 25, 2026
@vadimpiven
Copy link
Copy Markdown
Member Author

@greptileai

Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
Comment thread plan.md Outdated
@vadimpiven
Copy link
Copy Markdown
Member Author

@greptileai

1 similar comment
@vadimpiven
Copy link
Copy Markdown
Member Author

@greptileai

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 30, 2026

Codecov Report

❌ Patch coverage is 45.70447% with 158 lines in your changes missing coverage. Please review.
✅ Project coverage is 54.32%. Comparing base (25bd611) to head (6b68910).
⚠️ Report is 5 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
lib/model/pf-spec-driver/src/spec_driver.ts 22.95% 45 Missing and 2 partials ⚠️
...l-middle-layer/src/js_render/computable_context.ts 24.19% 45 Missing and 2 partials ⚠️
...pl-middle-layer/src/js_render/service_injectors.ts 4.00% 24 Missing ⚠️
sdk/model/src/render/api.ts 0.00% 12 Missing ⚠️
lib/model/common/src/services/service_types.ts 53.33% 5 Missing and 2 partials ⚠️
lib/model/common/src/drivers/pframe/spec/spec.ts 0.00% 6 Missing ⚠️
lib/model/common/src/services/service_registry.ts 77.77% 6 Missing ⚠️
sdk/model/src/columns/column_collection_builder.ts 73.33% 4 Missing ⚠️
lib/node/pf-driver/src/driver_impl.ts 92.30% 2 Missing ⚠️
...e/pl-middle-layer/src/middle_layer/middle_layer.ts 80.00% 1 Missing ⚠️
... and 2 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1528      +/-   ##
==========================================
- Coverage   54.55%   54.32%   -0.24%     
==========================================
  Files         250      254       +4     
  Lines       14467    14525      +58     
  Branches     2999     3004       +5     
==========================================
- Hits         7893     7891       -2     
- Misses       5572     5633      +61     
+ Partials     1002     1001       -1     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vadimpiven
Copy link
Copy Markdown
Member Author

@greptileai

Address review findings: abort listener cleanup, lazy proxy
getOwnPropertyDescriptor, ServiceCallAbortedError definition,
block:dispose ordering, test reset utility, terminology pass.
ServiceId (branded string, PluginHandle pattern), ServiceRegistry,
ServiceInjector for VM injection, BlockServiceContext for Ui-side
dispose, lazyMap for lazy instantiation, full model-side and
Ui-side injection paths, backward compatibility, per-file changes.
- Split service code into focused modules: service_declarations, service_core,
  service_registry, service_capabilities, block_services, service_resolve, service_bridge
- Extract service factory registrations to dedicated files (service_factories.ts)
- Remove legacy SpecDriver from computable_context, migrate to service system
- Restore accidentally removed createPFrame/createPTable/createPTableV2 VM exports
- Add unsupported valueType filtering in SpecDriver.createSpecFrame
- Rename ModelServiceDrivers/UiServiceDrivers to ModelServices/UiServices
- Tighten UiServices type constraint to Partial<UiServices>
- Add BlockDefaultUiServices with proper type derivation through PlatformaV3
- Add block-level service tests (model outputs + UI indicators)
- Add registerServiceCapabilities helper for runtime capability registration
- Add unit tests for service infrastructure (19 tests)
- Add compile-time type assertions for service access restrictions
- Fix createAppV3.test.ts mock to include serviceDispatch
- Pass blockServiceNames to BlockRenderCtx in all block output lambdas
…OD_MAP

Extract createUiServiceInjectors, resolveUiInjector, SERVICE_METHOD_MAP, and
buildServiceInfo from desktop app into pl-model-common. Method names are
auto-derived at module load from the injector factory shape, eliminating the
need for runtime worker calls or caching.
…e, this binding

- Add Disposable to ColumnCollectionImpl and AnchoredColumnCollectionImpl
  to dispose spec frames and prevent WASM resource leaks
- Warn on unsupported valueType in SpecDriver instead of silent drop
- Use Map.has() instead of truthiness check in ServiceRegistryBase
- Preserve this binding in render/api.ts service dispatch
- Add dispose() method and Disposable interface to ColumnCollection and
  AnchoredColumnCollection to prevent spec frame WASM resource leaks
- Use 'using' in createPlDataTableV3 for automatic cleanup
- Revert console.warn on unsupported valueType (filtering is by design)
The test doesn't run the workflow, so calculationStatus is NotCalculated.
Use awaitStableValue() on block state instead of awaitBlockDone() which
requires calculationStatus === Done. Accept createPlDataTable error
(no result pool data in test environment).
Logging now happens before the actual operation so that
the log entry is visible even when the call crashes.
Both RefCountPoolBase and RefCountManualPoolBase iterated
envelopes but checked isDisposable on the envelope object
rather than envelope.resource, so inner resources were never
disposed on pool teardown.
Old blocks use the pl7.app/parents annotation (axis names) to express
parent-child relationships, but the Rust/WASM engine only reads numeric
parentAxes indices. This caused linker columns like pl7.app/sc/cellLinker
to fail with "must have exactly 2 connected components, got 3".

Add resolveAnnotationParents() helper in pl-model-common that pipes
axesSpec through getNormalizedAxesList + getDenormalizedAxesList to
convert annotation-based parents to numeric indices. Apply it in:
- pf-spec-driver createSpecFrame (spec-only WASM path)
- pf-driver createPFrame (data WASM path)
- pf-driver createPTableV2 (spec frame for query evaluation)
- createSpecFrame returns PoolEntry<SpecFrameHandle> with idempotent
  unref instead of bare handle; disposeSpecFrame removed
- Injector returns VM object {key, unref} and registers addOnDestroy
  for automatic cleanup on computable destruction
- Add PoolEntry/PoolEntryGuard to pl-model-common for cross-package
  pool lifecycle management
- Add requireComputableCtx getter, migrate all computableCtx guards
- Migrate createPFrame/createPTable/createPTableV2 to PoolEntryGuard
- Add ServiceRegistryBase.dispose() for proper service cleanup
- Migrate PFramePool from RefCountManualPoolBase to RefCountPoolBase;
  delete RefCountManualPoolBase (no remaining consumers)
- SpecDriver implements AsyncDisposable (pool is async)
oxfmt extends does not merge ignorePatterns — local overrides replace
the base config entirely. Add coverage/ to all .oxfmtrc.json files
so test coverage artifacts are never checked for formatting.
@vadimpiven
Copy link
Copy Markdown
Member Author

@greptileai

Iterator helpers are an ES2024 feature not available in QuickJS.
Revert to Array.from() with mapping function.
Comment thread lib/model/common/src/services/service_injectors.ts Outdated
.map((key) => Services[key]);
}

export type KnownServiceName = (typeof Services)[keyof typeof Services] & string;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's do ServiceName = (typeof Services)[keyof typeof Services] & string;
and remove brand type and KnownServiceName

Comment thread lib/model/common/src/services/service_types.ts Outdated
Comment thread lib/node/pl-middle-layer/src/js_render/service_injectors.ts Outdated
@vadimpiven vadimpiven enabled auto-merge March 31, 2026 17:18
@vadimpiven vadimpiven added this pull request to the merge queue Mar 31, 2026
Merged via the queue into main with commit 74a2ffa Mar 31, 2026
17 checks passed
@vadimpiven vadimpiven deleted the feat/services branch March 31, 2026 18:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants