Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

async api handlers #1698

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 27 additions & 6 deletions v3/src/components/web-view/request-queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,38 @@ export class RequestQueue {
return this.requestQueue.length
}

get nextItem() {
return this.requestQueue[0]
}

@action
push(pair: RequestPair) {
this.requestQueue.push(pair)
}


@action
shift() {
return this.requestQueue.shift()
clear() {
this.requestQueue.splice(0)
}

/**
* Process all of the current items in the array. A copy of the current items
* is made and the current items are cleared.
*
* processItems does not wait for async processor functions. It will call the processor
* function for every item even if one of them is waiting for something to finish.
*
* The approach copying the array and then clearing it means the array is only
* updated one time. So if this function is observed, it will only trigger a single update.
*
* @param processor
*/
processItems(processor: (item: RequestPair) => void) {
// copy the items
const items = this.requestQueue.slice(0)
// clear the items to prepare for the next one to be added
this.clear()
// Call the processor for each item
for (const item of items) {
processor(item)
}
}

}
96 changes: 54 additions & 42 deletions v3/src/components/web-view/use-data-interactive-controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import iframePhone from "iframe-phone"
import { autorun } from "mobx"
import { reaction } from "mobx"
import React, { useEffect } from "react"
import { getDIHandler } from "../../data-interactive/data-interactive-handler"
import {
Expand Down Expand Up @@ -52,53 +52,65 @@
webViewModel?.setDataInteractiveController(rpcEndpoint)
webViewModel?.applyModelChange(() => {}, {log: {message: "Plugin initialized", args:{}, category: "plugin"}})

const disposer = autorun(() => {
const canProcessRequest = !uiState.isEditingBlockingCell
if (canProcessRequest && requestQueue.length > 0) {
uiState.captureEditingStateBeforeInterruption()
let tableModified = false
while (requestQueue.length > 0) {
const { request, callback } = requestQueue.nextItem
debugLog(DEBUG_PLUGINS, `Processing data-interactive: ${JSON.stringify(request)}`)
let result: DIRequestResponse = { success: false }
// A reaction is used here instead of an autorun so properties accessed by each handler are not
// observed. We only want to run the loop when a new request comes in, not when something changes
// that a handler accessed.
const disposer = reaction(() => {
return {
canProcessRequest: !uiState.isEditingBlockingCell,
queueLength: requestQueue.length
}
},
({ canProcessRequest, queueLength }) => {
if (!canProcessRequest || queueLength === 0) return

uiState.captureEditingStateBeforeInterruption()
let tableModified = false

requestQueue.processItems(async ({ request, callback }) => {
debugLog(DEBUG_PLUGINS, `Processing data-interactive: ${JSON.stringify(request)}`)
let result: DIRequestResponse = { success: false }

const errorResult = (error: string) => ({ success: false, values: { error }} as const)
const processAction = (action: DIAction) => {
if (!action) return errorResult(t("V3.DI.Error.noAction"))
if (!tile) return errorResult(t("V3.DI.Error.noTile"))
const errorResult = (error: string) => ({ success: false, values: { error }} as const)
const processAction = async (action: DIAction) => {
if (!action) return errorResult(t("V3.DI.Error.noAction"))
if (!tile) return errorResult(t("V3.DI.Error.noTile"))

const resourceSelector = parseResourceSelector(action.resource)
const resources = resolveResources(resourceSelector, action.action, tile)
const type = resourceSelector.type ?? ""
const a = action.action
const func = getDIHandler(type)?.[a as keyof DIHandler]
if (!func) return errorResult(t("V3.DI.Error.unsupportedAction", {vars: [a, type]}))
const resourceSelector = parseResourceSelector(action.resource)
const resources = resolveResources(resourceSelector, action.action, tile)
const type = resourceSelector.type ?? ""
const a = action.action
const func = getDIHandler(type)?.[a as keyof DIHandler]
if (!func) return errorResult(t("V3.DI.Error.unsupportedAction", {vars: [a, type]}))

const actionResult = func?.(resources, action.values)
if (actionResult &&
["create", "delete", "notify"].includes(a) &&
!["component", "global", "interactiveFrame"].includes(type)
) {
// Increment request batches processed if a table may have been modified
tableModified = true
}
return actionResult ?? errorResult(t("V3.DI.Error.undefinedResponse"))
const actionResult = await func?.(resources, action.values)
if (actionResult &&
["create", "delete", "notify"].includes(a) &&
!["component", "global", "interactiveFrame"].includes(type)

Check warning on line 89 in v3/src/components/web-view/use-data-interactive-controller.ts

View check run for this annotation

Codecov / codecov/patch

v3/src/components/web-view/use-data-interactive-controller.ts#L89

Added line #L89 was not covered by tests
) {
// Increment request batches processed if a table may have been modified
tableModified = true

Check warning on line 92 in v3/src/components/web-view/use-data-interactive-controller.ts

View check run for this annotation

Codecov / codecov/patch

v3/src/components/web-view/use-data-interactive-controller.ts#L92

Added line #L92 was not covered by tests
}
if (Array.isArray(request)) {
result = request.map(action => processAction(action))
} else {
result = processAction(request)
return actionResult ?? errorResult(t("V3.DI.Error.undefinedResponse"))
}
if (Array.isArray(request)) {
result = []
for (const action of request) {
result.push(await processAction(action))
}

debugLog(DEBUG_PLUGINS, `Responding with`, result)
callback(result)
requestQueue.shift()
} else {

Check warning on line 101 in v3/src/components/web-view/use-data-interactive-controller.ts

View check run for this annotation

Codecov / codecov/patch

v3/src/components/web-view/use-data-interactive-controller.ts#L101

Added line #L101 was not covered by tests
result = await processAction(request)
}
// TODO Only increment if a table may have changed
// - many actions and resources could be ignored
// - could specify which dataContext has been updated
if (tableModified) uiState.incrementInterruptionCount()
}

debugLog(DEBUG_PLUGINS, `Responding with`, result)
callback(result)
})

// TODO Only increment if a table may have changed
// - many actions and resources could be ignored
// - could specify which dataContext has been updated
if (tableModified) uiState.incrementInterruptionCount()

}, { name: "DataInteractiveController request processor autorun" })

return () => {
Expand Down
6 changes: 3 additions & 3 deletions v3/src/data-interactive/data-interactive-handler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { DIHandler } from "./data-interactive-types"
import { DIAsyncHandler, DIHandler } from "./data-interactive-types"

const diHandlers: Map<string, DIHandler> = new Map()
const diHandlers: Map<string, DIHandler | DIAsyncHandler> = new Map()

export function registerDIHandler(resource: string, handler: DIHandler) {
export function registerDIHandler(resource: string, handler: DIHandler | DIAsyncHandler) {
diHandlers.set(resource, handler)
}

Expand Down
15 changes: 15 additions & 0 deletions v3/src/data-interactive/data-interactive-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,14 @@ export function isErrorResult(result: unknown): result is DIErrorResult {
}

export type DIHandlerFn = (resources: DIResources, values?: DIValues, metadata?: DIMetadata) => DIHandlerFnResult
export type DIHandlerAsyncFn = (...args: Parameters<DIHandlerFn>) => DIHandlerFnResult | Promise<DIHandlerFnResult>

export const diNotImplementedYetResult = {success: false, values: {error: "not implemented (yet)"}} as const
export const diNotImplementedYet: DIHandlerFn = () => diNotImplementedYetResult

// This approach of defining both a synchronous and asynchronous handler makes it easier
// for simple synchronous handlers to write tests. They don't need to await every call
// when we know the calls are synchronous.
interface DIBaseHandler {
get?: DIHandlerFn
create?: DIHandlerFn
Expand All @@ -240,6 +244,17 @@ interface DIBaseHandler {
export type ActionName = keyof DIBaseHandler
export type DIHandler = RequireAtLeastOne<DIBaseHandler, ActionName>

interface DIBaseAsyncHandler {
get?: DIHandlerAsyncFn
create?: DIHandlerAsyncFn
update?: DIHandlerAsyncFn
delete?: DIHandlerAsyncFn
notify?: DIHandlerAsyncFn
register?: DIHandlerAsyncFn
unregister?: DIHandlerAsyncFn
}
export type DIAsyncHandler = RequireAtLeastOne<DIBaseAsyncHandler, ActionName>

export interface DIResourceSelector {
attribute?: string
attributeLocation?: string
Expand Down
Loading