package service import ( "fmt" "strings" "time" "browser.local/platform/domain" ) func (svc *CoreService) projectPluginLifecycleState(instance domain.ServerInstance, plugin domain.GamePlugin, capability string, result domain.JobExecutionResult, stamp time.Time) error { if len(plugin.GameClientBridge.LifecycleProjections) == 0 { return nil } for _, projection := range plugin.GameClientBridge.LifecycleProjections { if !containsString(projection.Capabilities, capability) || len(projection.Target.FixedValues) == 0 { continue } if len(projection.ProcessStates) > 0 && !containsString(projection.ProcessStates, strings.TrimSpace(result.ProcessState)) { continue } if err := svc.applyPluginBulkProjection(instance, plugin, projection.Target, stamp); err != nil { return err } } return nil } func (svc *CoreService) applyPluginBulkProjection(instance domain.ServerInstance, plugin domain.GamePlugin, target domain.GameClientBridgeBulkProjectionTargetDeclaration, stamp time.Time) error { rows, err := svc.store.PluginDataRecords().List(domain.PluginDataFilter{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: target.Collection}) if err != nil { return err } mutations := make([]domain.PluginDataMutation, 0, len(rows)) activityMutations := []domain.PluginDataMutation{} for _, row := range rows { if strings.TrimSpace(fmt.Sprint(row.Value[target.MatchField])) != target.MatchValue { continue } value := mergePluginDataValues(row.Value, pluginBulkProjectionValues(target.FixedValues, stamp, row.Value, target.ObservedAtField)) mutations = append(mutations, domain.PluginDataMutation{Operation: domain.PluginDataMutationPut, Key: row.Key, Value: value}) if target.ActivityTarget != nil { activity := pluginBulkActivityValue(*target.ActivityTarget, row.Value, stamp) activityKey, keyErr := pluginDataRowKey(activity, target.ActivityTarget.UpsertKeys) if keyErr != nil { return keyErr } activityMutations = append(activityMutations, domain.PluginDataMutation{Operation: domain.PluginDataMutationPut, Key: activityKey, Value: activity}) } } if len(mutations) > 0 { if _, err := svc.applyPluginDataTransaction(domain.PluginDataTransaction{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: target.Collection, Mutations: mutations}); err != nil { return err } } if len(activityMutations) > 0 && target.ActivityTarget != nil { if _, err := svc.applyPluginDataTransaction(domain.PluginDataTransaction{PluginID: plugin.ID, ServerInstanceID: instance.ID, Collection: target.ActivityTarget.Collection, Mutations: activityMutations}); err != nil { return err } } return nil } func pluginBulkProjectionValues(fixedValues map[string]string, stamp time.Time, row map[string]any, observedAtField string) map[string]any { value := make(map[string]any, len(fixedValues)+1) for key, fixed := range fixedValues { value[key] = renderQueryProjectionTemplate(fixed, row) } if observedAtField != "" { value[observedAtField] = stamp.UTC().Format(time.RFC3339Nano) } return value } func pluginBulkActivityValue(target domain.GameClientBridgeBulkActivityTargetDeclaration, row map[string]any, stamp time.Time) map[string]any { value := make(map[string]any, len(target.RowMappings)+len(target.FixedValues)+1) for destination, source := range target.RowMappings { value[destination] = row[source] } for key, fixed := range target.FixedValues { value[key] = renderQueryProjectionTemplate(fixed, row) } if target.ObservedAtField != "" { value[target.ObservedAtField] = stamp.UTC().Format(time.RFC3339Nano) } return value }