@frehilm/ordna-core 0.1.4 → 0.2.1
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/README.md +34 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +41 -1
- package/dist/config.js.map +1 -1
- package/dist/git.d.ts +10 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +14 -27
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/schema.d.ts +18 -1
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js.map +1 -1
- package/dist/storage/auto-detect.d.ts +82 -0
- package/dist/storage/auto-detect.d.ts.map +1 -0
- package/dist/storage/auto-detect.js +158 -0
- package/dist/storage/auto-detect.js.map +1 -0
- package/dist/storage/auto-push.d.ts +36 -0
- package/dist/storage/auto-push.d.ts.map +1 -0
- package/dist/storage/auto-push.js +100 -0
- package/dist/storage/auto-push.js.map +1 -0
- package/dist/storage/backend.d.ts +86 -0
- package/dist/storage/backend.d.ts.map +1 -0
- package/dist/storage/backend.js +14 -0
- package/dist/storage/backend.js.map +1 -0
- package/dist/storage/backends/file.d.ts +31 -0
- package/dist/storage/backends/file.d.ts.map +1 -0
- package/dist/storage/backends/file.js +213 -0
- package/dist/storage/backends/file.js.map +1 -0
- package/dist/storage/backends/hybrid.d.ts +39 -0
- package/dist/storage/backends/hybrid.d.ts.map +1 -0
- package/dist/storage/backends/hybrid.js +270 -0
- package/dist/storage/backends/hybrid.js.map +1 -0
- package/dist/storage/backends/namespace.d.ts +55 -0
- package/dist/storage/backends/namespace.d.ts.map +1 -0
- package/dist/storage/backends/namespace.js +907 -0
- package/dist/storage/backends/namespace.js.map +1 -0
- package/dist/storage/file-io.d.ts +38 -0
- package/dist/storage/file-io.d.ts.map +1 -0
- package/dist/storage/file-io.js +69 -0
- package/dist/storage/file-io.js.map +1 -0
- package/dist/storage/git-ref.d.ts +77 -0
- package/dist/storage/git-ref.d.ts.map +1 -0
- package/dist/storage/git-ref.js +184 -0
- package/dist/storage/git-ref.js.map +1 -0
- package/dist/storage/markdown.d.ts +17 -0
- package/dist/storage/markdown.d.ts.map +1 -0
- package/dist/storage/markdown.js +17 -0
- package/dist/storage/markdown.js.map +1 -0
- package/dist/storage/sync-ref.d.ts +82 -0
- package/dist/storage/sync-ref.d.ts.map +1 -0
- package/dist/storage/sync-ref.js +191 -0
- package/dist/storage/sync-ref.js.map +1 -0
- package/dist/store.d.ts +56 -8
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +93 -115
- package/dist/store.js.map +1 -1
- package/dist/watcher.d.ts +33 -1
- package/dist/watcher.d.ts.map +1 -1
- package/dist/watcher.js +12 -30
- package/dist/watcher.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { formatId } from "../ids.js";
|
|
2
|
+
const DEFAULT_REF = "refs/ordna/state";
|
|
3
|
+
const EMPTY_OID = ""; // CAS sentinel for "must not exist"
|
|
4
|
+
const EMPTY_STATE = { next_id: 1, ops: [] };
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a single git ref carrying a JSON `SyncState` blob.
|
|
7
|
+
*
|
|
8
|
+
* The CAS pattern: every write reads the current blob, computes the
|
|
9
|
+
* new state, hashes the new blob, and `update-ref`s with the old OID
|
|
10
|
+
* as the expected-old value. If the ref has moved underneath us
|
|
11
|
+
* (another writer slipped in), the CAS fails non-fast-forward and we
|
|
12
|
+
* fetch + retry once. Beyond that, we surface a clear "diverged"
|
|
13
|
+
* error so the user can intervene.
|
|
14
|
+
*
|
|
15
|
+
* Reads are cached in memory until invalidated (after a CAS conflict)
|
|
16
|
+
* or after a successful write (cache is updated to the new state).
|
|
17
|
+
*/
|
|
18
|
+
export class SyncRef {
|
|
19
|
+
git;
|
|
20
|
+
refname;
|
|
21
|
+
#cached = null;
|
|
22
|
+
constructor(git, refname = DEFAULT_REF) {
|
|
23
|
+
this.git = git;
|
|
24
|
+
this.refname = refname;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read the current state. Returns an empty state if the ref
|
|
28
|
+
* doesn't yet exist. Hits the cache when available.
|
|
29
|
+
*/
|
|
30
|
+
async read() {
|
|
31
|
+
if (this.#cached)
|
|
32
|
+
return this.#cached.state;
|
|
33
|
+
const fresh = await this.#readUncached();
|
|
34
|
+
this.#cached = fresh;
|
|
35
|
+
return fresh.state;
|
|
36
|
+
}
|
|
37
|
+
/** Drop the cache. Next `read()` hits the ref again. */
|
|
38
|
+
invalidate() {
|
|
39
|
+
this.#cached = null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Seed the ref with `initial` only if it doesn't yet exist. Used by
|
|
43
|
+
* namespace mode to migrate pre-state-ref repos: scan existing
|
|
44
|
+
* `refs/ordna/tasks/*` for the max id, then call this with
|
|
45
|
+
* `{ next_id: max + 1, ops: [] }`. Safe to call concurrently — if
|
|
46
|
+
* another process initialises first, we just adopt its value.
|
|
47
|
+
*/
|
|
48
|
+
async ensureInitialized(initial) {
|
|
49
|
+
const fresh = await this.#readUncached();
|
|
50
|
+
if (fresh.oid !== null) {
|
|
51
|
+
// Already initialised by someone (us in a previous run, or
|
|
52
|
+
// another process racing with us). Adopt their state.
|
|
53
|
+
this.#cached = fresh;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
await this.#writeCAS(null, initial);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
if (isCASConflict(err)) {
|
|
61
|
+
// Lost the bootstrap race; another writer landed first.
|
|
62
|
+
// Re-read to pick up their state.
|
|
63
|
+
this.invalidate();
|
|
64
|
+
await this.read();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async #readUncached() {
|
|
71
|
+
const refs = await this.git.forEachRef(this.refname);
|
|
72
|
+
const match = refs.find((r) => r.refname === this.refname);
|
|
73
|
+
if (!match)
|
|
74
|
+
return { oid: null, state: structuredClone(EMPTY_STATE) };
|
|
75
|
+
try {
|
|
76
|
+
const raw = await this.git.catBlob(match.oid);
|
|
77
|
+
const parsed = JSON.parse(raw);
|
|
78
|
+
// Defensive normalisation: tolerate missing fields in old blobs.
|
|
79
|
+
return {
|
|
80
|
+
oid: match.oid,
|
|
81
|
+
state: {
|
|
82
|
+
next_id: typeof parsed.next_id === "number" ? parsed.next_id : 1,
|
|
83
|
+
ops: Array.isArray(parsed.ops) ? parsed.ops : [],
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// Corrupt blob — fall back to empty state and let the next
|
|
89
|
+
// write overwrite it. Surfacing an error here would block
|
|
90
|
+
// every read; we prefer self-healing.
|
|
91
|
+
return { oid: match.oid, state: structuredClone(EMPTY_STATE) };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Allocate the next id, bump the counter, and persist atomically.
|
|
96
|
+
* Returns the formatted id (e.g. `T-007`) for the caller.
|
|
97
|
+
*
|
|
98
|
+
* On CAS conflict, fetches the ref from origin (if any) and
|
|
99
|
+
* retries once. Further conflicts throw a clear error.
|
|
100
|
+
*/
|
|
101
|
+
async allocateNextId(config) {
|
|
102
|
+
return this.#withRetry(async () => {
|
|
103
|
+
const { oid, state } = await this.#readUncachedAndCache();
|
|
104
|
+
const allocated = formatId(config, state.next_id);
|
|
105
|
+
const nextState = {
|
|
106
|
+
next_id: state.next_id + 1,
|
|
107
|
+
ops: state.ops,
|
|
108
|
+
};
|
|
109
|
+
await this.#writeCAS(oid, nextState);
|
|
110
|
+
return allocated;
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Append a single op to the audit log and persist atomically. */
|
|
114
|
+
async appendOp(op) {
|
|
115
|
+
await this.#withRetry(async () => {
|
|
116
|
+
const { oid, state } = await this.#readUncachedAndCache();
|
|
117
|
+
const nextState = {
|
|
118
|
+
next_id: state.next_id,
|
|
119
|
+
ops: [...state.ops, op],
|
|
120
|
+
};
|
|
121
|
+
await this.#writeCAS(oid, nextState);
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async #readUncachedAndCache() {
|
|
125
|
+
const fresh = await this.#readUncached();
|
|
126
|
+
this.#cached = fresh;
|
|
127
|
+
return fresh;
|
|
128
|
+
}
|
|
129
|
+
async #writeCAS(expectedOid, nextState) {
|
|
130
|
+
const blob = `${JSON.stringify(nextState, null, 2)}\n`;
|
|
131
|
+
const newOid = await this.git.hashObject(blob);
|
|
132
|
+
// CAS: `""` if the ref must not exist; otherwise the captured oid.
|
|
133
|
+
const expectedOld = expectedOid ?? EMPTY_OID;
|
|
134
|
+
await this.git.updateRef(this.refname, newOid, expectedOld);
|
|
135
|
+
// Successful write: update the cache to the new state.
|
|
136
|
+
this.#cached = { oid: newOid, state: nextState };
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Run an operation that does read-then-CAS-write. On CAS conflict,
|
|
140
|
+
* fetch the ref (best-effort if a remote exists) and retry once.
|
|
141
|
+
*/
|
|
142
|
+
async #withRetry(operation) {
|
|
143
|
+
try {
|
|
144
|
+
return await operation();
|
|
145
|
+
}
|
|
146
|
+
catch (err) {
|
|
147
|
+
if (!isCASConflict(err))
|
|
148
|
+
throw err;
|
|
149
|
+
// Fetch from origin if it exists, then retry. If fetch fails
|
|
150
|
+
// (no remote, network down), the retry still re-reads local
|
|
151
|
+
// state via forEachRef so concurrent in-process writers are
|
|
152
|
+
// handled too.
|
|
153
|
+
if (await this.git.hasRemote()) {
|
|
154
|
+
try {
|
|
155
|
+
await this.git.fetchRef(this.refname);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Best-effort; fall through to retry against local state.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.invalidate();
|
|
162
|
+
try {
|
|
163
|
+
return await operation();
|
|
164
|
+
}
|
|
165
|
+
catch (retryErr) {
|
|
166
|
+
if (isCASConflict(retryErr)) {
|
|
167
|
+
throw new Error(`ordna: sync ref ${this.refname} diverged. Run \`git fetch origin '+${this.refname}:${this.refname}'\` to reconcile, then retry.`);
|
|
168
|
+
}
|
|
169
|
+
throw retryErr;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Heuristic: a CAS conflict surfaces as a `git update-ref` failure
|
|
176
|
+
* whose stderr mentions either a missing-ref / wrong-old / non-fast-
|
|
177
|
+
* forward condition. We pattern-match on the thrown message rather
|
|
178
|
+
* than introducing a typed error from `GitRunner` — keeps the runner
|
|
179
|
+
* generic.
|
|
180
|
+
*/
|
|
181
|
+
function isCASConflict(err) {
|
|
182
|
+
if (!(err instanceof Error))
|
|
183
|
+
return false;
|
|
184
|
+
const msg = err.message.toLowerCase();
|
|
185
|
+
return (msg.includes("update-ref") &&
|
|
186
|
+
(msg.includes("cannot lock ref") ||
|
|
187
|
+
msg.includes("is at") ||
|
|
188
|
+
msg.includes("expected") ||
|
|
189
|
+
msg.includes("missing")));
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=sync-ref.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sync-ref.js","sourceRoot":"","sources":["../../src/storage/sync-ref.ts"],"names":[],"mappings":"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"}
|
package/dist/store.d.ts
CHANGED
|
@@ -1,22 +1,70 @@
|
|
|
1
1
|
import { type OrdnaConfig } from "./config.js";
|
|
2
2
|
import type { Task, TaskCreateInput, TaskUpdateInput } from "./schema.js";
|
|
3
|
+
import { type Backend, type FetchResult, type ListOptions, ARCHIVED_STATUS, isKnownStatus } from "./storage/backend.js";
|
|
4
|
+
/**
|
|
5
|
+
* Public context object passed to every store function. The `backend`
|
|
6
|
+
* field carries the storage strategy chosen at `createContext` time;
|
|
7
|
+
* the strategy interface itself is private (not exported from
|
|
8
|
+
* `index.ts`).
|
|
9
|
+
*
|
|
10
|
+
* `backend` is typed as `Backend`, which is unexported, so consumers
|
|
11
|
+
* can read the field but can't declare their own `Backend`-typed
|
|
12
|
+
* variables without importing internals. That's intentional — the
|
|
13
|
+
* field is public for now (future cleanup may hide it behind a
|
|
14
|
+
* method) but the interface stays a TS-internal seam.
|
|
15
|
+
*/
|
|
3
16
|
export interface StoreContext {
|
|
4
17
|
cwd: string;
|
|
5
18
|
config: OrdnaConfig;
|
|
6
19
|
tasksDir: string;
|
|
20
|
+
backend: Backend;
|
|
7
21
|
}
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
22
|
+
export { ARCHIVED_STATUS, isKnownStatus };
|
|
23
|
+
/**
|
|
24
|
+
* @deprecated Use `ListOptions` from the backend layer (not exported).
|
|
25
|
+
* Kept as the public type name for back-compat with
|
|
26
|
+
* 0.1.x consumers. Same shape.
|
|
27
|
+
*/
|
|
28
|
+
export type ListTasksOptions = ListOptions;
|
|
29
|
+
/**
|
|
30
|
+
* Build a context bound to the active working directory.
|
|
31
|
+
*
|
|
32
|
+
* Stays SYNCHRONOUS. The chosen backend is constructed cheaply (no
|
|
33
|
+
* I/O); the first method call on the backend triggers lazy `init()`
|
|
34
|
+
* internally. This is what lets the IDE that embeds core construct
|
|
35
|
+
* contexts without awaiting.
|
|
36
|
+
*
|
|
37
|
+
* Backend selection is driven by `config.storage`. Combinations that
|
|
38
|
+
* make no sense are rejected eagerly so the user sees a clear error
|
|
39
|
+
* up front rather than a confusing failure deep inside a method call.
|
|
40
|
+
*
|
|
41
|
+
* - `storage: hybrid` requires a git repository (`.git/` present at
|
|
42
|
+
* `cwd` or any parent)
|
|
43
|
+
* - `storage: hybrid` + `schema: backlog` is rejected — the audit-
|
|
44
|
+
* log model and Backlog's filename conventions are out of scope
|
|
45
|
+
* for v1 to combine
|
|
46
|
+
* - `storage: namespace` is reserved for T-032; rejected for now
|
|
47
|
+
*/
|
|
15
48
|
export declare function createContext(cwd?: string): StoreContext;
|
|
16
|
-
export declare function listTasks(ctx?: StoreContext, options?:
|
|
49
|
+
export declare function listTasks(ctx?: StoreContext, options?: ListOptions): Promise<Task[]>;
|
|
17
50
|
export declare function getTask(id: string, ctx?: StoreContext): Promise<Task | null>;
|
|
18
51
|
export declare function createTask(input: TaskCreateInput, ctx?: StoreContext): Promise<Task>;
|
|
19
52
|
export declare function updateTask(id: string, patch: TaskUpdateInput, ctx?: StoreContext): Promise<Task>;
|
|
53
|
+
/**
|
|
54
|
+
* Move a task to a new status. The `depends_on` gate stays in core so
|
|
55
|
+
* backends don't have to re-implement Ordna's business rules — the
|
|
56
|
+
* gate fires here, then the actual write is delegated to the backend.
|
|
57
|
+
*/
|
|
20
58
|
export declare function moveTask(id: string, status: string, ctx?: StoreContext): Promise<Task>;
|
|
21
59
|
export declare function deleteTask(id: string, ctx?: StoreContext): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Pull remote updates into the active backend. Only namespace
|
|
62
|
+
* implements this in v1 — see `Backend.fetch` for the rationale.
|
|
63
|
+
*
|
|
64
|
+
* File / hybrid throw a clear error so callers (UI buttons, CLI
|
|
65
|
+
* commands) can show a useful message rather than silently no-op.
|
|
66
|
+
*/
|
|
67
|
+
export declare function fetchTasks(ctx?: StoreContext): Promise<FetchResult>;
|
|
68
|
+
/** True if the active backend exposes a `fetch()` capability. */
|
|
69
|
+
export declare function canFetch(ctx: StoreContext): boolean;
|
|
22
70
|
//# sourceMappingURL=store.d.ts.map
|
package/dist/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,WAAW,EAA+B,MAAM,aAAa,CAAC;AAC5E,OAAO,KAAK,EAAE,IAAI,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1E,OAAO,EACN,KAAK,OAAO,EACZ,KAAK,WAAW,EAChB,KAAK,WAAW,EAChB,eAAe,EACf,aAAa,EACb,MAAM,sBAAsB,CAAC;AAK9B;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,YAAY;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CACjB;AAED,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,CAAC;AAE1C;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAE3C;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,aAAa,CAAC,GAAG,GAAE,MAAsB,GAAG,YAAY,CA4BvE;AAkBD,wBAAsB,SAAS,CAC9B,GAAG,GAAE,YAA8B,EACnC,OAAO,GAAE,WAAgB,GACvB,OAAO,CAAC,IAAI,EAAE,CAAC,CAEjB;AAED,wBAAsB,OAAO,CAC5B,EAAE,EAAE,MAAM,EACV,GAAG,GAAE,YAA8B,GACjC,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAEtB;AAED,wBAAsB,UAAU,CAC/B,KAAK,EAAE,eAAe,EACtB,GAAG,GAAE,YAA8B,GACjC,OAAO,CAAC,IAAI,CAAC,CAEf;AAED,wBAAsB,UAAU,CAC/B,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,eAAe,EACtB,GAAG,GAAE,YAA8B,GACjC,OAAO,CAAC,IAAI,CAAC,CAQf;AAED;;;;GAIG;AACH,wBAAsB,QAAQ,CAC7B,EAAE,EAAE,MAAM,EACV,MAAM,EAAE,MAAM,EACd,GAAG,GAAE,YAA8B,GACjC,OAAO,CAAC,IAAI,CAAC,CAuBf;AAED,wBAAsB,UAAU,CAC/B,EAAE,EAAE,MAAM,EACV,GAAG,GAAE,YAA8B,GACjC,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC/B,GAAG,GAAE,YAA8B,GACjC,OAAO,CAAC,WAAW,CAAC,CAOtB;AAED,iEAAiE;AACjE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAEnD"}
|
package/dist/store.js
CHANGED
|
@@ -1,135 +1,99 @@
|
|
|
1
|
-
import { existsSync
|
|
2
|
-
import {
|
|
3
|
-
import { basename, join } from "node:path";
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
4
3
|
import { loadConfig, resolveTasksDir } from "./config.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
import { ARCHIVED_STATUS, isKnownStatus, } from "./storage/backend.js";
|
|
5
|
+
import { FileBackend } from "./storage/backends/file.js";
|
|
6
|
+
import { HybridBackend } from "./storage/backends/hybrid.js";
|
|
7
|
+
import { NamespaceBackend } from "./storage/backends/namespace.js";
|
|
8
|
+
export { ARCHIVED_STATUS, isKnownStatus };
|
|
9
|
+
/**
|
|
10
|
+
* Build a context bound to the active working directory.
|
|
11
|
+
*
|
|
12
|
+
* Stays SYNCHRONOUS. The chosen backend is constructed cheaply (no
|
|
13
|
+
* I/O); the first method call on the backend triggers lazy `init()`
|
|
14
|
+
* internally. This is what lets the IDE that embeds core construct
|
|
15
|
+
* contexts without awaiting.
|
|
16
|
+
*
|
|
17
|
+
* Backend selection is driven by `config.storage`. Combinations that
|
|
18
|
+
* make no sense are rejected eagerly so the user sees a clear error
|
|
19
|
+
* up front rather than a confusing failure deep inside a method call.
|
|
20
|
+
*
|
|
21
|
+
* - `storage: hybrid` requires a git repository (`.git/` present at
|
|
22
|
+
* `cwd` or any parent)
|
|
23
|
+
* - `storage: hybrid` + `schema: backlog` is rejected — the audit-
|
|
24
|
+
* log model and Backlog's filename conventions are out of scope
|
|
25
|
+
* for v1 to combine
|
|
26
|
+
* - `storage: namespace` is reserved for T-032; rejected for now
|
|
27
|
+
*/
|
|
14
28
|
export function createContext(cwd = process.cwd()) {
|
|
15
29
|
const config = loadConfig({ cwd });
|
|
16
30
|
const tasksDir = resolveTasksDir(config, cwd);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
.
|
|
31
|
+
if (config.storage === "hybrid") {
|
|
32
|
+
assertGitRepo(cwd, "hybrid");
|
|
33
|
+
if (config.schema === "backlog") {
|
|
34
|
+
throw new Error("ordna: `storage: hybrid` is not supported with `schema: backlog` in v1. Use `schema: ordna`, or stay on `storage: file` if you need Backlog.md compatibility.");
|
|
35
|
+
}
|
|
36
|
+
const backend = new HybridBackend(cwd, config, tasksDir);
|
|
37
|
+
return { cwd, config, tasksDir, backend };
|
|
38
|
+
}
|
|
39
|
+
if (config.storage === "namespace") {
|
|
40
|
+
assertGitRepo(cwd, "namespace");
|
|
41
|
+
if (config.schema === "backlog") {
|
|
42
|
+
throw new Error("ordna: `storage: namespace` is not supported with `schema: backlog`. Backlog's filename convention has no analogue in a ref-only store; use `schema: ordna`, or stay on `storage: file` if you need Backlog.md compatibility.");
|
|
43
|
+
}
|
|
44
|
+
const backend = new NamespaceBackend(cwd, config);
|
|
45
|
+
return { cwd, config, tasksDir, backend };
|
|
46
|
+
}
|
|
47
|
+
const backend = new FileBackend(cwd, config, tasksDir);
|
|
48
|
+
return { cwd, config, tasksDir, backend };
|
|
28
49
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
function assertGitRepo(cwd, mode) {
|
|
51
|
+
// Walk up from cwd looking for a `.git/` directory. A bare repo
|
|
52
|
+
// would lack the working tree but still has `.git` as a file
|
|
53
|
+
// pointer or directory; we accept either.
|
|
54
|
+
let dir = cwd;
|
|
55
|
+
for (let i = 0; i < 64; i++) {
|
|
56
|
+
if (existsSync(join(dir, ".git")))
|
|
57
|
+
return;
|
|
58
|
+
const parent = join(dir, "..");
|
|
59
|
+
if (parent === dir)
|
|
60
|
+
break;
|
|
61
|
+
dir = parent;
|
|
35
62
|
}
|
|
36
|
-
|
|
63
|
+
throw new Error(`ordna: \`storage: ${mode}\` requires a git repository. Run \`git init\` in this directory, or switch back to \`storage: file\` in .ordna/config.yaml.`);
|
|
37
64
|
}
|
|
38
65
|
export async function listTasks(ctx = createContext(), options = {}) {
|
|
39
|
-
|
|
40
|
-
return [];
|
|
41
|
-
const entries = readdirSync(ctx.tasksDir, { withFileTypes: true });
|
|
42
|
-
const tasks = [];
|
|
43
|
-
for (const entry of entries) {
|
|
44
|
-
if (!entry.isFile() || !entry.name.endsWith(".md"))
|
|
45
|
-
continue;
|
|
46
|
-
const filePath = join(ctx.tasksDir, entry.name);
|
|
47
|
-
const raw = await readFile(filePath, "utf8");
|
|
48
|
-
try {
|
|
49
|
-
tasks.push(parseTask(raw, filePath));
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
// Skip malformed tasks silently; surfaced via dedicated validator later.
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
let filtered = tasks;
|
|
56
|
-
if (options.status)
|
|
57
|
-
filtered = filtered.filter((t) => t.status === options.status);
|
|
58
|
-
if (options.assignee)
|
|
59
|
-
filtered = filtered.filter((t) => t.assignee === options.assignee);
|
|
60
|
-
if (options.tag)
|
|
61
|
-
filtered = filtered.filter((t) => t.tags.includes(options.tag));
|
|
62
|
-
filtered.sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true }));
|
|
63
|
-
return filtered;
|
|
66
|
+
return ctx.backend.list(options);
|
|
64
67
|
}
|
|
65
68
|
export async function getTask(id, ctx = createContext()) {
|
|
66
|
-
|
|
67
|
-
return tasks.find((t) => t.id === id) ?? null;
|
|
69
|
+
return ctx.backend.get(id);
|
|
68
70
|
}
|
|
69
71
|
export async function createTask(input, ctx = createContext()) {
|
|
70
|
-
|
|
71
|
-
mkdirSync(ctx.tasksDir, { recursive: true });
|
|
72
|
-
const id = nextId(ctx.config, ctx.tasksDir);
|
|
73
|
-
const status = input.status ?? ctx.config.statuses[0];
|
|
74
|
-
if (!status)
|
|
75
|
-
throw new Error("Config has no statuses defined.");
|
|
76
|
-
if (!isKnownStatus(ctx.config, status)) {
|
|
77
|
-
throw new Error(`Status "${status}" is not in configured statuses.`);
|
|
78
|
-
}
|
|
79
|
-
const now = today();
|
|
80
|
-
const task = {
|
|
81
|
-
id,
|
|
82
|
-
title: input.title,
|
|
83
|
-
status,
|
|
84
|
-
assignee: input.assignee ?? null,
|
|
85
|
-
priority: input.priority ?? null,
|
|
86
|
-
tags: input.tags ?? [],
|
|
87
|
-
depends_on: input.depends_on ?? [],
|
|
88
|
-
created_at: now,
|
|
89
|
-
updated_at: now,
|
|
90
|
-
sections: defaultSectionsFor(ctx.config.schema),
|
|
91
|
-
extra_frontmatter: {},
|
|
92
|
-
filePath: "",
|
|
93
|
-
rawContent: "",
|
|
94
|
-
};
|
|
95
|
-
const filename = filenameFor(id, task.title, ctx.config.schema, ctx.config);
|
|
96
|
-
task.filePath = join(ctx.tasksDir, filename);
|
|
97
|
-
const serialized = serializeTask(task, ctx.config.schema);
|
|
98
|
-
task.rawContent = serialized;
|
|
99
|
-
await writeFile(task.filePath, serialized, "utf8");
|
|
100
|
-
return task;
|
|
72
|
+
return ctx.backend.create(input);
|
|
101
73
|
}
|
|
102
74
|
export async function updateTask(id, patch, ctx = createContext()) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
throw new Error(`
|
|
106
|
-
const next = {
|
|
107
|
-
...existing,
|
|
108
|
-
title: patch.title ?? existing.title,
|
|
109
|
-
status: patch.status ?? existing.status,
|
|
110
|
-
assignee: patch.assignee !== undefined ? patch.assignee : existing.assignee,
|
|
111
|
-
priority: patch.priority !== undefined ? patch.priority : existing.priority,
|
|
112
|
-
tags: patch.tags ?? existing.tags,
|
|
113
|
-
depends_on: patch.depends_on ?? existing.depends_on,
|
|
114
|
-
sections: patch.sections ?? existing.sections,
|
|
115
|
-
updated_at: today(),
|
|
116
|
-
};
|
|
117
|
-
if (next.status !== existing.status && !isKnownStatus(ctx.config, next.status)) {
|
|
118
|
-
throw new Error(`Status "${next.status}" is not in configured statuses.`);
|
|
75
|
+
if (patch.status !== undefined &&
|
|
76
|
+
!isKnownStatus(ctx.config, patch.status)) {
|
|
77
|
+
throw new Error(`Status "${patch.status}" is not in configured statuses.`);
|
|
119
78
|
}
|
|
120
|
-
|
|
121
|
-
next.rawContent = serialized;
|
|
122
|
-
await writeFile(existing.filePath, serialized, "utf8");
|
|
123
|
-
return next;
|
|
79
|
+
return ctx.backend.update(id, patch);
|
|
124
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* Move a task to a new status. The `depends_on` gate stays in core so
|
|
83
|
+
* backends don't have to re-implement Ordna's business rules — the
|
|
84
|
+
* gate fires here, then the actual write is delegated to the backend.
|
|
85
|
+
*/
|
|
125
86
|
export async function moveTask(id, status, ctx = createContext()) {
|
|
87
|
+
if (!isKnownStatus(ctx.config, status)) {
|
|
88
|
+
throw new Error(`Status "${status}" is not in configured statuses.`);
|
|
89
|
+
}
|
|
126
90
|
const terminal = ctx.config.statuses[ctx.config.statuses.length - 1];
|
|
127
91
|
if (status === terminal) {
|
|
128
|
-
const task = await
|
|
92
|
+
const task = await ctx.backend.get(id);
|
|
129
93
|
if (!task)
|
|
130
94
|
throw new Error(`Task ${id} not found.`);
|
|
131
95
|
if (task.depends_on.length > 0) {
|
|
132
|
-
const all = await
|
|
96
|
+
const all = await ctx.backend.list();
|
|
133
97
|
const byId = new Map(all.map((t) => [t.id, t]));
|
|
134
98
|
const unfinished = task.depends_on.filter((dep) => {
|
|
135
99
|
const d = byId.get(dep);
|
|
@@ -140,12 +104,26 @@ export async function moveTask(id, status, ctx = createContext()) {
|
|
|
140
104
|
}
|
|
141
105
|
}
|
|
142
106
|
}
|
|
143
|
-
return
|
|
107
|
+
return ctx.backend.update(id, { status });
|
|
144
108
|
}
|
|
145
109
|
export async function deleteTask(id, ctx = createContext()) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
110
|
+
return ctx.backend.delete(id);
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Pull remote updates into the active backend. Only namespace
|
|
114
|
+
* implements this in v1 — see `Backend.fetch` for the rationale.
|
|
115
|
+
*
|
|
116
|
+
* File / hybrid throw a clear error so callers (UI buttons, CLI
|
|
117
|
+
* commands) can show a useful message rather than silently no-op.
|
|
118
|
+
*/
|
|
119
|
+
export async function fetchTasks(ctx = createContext()) {
|
|
120
|
+
if (typeof ctx.backend.fetch !== "function") {
|
|
121
|
+
throw new Error(`storage: ${ctx.backend.kind} doesn't support fetch (namespace only in v1)`);
|
|
122
|
+
}
|
|
123
|
+
return ctx.backend.fetch();
|
|
124
|
+
}
|
|
125
|
+
/** True if the active backend exposes a `fetch()` capability. */
|
|
126
|
+
export function canFetch(ctx) {
|
|
127
|
+
return typeof ctx.backend.fetch === "function";
|
|
150
128
|
}
|
|
151
129
|
//# sourceMappingURL=store.js.map
|
package/dist/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAoB,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE5E,OAAO,EAIN,eAAe,EACf,aAAa,GACb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;AAqBnE,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,CAAC;AAS1C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc,OAAO,CAAC,GAAG,EAAE;IACxD,MAAM,MAAM,GAAG,UAAU,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC;IACnC,MAAM,QAAQ,GAAG,eAAe,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAE9C,IAAI,MAAM,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;QACjC,aAAa,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC7B,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CACd,+JAA+J,CAC/J,CAAC;QACH,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;QACzD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC3C,CAAC;IAED,IAAI,MAAM,CAAC,OAAO,KAAK,WAAW,EAAE,CAAC;QACpC,aAAa,CAAC,GAAG,EAAE,WAAW,CAAC,CAAC;QAChC,IAAI,MAAM,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACjC,MAAM,IAAI,KAAK,CACd,+NAA+N,CAC/N,CAAC;QACH,CAAC;QACD,MAAM,OAAO,GAAG,IAAI,gBAAgB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAClD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC3C,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC;IACvD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAC3C,CAAC;AAED,SAAS,aAAa,CAAC,GAAW,EAAE,IAA4B;IAC/D,gEAAgE;IAChE,6DAA6D;IAC7D,0CAA0C;IAC1C,IAAI,GAAG,GAAG,GAAG,CAAC;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7B,IAAI,UAAU,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAAE,OAAO;QAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC/B,IAAI,MAAM,KAAK,GAAG;YAAE,MAAM;QAC1B,GAAG,GAAG,MAAM,CAAC;IACd,CAAC;IACD,MAAM,IAAI,KAAK,CACd,qBAAqB,IAAI,8HAA8H,CACvJ,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC9B,MAAoB,aAAa,EAAE,EACnC,UAAuB,EAAE;IAEzB,OAAO,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,OAAO,CAC5B,EAAU,EACV,MAAoB,aAAa,EAAE;IAEnC,OAAO,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAC5B,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC/B,KAAsB,EACtB,MAAoB,aAAa,EAAE;IAEnC,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAClC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC/B,EAAU,EACV,KAAsB,EACtB,MAAoB,aAAa,EAAE;IAEnC,IACC,KAAK,CAAC,MAAM,KAAK,SAAS;QAC1B,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,EACvC,CAAC;QACF,MAAM,IAAI,KAAK,CAAC,WAAW,KAAK,CAAC,MAAM,kCAAkC,CAAC,CAAC;IAC5E,CAAC;IACD,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;AACtC,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC7B,EAAU,EACV,MAAc,EACd,MAAoB,aAAa,EAAE;IAEnC,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC;QACxC,MAAM,IAAI,KAAK,CAAC,WAAW,MAAM,kCAAkC,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACrE,IAAI,MAAM,KAAK,QAAQ,EAAE,CAAC;QACzB,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACvC,IAAI,CAAC,IAAI;YAAE,MAAM,IAAI,KAAK,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAC;QACpD,IAAI,IAAI,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAChC,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YACrC,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;YAChD,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE;gBACjD,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBACxB,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,KAAK,QAAQ,CAAC;YACpC,CAAC,CAAC,CAAC;YACH,IAAI,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAC3B,MAAM,IAAI,KAAK,CACd,eAAe,EAAE,OAAO,MAAM,sBAAsB,QAAQ,KAAK,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CACxF,CAAC;YACH,CAAC;QACF,CAAC;IACF,CAAC;IACD,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAC/B,EAAU,EACV,MAAoB,aAAa,EAAE;IAEnC,OAAO,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC/B,MAAoB,aAAa,EAAE;IAEnC,IAAI,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,UAAU,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CACd,YAAY,GAAG,CAAC,OAAO,CAAC,IAAI,+CAA+C,CAC3E,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;AAC5B,CAAC;AAED,iEAAiE;AACjE,MAAM,UAAU,QAAQ,CAAC,GAAiB;IACzC,OAAO,OAAO,GAAG,CAAC,OAAO,CAAC,KAAK,KAAK,UAAU,CAAC;AAChD,CAAC"}
|
package/dist/watcher.d.ts
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { StoreContext } from "./store.js";
|
|
2
2
|
import type { Task } from "./schema.js";
|
|
3
|
+
/**
|
|
4
|
+
* Event emitted by the backend's watcher.
|
|
5
|
+
*
|
|
6
|
+
* - `added` : a new task appeared
|
|
7
|
+
* - `changed` : an existing task's content changed
|
|
8
|
+
* - `removed` : a task was deleted; carries `filePath` for file/hybrid
|
|
9
|
+
* (and the synthetic `ref:<refname>` for namespace)
|
|
10
|
+
* - `renamed` : namespace auto-renumber resolved a push-collision by
|
|
11
|
+
* reallocating a fresh ID; carries both the old and new
|
|
12
|
+
* IDs so the UI can show "previously known as X" and
|
|
13
|
+
* clear any cached views keyed on `oldId`. Only the
|
|
14
|
+
* namespace backend emits this.
|
|
15
|
+
*/
|
|
3
16
|
export type TaskEvent = {
|
|
4
17
|
type: "added";
|
|
5
18
|
task: Task;
|
|
@@ -9,10 +22,29 @@ export type TaskEvent = {
|
|
|
9
22
|
} | {
|
|
10
23
|
type: "removed";
|
|
11
24
|
filePath: string;
|
|
25
|
+
} | {
|
|
26
|
+
type: "renamed";
|
|
27
|
+
oldId: string;
|
|
28
|
+
newId: string;
|
|
29
|
+
task: Task;
|
|
12
30
|
};
|
|
13
31
|
export type TaskEventListener = (event: TaskEvent) => void;
|
|
14
32
|
export interface WatchOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Reserved for future use. Today this is unused — the active
|
|
35
|
+
* backend controls its own watch behaviour.
|
|
36
|
+
*/
|
|
15
37
|
ignoreInitial?: boolean;
|
|
16
38
|
}
|
|
17
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Subscribe to task changes.
|
|
41
|
+
*
|
|
42
|
+
* Delegates to the active backend's `watch` method. The file backend
|
|
43
|
+
* uses chokidar on `<tasksDir>`; future remote backends (hybrid uses
|
|
44
|
+
* the same chokidar pattern; namespace polls `git for-each-ref`)
|
|
45
|
+
* plug their own implementations behind the same listener contract.
|
|
46
|
+
*
|
|
47
|
+
* Returns an unsubscribe function. Call it on shutdown.
|
|
48
|
+
*/
|
|
49
|
+
export declare function watchTasks(ctx: StoreContext, listener: TaskEventListener, _options?: WatchOptions): () => Promise<void>;
|
|
18
50
|
//# sourceMappingURL=watcher.d.ts.map
|
package/dist/watcher.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,SAAS,GAClB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GAC7B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,GAC/B;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,GACrC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,IAAI,CAAA;CAAE,CAAC;AAEjE,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;AAE3D,MAAM,WAAW,YAAY;IAC5B;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CACzB,GAAG,EAAE,YAAY,EACjB,QAAQ,EAAE,iBAAiB,EAC3B,QAAQ,GAAE,YAAiB,GACzB,MAAM,OAAO,CAAC,IAAI,CAAC,CAErB"}
|