@pi-in-go/pigpen-ahp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CREDITS.md +27 -0
- package/LICENSE +23 -0
- package/README.md +148 -0
- package/extensions/ahp/access_options_test.go +116 -0
- package/extensions/ahp/extension.go +54 -0
- package/extensions/ahp/go.mod +12 -0
- package/extensions/ahp/go.sum +2 -0
- package/extensions/ahp/go.work +8 -0
- package/extensions/ahp/internal/channels/chat.go +110 -0
- package/extensions/ahp/internal/channels/root.go +32 -0
- package/extensions/ahp/internal/channels/session.go +128 -0
- package/extensions/ahp/internal/channels/session_test.go +108 -0
- package/extensions/ahp/internal/compose/compose.go +153 -0
- package/extensions/ahp/internal/compose/compose_test.go +120 -0
- package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
- package/extensions/ahp/internal/host/handshake_test.go +206 -0
- package/extensions/ahp/internal/host/host.go +1305 -0
- package/extensions/ahp/internal/host/reconnect_test.go +197 -0
- package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
- package/extensions/ahp/internal/host/schema_test.go +79 -0
- package/extensions/ahp/internal/host/store.go +291 -0
- package/extensions/ahp/internal/host/store_update_test.go +85 -0
- package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
- package/extensions/ahp/internal/host/surface_test.go +95 -0
- package/extensions/ahp/internal/host/workarounds.go +458 -0
- package/extensions/ahp/internal/host/workarounds_test.go +96 -0
- package/extensions/ahp/internal/live/live.go +328 -0
- package/extensions/ahp/internal/live/live_test.go +91 -0
- package/extensions/ahp/internal/live/store.go +113 -0
- package/extensions/ahp/internal/mapper/activity.go +199 -0
- package/extensions/ahp/internal/mapper/activity_test.go +188 -0
- package/extensions/ahp/internal/mapper/bench_test.go +38 -0
- package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
- package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
- package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
- package/extensions/ahp/internal/mapper/js.go +131 -0
- package/extensions/ahp/internal/mapper/mapper.go +600 -0
- package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
- package/extensions/ahp/internal/mapper/messageinput.go +272 -0
- package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
- package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
- package/extensions/ahp/internal/mapper/title.go +34 -0
- package/extensions/ahp/internal/mapper/usermsg.go +87 -0
- package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
- package/extensions/ahp/internal/pi/backend.go +64 -0
- package/extensions/ahp/internal/pi/catalogue.go +370 -0
- package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
- package/extensions/ahp/internal/pi/chatdriver.go +533 -0
- package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
- package/extensions/ahp/internal/pi/clientactions.go +212 -0
- package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
- package/extensions/ahp/internal/pi/completions.go +230 -0
- package/extensions/ahp/internal/pi/completions_test.go +272 -0
- package/extensions/ahp/internal/pi/config_test.go +269 -0
- package/extensions/ahp/internal/pi/deletesession.go +69 -0
- package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
- package/extensions/ahp/internal/pi/disposal_test.go +362 -0
- package/extensions/ahp/internal/pi/fixture_test.go +308 -0
- package/extensions/ahp/internal/pi/foreign_test.go +122 -0
- package/extensions/ahp/internal/pi/harness_test.go +83 -0
- package/extensions/ahp/internal/pi/history.go +269 -0
- package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
- package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
- package/extensions/ahp/internal/pi/hydration_test.go +209 -0
- package/extensions/ahp/internal/pi/hydrator.go +212 -0
- package/extensions/ahp/internal/pi/imageinput.go +224 -0
- package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
- package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
- package/extensions/ahp/internal/pi/models.go +212 -0
- package/extensions/ahp/internal/pi/models_test.go +128 -0
- package/extensions/ahp/internal/pi/paging.go +95 -0
- package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
- package/extensions/ahp/internal/pi/projecttrust.go +222 -0
- package/extensions/ahp/internal/pi/registry.go +924 -0
- package/extensions/ahp/internal/pi/restart_test.go +185 -0
- package/extensions/ahp/internal/pi/services.go +70 -0
- package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
- package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
- package/extensions/ahp/internal/pi/sessionstore.go +83 -0
- package/extensions/ahp/internal/pi/summary_test.go +426 -0
- package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
- package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
- package/extensions/ahp/internal/pisession/json.go +31 -0
- package/extensions/ahp/internal/pisession/pisession.go +580 -0
- package/extensions/ahp/internal/settings/access.go +63 -0
- package/extensions/ahp/internal/settings/access_test.go +52 -0
- package/extensions/ahp/internal/settings/settings.go +117 -0
- package/extensions/ahp/internal/settings/settings_test.go +104 -0
- package/extensions/ahp/internal/svc/etag_other.go +15 -0
- package/extensions/ahp/internal/svc/etag_unix.go +34 -0
- package/extensions/ahp/internal/svc/glob.go +197 -0
- package/extensions/ahp/internal/svc/mime.go +37 -0
- package/extensions/ahp/internal/svc/paths.go +108 -0
- package/extensions/ahp/internal/svc/pty.go +64 -0
- package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
- package/extensions/ahp/internal/svc/pty_linux.go +36 -0
- package/extensions/ahp/internal/svc/pty_other.go +9 -0
- package/extensions/ahp/internal/svc/pty_test.go +114 -0
- package/extensions/ahp/internal/svc/pty_unix.go +147 -0
- package/extensions/ahp/internal/svc/resource.go +495 -0
- package/extensions/ahp/internal/svc/resource_test.go +501 -0
- package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
- package/extensions/ahp/internal/svc/stat_linux.go +9 -0
- package/extensions/ahp/internal/svc/terminal.go +454 -0
- package/extensions/ahp/internal/svc/terminal_test.go +512 -0
- package/extensions/ahp/internal/svc/watch.go +590 -0
- package/extensions/ahp/internal/svc/watch_test.go +627 -0
- package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
- package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
- package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
- package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
- package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
- package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
- package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
- package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
- package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
- package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
- package/extensions/ahp/internal/testkit/schema.go +360 -0
- package/extensions/ahp/internal/testkit/testkit.go +367 -0
- package/extensions/ahp/internal/twin/twin.go +27 -0
- package/extensions/ahp/internal/wire/dispatchable.go +106 -0
- package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
- package/extensions/ahp/internal/wire/helpers_test.go +5 -0
- package/extensions/ahp/internal/wire/wire.go +218 -0
- package/extensions/ahp/internal/wire/wire_test.go +123 -0
- package/extensions/ahp/internal/ws/helpers_test.go +7 -0
- package/extensions/ahp/internal/ws/vscode_test.go +198 -0
- package/extensions/ahp/internal/ws/ws.go +674 -0
- package/extensions/ahp/internal/ws/ws_test.go +324 -0
- package/extensions/ahp/realpig_test.go +589 -0
- package/extensions/ahp/runtime.go +311 -0
- package/extensions/ahp/runtime_test.go +110 -0
- package/extensions/ahp/testdata/upstream-tests.json +3833 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
- package/extensions/ahp/twins_test.go +91 -0
- package/package.json +40 -0
- package/proof/PORT.md +121 -0
- package/proof/mutations.json +152 -0
- package/proof/oracle/LICENSE +21 -0
- package/proof/oracle/README.md +120 -0
- package/proof/oracle/UPSTREAM.md +7 -0
- package/proof/oracle/package.json +68 -0
- package/proof/oracle/src/bin/cli.ts +66 -0
- package/proof/oracle/src/bin/tunnel.ts +128 -0
- package/proof/oracle/src/channels/chat.ts +134 -0
- package/proof/oracle/src/channels/root.ts +39 -0
- package/proof/oracle/src/channels/session.ts +118 -0
- package/proof/oracle/src/channels/terminal.ts +11 -0
- package/proof/oracle/src/core/channels.ts +108 -0
- package/proof/oracle/src/core/client-workarounds.ts +358 -0
- package/proof/oracle/src/core/connection.ts +41 -0
- package/proof/oracle/src/core/host.ts +882 -0
- package/proof/oracle/src/core/sequencer.ts +75 -0
- package/proof/oracle/src/core/state-store.ts +162 -0
- package/proof/oracle/src/core/uri.ts +25 -0
- package/proof/oracle/src/host/direct-settings.ts +110 -0
- package/proof/oracle/src/host/pi-host.ts +220 -0
- package/proof/oracle/src/host/serve.ts +71 -0
- package/proof/oracle/src/host/terminal-service.ts +346 -0
- package/proof/oracle/src/pi/activity.ts +171 -0
- package/proof/oracle/src/pi/changeset-service.ts +646 -0
- package/proof/oracle/src/pi/changeset-uri.ts +64 -0
- package/proof/oracle/src/pi/chat-driver.ts +529 -0
- package/proof/oracle/src/pi/completions.ts +160 -0
- package/proof/oracle/src/pi/delete-session.ts +53 -0
- package/proof/oracle/src/pi/event-mapper.ts +648 -0
- package/proof/oracle/src/pi/git-changes.ts +605 -0
- package/proof/oracle/src/pi/history.ts +305 -0
- package/proof/oracle/src/pi/image-input.ts +56 -0
- package/proof/oracle/src/pi/image-mime.ts +8 -0
- package/proof/oracle/src/pi/in-process-backend.ts +155 -0
- package/proof/oracle/src/pi/message-input.ts +212 -0
- package/proof/oracle/src/pi/models.ts +124 -0
- package/proof/oracle/src/pi/project-trust.ts +66 -0
- package/proof/oracle/src/pi/provider.ts +2 -0
- package/proof/oracle/src/pi/resource-paths.ts +80 -0
- package/proof/oracle/src/pi/resource-service.ts +356 -0
- package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
- package/proof/oracle/src/pi/resource-watch.ts +357 -0
- package/proof/oracle/src/pi/session-catalogue.ts +318 -0
- package/proof/oracle/src/pi/session-config.ts +84 -0
- package/proof/oracle/src/pi/session-history.ts +75 -0
- package/proof/oracle/src/pi/session-hydrator.ts +219 -0
- package/proof/oracle/src/pi/session-registry.ts +815 -0
- package/proof/oracle/src/pi/session-storage.ts +29 -0
- package/proof/oracle/src/pi/session-title.ts +18 -0
- package/proof/oracle/src/pi/turn-paging.ts +94 -0
- package/proof/oracle/src/pi/user-message.ts +66 -0
- package/proof/oracle/src/protocol/errors.ts +44 -0
- package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
- package/proof/oracle/src/protocol/version.ts +39 -0
- package/proof/oracle/src/transport/websocket.ts +136 -0
- package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
- package/proof/oracle/src/tunnel/discovery.ts +37 -0
- package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
- package/proof/oracle/test/activity.test.ts +171 -0
- package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
- package/proof/oracle/test/changeset-uri.test.ts +37 -0
- package/proof/oracle/test/changeset.test.serial.ts +487 -0
- package/proof/oracle/test/chat-driver.test.ts +691 -0
- package/proof/oracle/test/client-actions.test.ts +409 -0
- package/proof/oracle/test/client-workarounds.test.ts +320 -0
- package/proof/oracle/test/completions.test.ts +323 -0
- package/proof/oracle/test/delete-session.test.ts +142 -0
- package/proof/oracle/test/direct-settings.test.ts +81 -0
- package/proof/oracle/test/event-mapper.test.ts +619 -0
- package/proof/oracle/test/fetch-turns.test.ts +207 -0
- package/proof/oracle/test/fixtures/abort.json +711 -0
- package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
- package/proof/oracle/test/fixtures/compaction.json +6285 -0
- package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
- package/proof/oracle/test/fixtures/plain-text.json +1856 -0
- package/proof/oracle/test/fixtures/single-tool.json +1128 -0
- package/proof/oracle/test/fixtures/steering.json +3926 -0
- package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
- package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
- package/proof/oracle/test/fixtures/tool-error.json +2850 -0
- package/proof/oracle/test/fixtures/tool-find.json +1162 -0
- package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
- package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
- package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
- package/proof/oracle/test/fixtures/tool-write.json +1271 -0
- package/proof/oracle/test/handshake.test.ts +275 -0
- package/proof/oracle/test/harness.ts +153 -0
- package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
- package/proof/oracle/test/image-input.test.ts +28 -0
- package/proof/oracle/test/image-session.test.ts +69 -0
- package/proof/oracle/test/live-turn.test.ts +304 -0
- package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
- package/proof/oracle/test/message-input.test.ts +181 -0
- package/proof/oracle/test/model-discovery.test.ts +170 -0
- package/proof/oracle/test/models.test.ts +109 -0
- package/proof/oracle/test/pi-host.test.ts +91 -0
- package/proof/oracle/test/pi-replay.test.ts +141 -0
- package/proof/oracle/test/project-trust.test.ts +125 -0
- package/proof/oracle/test/protocol-surface.test.ts +121 -0
- package/proof/oracle/test/pty.test.ts +100 -0
- package/proof/oracle/test/reconnect.test.ts +389 -0
- package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
- package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
- package/proof/oracle/test/resource.test.ts +499 -0
- package/proof/oracle/test/schema.test.ts +88 -0
- package/proof/oracle/test/session-catalogue.test.ts +314 -0
- package/proof/oracle/test/session-config.test.ts +224 -0
- package/proof/oracle/test/session-disposal.test.ts +364 -0
- package/proof/oracle/test/session-hydration.test.ts +194 -0
- package/proof/oracle/test/session-lifecycle.test.ts +416 -0
- package/proof/oracle/test/session-storage.test.serial.ts +32 -0
- package/proof/oracle/test/session-summary.test.ts +418 -0
- package/proof/oracle/test/subscriptions.test.ts +198 -0
- package/proof/oracle/test/support/assertions.ts +32 -0
- package/proof/oracle/test/support/async.ts +26 -0
- package/proof/oracle/test/support/hydrated-session.ts +205 -0
- package/proof/oracle/test/support/images.ts +8 -0
- package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
- package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
- package/proof/oracle/test/support/replay.ts +194 -0
- package/proof/oracle/test/support/schema.ts +221 -0
- package/proof/oracle/test/support/session-files.ts +12 -0
- package/proof/oracle/test/support/session-storage.ts +22 -0
- package/proof/oracle/test/support/upstream.ts +25 -0
- package/proof/oracle/test/support/watch-events.ts +108 -0
- package/proof/oracle/test/terminal-service.test.ts +495 -0
- package/proof/oracle/test/truncate.test.ts +230 -0
- package/proof/oracle/test/tunnel.test.ts +258 -0
- package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
- package/proof/oracle/test/uri.test.ts +24 -0
- package/proof/oracle/test/watch-events.test.ts +118 -0
- package/proof/oracle/tsconfig.json +32 -0
- package/proof/proof-piglet/piglet.yaml +18 -0
- package/proof/tools/fakellm/go.mod +3 -0
- package/proof/tools/fakellm/main.go +109 -0
- package/proof/tools/gen-dispatchable.py +30 -0
- package/provenance.json +28 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
package svc
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"crypto/rand"
|
|
6
|
+
"crypto/sha256"
|
|
7
|
+
"encoding/hex"
|
|
8
|
+
"encoding/json"
|
|
9
|
+
"errors"
|
|
10
|
+
"fmt"
|
|
11
|
+
"io"
|
|
12
|
+
"io/fs"
|
|
13
|
+
"os"
|
|
14
|
+
"path/filepath"
|
|
15
|
+
"sort"
|
|
16
|
+
"sync"
|
|
17
|
+
"syscall"
|
|
18
|
+
"time"
|
|
19
|
+
|
|
20
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/host"
|
|
21
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/wire"
|
|
22
|
+
ahptypes "github.com/microsoft/agent-host-protocol/clients/go/ahptypes"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
// Clock schedules the debounce and grace timers; tests drive it by hand.
|
|
26
|
+
type Clock interface {
|
|
27
|
+
AfterFunc(d time.Duration, f func()) Stopper
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Stopper cancels a scheduled function.
|
|
31
|
+
type Stopper interface{ Stop() bool }
|
|
32
|
+
|
|
33
|
+
type realClock struct{}
|
|
34
|
+
|
|
35
|
+
func (realClock) AfterFunc(d time.Duration, f func()) Stopper { return time.AfterFunc(d, f) }
|
|
36
|
+
|
|
37
|
+
// Defaults of the upstream service; the poll interval replaces chokidar's native events.
|
|
38
|
+
const (
|
|
39
|
+
DefaultGrace = 30 * time.Second
|
|
40
|
+
DefaultDebounce = 50 * time.Millisecond
|
|
41
|
+
DefaultPollInterval = time.Second
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
// WatchOptions configures a WatchService.
|
|
45
|
+
type WatchOptions struct {
|
|
46
|
+
Paths *PathPolicy
|
|
47
|
+
Grace time.Duration // 0 = DefaultGrace
|
|
48
|
+
Debounce time.Duration // 0 = DefaultDebounce
|
|
49
|
+
PollInterval time.Duration // 0 = DefaultPollInterval
|
|
50
|
+
Log func(string)
|
|
51
|
+
Clock Clock // nil = wall clock
|
|
52
|
+
|
|
53
|
+
// test hooks
|
|
54
|
+
afterPath func() // after the path policy resolved a create's URI
|
|
55
|
+
afterReady func() // after the first scan of a create
|
|
56
|
+
onClosed func() // each time a poller is stopped
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// WatchService is the resource watch channel factory (port of resource-watch.ts). Upstream leans
|
|
60
|
+
// on chokidar's native events; the standard library has no portable file notification, so this
|
|
61
|
+
// port keeps a snapshot of the watched tree and diffs it every PollInterval. Everything above the
|
|
62
|
+
// watcher (policy, batching, grace, disposal) is the upstream design. It implements
|
|
63
|
+
// host.ResourceWatchHandler.
|
|
64
|
+
type WatchService struct {
|
|
65
|
+
host *host.Host
|
|
66
|
+
opts WatchOptions
|
|
67
|
+
paths *PathPolicy
|
|
68
|
+
clock Clock
|
|
69
|
+
unhook func()
|
|
70
|
+
|
|
71
|
+
mu sync.Mutex
|
|
72
|
+
watches map[string]*activeWatch
|
|
73
|
+
pending sync.WaitGroup
|
|
74
|
+
disposed bool
|
|
75
|
+
disposal chan struct{}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type activeWatch struct {
|
|
79
|
+
channel string
|
|
80
|
+
resourceRoot string
|
|
81
|
+
watchedRoot string
|
|
82
|
+
isDir bool
|
|
83
|
+
recursive bool
|
|
84
|
+
excludes []string
|
|
85
|
+
includes []string
|
|
86
|
+
|
|
87
|
+
stopPoll chan struct{}
|
|
88
|
+
pollDone chan struct{}
|
|
89
|
+
|
|
90
|
+
// guarded by WatchService.mu
|
|
91
|
+
changes map[string]ahptypes.ResourceChangeType
|
|
92
|
+
flushTimer Stopper
|
|
93
|
+
graceTimer Stopper
|
|
94
|
+
released bool
|
|
95
|
+
snapshot map[string]watchEntry
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// NewWatchService creates the service and hooks it to the host's subscriber counts.
|
|
99
|
+
func NewWatchService(h *host.Host, opts WatchOptions) *WatchService {
|
|
100
|
+
s := &WatchService{host: h, opts: opts, paths: opts.Paths, clock: opts.Clock, watches: map[string]*activeWatch{}, disposal: make(chan struct{})}
|
|
101
|
+
if s.paths == nil {
|
|
102
|
+
s.paths = &PathPolicy{}
|
|
103
|
+
}
|
|
104
|
+
if s.clock == nil {
|
|
105
|
+
s.clock = realClock{}
|
|
106
|
+
}
|
|
107
|
+
s.unhook = h.OnSubscriberCountChanged(s.onSubscriberCount)
|
|
108
|
+
return s
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
func (s *WatchService) grace() time.Duration {
|
|
112
|
+
if s.opts.Grace > 0 {
|
|
113
|
+
return s.opts.Grace
|
|
114
|
+
}
|
|
115
|
+
return DefaultGrace
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
func (s *WatchService) debounce() time.Duration {
|
|
119
|
+
if s.opts.Debounce > 0 {
|
|
120
|
+
return s.opts.Debounce
|
|
121
|
+
}
|
|
122
|
+
return DefaultDebounce
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func (s *WatchService) pollInterval() time.Duration {
|
|
126
|
+
if s.opts.PollInterval > 0 {
|
|
127
|
+
return s.opts.PollInterval
|
|
128
|
+
}
|
|
129
|
+
return DefaultPollInterval
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func (s *WatchService) log(format string, args ...any) {
|
|
133
|
+
if s.opts.Log != nil {
|
|
134
|
+
s.opts.Log(fmt.Sprintf(format, args...))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ActiveCount is how many watches are live.
|
|
139
|
+
func (s *WatchService) ActiveCount() int {
|
|
140
|
+
s.mu.Lock()
|
|
141
|
+
defer s.mu.Unlock()
|
|
142
|
+
return len(s.watches)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
var errDisposed = errors.New("ResourceWatchService is disposed")
|
|
146
|
+
|
|
147
|
+
// Dispose stops accepting creations, drains the ones in flight and releases every watch. Every
|
|
148
|
+
// call returns the same channel, closed when shutdown completed.
|
|
149
|
+
func (s *WatchService) Dispose() <-chan struct{} {
|
|
150
|
+
s.mu.Lock()
|
|
151
|
+
if s.disposed {
|
|
152
|
+
s.mu.Unlock()
|
|
153
|
+
return s.disposal
|
|
154
|
+
}
|
|
155
|
+
s.disposed = true
|
|
156
|
+
channels := make([]string, 0, len(s.watches))
|
|
157
|
+
for channel := range s.watches {
|
|
158
|
+
channels = append(channels, channel)
|
|
159
|
+
}
|
|
160
|
+
s.mu.Unlock()
|
|
161
|
+
s.unhook()
|
|
162
|
+
go func() {
|
|
163
|
+
var wg sync.WaitGroup
|
|
164
|
+
for _, channel := range channels {
|
|
165
|
+
wg.Add(1)
|
|
166
|
+
go func() { defer wg.Done(); s.release(channel) }()
|
|
167
|
+
}
|
|
168
|
+
wg.Wait()
|
|
169
|
+
s.pending.Wait()
|
|
170
|
+
close(s.disposal)
|
|
171
|
+
}()
|
|
172
|
+
return s.disposal
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
func readPatterns(raw *json.RawMessage, name string) ([]string, error) {
|
|
176
|
+
if raw == nil {
|
|
177
|
+
return nil, nil
|
|
178
|
+
}
|
|
179
|
+
var wrapper map[string]json.RawMessage
|
|
180
|
+
if err := json.Unmarshal(*raw, &wrapper); err != nil || wrapper == nil {
|
|
181
|
+
return nil, wire.InvalidParams(name + " must contain an items array")
|
|
182
|
+
}
|
|
183
|
+
items, ok := wrapper["items"]
|
|
184
|
+
if !ok {
|
|
185
|
+
return nil, wire.InvalidParams(name + " must contain an items array")
|
|
186
|
+
}
|
|
187
|
+
var list []json.RawMessage
|
|
188
|
+
if err := json.Unmarshal(items, &list); err != nil || list == nil {
|
|
189
|
+
return nil, wire.InvalidParams(name + ".items must be an array of strings")
|
|
190
|
+
}
|
|
191
|
+
out := make([]string, 0, len(list))
|
|
192
|
+
for _, item := range list {
|
|
193
|
+
var str string
|
|
194
|
+
if err := json.Unmarshal(item, &str); err != nil {
|
|
195
|
+
return nil, wire.InvalidParams(name + ".items must be an array of strings")
|
|
196
|
+
}
|
|
197
|
+
out = append(out, str)
|
|
198
|
+
}
|
|
199
|
+
return out, nil
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
func watchError(err error, uri string) error {
|
|
203
|
+
var we *wire.Error
|
|
204
|
+
switch {
|
|
205
|
+
case errors.As(err, &we):
|
|
206
|
+
return err
|
|
207
|
+
case errors.Is(err, fs.ErrNotExist):
|
|
208
|
+
return wire.NotFound(uri)
|
|
209
|
+
case errors.Is(err, fs.ErrPermission):
|
|
210
|
+
return wire.Coded(wire.CodePermissionDenied, "Permission denied: "+uri)
|
|
211
|
+
case errors.Is(err, syscall.EINVAL), errors.Is(err, syscall.ENOTDIR):
|
|
212
|
+
return wire.InvalidParams("Cannot watch " + uri)
|
|
213
|
+
}
|
|
214
|
+
return wire.Coded(wire.CodeInternalError, "Cannot watch "+uri+": "+err.Error())
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
func newUUID() string {
|
|
218
|
+
var b [16]byte
|
|
219
|
+
_, _ = rand.Read(b[:])
|
|
220
|
+
b[6] = b[6]&0x0f | 0x40
|
|
221
|
+
b[8] = b[8]&0x3f | 0x80
|
|
222
|
+
h := hex.EncodeToString(b[:])
|
|
223
|
+
return h[0:8] + "-" + h[8:12] + "-" + h[12:16] + "-" + h[16:20] + "-" + h[20:]
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Create implements host.ResourceWatchHandler.
|
|
227
|
+
func (s *WatchService) Create(_ context.Context, params ahptypes.CreateResourceWatchParams) (ahptypes.CreateResourceWatchResult, error) {
|
|
228
|
+
s.mu.Lock()
|
|
229
|
+
if s.disposed {
|
|
230
|
+
s.mu.Unlock()
|
|
231
|
+
return ahptypes.CreateResourceWatchResult{}, errDisposed
|
|
232
|
+
}
|
|
233
|
+
s.pending.Add(1)
|
|
234
|
+
s.mu.Unlock()
|
|
235
|
+
defer s.pending.Done()
|
|
236
|
+
return s.create(params)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
func (s *WatchService) isDisposed() bool {
|
|
240
|
+
s.mu.Lock()
|
|
241
|
+
defer s.mu.Unlock()
|
|
242
|
+
return s.disposed
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
func (s *WatchService) create(params ahptypes.CreateResourceWatchParams) (ahptypes.CreateResourceWatchResult, error) {
|
|
246
|
+
var zero ahptypes.CreateResourceWatchResult
|
|
247
|
+
recursive := params.Recursive != nil && *params.Recursive
|
|
248
|
+
excludes, err := readPatterns(params.Excludes, "excludes")
|
|
249
|
+
if err != nil {
|
|
250
|
+
return zero, err
|
|
251
|
+
}
|
|
252
|
+
includes, err := readPatterns(params.Includes, "includes")
|
|
253
|
+
if err != nil {
|
|
254
|
+
return zero, err
|
|
255
|
+
}
|
|
256
|
+
resourceRoot, err := s.paths.PathFor(params.Uri)
|
|
257
|
+
if err != nil {
|
|
258
|
+
return zero, err
|
|
259
|
+
}
|
|
260
|
+
if s.opts.afterPath != nil {
|
|
261
|
+
s.opts.afterPath()
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
info, err := os.Stat(resourceRoot)
|
|
265
|
+
if err != nil {
|
|
266
|
+
return zero, watchError(err, params.Uri)
|
|
267
|
+
}
|
|
268
|
+
isDir := info.IsDir()
|
|
269
|
+
if !isDir && recursive {
|
|
270
|
+
return zero, wire.InvalidParams("Cannot watch a file recursively: " + params.Uri)
|
|
271
|
+
}
|
|
272
|
+
watchedRoot, err := filepath.EvalSymlinks(resourceRoot)
|
|
273
|
+
if err != nil {
|
|
274
|
+
return zero, watchError(err, params.Uri)
|
|
275
|
+
}
|
|
276
|
+
if s.isDisposed() {
|
|
277
|
+
return zero, errDisposed
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
active := &activeWatch{
|
|
281
|
+
channel: wire.ResourceWatchScheme + "/" + newUUID(), resourceRoot: resourceRoot, watchedRoot: watchedRoot,
|
|
282
|
+
isDir: isDir, recursive: recursive, excludes: excludes, includes: includes,
|
|
283
|
+
changes: map[string]ahptypes.ResourceChangeType{}, stopPoll: make(chan struct{}), pollDone: make(chan struct{}),
|
|
284
|
+
}
|
|
285
|
+
active.snapshot = scanTree(active, nil)
|
|
286
|
+
if s.opts.afterReady != nil {
|
|
287
|
+
s.opts.afterReady()
|
|
288
|
+
}
|
|
289
|
+
if s.isDisposed() {
|
|
290
|
+
s.stopPoller(active)
|
|
291
|
+
return zero, errDisposed
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
watchState := ahptypes.ResourceWatchState{Root: params.Uri, Recursive: recursive}
|
|
295
|
+
if len(excludes) > 0 {
|
|
296
|
+
raw, _ := json.Marshal(map[string][]string{"items": excludes})
|
|
297
|
+
message := json.RawMessage(raw)
|
|
298
|
+
watchState.Excludes = &message
|
|
299
|
+
}
|
|
300
|
+
if len(includes) > 0 {
|
|
301
|
+
raw, _ := json.Marshal(map[string][]string{"items": includes})
|
|
302
|
+
message := json.RawMessage(raw)
|
|
303
|
+
watchState.Includes = &message
|
|
304
|
+
}
|
|
305
|
+
if err := s.host.Store().Create(active.channel, &watchState); err != nil {
|
|
306
|
+
s.stopPoller(active)
|
|
307
|
+
return zero, err
|
|
308
|
+
}
|
|
309
|
+
s.mu.Lock()
|
|
310
|
+
if s.disposed {
|
|
311
|
+
s.mu.Unlock()
|
|
312
|
+
s.stopPoller(active)
|
|
313
|
+
s.host.DeleteChannel(active.channel)
|
|
314
|
+
return zero, errDisposed
|
|
315
|
+
}
|
|
316
|
+
s.watches[active.channel] = active
|
|
317
|
+
s.startGraceLocked(active)
|
|
318
|
+
s.mu.Unlock()
|
|
319
|
+
go s.poll(active)
|
|
320
|
+
return ahptypes.CreateResourceWatchResult{Channel: active.channel}, nil
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
func (s *WatchService) stopPoller(active *activeWatch) {
|
|
324
|
+
select {
|
|
325
|
+
case <-active.stopPoll:
|
|
326
|
+
default:
|
|
327
|
+
close(active.stopPoll)
|
|
328
|
+
}
|
|
329
|
+
if s.opts.onClosed != nil {
|
|
330
|
+
s.opts.onClosed()
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// ── polling ─────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
type watchEntry struct {
|
|
337
|
+
dir bool
|
|
338
|
+
sig string
|
|
339
|
+
hash string // content digest kept for the next scan while the file is "racy", see racyWindow
|
|
340
|
+
cmp string // digest taken by this scan, for comparison with the previous scan's hash only
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// racyWindow is git's "racy timestamp" problem: the kernel stamps a file with a coarse clock (a
|
|
344
|
+
// few milliseconds, whole seconds on some file systems), so a rewrite of equal size right after a
|
|
345
|
+
// scan leaves the stat signature unchanged. A file modified within this window of the scan is
|
|
346
|
+
// therefore also compared by content on the next scan. Larger files are compared by stat only.
|
|
347
|
+
const (
|
|
348
|
+
racyWindow = 2 * time.Second
|
|
349
|
+
racyMaxSize = 1 << 20
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
func contentHash(path string) string {
|
|
353
|
+
f, err := os.Open(path)
|
|
354
|
+
if err != nil {
|
|
355
|
+
return ""
|
|
356
|
+
}
|
|
357
|
+
defer f.Close()
|
|
358
|
+
h := sha256.New()
|
|
359
|
+
if _, err := io.Copy(h, io.LimitReader(f, racyMaxSize+1)); err != nil {
|
|
360
|
+
return ""
|
|
361
|
+
}
|
|
362
|
+
return hex.EncodeToString(h.Sum(nil))
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// scanTree lists the watched scope: the root itself, and for a directory its children (all
|
|
366
|
+
// descendants when recursive). Excluded paths are pruned and symlinks are entries, never followed.
|
|
367
|
+
func scanTree(a *activeWatch, previous map[string]watchEntry) map[string]watchEntry {
|
|
368
|
+
scanned := time.Now()
|
|
369
|
+
out := map[string]watchEntry{}
|
|
370
|
+
info, err := os.Lstat(a.watchedRoot)
|
|
371
|
+
if err != nil {
|
|
372
|
+
return out
|
|
373
|
+
}
|
|
374
|
+
out[""] = entryOf(info, a.watchedRoot, "", previous, scanned)
|
|
375
|
+
if !info.IsDir() {
|
|
376
|
+
return out
|
|
377
|
+
}
|
|
378
|
+
var walk func(dir, rel string, depth int)
|
|
379
|
+
walk = func(dir, rel string, depth int) {
|
|
380
|
+
children, err := os.ReadDir(dir)
|
|
381
|
+
if err != nil {
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
for _, child := range children {
|
|
385
|
+
childRel := child.Name()
|
|
386
|
+
if rel != "" {
|
|
387
|
+
childRel = rel + "/" + child.Name()
|
|
388
|
+
}
|
|
389
|
+
if IsExcluded(childRel, a.excludes) {
|
|
390
|
+
continue
|
|
391
|
+
}
|
|
392
|
+
childInfo, err := child.Info()
|
|
393
|
+
if err != nil {
|
|
394
|
+
continue
|
|
395
|
+
}
|
|
396
|
+
out[childRel] = entryOf(childInfo, filepath.Join(dir, child.Name()), childRel, previous, scanned)
|
|
397
|
+
if childInfo.IsDir() && a.recursive {
|
|
398
|
+
walk(filepath.Join(dir, child.Name()), childRel, depth+1)
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
walk(a.watchedRoot, "", 1)
|
|
403
|
+
return out
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
func entryOf(info os.FileInfo, path, rel string, previous map[string]watchEntry, scanned time.Time) watchEntry {
|
|
407
|
+
if info.IsDir() {
|
|
408
|
+
return watchEntry{dir: true}
|
|
409
|
+
}
|
|
410
|
+
entry := watchEntry{sig: etagOf(info)}
|
|
411
|
+
if !info.Mode().IsRegular() || info.Size() > racyMaxSize {
|
|
412
|
+
return entry
|
|
413
|
+
}
|
|
414
|
+
old, seen := previous[rel]
|
|
415
|
+
recent := scanned.Sub(info.ModTime()) < racyWindow
|
|
416
|
+
// A previously hashed file with an unchanged stat is hashed once more: that scan is the one
|
|
417
|
+
// that shows whether the equal-looking stat hid a rewrite.
|
|
418
|
+
if recent || (seen && old.hash != "" && old.sig == entry.sig) {
|
|
419
|
+
entry.cmp = contentHash(path)
|
|
420
|
+
if recent {
|
|
421
|
+
entry.hash = entry.cmp
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return entry
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// changed reports whether a file entry differs from its earlier scan.
|
|
428
|
+
func (e watchEntry) changed(old watchEntry) bool {
|
|
429
|
+
if e.sig != old.sig {
|
|
430
|
+
return true
|
|
431
|
+
}
|
|
432
|
+
return e.cmp != "" && old.hash != "" && e.cmp != old.hash
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
func (s *WatchService) poll(a *activeWatch) {
|
|
436
|
+
defer close(a.pollDone)
|
|
437
|
+
ticker := time.NewTicker(s.pollInterval())
|
|
438
|
+
defer ticker.Stop()
|
|
439
|
+
for {
|
|
440
|
+
select {
|
|
441
|
+
case <-a.stopPoll:
|
|
442
|
+
return
|
|
443
|
+
case <-ticker.C:
|
|
444
|
+
}
|
|
445
|
+
previous := a.snapshot
|
|
446
|
+
next := scanTree(a, previous)
|
|
447
|
+
a.snapshot = next
|
|
448
|
+
for rel, entry := range next {
|
|
449
|
+
old, existed := previous[rel]
|
|
450
|
+
switch {
|
|
451
|
+
case !existed:
|
|
452
|
+
s.record(a, rel, ahptypes.ResourceChangeTypeAdded)
|
|
453
|
+
case old.dir != entry.dir:
|
|
454
|
+
s.record(a, rel, ahptypes.ResourceChangeTypeDeleted)
|
|
455
|
+
s.record(a, rel, ahptypes.ResourceChangeTypeAdded)
|
|
456
|
+
case !entry.dir && entry.changed(old):
|
|
457
|
+
s.record(a, rel, ahptypes.ResourceChangeTypeUpdated)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
for rel := range previous {
|
|
461
|
+
if _, still := next[rel]; !still {
|
|
462
|
+
s.record(a, rel, ahptypes.ResourceChangeTypeDeleted)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
func (s *WatchService) record(a *activeWatch, rel string, change ahptypes.ResourceChangeType) {
|
|
469
|
+
if !MatchesPatterns(rel, a.includes, a.excludes) {
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
path := a.resourceRoot
|
|
473
|
+
if rel != "" {
|
|
474
|
+
path = filepath.Join(a.resourceRoot, filepath.FromSlash(rel))
|
|
475
|
+
}
|
|
476
|
+
s.mu.Lock()
|
|
477
|
+
defer s.mu.Unlock()
|
|
478
|
+
if a.released {
|
|
479
|
+
return
|
|
480
|
+
}
|
|
481
|
+
merged := MergeChange(a.changes[path], change)
|
|
482
|
+
if merged == "" {
|
|
483
|
+
delete(a.changes, path)
|
|
484
|
+
} else {
|
|
485
|
+
a.changes[path] = merged
|
|
486
|
+
}
|
|
487
|
+
if len(a.changes) == 0 {
|
|
488
|
+
if a.flushTimer != nil {
|
|
489
|
+
a.flushTimer.Stop()
|
|
490
|
+
a.flushTimer = nil
|
|
491
|
+
}
|
|
492
|
+
return
|
|
493
|
+
}
|
|
494
|
+
if a.flushTimer != nil {
|
|
495
|
+
return
|
|
496
|
+
}
|
|
497
|
+
a.flushTimer = s.clock.AfterFunc(s.debounce(), func() {
|
|
498
|
+
s.mu.Lock()
|
|
499
|
+
a.flushTimer = nil
|
|
500
|
+
s.mu.Unlock()
|
|
501
|
+
s.flush(a)
|
|
502
|
+
})
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
func (s *WatchService) flush(a *activeWatch) {
|
|
506
|
+
s.mu.Lock()
|
|
507
|
+
if a.released || s.watches[a.channel] != a {
|
|
508
|
+
a.changes = map[string]ahptypes.ResourceChangeType{}
|
|
509
|
+
s.mu.Unlock()
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
paths := make([]string, 0, len(a.changes))
|
|
513
|
+
for path := range a.changes {
|
|
514
|
+
paths = append(paths, path)
|
|
515
|
+
}
|
|
516
|
+
sort.Slice(paths, func(i, j int) bool { return localeLess(paths[i], paths[j]) })
|
|
517
|
+
items := make([]ahptypes.ResourceChange, 0, len(paths))
|
|
518
|
+
for _, path := range paths {
|
|
519
|
+
items = append(items, ahptypes.ResourceChange{Uri: wire.PathToFileURI(path), Type: a.changes[path]})
|
|
520
|
+
}
|
|
521
|
+
a.changes = map[string]ahptypes.ResourceChangeType{}
|
|
522
|
+
s.mu.Unlock()
|
|
523
|
+
if len(items) == 0 {
|
|
524
|
+
return
|
|
525
|
+
}
|
|
526
|
+
raw, _ := json.Marshal(map[string]any{"items": items})
|
|
527
|
+
s.host.DispatchServerAction(a.channel, ahptypes.StateAction{Value: &ahptypes.ResourceWatchChangedAction{
|
|
528
|
+
Type: ahptypes.ActionTypeResourceWatchChanged, Changes: raw,
|
|
529
|
+
}})
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ── lifetime ────────────────────────────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
func (s *WatchService) onSubscriberCount(channel string, count int) {
|
|
535
|
+
s.mu.Lock()
|
|
536
|
+
defer s.mu.Unlock()
|
|
537
|
+
a := s.watches[channel]
|
|
538
|
+
if a == nil {
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
if count > 0 {
|
|
542
|
+
if a.graceTimer != nil {
|
|
543
|
+
a.graceTimer.Stop()
|
|
544
|
+
a.graceTimer = nil
|
|
545
|
+
}
|
|
546
|
+
return
|
|
547
|
+
}
|
|
548
|
+
s.startGraceLocked(a)
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
func (s *WatchService) startGraceLocked(a *activeWatch) {
|
|
552
|
+
if a.graceTimer != nil {
|
|
553
|
+
return
|
|
554
|
+
}
|
|
555
|
+
var timer Stopper
|
|
556
|
+
timer = s.clock.AfterFunc(s.grace(), func() {
|
|
557
|
+
s.mu.Lock()
|
|
558
|
+
if a.graceTimer == timer {
|
|
559
|
+
a.graceTimer = nil
|
|
560
|
+
}
|
|
561
|
+
s.mu.Unlock()
|
|
562
|
+
if s.host.SubscriberCount(a.channel) == 0 {
|
|
563
|
+
s.log("releasing unwatched %s", a.channel)
|
|
564
|
+
s.release(a.channel)
|
|
565
|
+
}
|
|
566
|
+
})
|
|
567
|
+
a.graceTimer = timer
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
func (s *WatchService) release(channel string) {
|
|
571
|
+
s.mu.Lock()
|
|
572
|
+
a := s.watches[channel]
|
|
573
|
+
if a == nil {
|
|
574
|
+
s.mu.Unlock()
|
|
575
|
+
return
|
|
576
|
+
}
|
|
577
|
+
delete(s.watches, channel)
|
|
578
|
+
a.released = true
|
|
579
|
+
if a.flushTimer != nil {
|
|
580
|
+
a.flushTimer.Stop()
|
|
581
|
+
}
|
|
582
|
+
if a.graceTimer != nil {
|
|
583
|
+
a.graceTimer.Stop()
|
|
584
|
+
}
|
|
585
|
+
a.changes = map[string]ahptypes.ResourceChangeType{}
|
|
586
|
+
s.mu.Unlock()
|
|
587
|
+
s.host.DeleteChannel(channel)
|
|
588
|
+
s.stopPoller(a)
|
|
589
|
+
<-a.pollDone
|
|
590
|
+
}
|