Files
aa6abc5ed6 feat(event): add personal event subscriptions (#589)
* dws event

* fix consume fail

* test: add stream ticket injection probe

* feat: add portal ticket stream mode

* user event

* fix: allow portal ticket normal without app secret

* event

* user event

* eventType filter

* refactor(event): 优化IPC端点路径处理和改进相关测试

- 用dwsevent.IPCEndpoint替代原先根据GOOS判断的路径逻辑
- 新增event包实现Unix socket路径长度限制及长路径fallback机制
- 添加endpoint_test.go覆盖路径短长及唯一性的单元测试
- 修改busctl模块使用统一的IPC端点获取方法,避免重复实现
- transport_unix.go新增checkSocketPath函数检查路径长度,防止EINVAL错误
- 在监听和连接Unix socket时加入路径限制检查,提升错误明晰度
- 去除多个文件中无用的runtime导入,简化代码依赖

* opt

* event skill

* default value

* install script event

* fix: remove subscribe id event fanout filter

* fix(personal): 修正指定发送人消息描述错误

* more im event

* filter subId

* fix: align personal event schema with stream payload

* fix: avoid duplicate app helper name

* feat: simplify personal event schemas

* feat: simplify event schema output

* docs: refine dingtalk event skill references

* feat: align personal event consume flags

* fix event stop and status visibility

* hide app event public entrypoints

* hide incomplete personal sender event

* remove external event reference comments

* chore(event): prepare official release

* fix(event): harden personal stream lifecycle

---------

Co-authored-by: 玉澜 <yulan.wqy@alibaba-inc.com>
Co-authored-by: zhengyubai <zhengyubai618@gmail.com>
2026-07-10 17:54:43 +08:00

182 lines
6.1 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache 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
//
// http://www.apache.org/licenses/LICENSE-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.
package source
import (
"sync"
"time"
)
// State enumerates the connection states surfaced by `dws event status`.
//
// Stream SDK v0.9.1 does not expose a user-registerable ConnectionState
// hook (verified P0 §1 escape-hatch table row #4), so the state machine
// runs in "inferred" mode: it derives state from observable signals —
// Start() return value, last-event-received timestamp, last-error timestamp,
// SDK reconnect log lines parsed by the source layer. The Source field
// `state_source` (env-style annotation) is emitted alongside the state so
// downstream tools can distinguish authoritative from inferred values when
// SDK hook support arrives in a future release.
type State string
const (
// StateDisconnected is the zero state before Start has been called.
StateDisconnected State = "disconnected"
// StateConnecting is set on entry to Start, before SDK dial completes.
StateConnecting State = "connecting"
// StateConnected is set when Start returns nil (dial succeeded).
StateConnected State = "connected"
// StateIdle is set when the connection is still up but no event has been
// observed for a quiet window (default 60s). Distinguishes "everything
// fine, just no traffic" from "looks connected but broken".
StateIdle State = "idle"
// StateReconnecting is set when the SDK has signalled a reconnect (via
// log line parse or future hook). Cleared on next event received.
StateReconnecting State = "reconnecting"
// StateDegraded is set when reconnects keep happening but no events flow.
// Useful to surface "looks alive, actually broken" cases via status.
StateDegraded State = "degraded"
// StateStopped is set after Close() / context cancellation.
StateStopped State = "stopped"
)
// SourceKind identifies whether the state was reported by an authoritative
// SDK hook or inferred from observable side-channels.
type SourceKind string
const (
// SourceHook is reserved for future SDK versions that expose
// ConnectionState / keepalive callbacks. v1 does not emit this.
SourceHook SourceKind = "hook"
// SourceInferred is the only value emitted by v1.
SourceInferred SourceKind = "inferred"
)
// IdleAfter is the duration of no events after which StateConnected
// transitions to StateIdle. Exposed for tests to bypass real-time waits.
var IdleAfter = 60 * time.Second
// Machine tracks the connection state. Methods are safe for concurrent use.
type Machine struct {
mu sync.RWMutex
state State
source SourceKind
lastEventAt time.Time
lastReconnectAt time.Time
reconnectCount int
now func() time.Time
}
// NewMachine returns a Machine in the StateDisconnected initial state.
func NewMachine() *Machine {
return &Machine{
state: StateDisconnected,
source: SourceInferred,
now: time.Now,
}
}
// State returns the current effective state, applying time-based idle
// detection on top of the stored state. This is the public read accessor
// used by `event status` formatting.
func (m *Machine) State() State {
m.mu.RLock()
defer m.mu.RUnlock()
return m.effectiveStateLocked()
}
// Snapshot returns the full state view for status rendering: state +
// source + last event/reconnect timestamps + reconnect count.
type Snapshot struct {
State State `json:"state"`
StateSource SourceKind `json:"state_source"`
LastEventAt time.Time `json:"last_event_at,omitempty"`
LastReconnectAt time.Time `json:"last_reconnect_at,omitempty"`
ReconnectCount int `json:"reconnect_count"`
}
// Snapshot returns the current state view atomically.
func (m *Machine) Snapshot() Snapshot {
m.mu.RLock()
defer m.mu.RUnlock()
return Snapshot{
State: m.effectiveStateLocked(),
StateSource: m.source,
LastEventAt: m.lastEventAt,
LastReconnectAt: m.lastReconnectAt,
ReconnectCount: m.reconnectCount,
}
}
// OnConnecting moves to StateConnecting. Called by Source.Start before the
// SDK dial completes.
func (m *Machine) OnConnecting() {
m.mu.Lock()
defer m.mu.Unlock()
m.state = StateConnecting
}
// OnConnected moves to StateConnected. Called when SDK Start returns nil.
func (m *Machine) OnConnected() {
m.mu.Lock()
defer m.mu.Unlock()
m.state = StateConnected
}
// OnEvent records the timestamp of the most recently received event. Resets
// any Reconnecting or Idle state back to Connected (the connection is
// demonstrably alive).
func (m *Machine) OnEvent() {
m.mu.Lock()
defer m.mu.Unlock()
m.lastEventAt = m.now()
if m.state == StateReconnecting || m.state == StateIdle || m.state == StateDegraded {
m.state = StateConnected
}
}
// OnReconnect marks a reconnect signal observed (from SDK log or future
// hook). Increments the counter and stamps the time.
func (m *Machine) OnReconnect() {
m.mu.Lock()
defer m.mu.Unlock()
m.lastReconnectAt = m.now()
m.reconnectCount++
m.state = StateReconnecting
}
// OnStopped marks the SDK as closed (graceful exit or Close()).
func (m *Machine) OnStopped() {
m.mu.Lock()
defer m.mu.Unlock()
m.state = StateStopped
}
// effectiveStateLocked applies idle-detection on top of the stored state.
// Caller must hold m.mu (read or write).
func (m *Machine) effectiveStateLocked() State {
switch m.state {
case StateConnected:
// Connected → Idle after IdleAfter with no events. If we've also
// seen recent reconnects with no recovered events, surface Degraded.
if !m.lastEventAt.IsZero() && m.now().Sub(m.lastEventAt) >= IdleAfter {
if m.reconnectCount > 0 && m.lastReconnectAt.After(m.lastEventAt) {
return StateDegraded
}
return StateIdle
}
}
return m.state
}