@marianmeres/ownsuite 1.0.2 → 2.0.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/AGENTS.md +122 -16
- package/API.md +79 -18
- package/README.md +21 -2
- package/dist/adapters/mock.d.ts +10 -3
- package/dist/adapters/mock.js +79 -25
- package/dist/domains/base.d.ts +66 -10
- package/dist/domains/base.js +165 -13
- package/dist/domains/owned-collection.d.ts +29 -4
- package/dist/domains/owned-collection.js +240 -120
- package/dist/ownsuite.d.ts +37 -5
- package/dist/ownsuite.js +92 -10
- package/dist/types/adapter.d.ts +4 -0
- package/dist/types/state.d.ts +10 -0
- package/docs/future-improvements.md +81 -0
- package/package.json +15 -6
package/dist/types/state.d.ts
CHANGED
|
@@ -34,10 +34,20 @@ export interface DomainStateWrapper<T> {
|
|
|
34
34
|
* its own `ownerId` — the server resolves it from the authenticated subject
|
|
35
35
|
* via the `/me/*` mount. The context is still provided so adapters can pass
|
|
36
36
|
* arbitrary host-app data (correlation ids, feature flags, etc.) through.
|
|
37
|
+
*
|
|
38
|
+
* The manager also injects a per-operation `signal` into `ctx` for every
|
|
39
|
+
* adapter call. Adapters that care about cancellation should forward it
|
|
40
|
+
* to `fetch()`; adapters that don't can ignore it.
|
|
37
41
|
*/
|
|
38
42
|
export interface OwnsuiteContext {
|
|
39
43
|
/** Hint — not used for authorization. The server is authoritative. */
|
|
40
44
|
subjectId?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Per-operation abort signal injected by the manager. Adapters should
|
|
47
|
+
* forward this to `fetch(url, { signal: ctx.signal })`. Aborts fire on
|
|
48
|
+
* `reset()`, `destroy()`, and when a newer read supersedes an older one.
|
|
49
|
+
*/
|
|
50
|
+
signal?: AbortSignal;
|
|
41
51
|
/** Additional context properties for adapter-specific needs. */
|
|
42
52
|
[key: string]: unknown;
|
|
43
53
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Ownsuite — Future Improvements
|
|
2
|
+
|
|
3
|
+
A design review of the current client-side data-flow model, with gaps to consider as usage grows. Not a commitment — a backlog for discussion.
|
|
4
|
+
|
|
5
|
+
## Context
|
|
6
|
+
|
|
7
|
+
Ownsuite is a thin **owner-scoped CRUD store**, not a query cache. Conceptually closer to Redux-Toolkit `createSlice` + thunks than to TanStack Query / SWR. The current design is honest and sufficient when consumers only need "the current user's Xs." The items below matter once UIs need multiple filtered views, pagination, or finer-grained pending/error signals.
|
|
8
|
+
|
|
9
|
+
## Where the current design aligns with best practice
|
|
10
|
+
|
|
11
|
+
- Store-per-domain FSM (`initializing → ready ↔ syncing → error`) exposed as a Svelte store — matches the idle/loading/success/error mental model.
|
|
12
|
+
- Optimistic update + per-row rollback on `update`/`delete`; no optimistic `create` (correct — no client-assigned id).
|
|
13
|
+
- Server as source of truth; no `localStorage` for owner-scoped data (avoids cross-user cache poisoning).
|
|
14
|
+
- 404-not-403 contract on ownership mismatches (avoids existence leaks).
|
|
15
|
+
- Transport-agnostic adapter boundary — easy to mock and test.
|
|
16
|
+
- Typed pubsub event bus for cross-domain coordination.
|
|
17
|
+
- **Mutation serialization + abort-supersede reads** (2.0) — eliminates the "stale-snapshot" class of races.
|
|
18
|
+
- **AbortSignal plumbing** through `ctx.signal` (2.0) — adapters opt into cancellation.
|
|
19
|
+
- **Explicit `destroy()` lifecycle** (2.0) — aborts in-flight work, clears subscribers.
|
|
20
|
+
- **`getOne()` no longer trips domain error** (2.0) — read misses don't invalidate the list view.
|
|
21
|
+
|
|
22
|
+
## Remaining gaps
|
|
23
|
+
|
|
24
|
+
### 1. Single list slot per domain — no query-keyed cache
|
|
25
|
+
|
|
26
|
+
`list(query)` stores one `rows` array per domain. Calling `list({ status: "open" })` then `list({ status: "closed" })` overwrites the same slot — you cannot hold two filtered views concurrently. The domain is effectively a singleton list, not a query cache.
|
|
27
|
+
|
|
28
|
+
**Biggest conceptual limit.** Everything else below is additive; this one is structural. Addressing it means moving from `state.data` to `state.queries: Map<queryKey, { rows, meta, lastSyncedAt }>`.
|
|
29
|
+
|
|
30
|
+
### 2. No staleness / revalidation policy
|
|
31
|
+
|
|
32
|
+
`lastSyncedAt` is tracked but unused. No `staleTime`, no focus/reconnect refetch, no TTL. Consumers orchestrate refresh manually.
|
|
33
|
+
|
|
34
|
+
**Possible direction:** per-domain `staleTime` config; optional focus/reconnect listeners behind a flag.
|
|
35
|
+
|
|
36
|
+
### 3. No per-row pending state during optimistic updates
|
|
37
|
+
|
|
38
|
+
During `syncing`, the list already reflects the optimistic mutation. Subscribers cannot distinguish "confirmed" rows from "pending" ones. TanStack exposes `isPending` per mutation.
|
|
39
|
+
|
|
40
|
+
**Possible direction:** track pending row ids in state; expose as a `Set<rowId>` alongside `rows`.
|
|
41
|
+
|
|
42
|
+
### 4. Error state still couples to data availability
|
|
43
|
+
|
|
44
|
+
One failed `update` puts the whole domain in `error`, even though `data` is still valid. Consumers rendering the list must remember to ignore `state === "error"` when `data` exists. 2.0 narrowed the blast radius (getOne no longer trips this), but the write path still does.
|
|
45
|
+
|
|
46
|
+
**Possible direction:** decouple — keep `state` at `ready`, surface the last error as a sibling signal (`lastError`). Modern pattern (TanStack `isError` + `data` can coexist).
|
|
47
|
+
|
|
48
|
+
### 5. No pagination / infinite-scroll primitives
|
|
49
|
+
|
|
50
|
+
`meta` is opaque `Record<string, unknown>`; cursor/offset merging is left to the consumer. Most real owner-scoped UIs (orders, addresses) eventually need this.
|
|
51
|
+
|
|
52
|
+
**Possible direction:** first-class `loadMore()` on the manager, with a pluggable merge strategy (append vs. replace vs. cursor).
|
|
53
|
+
|
|
54
|
+
### 6. `registerDomain` hard-codes `OwnedCollectionManager`
|
|
55
|
+
|
|
56
|
+
Already called out in `AGENTS.md`. Pluggability requires an API change later — cheaper to design the registration surface for it now than to migrate.
|
|
57
|
+
|
|
58
|
+
## Resolved in 2.0.0
|
|
59
|
+
|
|
60
|
+
- ~~Stale-snapshot race in create/update onSuccess~~ — `onSuccess` now reads the live store.
|
|
61
|
+
- ~~No request deduplication / in-flight cancellation~~ — mutations serialize; reads abort-supersede.
|
|
62
|
+
- ~~Whole-list rollback snapshot~~ — rollback is now per-row.
|
|
63
|
+
- ~~No `AbortSignal` plumbing~~ — adapters receive `ctx.signal`.
|
|
64
|
+
- ~~`getOne` trips whole-domain error state~~ — returns `null`, no state transition.
|
|
65
|
+
- ~~`update(id)` for absent id prepends phantom row~~ — no-op now.
|
|
66
|
+
- ~~`reset()` emits no event~~ — emits `domain:state:changed`.
|
|
67
|
+
- ~~No `destroy()` lifecycle~~ — `Ownsuite.destroy()` + `BaseDomainManager.destroy()` added.
|
|
68
|
+
- ~~Silent boot failures~~ — `suite.hasErrors()` / `suite.errors()` added.
|
|
69
|
+
- ~~`setContext` doesn't invalidate caches~~ — `{ replace, refresh }` options added.
|
|
70
|
+
- ~~`autoInitialize` dead `.catch()` code~~ — removed.
|
|
71
|
+
- ~~`initialize(['typo'])` silently no-ops~~ — logs a warning.
|
|
72
|
+
- ~~Mock adapter allows client-supplied `model_id`~~ — rejects by default.
|
|
73
|
+
- ~~Empty-string row ids accepted~~ — rejected in default `getRowId`.
|
|
74
|
+
|
|
75
|
+
## Prioritization sketch
|
|
76
|
+
|
|
77
|
+
1. **#4 decouple error from state** — small API change, big DX win.
|
|
78
|
+
2. **#3 per-row pending** — needed before any serious list UI.
|
|
79
|
+
3. **#1 query-keyed cache** — structural; defer until a concrete consumer needs two views of the same domain.
|
|
80
|
+
4. **#5 pagination** — defer until a concrete consumer needs it; design alongside #1.
|
|
81
|
+
5. Remaining items — opportunistic.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@marianmeres/ownsuite",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/mod.js",
|
|
6
6
|
"types": "dist/mod.d.ts",
|
|
@@ -10,14 +10,23 @@
|
|
|
10
10
|
"import": "./dist/mod.js"
|
|
11
11
|
}
|
|
12
12
|
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"LICENSE",
|
|
16
|
+
"README.md",
|
|
17
|
+
"API.md",
|
|
18
|
+
"AGENTS.md",
|
|
19
|
+
"CLAUDE.md",
|
|
20
|
+
"docs"
|
|
21
|
+
],
|
|
13
22
|
"author": "Marian Meres",
|
|
14
23
|
"license": "MIT",
|
|
15
24
|
"dependencies": {
|
|
16
|
-
"@marianmeres/clog": "^3.
|
|
17
|
-
"@marianmeres/collection-types": "^1.
|
|
18
|
-
"@marianmeres/http-utils": "^2.
|
|
19
|
-
"@marianmeres/pubsub": "^
|
|
20
|
-
"@marianmeres/store": "^
|
|
25
|
+
"@marianmeres/clog": "^3.17.1",
|
|
26
|
+
"@marianmeres/collection-types": "^1.37.0",
|
|
27
|
+
"@marianmeres/http-utils": "^2.6.0",
|
|
28
|
+
"@marianmeres/pubsub": "^3.0.0",
|
|
29
|
+
"@marianmeres/store": "^3.0.1"
|
|
21
30
|
},
|
|
22
31
|
"repository": {
|
|
23
32
|
"type": "git",
|