mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Add process temporal (#9744)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Vendored
+2
-1
@@ -666,7 +666,8 @@
|
||||
"SECRET": "secret",
|
||||
"ACCOUNTS_URL": "http://localhost:3000",
|
||||
"QUEUE_CONFIG": "localhost:19092",
|
||||
"QUEUE_REGION": "cockroach"
|
||||
"QUEUE_REGION": "cockroach",
|
||||
"TEMPORAL_ADDRESS": "localhost:7233"
|
||||
},
|
||||
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
|
||||
"runtimeVersion": "20",
|
||||
|
||||
Generated
+2209
-1740
File diff suppressed because it is too large
Load Diff
@@ -567,6 +567,8 @@ services:
|
||||
- STORAGE=minio|minio?accessKey=minioadmin&secretKey=minioadmin
|
||||
- WORKSPACE_STORAGE=${STORAGE_CONFIG}
|
||||
- OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4318/v1/traces
|
||||
- QUEUE_CONFIG=${QUEUE_CONFIG}
|
||||
- TEMPORAL_ADDRESS=localhost:7233
|
||||
restart: unless-stopped
|
||||
backup-api:
|
||||
image: hardcoreeng/backup-api
|
||||
|
||||
@@ -1073,6 +1073,22 @@ export function createModel (builder: Builder): void {
|
||||
process.trigger.OnSubProcessesDone
|
||||
)
|
||||
|
||||
builder.createDoc(
|
||||
process.class.Trigger,
|
||||
core.space.Model,
|
||||
{
|
||||
label: process.string.WaitUntil,
|
||||
icon: process.icon.Time,
|
||||
requiredParams: [],
|
||||
init: false,
|
||||
auto: true,
|
||||
editor: process.component.TimeEditor,
|
||||
presenter: process.component.TimePresenter,
|
||||
checkFunction: process.triggerCheck.Time
|
||||
},
|
||||
process.trigger.OnTime
|
||||
)
|
||||
|
||||
builder.createDoc(card.class.MasterTagEditorSection, core.space.Model, {
|
||||
id: 'processes',
|
||||
label: process.string.Processes,
|
||||
|
||||
@@ -63,6 +63,10 @@ export function createModel (builder: Builder): void {
|
||||
serverCheckFunc: serverProcess.func.OnCardUpdateCheck
|
||||
})
|
||||
|
||||
builder.mixin(process.trigger.OnTime, process.class.Trigger, serverProcess.mixin.TriggerImpl, {
|
||||
serverCheckFunc: serverProcess.func.CheckTime
|
||||
})
|
||||
|
||||
builder.mixin(process.trigger.OnSubProcessesDone, process.class.Trigger, serverProcess.mixin.TriggerImpl, {
|
||||
preventRollback: true
|
||||
})
|
||||
|
||||
@@ -66,8 +66,6 @@ export default mergeIds(viewId, view, {
|
||||
BooleanPresenter: '' as AnyComponent,
|
||||
BooleanEditor: '' as AnyComponent,
|
||||
TimestampPresenter: '' as AnyComponent,
|
||||
DateEditor: '' as AnyComponent,
|
||||
DatePresenter: '' as AnyComponent,
|
||||
DateTimePresenter: '' as AnyComponent,
|
||||
TableBrowser: '' as AnyComponent,
|
||||
YoutubePresenter: '' as AnyComponent,
|
||||
|
||||
@@ -32,4 +32,8 @@
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M26 8.00001C27.1046 8.00001 28 7.10457 28 6.00001C28 4.89544 27.1046 4.00001 26 4.00001C24.8955 4.00001 24 4.89544 24 6.00001C24 7.10457 24.8955 8.00001 26 8.00001ZM26 10C28.2092 10 30 8.20914 30 6.00001C30 3.79087 28.2092 2.00001 26 2.00001C23.7909 2.00001 22 3.79087 22 6.00001C22 8.20914 23.7909 10 26 10Z"/>
|
||||
</symbol>
|
||||
<symbol id="time" viewBox="0 0 14 14">
|
||||
<path d="M7.43758 3.49984C7.43758 3.25821 7.24171 3.06234 7.00008 3.06234C6.75846 3.06234 6.56258 3.25821 6.56258 3.49984V6.99984C6.56258 7.11587 6.60868 7.22715 6.69072 7.3092L8.44072 9.0592C8.61158 9.23005 8.88859 9.23005 9.05944 9.0592C9.2303 8.88834 9.2303 8.61133 9.05944 8.44048L7.43758 6.81862V3.49984Z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.00008 0.729004C3.5368 0.729004 0.729248 3.53655 0.729248 6.99984C0.729248 10.4631 3.5368 13.2707 7.00008 13.2707C10.4634 13.2707 13.2709 10.4631 13.2709 6.99984C13.2709 3.53655 10.4634 0.729004 7.00008 0.729004ZM1.60425 6.99984C1.60425 4.0198 4.02005 1.604 7.00008 1.604C9.98012 1.604 12.3959 4.0198 12.3959 6.99984C12.3959 9.97987 9.98012 12.3957 7.00008 12.3957C4.02005 12.3957 1.60425 9.97987 1.60425 6.99984Z"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 9.0 KiB After Width: | Height: | Size: 9.8 KiB |
@@ -96,7 +96,8 @@
|
||||
"Remove": "Odstranit",
|
||||
"RemoveFirst": "Odstranit první",
|
||||
"RemoveLast": "Odstranit poslední",
|
||||
"Cancelled": "Zrušeno"
|
||||
"Cancelled": "Zrušeno",
|
||||
"WaitUntil": "Čekat do"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Metoda nenalezena: {methodId}",
|
||||
|
||||
@@ -96,8 +96,9 @@
|
||||
"Remove": "Entfernen",
|
||||
"RemoveFirst": "Erstes entfernen",
|
||||
"RemoveLast": "Letztes entfernen",
|
||||
"Cancelled": "Abgebrochen"
|
||||
},
|
||||
"Cancelled": "Abgebrochen",
|
||||
"WaitUntil": "Warten bis"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Methode nicht gefunden: {methodId}",
|
||||
"AttributeNotExists": "Attribut existiert nicht: {key}",
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
"Remove": "Remove",
|
||||
"RemoveFirst": "Remove first",
|
||||
"RemoveLast": "Remove last",
|
||||
"Cancelled": "Cancelled"
|
||||
"Cancelled": "Cancelled",
|
||||
"WaitUntil": "Wait until"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Method not found: {methodId}",
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
"ArraySizeGte": "Tamaño mayor o igual que",
|
||||
"ArraySizeLt": "Tamaño menor que",
|
||||
"ArraySizeLte": "Tamaño menor o igual que",
|
||||
"Cancelled": "Cancelado"
|
||||
"Cancelled": "Cancelado",
|
||||
"WaitUntil": "Esperar hasta"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Método no encontrado: {methodId}",
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
"ArraySizeGte": "Taille supérieure ou égale à",
|
||||
"ArraySizeLt": "Taille inférieure à",
|
||||
"ArraySizeLte": "Taille inférieure ou égale à",
|
||||
"Cancelled": "Annulé"
|
||||
"Cancelled": "Annulé",
|
||||
"WaitUntil": "Attendre jusqu'à"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Méthode introuvable : {methodId}",
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
"ArraySizeGte": "Dimensione maggiore o uguale a",
|
||||
"ArraySizeLt": "Dimensione minore di",
|
||||
"ArraySizeLte": "Dimensione minore o uguale a",
|
||||
"Cancelled": "Annullato"
|
||||
"Cancelled": "Annullato",
|
||||
"WaitUntil": "Aspetta fino a"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Metodo non trovato: {methodId}",
|
||||
|
||||
@@ -100,7 +100,8 @@
|
||||
"ArraySizeGte": "サイズが以上",
|
||||
"ArraySizeLt": "サイズがより小さい",
|
||||
"ArraySizeLte": "サイズが以下",
|
||||
"Cancelled": "キャンセル済み"
|
||||
"Cancelled": "キャンセル済み",
|
||||
"WaitUntil": "待機する"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "メソッドが見つかりません: {methodId}",
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
"ArraySizeGte": "Tamanho maior ou igual a",
|
||||
"ArraySizeLt": "Tamanho menor que",
|
||||
"ArraySizeLte": "Tamanho menor ou igual a",
|
||||
"Cancelled": "Cancelado"
|
||||
"Cancelled": "Cancelado",
|
||||
"WaitUntil": "Aguardar até"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Método não encontrado: {methodId}",
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
"ArraySizeGte": "Количество больше или равно",
|
||||
"ArraySizeLt": "Количество меньше",
|
||||
"ArraySizeLte": "Количество меньше или равно",
|
||||
"Cancelled": "Отменено"
|
||||
"Cancelled": "Отменено",
|
||||
"WaitUntil": "Ждать до"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "Метод не найден: {methodId}",
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
"ArraySizeGte": "大小大于等于",
|
||||
"ArraySizeLt": "大小小于",
|
||||
"ArraySizeLte": "大小小于等于",
|
||||
"Cancelled": "已取消"
|
||||
"Cancelled": "已取消",
|
||||
"WaitUntil": "等待直到"
|
||||
},
|
||||
"error": {
|
||||
"MethodNotFound": "找不到方法:{methodId}",
|
||||
|
||||
@@ -25,5 +25,6 @@ loadMetadata(process.icon, {
|
||||
ToDoRemove: `${icons}#todo-remove`,
|
||||
WaitSubprocesses: `${icons}#subprocesses`,
|
||||
Start: `${icons}#start`,
|
||||
OnCardUpdate: `${icons}#card-update`
|
||||
OnCardUpdate: `${icons}#card-update`,
|
||||
Time: `${icons}#time`
|
||||
})
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
<!--
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import core, { AnyAttribute, generateId } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Process } from '@hcengineering/process'
|
||||
import { AnySvelteComponent } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import plugin from '../../plugin'
|
||||
import { getContext } from '../../utils'
|
||||
import ProcessAttribute from '../ProcessAttribute.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let readonly: boolean
|
||||
export let process: Process
|
||||
export let params: Record<string, any>
|
||||
|
||||
const client = getClient()
|
||||
const h = client.getHierarchy()
|
||||
$: context = getContext(client, process, core.class.TypeDate, 'attribute')
|
||||
|
||||
const attribute: AnyAttribute = {
|
||||
attributeOf: process.masterTag,
|
||||
name: '',
|
||||
type: {
|
||||
label: core.string.Date,
|
||||
_class: core.class.TypeDate
|
||||
},
|
||||
_id: generateId(),
|
||||
space: core.space.Model,
|
||||
modifiedOn: 0,
|
||||
modifiedBy: core.account.System,
|
||||
_class: core.class.Attribute,
|
||||
label: plugin.string.WaitUntil
|
||||
}
|
||||
|
||||
let editor: AnySvelteComponent | undefined
|
||||
|
||||
function getEditor (): void {
|
||||
try {
|
||||
const inlineEditor = h.as(h.getClass(core.class.TypeDate), view.mixin.AttributeEditor).inlineEditor
|
||||
void getResource(inlineEditor)
|
||||
.then((p) => {
|
||||
editor = p
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e)
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
getEditor()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
<ProcessAttribute
|
||||
{process}
|
||||
masterTag={process.masterTag}
|
||||
{context}
|
||||
presenterClass={{
|
||||
attrClass: core.class.TypeDate,
|
||||
category: 'attribute'
|
||||
}}
|
||||
{attribute}
|
||||
{editor}
|
||||
bind:value={params.value}
|
||||
on:change={(e) => {
|
||||
params.value = e.detail
|
||||
dispatch('change', params)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 3fr;
|
||||
grid-auto-rows: minmax(2rem, max-content);
|
||||
justify-content: start;
|
||||
align-items: center;
|
||||
row-gap: 0.5rem;
|
||||
column-gap: 1rem;
|
||||
margin-top: 0.5rem;
|
||||
height: min-content;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
<!--
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import core from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { parseContext, Process } from '@hcengineering/process'
|
||||
import { Component } from '@hcengineering/ui'
|
||||
import view from '@hcengineering/view'
|
||||
import { getContext } from '../../utils'
|
||||
import ContextValuePresenter from '../attributeEditors/ContextValuePresenter.svelte'
|
||||
|
||||
export let process: Process
|
||||
export let params: Record<string, any>
|
||||
|
||||
const client = getClient()
|
||||
const h = client.getHierarchy()
|
||||
$: context = getContext(client, process, core.class.TypeDate, 'attribute')
|
||||
|
||||
$: contextValue = parseContext(params.value)
|
||||
</script>
|
||||
|
||||
{#if contextValue && context}
|
||||
<ContextValuePresenter {contextValue} {context} {process} />
|
||||
{:else}
|
||||
<Component is={view.component.DatePresenter} props={{ value: params.value, readonly: true }} />
|
||||
{/if}
|
||||
@@ -41,33 +41,41 @@ import ToDoRemoveParamsEditor from './components/settings/ToDoRemoveParamsEditor
|
||||
import TransitionEditor from './components/settings/TransitionEditor.svelte'
|
||||
import TriggerPresenter from './components/settings/TriggerPresenter.svelte'
|
||||
import UpdateCardEditor from './components/settings/UpdateCardEditor.svelte'
|
||||
import ArrayElementEditor from './components/transformEditors/ArrayElementEditor.svelte'
|
||||
import DateOffsetEditor from './components/transformEditors/DateOffsetEditor.svelte'
|
||||
import NumberEditor from './components/transformEditors/NumberEditor.svelte'
|
||||
import ArrayElementEditor from './components/transformEditors/ArrayElementEditor.svelte'
|
||||
|
||||
import ArrayCriteria from './components/criterias/ArrayCriteria.svelte'
|
||||
import BooleanCriteria from './components/criterias/BooleanCriteria.svelte'
|
||||
import DateCriteria from './components/criterias/DateCriteria.svelte'
|
||||
import EnumCriteria from './components/criterias/EnumCriteria.svelte'
|
||||
import NumberCriteria from './components/criterias/NumberCriteria.svelte'
|
||||
import RefCriteria from './components/criterias/RefCriteria.svelte'
|
||||
import StringCriteria from './components/criterias/StringCriteria.svelte'
|
||||
import LogActionPresenter from './components/LogActionPresenter.svelte'
|
||||
import NotifierExtension from './components/NotifierExtension.svelte'
|
||||
import AddRelationPresenter from './components/presenters/AddRelationPresenter.svelte'
|
||||
import CreateCardPresenter from './components/presenters/CreateCardPresenter.svelte'
|
||||
import AddRelationEditor from './components/settings/AddRelationEditor.svelte'
|
||||
import CreateCardEditor from './components/settings/CreateCardEditor.svelte'
|
||||
import CardUpdateEditor from './components/settings/CardUpdateEditor.svelte'
|
||||
import CardUpdatePresenter from './components/settings/CardUpdatePresenter.svelte'
|
||||
import CreateCardEditor from './components/settings/CreateCardEditor.svelte'
|
||||
import TimeEditor from './components/settings/TimeEditor.svelte'
|
||||
import TimePresenter from './components/settings/TimePresenter.svelte'
|
||||
import ToDoSettingPresenter from './components/settings/ToDoPresenter.svelte'
|
||||
import TransitionRefPresenter from './components/settings/TransitionRefPresenter.svelte'
|
||||
import AppendEditor from './components/transformEditors/AppendEditor.svelte'
|
||||
import CutEditor from './components/transformEditors/CutEditor.svelte'
|
||||
import ReplaceEditor from './components/transformEditors/ReplaceEditor.svelte'
|
||||
import SplitEditor from './components/transformEditors/SplitEditor.svelte'
|
||||
import DateCriteria from './components/criterias/DateCriteria.svelte'
|
||||
import BooleanCriteria from './components/criterias/BooleanCriteria.svelte'
|
||||
import NumberCriteria from './components/criterias/NumberCriteria.svelte'
|
||||
import StringCriteria from './components/criterias/StringCriteria.svelte'
|
||||
import RefCriteria from './components/criterias/RefCriteria.svelte'
|
||||
import EnumCriteria from './components/criterias/EnumCriteria.svelte'
|
||||
import ArrayCriteria from './components/criterias/ArrayCriteria.svelte'
|
||||
import CardUpdatePresenter from './components/settings/CardUpdatePresenter.svelte'
|
||||
import ToDoSettingPresenter from './components/settings/ToDoPresenter.svelte'
|
||||
import { ProcessMiddleware } from './middleware'
|
||||
import { continueExecution, showDoneQuery, todoTranstionCheck, updateCardTranstionCheck } from './utils'
|
||||
import {
|
||||
continueExecution,
|
||||
showDoneQuery,
|
||||
timeTransitionCheck,
|
||||
todoTranstionCheck,
|
||||
updateCardTranstionCheck
|
||||
} from './utils'
|
||||
|
||||
export * from './query'
|
||||
|
||||
@@ -114,7 +122,9 @@ export default async (): Promise<Resources> => ({
|
||||
AddRelationEditor,
|
||||
AddRelationPresenter,
|
||||
CardUpdatePresenter,
|
||||
ToDoSettingPresenter
|
||||
ToDoSettingPresenter,
|
||||
TimeEditor,
|
||||
TimePresenter
|
||||
},
|
||||
criteriaEditor: {
|
||||
DateCriteria,
|
||||
@@ -136,7 +146,8 @@ export default async (): Promise<Resources> => ({
|
||||
},
|
||||
triggerCheck: {
|
||||
UpdateCheck: updateCardTranstionCheck,
|
||||
ToDo: todoTranstionCheck
|
||||
ToDo: todoTranstionCheck,
|
||||
Time: timeTransitionCheck
|
||||
},
|
||||
function: {
|
||||
ShowDoneQuery: showDoneQuery,
|
||||
|
||||
@@ -63,7 +63,9 @@ export default mergeIds(processId, process, {
|
||||
AddRelationPresenter: '' as AnyComponent,
|
||||
CardUpdateEditor: '' as AnyComponent,
|
||||
CardUpdatePresenter: '' as AnyComponent,
|
||||
ToDoSettingPresenter: '' as AnyComponent
|
||||
ToDoSettingPresenter: '' as AnyComponent,
|
||||
TimeEditor: '' as AnyComponent,
|
||||
TimePresenter: '' as AnyComponent
|
||||
},
|
||||
criteriaEditor: {
|
||||
StringCriteria: '' as AnyComponent,
|
||||
@@ -153,6 +155,7 @@ export default mergeIds(processId, process, {
|
||||
EnterValue: '' as IntlString,
|
||||
OnToDoDone: '' as IntlString,
|
||||
OnSubProcessesDone: '' as IntlString,
|
||||
WaitUntil: '' as IntlString,
|
||||
OnCardUpdate: '' as IntlString,
|
||||
Result: '' as IntlString,
|
||||
RequestResult: '' as IntlString,
|
||||
|
||||
@@ -574,6 +574,11 @@ export function todoTranstionCheck (params: Record<string, any>, doc: Doc): bool
|
||||
return doc._id === params._id
|
||||
}
|
||||
|
||||
export function timeTransitionCheck (params: Record<string, any>): boolean {
|
||||
if (params.value === undefined) return false
|
||||
return params.value <= Date.now()
|
||||
}
|
||||
|
||||
export function updateCardTranstionCheck (params: Record<string, any>, doc: Doc, hierarchy: Hierarchy): boolean {
|
||||
const res = matchQuery([doc], params, doc._class, hierarchy, true)
|
||||
return res.length > 0
|
||||
|
||||
@@ -209,11 +209,13 @@ export default plugin(processId, {
|
||||
OnToDoClose: '' as Ref<Trigger>,
|
||||
OnToDoRemove: '' as Ref<Trigger>,
|
||||
OnExecutionStart: '' as Ref<Trigger>,
|
||||
OnExecutionContinue: '' as Ref<Trigger>
|
||||
OnExecutionContinue: '' as Ref<Trigger>,
|
||||
OnTime: '' as Ref<Trigger>
|
||||
},
|
||||
triggerCheck: {
|
||||
ToDo: '' as Resource<CheckFunc>,
|
||||
UpdateCheck: '' as Resource<CheckFunc>
|
||||
UpdateCheck: '' as Resource<CheckFunc>,
|
||||
Time: '' as Resource<CheckFunc>
|
||||
},
|
||||
string: {
|
||||
Method: '' as IntlString,
|
||||
@@ -250,7 +252,8 @@ export default plugin(processId, {
|
||||
OnCardUpdate: '' as Asset,
|
||||
WaitSubprocesses: '' as Asset,
|
||||
ToDoRemove: '' as Asset,
|
||||
Start: '' as Asset
|
||||
Start: '' as Asset,
|
||||
Time: '' as Asset
|
||||
},
|
||||
function: {
|
||||
FirstValue: '' as Ref<ProcessFunction>,
|
||||
|
||||
@@ -183,7 +183,9 @@ const view = plugin(viewId, {
|
||||
PersonIdFilter: '' as AnyComponent,
|
||||
RolePresenter: '' as AnyComponent,
|
||||
ReadOnlyNotification: '' as AnyComponent,
|
||||
ForbiddenNotification: '' as AnyComponent
|
||||
ForbiddenNotification: '' as AnyComponent,
|
||||
DatePresenter: '' as AnyComponent,
|
||||
DateEditor: '' as AnyComponent
|
||||
},
|
||||
ids: {
|
||||
IconWithEmoji: '' as Asset
|
||||
|
||||
@@ -2687,6 +2687,11 @@
|
||||
"packageName": "@hcengineering/huly-mail-resources",
|
||||
"projectFolder": "plugins/huly-mail-resources",
|
||||
"shouldPublish": false
|
||||
},
|
||||
{
|
||||
"packageName": "@hcengineering/pod-worker",
|
||||
"projectFolder": "services/worker",
|
||||
"shouldPublish": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -43,6 +43,11 @@ export function OnCardUpdateCheck (
|
||||
return res.length > 0
|
||||
}
|
||||
|
||||
export function CheckTime (params: Record<string, any>): boolean {
|
||||
if (params.value === undefined) return false
|
||||
return params.value <= Date.now()
|
||||
}
|
||||
|
||||
export async function AddRelation (
|
||||
params: MethodParams<Relation>,
|
||||
execution: Execution,
|
||||
|
||||
@@ -66,7 +66,8 @@ import {
|
||||
CreateCard,
|
||||
AddRelation,
|
||||
CheckToDo,
|
||||
OnCardUpdateCheck
|
||||
OnCardUpdateCheck,
|
||||
CheckTime
|
||||
} from './functions'
|
||||
import { ToDoCancellRollback, ToDoCloseRollback } from './rollback'
|
||||
|
||||
@@ -334,7 +335,8 @@ export default async () => ({
|
||||
CreateCard,
|
||||
AddRelation,
|
||||
CheckToDo,
|
||||
OnCardUpdateCheck
|
||||
OnCardUpdateCheck,
|
||||
CheckTime
|
||||
},
|
||||
transform: {
|
||||
FirstValue,
|
||||
|
||||
@@ -47,7 +47,8 @@ export default plugin(serverProcessId, {
|
||||
AddRelation: '' as Resource<ExecuteFunc>,
|
||||
WaitSubProcess: '' as Resource<ExecuteFunc>,
|
||||
CheckToDo: '' as Resource<CheckFunc>,
|
||||
OnCardUpdateCheck: '' as Resource<CheckFunc>
|
||||
OnCardUpdateCheck: '' as Resource<CheckFunc>,
|
||||
CheckTime: '' as Resource<CheckFunc>
|
||||
},
|
||||
transform: {
|
||||
FirstValue: '' as Resource<TransformFunc>,
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
"@hcengineering/server-core": "^0.6.1",
|
||||
"@hcengineering/server-token": "^0.6.11",
|
||||
"@hcengineering/account-client": "^0.6.0",
|
||||
"@temporalio/client": "1.12.3",
|
||||
"dotenv": "~16.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,13 +21,17 @@ export interface Config {
|
||||
QueueConfig: string
|
||||
QueueRegion: string
|
||||
AccountsUrl: string
|
||||
TemporalAddress: string
|
||||
TemporalNamespace: string
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
Secret: process.env.SECRET ?? 'secret',
|
||||
QueueConfig: process.env.QUEUE_CONFIG ?? '',
|
||||
QueueRegion: process.env.QUEUE_REGION ?? '',
|
||||
AccountsUrl: process.env.ACCOUNTS_URL ?? ''
|
||||
AccountsUrl: process.env.ACCOUNTS_URL ?? '',
|
||||
TemporalAddress: process.env.TEMPORAL_ADDRESS ?? 'localhost:7233',
|
||||
TemporalNamespace: process.env.TEMPORAL_NAMESPACE ?? 'huly'
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
@@ -19,13 +19,14 @@ import { newMetrics } from '@hcengineering/core'
|
||||
import { getPlatformQueue } from '@hcengineering/kafka'
|
||||
import { setMetadata } from '@hcengineering/platform'
|
||||
import { initStatisticsContext, QueueTopic } from '@hcengineering/server-core'
|
||||
import { join } from 'path'
|
||||
import serverToken from '@hcengineering/server-token'
|
||||
import config from './config'
|
||||
import { ProcessMessage } from '@hcengineering/server-process'
|
||||
import { SERVICE_NAME } from './utils'
|
||||
import { messageHandler } from './main'
|
||||
import serverToken from '@hcengineering/server-token'
|
||||
import { join } from 'path'
|
||||
import config from './config'
|
||||
import { prepare } from './init'
|
||||
import { messageHandler } from './main'
|
||||
import { closeTemporal } from './temporal'
|
||||
import { SERVICE_NAME } from './utils'
|
||||
|
||||
async function main (): Promise<void> {
|
||||
prepare()
|
||||
@@ -62,6 +63,7 @@ async function main (): Promise<void> {
|
||||
)
|
||||
|
||||
const shutdown = (): void => {
|
||||
void closeTemporal()
|
||||
void Promise.all([consumer.close()]).then(() => {
|
||||
process.exit()
|
||||
})
|
||||
|
||||
+167
-15
@@ -56,7 +56,10 @@ import serverProcess, {
|
||||
TriggerImpl
|
||||
} from '@hcengineering/server-process'
|
||||
import { isError } from './errors'
|
||||
import { getClient, pickTransition, releaseClient } from './utils'
|
||||
import { getClient, releaseClient } from './utils'
|
||||
import { getTemporalClient } from './temporal'
|
||||
import { Client as TemporalClient } from '@temporalio/client'
|
||||
import config from './config'
|
||||
|
||||
const activeExecutions = new Set<Ref<Execution>>()
|
||||
|
||||
@@ -64,7 +67,7 @@ export async function messageHandler (record: ProcessMessage, ws: WorkspaceUuid,
|
||||
try {
|
||||
const client = await getClient(ws, record.account)
|
||||
try {
|
||||
const control = {
|
||||
const control: ProcessControl = {
|
||||
ctx,
|
||||
client,
|
||||
cache: new Map<string, any>(),
|
||||
@@ -114,14 +117,20 @@ async function processCardExecutions (control: ProcessControl, record: ProcessMe
|
||||
states.add(transition.from)
|
||||
}
|
||||
}
|
||||
if (states.size === 0) return
|
||||
const executions = await control.client.findAll(process.class.Execution, {
|
||||
card: record.card,
|
||||
status: ExecutionStatus.Active,
|
||||
currentState: { $in: Array.from(states) }
|
||||
})
|
||||
for (const execution of executions) {
|
||||
await processExecution(control, record, execution)
|
||||
if (states.size > 0) {
|
||||
const executions = await control.client.findAll(process.class.Execution, {
|
||||
card: record.card,
|
||||
status: ExecutionStatus.Active,
|
||||
currentState: { $in: Array.from(states) }
|
||||
})
|
||||
for (const execution of executions) {
|
||||
await processExecution(control, record, execution)
|
||||
}
|
||||
}
|
||||
|
||||
// we should update timers, probably context value changed
|
||||
if (record.event === process.trigger.OnCardUpdate) {
|
||||
await updateTimers(control, record)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +273,7 @@ async function executeTransition (execution: Execution, transition: Transition,
|
||||
status: isDone ? ExecutionStatus.Done : ExecutionStatus.Active,
|
||||
error: null
|
||||
})
|
||||
executionUpdate.context = execution.context
|
||||
res.push(client.txFactory.createTxUpdateDoc(execution._class, execution.space, execution._id, executionUpdate))
|
||||
res.push(
|
||||
client.txFactory.createTxCreateDoc(process.class.ExecutionLog, execution.space, {
|
||||
@@ -286,12 +296,127 @@ async function executeTransition (execution: Execution, transition: Transition,
|
||||
await checkParent(execution, control)
|
||||
}
|
||||
TxProcessor.applyUpdate(execution, executionUpdate)
|
||||
await checkNext(control, execution)
|
||||
const next = await checkNext(control, execution)
|
||||
if (!next) {
|
||||
await setNextTimers(control, execution)
|
||||
}
|
||||
} else {
|
||||
await client.update(execution, { error: errors })
|
||||
}
|
||||
}
|
||||
|
||||
async function updateTimers (control: ProcessControl, record: ProcessMessage): Promise<void> {
|
||||
const transitions = control.client.getModel().findAllSync(process.class.Transition, {
|
||||
trigger: process.trigger.OnTime
|
||||
})
|
||||
const states = new Set<Ref<State>>()
|
||||
for (const transition of transitions) {
|
||||
const isContext = parseContext(transition.triggerParams?.value)
|
||||
// we shouldn't update timers for non-context values
|
||||
if (isContext !== undefined) {
|
||||
if (transition.from !== null) {
|
||||
states.add(transition.from)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (states.size === 0) return
|
||||
const executions = await control.client.findAll(process.class.Execution, {
|
||||
card: record.card,
|
||||
status: ExecutionStatus.Active,
|
||||
currentState: { $in: Array.from(states) }
|
||||
})
|
||||
if (executions.length === 0) return
|
||||
if (record.card !== undefined && !control.cache.has(record.card)) {
|
||||
const card = await control.client.findOne(cardPlugin.class.Card, { _id: record.card })
|
||||
if (card !== undefined) {
|
||||
control.cache.set(record.card, card)
|
||||
}
|
||||
}
|
||||
for (const execution of executions) {
|
||||
try {
|
||||
await updateExecutionTimers(control, execution)
|
||||
} catch (err) {
|
||||
console.error('Error updating execution timers:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateExecutionTimers (control: ProcessControl, execution: Execution): Promise<void> {
|
||||
const transitions = control.client.getModel().findAllSync(process.class.Transition, {
|
||||
from: execution.currentState,
|
||||
process: execution.process,
|
||||
trigger: process.trigger.OnTime
|
||||
})
|
||||
if (transitions.length === 0) return
|
||||
const temporalClient = await getTemporalClient()
|
||||
for (const transition of transitions) {
|
||||
await setTimer(control, execution, transition, temporalClient)
|
||||
}
|
||||
}
|
||||
|
||||
async function setNextTimers (control: ProcessControl, execution: Execution): Promise<void> {
|
||||
const temporalClient = await getTemporalClient()
|
||||
await cleanTimers(execution, temporalClient)
|
||||
const transitions = control.client.getModel().findAllSync(process.class.Transition, {
|
||||
from: execution.currentState,
|
||||
process: execution.process,
|
||||
trigger: process.trigger.OnTime
|
||||
})
|
||||
for (const transition of transitions) {
|
||||
await setTimer(control, execution, transition, temporalClient)
|
||||
}
|
||||
}
|
||||
|
||||
async function cleanTimers (execution: Execution, temporalClient: TemporalClient): Promise<void> {
|
||||
try {
|
||||
const res = await temporalClient.workflowService.listWorkflowExecutions({
|
||||
namespace: config.TemporalNamespace,
|
||||
query: `WorkflowType="processTimeWorkflow" AND ExecutionStatus="Running" AND ProcessExecution="${execution._id}"`
|
||||
})
|
||||
|
||||
for (const ex of res.executions) {
|
||||
try {
|
||||
await temporalClient.workflowService.terminateWorkflowExecution({
|
||||
workflowExecution: {
|
||||
workflowId: ex.execution?.workflowId,
|
||||
runId: ex.execution?.runId
|
||||
},
|
||||
reason: 'Outdated'
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('Error terminating workflow execution:', err)
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error cleaning timers:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function setTimer (
|
||||
control: ProcessControl,
|
||||
execution: Execution,
|
||||
transition: Transition,
|
||||
temporalClient: TemporalClient
|
||||
): Promise<void> {
|
||||
const filled = await fillParams(transition.triggerParams, execution, control)
|
||||
const targetDate: number = filled.value
|
||||
if (targetDate === undefined || typeof targetDate !== 'number' || targetDate === 0 || Number.isNaN(targetDate)) return
|
||||
try {
|
||||
await temporalClient.workflow.signalWithStart('processTimeWorkflow', {
|
||||
taskQueue: 'process',
|
||||
signal: 'setDate',
|
||||
args: [targetDate, 'workspace', execution._id],
|
||||
signalArgs: [targetDate],
|
||||
workflowId: `${execution._id}_${transition._id}`,
|
||||
searchAttributes: {
|
||||
ProcessExecution: [execution._id]
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('Error setting timer:', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function executeAction<T extends Doc> (
|
||||
action: Step<T>,
|
||||
transition: Ref<Transition>,
|
||||
@@ -455,7 +580,7 @@ async function fillParams<T extends Doc> (
|
||||
): Promise<MethodParams<T>> {
|
||||
const res: MethodParams<T> = {}
|
||||
for (const key in params) {
|
||||
const value = (params as any)[key]
|
||||
const value = control.client.getHierarchy().clone((params as any)[key])
|
||||
const valueResult = await getContextValue(value, control, execution)
|
||||
;(res as any)[key] = valueResult
|
||||
}
|
||||
@@ -486,7 +611,7 @@ async function getContextValue (value: any, control: ProcessControl, execution:
|
||||
let value: any | undefined
|
||||
try {
|
||||
if (context.type === 'attribute') {
|
||||
value = await getAttributeValue(control, execution, context)
|
||||
value = getAttributeValue(control, execution, context)
|
||||
} else if (context.type === 'relation') {
|
||||
value = await getRelationValue(control, execution, context)
|
||||
} else if (context.type === 'nested') {
|
||||
@@ -505,6 +630,11 @@ async function getContextValue (value: any, control: ProcessControl, execution:
|
||||
}
|
||||
throw err
|
||||
}
|
||||
} else if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
for (const key in value) {
|
||||
value[key] = await getContextValue(value[key], control, execution)
|
||||
}
|
||||
return value
|
||||
} else {
|
||||
return value
|
||||
}
|
||||
@@ -590,7 +720,7 @@ async function checkParent (execution: Execution, control: ProcessControl): Prom
|
||||
await executeTransition(parent, transition, control)
|
||||
}
|
||||
|
||||
async function checkNext (control: ProcessControl, execution: Execution): Promise<void> {
|
||||
async function checkNext (control: ProcessControl, execution: Execution): Promise<boolean> {
|
||||
const autoTriggers = control.client.getModel().findAllSync(process.class.Trigger, { auto: true })
|
||||
const transitions = control.client.getModel().findAllSync(process.class.Transition, {
|
||||
from: execution.currentState,
|
||||
@@ -600,13 +730,35 @@ async function checkNext (control: ProcessControl, execution: Execution): Promis
|
||||
if (transitions.length > 0) {
|
||||
const doc = await control.client.findOne(cardPlugin.class.Card, { _id: execution.card })
|
||||
if (doc !== undefined) {
|
||||
control.cache.set(execution.card, doc)
|
||||
const transition = await pickTransition(control, execution, transitions, {
|
||||
card: doc
|
||||
})
|
||||
if (transition !== undefined) {
|
||||
control.cache.set(execution.card, doc)
|
||||
await executeTransition(execution, transition, control)
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
export async function pickTransition (
|
||||
control: ProcessControl,
|
||||
execution: Execution,
|
||||
transitions: Transition[],
|
||||
context: Record<string, any>
|
||||
): Promise<Transition | undefined> {
|
||||
for (const tr of transitions) {
|
||||
const trigger = control.client.getModel().findObject(tr.trigger)
|
||||
if (trigger === undefined) continue
|
||||
if (trigger.checkFunction === undefined) return tr
|
||||
const impl = control.client.getHierarchy().as(trigger, serverProcess.mixin.TriggerImpl)
|
||||
if (impl?.serverCheckFunc === undefined) return tr
|
||||
const filled = await fillParams(tr.triggerParams, execution, control)
|
||||
const checkFunc = await getResource(impl.serverCheckFunc)
|
||||
if (checkFunc === undefined) continue
|
||||
const res = await checkFunc(filled, context, control.client.getHierarchy())
|
||||
if (res) return tr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { Client, Connection } from '@temporalio/client'
|
||||
import config from './config'
|
||||
|
||||
let temporalConnection: Connection | Promise<Connection> | undefined
|
||||
|
||||
export async function closeTemporal (): Promise<void> {
|
||||
if (temporalConnection !== undefined) {
|
||||
if (temporalConnection instanceof Promise) {
|
||||
temporalConnection = await temporalConnection
|
||||
}
|
||||
await temporalConnection.close()
|
||||
}
|
||||
}
|
||||
|
||||
export async function getTemporalClient (): Promise<Client> {
|
||||
if (temporalConnection === undefined) {
|
||||
temporalConnection = Connection.connect({
|
||||
address: config.TemporalAddress
|
||||
})
|
||||
}
|
||||
const temporalClient = new Client({
|
||||
connection: await temporalConnection,
|
||||
namespace: config.TemporalNamespace
|
||||
})
|
||||
return temporalClient
|
||||
}
|
||||
@@ -16,9 +16,6 @@
|
||||
import { getClient as getAccountClient } from '@hcengineering/account-client'
|
||||
import { createRestTxOperations } from '@hcengineering/api-client'
|
||||
import core, { PersonId, systemAccountUuid, TxOperations, WorkspaceUuid } from '@hcengineering/core'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { Execution, parseContext, Transition } from '@hcengineering/process'
|
||||
import serverProcess, { ProcessControl } from '@hcengineering/server-process'
|
||||
import { generateToken } from '@hcengineering/server-token'
|
||||
import config from './config'
|
||||
|
||||
@@ -101,39 +98,3 @@ async function createClient (workspaceUuid: WorkspaceUuid, socialId?: PersonId):
|
||||
const client = await createRestTxOperations(transactorUrl, wsInfo.workspace, wsInfo.token, true)
|
||||
return client
|
||||
}
|
||||
|
||||
export async function pickTransition (
|
||||
control: ProcessControl,
|
||||
execution: Execution,
|
||||
transitions: Transition[],
|
||||
context: Record<string, any>
|
||||
): Promise<Transition | undefined> {
|
||||
for (const tr of transitions) {
|
||||
const trigger = control.client.getModel().findObject(tr.trigger)
|
||||
if (trigger === undefined) continue
|
||||
if (trigger.checkFunction === undefined) return tr
|
||||
const impl = control.client.getHierarchy().as(trigger, serverProcess.mixin.TriggerImpl)
|
||||
if (impl?.serverCheckFunc === undefined) return tr
|
||||
const filled = fillParams(tr.triggerParams, execution)
|
||||
const checkFunc = await getResource(impl.serverCheckFunc)
|
||||
if (checkFunc === undefined) continue
|
||||
const res = await checkFunc(filled, context, control.client.getHierarchy())
|
||||
if (res) return tr
|
||||
}
|
||||
}
|
||||
|
||||
function fillParams (params: Record<string, any>, execution: Execution): Record<string, any> {
|
||||
const res: Record<string, any> = {}
|
||||
for (const key in params) {
|
||||
const value = params[key]
|
||||
const context = parseContext(value)
|
||||
if (context === undefined) {
|
||||
res[key] = value
|
||||
continue
|
||||
}
|
||||
if (context.type === 'context') {
|
||||
res[key] = execution.context[context.id]
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
extends: ['./node_modules/@hcengineering/platform-rig/profiles/default/eslint.config.json'],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: './tsconfig.json'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
*
|
||||
!/lib/**
|
||||
!CHANGELOG.md
|
||||
/lib/**/__tests__/
|
||||
@@ -0,0 +1,14 @@
|
||||
FROM node:20-bullseye-slim
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
ENV NODE_ENV=production
|
||||
RUN npm install --ignore-scripts=false --verbose @temporalio/worker @temporalio/workflow --unsafe-perm
|
||||
|
||||
COPY bundle/bundle.js ./
|
||||
|
||||
CMD [ "node", "bundle.js" ]
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
|
||||
"rigPackageName": "@hcengineering/platform-rig"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
roots: ["./src"],
|
||||
coverageReporters: ["text-summary", "html"]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "@hcengineering/pod-worker",
|
||||
"version": "0.6.0",
|
||||
"main": "lib/index.js",
|
||||
"svelte": "src/index.ts",
|
||||
"types": "types/index.d.ts",
|
||||
"files": [
|
||||
"lib/**/*",
|
||||
"types/**/*",
|
||||
"tsconfig.json"
|
||||
],
|
||||
"author": "Hardcore Engineering Inc.",
|
||||
"scripts": {
|
||||
"build": "compile",
|
||||
"build:watch": "compile",
|
||||
"test": "jest --passWithNoTests --silent",
|
||||
"bundle": "node ../../common/scripts/esbuild.js --keep-names=true --sourcemap=external --minify --external=@temporalio/*",
|
||||
"_phase:bundle": "rushx bundle",
|
||||
"_phase:docker-build": "rushx docker:build",
|
||||
"_phase:docker-staging": "rushx docker:staging",
|
||||
"docker:build": "../../common/scripts/docker_build.sh hardcoreeng/worker .",
|
||||
"docker:tbuild": "docker build -t hardcoreeng/worker . --platform=linux/amd64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/worker",
|
||||
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/worker staging",
|
||||
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/worker",
|
||||
"run-local": "cross-env QUEUE_CONFIG='localhost:19092' REGION='cockroach' ts-node src/index.ts",
|
||||
"format": "format src",
|
||||
"_phase:build": "compile transpile src",
|
||||
"_phase:test": "jest --passWithNoTests --silent",
|
||||
"_phase:format": "format src",
|
||||
"_phase:validate": "compile validate"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node16": "^1.0.4",
|
||||
"cross-env": "~7.0.3",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/ws": "^8.5.11",
|
||||
"@typescript-eslint/eslint-plugin": "^6.11.0",
|
||||
"@hcengineering/platform-rig": "^0.6.0",
|
||||
"jest": "^29.7.0",
|
||||
"ts-jest": "^29.1.1",
|
||||
"@types/jest": "^29.5.5",
|
||||
"@types/uuid": "^8.3.1",
|
||||
"@typescript-eslint/parser": "^6.11.0",
|
||||
"esbuild": "^0.24.2",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-config-standard-with-typescript": "^40.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"prettier": "^3.1.0",
|
||||
"ts-node": "^10.8.0",
|
||||
"typescript": "^5.8.3",
|
||||
"@hcengineering/server-process": "^0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@temporalio/worker": "1.12.3",
|
||||
"@temporalio/workflow": "1.12.3",
|
||||
"@hcengineering/kafka": "^0.6.0",
|
||||
"@hcengineering/process": "^0.6.0",
|
||||
"@hcengineering/core": "^0.6.32",
|
||||
"@hcengineering/server-core": "^0.6.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import core, { MeasureMetricsContext, type Ref, type WorkspaceUuid } from '@hcengineering/core'
|
||||
import { getPlatformQueue } from '@hcengineering/kafka'
|
||||
import process, { type Execution } from '@hcengineering/process'
|
||||
import { QueueTopic } from '@hcengineering/server-core'
|
||||
import type { ProcessMessage } from '@hcengineering/server-process'
|
||||
|
||||
export async function SendTimeEvent (ws: WorkspaceUuid, _execution: Ref<Execution>): Promise<void> {
|
||||
const SERVICE_NAME = 'worker'
|
||||
const queue = getPlatformQueue(SERVICE_NAME)
|
||||
const ctx = new MeasureMetricsContext(SERVICE_NAME, {})
|
||||
|
||||
const producer = queue.getProducer<ProcessMessage>(ctx, QueueTopic.Process)
|
||||
|
||||
await producer.send(ctx, ws, [
|
||||
{
|
||||
account: core.account.System,
|
||||
event: process.trigger.OnTime,
|
||||
context: {},
|
||||
execution: _execution
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
export default {
|
||||
SendTimeEvent
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { runWorker } from './worker'
|
||||
|
||||
runWorker().catch((err) => {
|
||||
console.error(err)
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { NativeConnection, Worker } from '@temporalio/worker'
|
||||
import * as activities from './activities'
|
||||
|
||||
export async function runWorker (): Promise<void> {
|
||||
const connection = await NativeConnection.connect({
|
||||
address: process.env.TEMPORAL_ADDRESS ?? 'localhost:7233'
|
||||
})
|
||||
try {
|
||||
const worker = await Worker.create({
|
||||
connection,
|
||||
workflowsPath: require.resolve('./workflows'),
|
||||
activities,
|
||||
namespace: process.env.TEMPORAL_NAMESPACE ?? 'huly',
|
||||
taskQueue: 'process'
|
||||
})
|
||||
|
||||
await worker.run()
|
||||
} finally {
|
||||
await connection.close()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import type { Ref, WorkspaceUuid } from '@hcengineering/core'
|
||||
import type { Execution } from '@hcengineering/process'
|
||||
import { defineSignal, proxyActivities, setHandler, sleep } from '@temporalio/workflow'
|
||||
import activities from './activities'
|
||||
|
||||
const { SendTimeEvent } = proxyActivities<typeof activities>({
|
||||
startToCloseTimeout: '10 minute'
|
||||
})
|
||||
|
||||
const setDate = defineSignal<[number]>('setDate')
|
||||
|
||||
export async function processTimeWorkflow (
|
||||
_targetDate: number,
|
||||
ws: WorkspaceUuid,
|
||||
_execution: Ref<Execution>
|
||||
): Promise<void> {
|
||||
let targetDate: number = _targetDate
|
||||
let currentPromise: Promise<void> | undefined
|
||||
|
||||
while (true) {
|
||||
const when = new Date(targetDate).getTime()
|
||||
const delay = when - Date.now()
|
||||
|
||||
if (delay > 0) {
|
||||
currentPromise = sleep(delay)
|
||||
try {
|
||||
await Promise.race([
|
||||
currentPromise,
|
||||
new Promise((resolve, reject) => {
|
||||
setHandler(setDate, (newDate) => {
|
||||
targetDate = newDate
|
||||
reject(new Error('Date updated'))
|
||||
})
|
||||
})
|
||||
])
|
||||
} catch (error: any) {
|
||||
if (error instanceof Error && error.message === 'Date updated') {
|
||||
continue
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
await SendTimeEvent(ws, _execution)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./node_modules/@hcengineering/platform-rig/profiles/default/tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declarationDir": "./types",
|
||||
"tsBuildInfoFile": ".build/build.tsbuildinfo"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "lib", "dist", "types", "bundle"]
|
||||
}
|
||||
Reference in New Issue
Block a user