Follow-up from PR #282 (review comment #17, P1)
PR #282 (Add Functions gRPC core helpers) added the new packages/azure-functions-durable compat package (npm durable-functions v4), built on the @microsoft/durabletask-js gRPC core. In review, @andystaples flagged (P1) that the package removed the v3 entity-lock API without a bridge:
context.df.lock(...entityIds) — v3 yields a DurableLock
context.df.isLocked(...) — returns boolean
- types
DurableLock, LockState, LockingRulesViolationError
This was resolved in #282 as a documented breaking change (CHANGELOG migration to the core-native API), not restored. This issue tracks whether the v3 surface can actually be brought back for drop-in compatibility, or whether the documented migration is the permanent answer.
Why it wasn't restored in #282
Core already exposes the same capability, with a different shape:
v3 (durable-functions) |
core (@microsoft/durabletask-js) |
yield context.df.lock(...ids) -> DurableLock |
context.entities.lockEntities(...ids) -> Task<LockHandle> (handle.release()) |
context.df.isLocked() |
context.entities.isInCriticalSection() |
The hard part is lock(): v3 orchestrators yield it and receive a DurableLock. Core's lockEntities() returns a Task<LockHandle>. To preserve the v3 yield shape we would need to remap Task<LockHandle> -> Task<DurableLock>, but core's Task<T> has no .then / .map / derived-task primitive (packages/durabletask-js/src/task/task.ts). Adding one touches the replay engine, so it is determinism-sensitive and was intentionally out of scope for #282.
Investigation asks
isLocked() alias (likely cheap): can context.df.isLocked() be implemented directly on top of isInCriticalSection() in the compat layer with no engine change? Confirm the semantics match (any-lock-held vs specific-entity query).
lock() bridge (the real question):
- (a) Can a compat-layer wrapper adapt
LockHandle -> DurableLock (yield shape + .release() semantics) without an engine-level Task.map, e.g. by post-processing the resolved handle replay-safely? Determine whether the yield contract allows adapting the resolved value deterministically.
- (b) If not, is a replay-deterministic
Task.map / derived-task primitive feasible in core? Scope the engine + determinism risk. (This is broader than compat and would benefit other consumers.)
- Type re-exports: if
lock() is bridged, re-export compat DurableLock / LockState and map core lock-rule violations to LockingRulesViolationError.
Acceptance
Links
Follow-up to PR #282 review comment #17.
Follow-up from PR #282 (review comment #17, P1)
PR #282 (
Add Functions gRPC core helpers) added the newpackages/azure-functions-durablecompat package (npmdurable-functionsv4), built on the@microsoft/durabletask-jsgRPC core. In review, @andystaples flagged (P1) that the package removed the v3 entity-lock API without a bridge:context.df.lock(...entityIds)— v3 yields aDurableLockcontext.df.isLocked(...)— returnsbooleanDurableLock,LockState,LockingRulesViolationErrorThis was resolved in #282 as a documented breaking change (CHANGELOG migration to the core-native API), not restored. This issue tracks whether the v3 surface can actually be brought back for drop-in compatibility, or whether the documented migration is the permanent answer.
Why it wasn't restored in #282
Core already exposes the same capability, with a different shape:
durable-functions)@microsoft/durabletask-js)yield context.df.lock(...ids)->DurableLockcontext.entities.lockEntities(...ids)->Task<LockHandle>(handle.release())context.df.isLocked()context.entities.isInCriticalSection()The hard part is
lock(): v3 orchestratorsyieldit and receive aDurableLock. Core'slockEntities()returns aTask<LockHandle>. To preserve the v3 yield shape we would need to remapTask<LockHandle>->Task<DurableLock>, but core'sTask<T>has no.then/.map/ derived-task primitive (packages/durabletask-js/src/task/task.ts). Adding one touches the replay engine, so it is determinism-sensitive and was intentionally out of scope for #282.Investigation asks
isLocked()alias (likely cheap): cancontext.df.isLocked()be implemented directly on top ofisInCriticalSection()in the compat layer with no engine change? Confirm the semantics match (any-lock-held vs specific-entity query).lock()bridge (the real question):LockHandle->DurableLock(yield shape +.release()semantics) without an engine-levelTask.map, e.g. by post-processing the resolved handle replay-safely? Determine whether the yield contract allows adapting the resolved value deterministically.Task.map/ derived-task primitive feasible in core? Scope the engine + determinism risk. (This is broader than compat and would benefit other consumers.)lock()is bridged, re-export compatDurableLock/LockStateand map core lock-rule violations toLockingRulesViolationError.Acceptance
durable-functionsv4 and drop the migration note.durable-functions@4.0.0— Azure Functions Durable provider on the gRPC core (+ core host helpers, E2E CI, and release pipeline) #282 behavior) and record the rationale here.Links
durable-functions@4.0.0— Azure Functions Durable provider on the gRPC core (+ core host helpers, E2E CI, and release pipeline) #282packages/durabletask-js/src/entities/orchestration-entity-feature.ts(lockEntities,isInCriticalSection,LockHandle)packages/azure-functions-durable/CHANGELOG.md(Breaking changes vsdurable-functionsv3)Follow-up to PR #282 review comment #17.