Skip to content

Commit b678d87

Browse files
committed
feat: apply repository excludes to webhook sync
1 parent 6e14a16 commit b678d87

13 files changed

Lines changed: 239 additions & 59 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Inspired by [code-review-graph](https://github.com/tirth8205/code-review-graph)
1515
- **Evidence-driven code exploration**: DB-backed retrieval returns small file-level candidates with matched fields, evidence nodes, and optional docs before agents drill into exact graph nodes
1616
- **Browser Wiki UI**: `ccg-server` can serve generated docs, tree search, DB-backed retrieval, Context Tray copying, and an Obsidian-style graph viewer
1717
- **Custom annotations**: `@intent`, `@domainRule`, `@sideEffect`, `@mutates`, `@index` — search code by business context ([details](guide/annotations.md))
18-
- **Webhook sync**: GitHub / Gitea push events → auto clone + build with per-repo branch filtering and `.ccg.yaml` `include_paths` auto-loading ([details](guide/webhook.md))
18+
- **Webhook sync**: GitHub / Gitea push events → auto clone + build with per-repo branch filtering and `.ccg.yaml` build-scope (`include_paths` / `exclude`) auto-loading ([details](guide/webhook.md))
1919
- **Multi-DB**: SQLite (local), PostgreSQL
2020
- **Full-text search**: FTS5 (SQLite), tsvector+GIN (PostgreSQL)
2121

guide/cli-reference.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ docs:
198198
Restricts the build target paths. When set, only paths under the specified directories are parsed.
199199

200200
- CLI: `.ccg.yaml`'s `include_paths` is automatically applied during `ccg build`
201-
- Webhook: After cloning a repo, `.ccg.yaml`'s `include_paths` is auto-loaded to limit build scope
201+
- Webhook: After cloning a repo, `.ccg.yaml`'s `include_paths` and `exclude` are auto-loaded to set build scope
202202
- Incremental build (`ccg update`): `include_paths` filter applied when collecting changed files
203203

204204
```yaml
@@ -207,6 +207,19 @@ include_paths:
207207
- src/shared/
208208
```
209209

210+
### `exclude`
211+
212+
Filters matching files and directories from the build scope. It supports path prefixes, filename globs, full-path globs, and regular expressions.
213+
214+
- CLI and incremental builds apply `exclude` while collecting source files.
215+
- Webhook builds load `exclude` from the cloned repository's root `.ccg.yaml`.
216+
217+
```yaml
218+
exclude:
219+
- vendor/
220+
- "*_generated.go"
221+
```
222+
210223
### Regex Patterns
211224

212225
The `exclude` and `rules` pattern fields support regular expressions. Patterns containing `$`, `^`, `+`, `{}`, `|`, `\.`, `.*` are automatically detected as regex:

guide/ko/cli-reference.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ docs:
178178
빌드 대상 경로를 제한합니다. 설정된 경우 지정된 디렉토리 하위의 경로만 파싱됩니다.
179179

180180
- CLI: `ccg build` 중에 `.ccg.yaml`의 `include_paths`가 자동으로 적용됩니다.
181-
- 웹훅: 저장소를 복제한 후 빌드 범위를 제한하기 위해 `.ccg.yaml`의 `include_paths`를 자동으로 로드합니다.
181+
- 웹훅: 저장소를 복제한 후 빌드 범위를 설정하기 위해 `.ccg.yaml`의 `include_paths`와 `exclude`를 자동으로 로드합니다.
182182
- 증분 빌드(`ccg update`): 변경된 파일을 수집할 때 `include_paths` 필터가 적용됩니다.
183183

184184
```yaml
@@ -187,6 +187,19 @@ include_paths:
187187
- src/shared/
188188
```
189189

190+
### `exclude`
191+
192+
빌드 범위에서 일치하는 파일과 디렉터리를 제외합니다. 경로 접두사, 파일명 glob, 전체 경로 glob, 정규 표현식을 지원합니다.
193+
194+
- CLI와 증분 빌드는 소스 파일 수집 과정에서 `exclude`를 적용합니다.
195+
- 웹훅 빌드는 복제된 저장소 루트의 `.ccg.yaml`에서 `exclude`를 로드합니다.
196+
197+
```yaml
198+
exclude:
199+
- vendor/
200+
- "*_generated.go"
201+
```
202+
190203
### 정규식 패턴 (Regex Patterns)
191204

192205
`exclude` 및 `rules` 패턴 필드는 정규 표현식을 지원합니다. `$`, `^`, `+`, `{}`, `|`, `\.`, `.*`를 포함하는 패턴은 자동으로 정규식으로 감지됩니다.

guide/ko/webhook.md

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -170,28 +170,33 @@ CCG를 Streamable HTTP로 실행하면 exporter가 설정되지 않은 경우에
170170
- 지수적 증가: 1s → 2s → 4s → ... (MaxDelay에서 캡핑)
171171
- 대기 중인 재시도는 컨텍스트 취소(서버 종료) 시 즉시 취소됩니다.
172172
- 패닉(Panic)은 오류로 처리되어 재시도 대상이 됩니다.
173-
- 잘못된 `.ccg.yaml` `include_paths` 설정과 같이 유효하지 않은 저장소 설정은 현재 이벤트에 대해 재시도 불가능한 오류로 처리됩니다.
173+
- 잘못된 `.ccg.yaml` `include_paths` 또는 `exclude`처럼 유효하지 않은 저장소 빌드 범위 설정은 현재 이벤트에 대해 재시도 불가능한 오류로 처리됩니다.
174174
- MaxAttempts를 초과하면 `ERROR` 로그를 남기고 동기화를 포기합니다 (다음 push 이벤트 시 재시도 가능).
175175

176176
이러한 기본값은 `--webhook-attempt-timeout`, `--webhook-retry-attempts`, `--webhook-retry-base-delay`, `--webhook-retry-max-delay`로 조정할 수 있습니다.
177177

178-
## `.ccg.yaml` include_paths 자동 적용
178+
## `.ccg.yaml` 빌드 범위 자동 적용
179179

180-
웹훅 빌드 중에 복제된 저장소 내부의 `.ccg.yaml`에 있는 `include_paths` 설정을 자동으로 읽어 빌드 범위를 제한합니다.
180+
웹훅 빌드 중에 복제된 저장소 루트의 `.ccg.yaml`에서 `include_paths``exclude` 자동으로 읽습니다. `include_paths`빌드 범위를 좁히고, `exclude`는 그 범위에서 일치하는 파일 또는 디렉터리를 제외합니다.
181181

182182
```yaml
183183
# 저장소 내부의 .ccg.yaml
184184
include_paths:
185185
- src/
186186
- lib/
187+
188+
exclude:
189+
- vendor/
190+
- "*_generated.go"
187191
```
188192
189-
- `.ccg.yaml`이 없거나 `include_paths` 키가 없는 경우 전체 디렉토리가 빌드됩니다.
193+
- `.ccg.yaml`이 없으면 추가 제외 없이 전체 디렉토리가 빌드됩니다.
194+
- 두 키는 각각 선택 사항입니다. `include_paths`가 없으면 전체 저장소가 대상이고, `exclude`가 없으면 추가 경로 필터링이 없습니다.
190195
- CLI의 `--config` 플래그와 무관하게 작동합니다 (YAML 직접 파싱).
191196

192197
## 파싱 크기 제한 (Parse Size Limits)
193198

194-
웹훅 요청 본문의 크기는 저장소 파싱과는 별도로 제한됩니다. 웹훅 페이로드는 서버에 의해 제한되지만, 이후의 복제/빌드 단계는 기본적으로 소스 파싱 크기 제한이 없습니다. 기본적으로 CCG는 `include_paths`로 범위를 좁히지 않는 한 복제된 저장소의 모든 일치하는 소스 파일을 빌드합니다.
199+
웹훅 요청 본문의 크기는 저장소 파싱과는 별도로 제한됩니다. 웹훅 페이로드는 서버에 의해 제한되지만, 이후의 복제/빌드 단계는 기본적으로 소스 파싱 크기 제한이 없습니다. 기본적으로 CCG는 `include_paths`로 범위를 좁히거나 `exclude`로 경로를 제외하지 않는 한 복제된 저장소의 모든 일치하는 소스 파일을 빌드합니다.
195200

196201
대규모 저장소에 대해 파싱 예산이 필요한 경우, `--max-file-bytes`, `--max-total-parsed-bytes` 또는 일치하는 `.ccg.yaml` 설정을 명시적으로 구성하십시오. CCG는 기본 웹훅 파싱 제한을 두지 않습니다.
197202

@@ -225,7 +230,7 @@ CCG는 현재 웹훅 운영을 위한 `/metrics` 엔드포인트를 제공하지
225230
`/status`가 degraded 웹훅 동기화를 보고하거나, 큐 대기 시간이 계속 증가하거나, 배포 재시작 중 수락된 이벤트가 중단되었을 수 있을 때는 아래 절차를 사용하십시오.
226231

227232
1. `/status.webhook.recent_repos`와 로그에서 `repo`, `branch`, `last_error`를 확인합니다.
228-
2. 잘못된 `.ccg.yaml` `include_paths`처럼 재시도 불가능한 저장소 설정 오류를 수정합니다.
233+
2. 잘못된 `.ccg.yaml` `include_paths` 또는 `exclude`처럼 재시도 불가능한 저장소 빌드 범위 설정 오류를 수정합니다.
229234
3. upstream provider가 복구를 주도해야 한다면 같은 브랜치에 새 push를 발생시킵니다.
230235
4. 수동 복구가 필요하면 checkout 디렉터리 기준으로 네임스페이스를 업데이트합니다.
231236

guide/webhook.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,33 +190,39 @@ On clone or build failure, automatically retries with exponential backoff:
190190
- Exponential growth: 1s → 2s → 4s → ... (capped at MaxDelay)
191191
- Pending retries are immediately cancelled on context cancellation (server shutdown)
192192
- Panics are treated as errors and are eligible for retry
193-
- Invalid repository config, such as malformed `.ccg.yaml` `include_paths`, is treated as non-retryable for the current event
193+
- Invalid repository build-scope config, such as malformed `.ccg.yaml` `include_paths` or `exclude`, is treated as non-retryable for the current event
194194
- After exceeding MaxAttempts, logs an `ERROR` and abandons the sync (retryable on next push event)
195195

196196
These defaults can be tuned with `--webhook-attempt-timeout`,
197197
`--webhook-retry-attempts`, `--webhook-retry-base-delay`, and
198198
`--webhook-retry-max-delay`.
199199

200-
## `.ccg.yaml` include_paths Auto-Apply
200+
## `.ccg.yaml` Build Scope Auto-Apply
201201

202-
During webhook builds, the `include_paths` setting from `.ccg.yaml` inside the cloned repo is automatically read to restrict build scope.
202+
During webhook builds, CCG reads `include_paths` and `exclude` from `.ccg.yaml` at the cloned repository root. `include_paths` narrows the build scope, and `exclude` removes matching files or directories from that scope.
203203

204204
```yaml
205205
# .ccg.yaml inside the repo
206206
include_paths:
207207
- src/
208208
- lib/
209+
210+
exclude:
211+
- vendor/
212+
- "*_generated.go"
209213
```
210214
211-
- If `.ccg.yaml` is absent or has no `include_paths` key, the entire directory is built
215+
- If `.ccg.yaml` is absent, CCG builds the entire directory without additional exclusions
216+
- Each key is optional: no `include_paths` means the whole repository is eligible, while no `exclude` means no extra path filtering
212217
- Operates independently of the CLI's `--config` flag (direct YAML parsing, no viper)
213218

214219
## Parse Size Limits
215220

216221
Webhook request bodies are limited separately from repository parsing. The
217222
webhook payload is capped by the server, but the subsequent clone/build step has
218223
no default source parse size limit. By default, CCG builds every matching source
219-
file in the cloned repository unless `include_paths` narrows the scope.
224+
file in the cloned repository unless `include_paths` narrows the scope or
225+
`exclude` filters paths out.
220226

221227
If a deployment needs a parse budget for large repositories, configure it
222228
explicitly with `--max-file-bytes`, `--max-total-parsed-bytes`, or the matching

internal/adapters/outbound/configfiles/includes.go

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,32 +7,33 @@ import (
77
"path/filepath"
88

99
"go.yaml.in/yaml/v3"
10+
11+
"github.com/tae2089/code-context-graph/internal/app/reposync"
1012
)
1113

12-
var errIncludePathsConfig = errors.New("invalid include_paths config")
14+
var errBuildScopeConfig = errors.New("invalid repository build scope config")
1315

14-
// IncludePaths reads the repository-local CCG include path configuration.
15-
// @intent adapt established repository config parsing to the reposync application port.
16-
type IncludePaths struct{}
16+
// BuildScope reads repository-local CCG source selection configuration.
17+
// @intent adapt repository include and exclude configuration parsing to the reposync application port.
18+
type BuildScope struct{}
1719

18-
// Load reads include paths from a repository-local .ccg.yaml file.
19-
// @intent own repository configuration file I/O for the reposync adapter while preserving absent-file and parse-error behavior.
20-
func (IncludePaths) Load(repoDir string) ([]string, error) {
20+
// Load reads include paths and exclude patterns from a repository-local .ccg.yaml file.
21+
// @intent own repository build scope configuration I/O for webhook synchronization.
22+
// @return returns an empty scope when the repository has no .ccg.yaml file.
23+
func (BuildScope) Load(repoDir string) (reposync.BuildScope, error) {
2124
data, err := os.ReadFile(filepath.Join(repoDir, ".ccg.yaml"))
2225
if err != nil {
2326
if errors.Is(err, os.ErrNotExist) {
24-
return nil, nil
27+
return reposync.BuildScope{}, nil
2528
}
26-
return nil, err
29+
return reposync.BuildScope{}, err
2730
}
2831
var cfg struct {
29-
IncludePaths []string `yaml:"include_paths"`
32+
IncludePaths []string `yaml:"include_paths"`
33+
ExcludePatterns []string `yaml:"exclude"`
3034
}
3135
if err := yaml.Unmarshal(data, &cfg); err != nil {
32-
return nil, fmt.Errorf("%w: parse .ccg.yaml: %w", errIncludePathsConfig, err)
33-
}
34-
if len(cfg.IncludePaths) == 0 {
35-
return nil, nil
36+
return reposync.BuildScope{}, fmt.Errorf("%w: parse .ccg.yaml: %w", errBuildScopeConfig, err)
3637
}
37-
return cfg.IncludePaths, nil
38+
return reposync.BuildScope{IncludePaths: cfg.IncludePaths, ExcludePatterns: cfg.ExcludePatterns}, nil
3839
}

internal/adapters/outbound/configfiles/includes_test.go

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
"github.com/tae2089/code-context-graph/internal/adapters/outbound/configfiles"
99
)
1010

11-
func TestIncludePathsLoad(t *testing.T) {
11+
func TestBuildScopeLoad_IncludePaths(t *testing.T) {
1212
tests := []struct {
1313
name string
1414
content *string
@@ -30,20 +30,51 @@ func TestIncludePathsLoad(t *testing.T) {
3030
}
3131
}
3232

33-
got, err := (configfiles.IncludePaths{}).Load(dir)
33+
got, err := (configfiles.BuildScope{}).Load(dir)
3434
if (err != nil) != tt.wantErr {
3535
t.Fatalf("Load() error = %v, wantErr %v", err, tt.wantErr)
3636
}
37-
if len(got) != len(tt.want) {
38-
t.Fatalf("Load() = %v, want %v", got, tt.want)
37+
if len(got.IncludePaths) != len(tt.want) {
38+
t.Fatalf("IncludePaths = %v, want %v", got.IncludePaths, tt.want)
3939
}
4040
for i := range tt.want {
41-
if got[i] != tt.want[i] {
42-
t.Fatalf("Load()[%d] = %q, want %q", i, got[i], tt.want[i])
41+
if got.IncludePaths[i] != tt.want[i] {
42+
t.Fatalf("IncludePaths[%d] = %q, want %q", i, got.IncludePaths[i], tt.want[i])
4343
}
4444
}
4545
})
4646
}
4747
}
4848

49+
func TestBuildScopeLoad_ProvidesExcludePatterns(t *testing.T) {
50+
dir := t.TempDir()
51+
content := "include_paths:\n - cmd\n - internal\nexclude:\n - vendor\n - \"*_generated.go\"\n"
52+
if err := os.WriteFile(filepath.Join(dir, ".ccg.yaml"), []byte(content), 0o644); err != nil {
53+
t.Fatal(err)
54+
}
55+
56+
scope, err := (configfiles.BuildScope{}).Load(dir)
57+
if err != nil {
58+
t.Fatalf("Load() error = %v", err)
59+
}
60+
wantIncludes := []string{"cmd", "internal"}
61+
if len(scope.IncludePaths) != len(wantIncludes) {
62+
t.Fatalf("IncludePaths = %v, want %v", scope.IncludePaths, wantIncludes)
63+
}
64+
for i := range wantIncludes {
65+
if scope.IncludePaths[i] != wantIncludes[i] {
66+
t.Fatalf("IncludePaths[%d] = %q, want %q", i, scope.IncludePaths[i], wantIncludes[i])
67+
}
68+
}
69+
wantExcludes := []string{"vendor", "*_generated.go"}
70+
if len(scope.ExcludePatterns) != len(wantExcludes) {
71+
t.Fatalf("ExcludePatterns = %v, want %v", scope.ExcludePatterns, wantExcludes)
72+
}
73+
for i := range wantExcludes {
74+
if scope.ExcludePatterns[i] != wantExcludes[i] {
75+
t.Fatalf("ExcludePatterns[%d] = %q, want %q", i, scope.ExcludePatterns[i], wantExcludes[i])
76+
}
77+
}
78+
}
79+
4980
func stringPtr(value string) *string { return &value }

internal/adapters/outbound/reposyncgraph/updater.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ type Updater struct {
1515
Syncer workflow.IncrementalSyncer
1616
}
1717

18-
// Update preserves replace, size limit, namespace, and unreadable-file semantics.
18+
// Update preserves replace, source scope, size limit, namespace, and unreadable-file semantics.
1919
// @intent replace one synchronized repository namespace using the existing incremental ingest contract.
2020
func (u Updater) Update(ctx context.Context, req reposync.GraphRequest) (reposync.UpdateStats, error) {
21-
stats, err := u.Service.Update(requestctx.WithNamespace(ctx, req.Namespace), workflow.UpdateOptions{BuildOptions: workflow.BuildOptions{Dir: req.RepoDir, IncludePaths: req.IncludePaths, MaxFileBytes: req.MaxFileBytes, MaxTotalParsedBytes: req.MaxTotalParsedBytes}, Syncer: u.Syncer, Replace: true, FailOnUnreadable: req.FailOnUnreadable})
21+
stats, err := u.Service.Update(requestctx.WithNamespace(ctx, req.Namespace), workflow.UpdateOptions{BuildOptions: workflow.BuildOptions{Dir: req.RepoDir, IncludePaths: req.IncludePaths, ExcludePatterns: req.ExcludePatterns, MaxFileBytes: req.MaxFileBytes, MaxTotalParsedBytes: req.MaxTotalParsedBytes}, Syncer: u.Syncer, Replace: true, FailOnUnreadable: req.FailOnUnreadable})
2222
if err != nil {
2323
return reposync.UpdateStats{}, err
2424
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package reposyncgraph_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/tae2089/code-context-graph/internal/adapters/outbound/reposyncgraph"
10+
"github.com/tae2089/code-context-graph/internal/app/ingest"
11+
"github.com/tae2089/code-context-graph/internal/app/ingest/workflow"
12+
"github.com/tae2089/code-context-graph/internal/app/reposync"
13+
"github.com/tae2089/code-context-graph/internal/domain/graph"
14+
)
15+
16+
type recordingSyncer struct {
17+
files map[string]ingest.FileInfo
18+
}
19+
20+
func (s *recordingSyncer) SyncWithExisting(_ context.Context, files map[string]ingest.FileInfo, _ []string) (*ingest.SyncStats, error) {
21+
s.files = make(map[string]ingest.FileInfo, len(files))
22+
for path, file := range files {
23+
s.files[path] = file
24+
}
25+
return &ingest.SyncStats{Added: len(files)}, nil
26+
}
27+
28+
type sourceParser struct{}
29+
30+
func (sourceParser) Parse(string, []byte) ([]graph.Node, []graph.Edge, error) {
31+
return nil, nil, nil
32+
}
33+
34+
func (sourceParser) ParseWithContext(context.Context, string, []byte) ([]graph.Node, []graph.Edge, error) {
35+
return nil, nil, nil
36+
}
37+
38+
func TestUpdater_ExcludePatternsSkipMatchingFiles(t *testing.T) {
39+
repoDir := t.TempDir()
40+
for name, content := range map[string]string{
41+
"keep.go": "package sample\n",
42+
"skip.gen.go": "package sample\n",
43+
} {
44+
if err := os.WriteFile(filepath.Join(repoDir, name), []byte(content), 0o644); err != nil {
45+
t.Fatalf("write %s: %v", name, err)
46+
}
47+
}
48+
49+
syncer := &recordingSyncer{}
50+
updater := reposyncgraph.Updater{
51+
Service: &workflow.Service{Parsers: map[string]workflow.Parser{".go": sourceParser{}}},
52+
Syncer: syncer,
53+
}
54+
55+
if _, err := updater.Update(context.Background(), reposync.GraphRequest{
56+
RepoDir: repoDir,
57+
Namespace: "api",
58+
ExcludePatterns: []string{"*.gen.go"},
59+
}); err != nil {
60+
t.Fatalf("Update() error = %v", err)
61+
}
62+
if _, ok := syncer.files["keep.go"]; !ok {
63+
t.Fatalf("expected keep.go to be synced, got files=%v", syncer.files)
64+
}
65+
if _, ok := syncer.files["skip.gen.go"]; ok {
66+
t.Fatalf("expected skip.gen.go to be excluded, got files=%v", syncer.files)
67+
}
68+
}

internal/app/reposync/ports.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,25 @@ type Checkout interface {
1212
Sync(ctx context.Context, request CheckoutRequest) (repoDir string, err error)
1313
}
1414

15-
// IncludePaths loads repository-local graph include configuration.
15+
// BuildScope captures the repository-local source selection policy for one webhook sync.
16+
// @intent carry include and exclude configuration together so every webhook update uses one coherent build scope.
17+
type BuildScope struct {
18+
IncludePaths []string
19+
ExcludePatterns []string
20+
}
21+
22+
// BuildScopeLoader loads repository-local graph source selection configuration.
1623
// @intent separate repository config file parsing from repository sync orchestration.
17-
type IncludePaths interface {
18-
Load(repoDir string) ([]string, error)
24+
type BuildScopeLoader interface {
25+
Load(repoDir string) (BuildScope, error)
1926
}
2027

2128
// GraphRequest carries the exact webhook update contract to the ingest adapter.
22-
// @intent preserve namespace, replace limits, include paths, and readability policy across the app boundary.
29+
// @intent preserve namespace, source scope, replace limits, and readability policy across the app boundary.
2330
type GraphRequest struct {
2431
RepoDir, Namespace string
2532
IncludePaths []string
33+
ExcludePatterns []string
2634
MaxFileBytes, MaxTotalParsedBytes int64
2735
FailOnUnreadable bool
2836
}
@@ -40,6 +48,7 @@ type GraphUpdater interface {
4048
// CacheInvalidator drops query results only after a successful graph update.
4149
// @intent keep derived query cache invalidation after successful repository graph commit.
4250
type CacheInvalidator interface{ Invalidate() }
51+
4352
// CacheInvalidatorFunc adapts a runtime closure to the application port.
4453
// @intent adapt runtime cache invalidation closures to the app-owned port.
4554
type CacheInvalidatorFunc func()

0 commit comments

Comments
 (0)