@thomasfosterau/pulpit 0.0.0-stage → 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.
Files changed (143) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1272 -2
  3. package/dist/blobs.d.ts +249 -0
  4. package/dist/blobs.d.ts.map +1 -0
  5. package/dist/blobs.js +193 -0
  6. package/dist/blobs.js.map +1 -0
  7. package/dist/budgets.d.ts +255 -0
  8. package/dist/budgets.d.ts.map +1 -0
  9. package/dist/budgets.js +211 -0
  10. package/dist/budgets.js.map +1 -0
  11. package/dist/cache.d.ts +154 -0
  12. package/dist/cache.d.ts.map +1 -0
  13. package/dist/cache.js +118 -0
  14. package/dist/cache.js.map +1 -0
  15. package/dist/collections.d.ts +242 -0
  16. package/dist/collections.d.ts.map +1 -0
  17. package/dist/collections.js +215 -0
  18. package/dist/collections.js.map +1 -0
  19. package/dist/comments.d.ts +212 -0
  20. package/dist/comments.d.ts.map +1 -0
  21. package/dist/comments.js +192 -0
  22. package/dist/comments.js.map +1 -0
  23. package/dist/content.d.ts +231 -0
  24. package/dist/content.d.ts.map +1 -0
  25. package/dist/content.js +204 -0
  26. package/dist/content.js.map +1 -0
  27. package/dist/documents.d.ts +184 -0
  28. package/dist/documents.d.ts.map +1 -0
  29. package/dist/documents.js +190 -0
  30. package/dist/documents.js.map +1 -0
  31. package/dist/edges.d.ts +645 -0
  32. package/dist/edges.d.ts.map +1 -0
  33. package/dist/edges.js +558 -0
  34. package/dist/edges.js.map +1 -0
  35. package/dist/filter.d.ts +461 -0
  36. package/dist/filter.d.ts.map +1 -0
  37. package/dist/filter.js +387 -0
  38. package/dist/filter.js.map +1 -0
  39. package/dist/index.d.ts +133 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +133 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/internal/filter-columns.d.ts +8 -0
  44. package/dist/internal/filter-columns.d.ts.map +1 -0
  45. package/dist/internal/filter-columns.js +43 -0
  46. package/dist/internal/filter-columns.js.map +1 -0
  47. package/dist/internal/list-order.d.ts +26 -0
  48. package/dist/internal/list-order.d.ts.map +1 -0
  49. package/dist/internal/list-order.js +92 -0
  50. package/dist/internal/list-order.js.map +1 -0
  51. package/dist/internal/owned-columns.d.ts +93 -0
  52. package/dist/internal/owned-columns.d.ts.map +1 -0
  53. package/dist/internal/owned-columns.js +98 -0
  54. package/dist/internal/owned-columns.js.map +1 -0
  55. package/dist/internal/plain-rows.d.ts +32 -0
  56. package/dist/internal/plain-rows.d.ts.map +1 -0
  57. package/dist/internal/plain-rows.js +32 -0
  58. package/dist/internal/plain-rows.js.map +1 -0
  59. package/dist/internal/reference-columns.d.ts +80 -0
  60. package/dist/internal/reference-columns.d.ts.map +1 -0
  61. package/dist/internal/reference-columns.js +102 -0
  62. package/dist/internal/reference-columns.js.map +1 -0
  63. package/dist/moderation.d.ts +392 -0
  64. package/dist/moderation.d.ts.map +1 -0
  65. package/dist/moderation.js +375 -0
  66. package/dist/moderation.js.map +1 -0
  67. package/dist/paths.d.ts +204 -0
  68. package/dist/paths.d.ts.map +1 -0
  69. package/dist/paths.js +165 -0
  70. package/dist/paths.js.map +1 -0
  71. package/dist/proposals.d.ts +523 -0
  72. package/dist/proposals.d.ts.map +1 -0
  73. package/dist/proposals.js +505 -0
  74. package/dist/proposals.js.map +1 -0
  75. package/dist/publication.d.ts +148 -0
  76. package/dist/publication.d.ts.map +1 -0
  77. package/dist/publication.js +183 -0
  78. package/dist/publication.js.map +1 -0
  79. package/dist/purge.d.ts +224 -0
  80. package/dist/purge.d.ts.map +1 -0
  81. package/dist/purge.js +151 -0
  82. package/dist/purge.js.map +1 -0
  83. package/dist/reactions.d.ts +246 -0
  84. package/dist/reactions.d.ts.map +1 -0
  85. package/dist/reactions.js +183 -0
  86. package/dist/reactions.js.map +1 -0
  87. package/dist/registry.d.ts +316 -0
  88. package/dist/registry.d.ts.map +1 -0
  89. package/dist/registry.js +305 -0
  90. package/dist/registry.js.map +1 -0
  91. package/dist/search.d.ts +366 -0
  92. package/dist/search.d.ts.map +1 -0
  93. package/dist/search.js +273 -0
  94. package/dist/search.js.map +1 -0
  95. package/dist/social.d.ts +311 -0
  96. package/dist/social.d.ts.map +1 -0
  97. package/dist/social.js +205 -0
  98. package/dist/social.js.map +1 -0
  99. package/dist/subtype.d.ts +1613 -0
  100. package/dist/subtype.d.ts.map +1 -0
  101. package/dist/subtype.js +1300 -0
  102. package/dist/subtype.js.map +1 -0
  103. package/dist/testing.d.ts +675 -0
  104. package/dist/testing.d.ts.map +1 -0
  105. package/dist/testing.js +1022 -0
  106. package/dist/testing.js.map +1 -0
  107. package/dist/version-chain.d.ts +702 -0
  108. package/dist/version-chain.d.ts.map +1 -0
  109. package/dist/version-chain.js +489 -0
  110. package/dist/version-chain.js.map +1 -0
  111. package/dist/working-copy.d.ts +116 -0
  112. package/dist/working-copy.d.ts.map +1 -0
  113. package/dist/working-copy.js +95 -0
  114. package/dist/working-copy.js.map +1 -0
  115. package/package.json +145 -3
  116. package/src/blobs.ts +384 -0
  117. package/src/budgets.ts +338 -0
  118. package/src/cache.ts +218 -0
  119. package/src/collections.ts +427 -0
  120. package/src/comments.ts +392 -0
  121. package/src/content.ts +238 -0
  122. package/src/documents.ts +284 -0
  123. package/src/edges.ts +1185 -0
  124. package/src/filter.ts +523 -0
  125. package/src/index.ts +153 -0
  126. package/src/internal/filter-columns.ts +50 -0
  127. package/src/internal/list-order.ts +117 -0
  128. package/src/internal/owned-columns.ts +140 -0
  129. package/src/internal/plain-rows.ts +54 -0
  130. package/src/internal/reference-columns.ts +156 -0
  131. package/src/moderation.ts +728 -0
  132. package/src/paths.ts +267 -0
  133. package/src/proposals.ts +1014 -0
  134. package/src/publication.ts +225 -0
  135. package/src/purge.ts +313 -0
  136. package/src/reactions.ts +347 -0
  137. package/src/registry.ts +524 -0
  138. package/src/search.ts +540 -0
  139. package/src/social.ts +469 -0
  140. package/src/subtype.ts +3095 -0
  141. package/src/testing.ts +1873 -0
  142. package/src/version-chain.ts +1093 -0
  143. package/src/working-copy.ts +192 -0
package/dist/cache.js ADDED
@@ -0,0 +1,118 @@
1
+ /**
2
+ * **The reference resource cache** (pulpit#205) — `./subtype`'s
3
+ * {@link ResourceCacheSeam}, with a read-through `get`, over a minimal
4
+ * versioned key-value seam ({@link VersionedKv}).
5
+ *
6
+ * The behavioural spec is the four apps' own caches (website's, gardeneria's,
7
+ * preferenceflow's and in-the-dark's `apps/api/src/cache.ts`), which this
8
+ * replaces: one implementation of the contract the kernel already states.
9
+ *
10
+ * - **Compare-on-set** (pulpit#121). Every entry carries the version it
11
+ * speaks for, and the KV seam's one conditional write keeps the highest:
12
+ * a `set` or `delete` below the stored version changes nothing, and an
13
+ * eviction raises the version, so a racing writer's late write-through can
14
+ * never re-cache a resource older than one already replaced or evicted.
15
+ * - **An eviction wins a tie.** A tombstone outranks a value at the same
16
+ * version: only a strictly newer write clears it, so a read-through that
17
+ * loaded the row before an unpublish at that version can't re-cache it.
18
+ * The cache stores a rank, `2·version` for a value and `2·version + 1` for
19
+ * a tombstone, as the entry's `version`, so the seam still compares one
20
+ * number.
21
+ * - **Tombstones outlive values.** A tombstone is kept for
22
+ * `tombstoneTtlSeconds` (by default a day, and never less than the value
23
+ * TTL), which must exceed the longest a write can be in flight: a late
24
+ * write-through that arrives after its tombstone has expired is no longer
25
+ * ordered against it.
26
+ * - **Serveability on both sides** (`CACHE-AUTH-005`). `set` refuses (and
27
+ * evicts) a resource the `serveable` policy refuses, and `get` checks it
28
+ * again, so a policy change can't serve a stale cached private row.
29
+ * - **Decode or evict.** `get` decodes the stored value with the resource's
30
+ * schema; a value that no longer decodes (the schema moved) is evicted and
31
+ * read as a miss.
32
+ * - **A TTL backstop.** Every write carries the configured TTL, so an entry
33
+ * no write ever evicted still expires.
34
+ * - **Never fails.** A KV failure reads as a miss and skips a write: the
35
+ * cache is an optimisation, and the store stays the source of truth.
36
+ *
37
+ * The KV seam is the application's (a Durable Object, Redis, a SQL table):
38
+ * nothing here imports a platform. Workers KV alone has no conditional
39
+ * write, so it can't fill {@link VersionedKv.putIfNotBehind}.
40
+ *
41
+ * @copyright Thomas Foster
42
+ * @module pulpit/cache
43
+ * @since 0.1.0
44
+ */
45
+ import * as Effect from "effect/Effect";
46
+ import * as Option from "effect/Option";
47
+ import * as Schema from "effect/Schema";
48
+ /**
49
+ * The default time an eviction's tombstone is kept: one day, far longer than
50
+ * any request can be in flight.
51
+ *
52
+ * @since 0.1.0
53
+ */
54
+ export const DEFAULT_TOMBSTONE_TTL_SECONDS = 86_400;
55
+ /**
56
+ * The reference {@link ResourceCacheSeam} over a {@link VersionedKv}.
57
+ *
58
+ * @example
59
+ * ```ts
60
+ * import { deepStrictEqual } from "node:assert";
61
+ * import * as Effect from "effect/Effect";
62
+ * import * as Option from "effect/Option";
63
+ * import * as Schema from "effect/Schema";
64
+ * import { makeResourceCache } from "@thomasfosterau/pulpit/cache";
65
+ * import { makeInMemoryVersionedKv } from "@thomasfosterau/pulpit/testing";
66
+ *
67
+ * const Post = Schema.Struct({ id: Schema.String, visibility: Schema.String });
68
+ * const cache = makeResourceCache({
69
+ * kv: makeInMemoryVersionedKv(),
70
+ * schema: Post,
71
+ * serveable: (post) => post.visibility === "public",
72
+ * prefix: "posts:v1:",
73
+ * ttlSeconds: 3600,
74
+ * });
75
+ * const program = Effect.gen(function* () {
76
+ * yield* cache.set("p-1", { id: "p-1", visibility: "public" }, 1n);
77
+ * yield* cache.set("p-1", { id: "p-1", visibility: "private" }, 0n); // behind: no-op
78
+ * return yield* cache.get("p-1");
79
+ * });
80
+ * deepStrictEqual(Effect.runSync(program), Option.some({ id: "p-1", visibility: "public" }));
81
+ * ```
82
+ *
83
+ * @since 0.1.0
84
+ */
85
+ export const makeResourceCache = (options) => {
86
+ const { kv, serveable, prefix } = options;
87
+ const ttl = Math.max(1, Math.trunc(options.ttlSeconds) || 1);
88
+ const tombstoneTtl = Math.max(ttl, Math.trunc(options.tombstoneTtlSeconds ?? DEFAULT_TOMBSTONE_TTL_SECONDS) || ttl);
89
+ const encode = Schema.encodeOption(options.schema);
90
+ const decode = Schema.decodeUnknownOption(options.schema);
91
+ const keyOf = (id) => `${prefix}${id}`;
92
+ /**
93
+ * The conditional write at the entry's rank (`2·version`, plus one for a
94
+ * tombstone, so an eviction wins a tie), a KV failure skipped (the cache
95
+ * never fails). A tombstone carries the longer TTL.
96
+ */
97
+ const write = (id, version, value) => Effect.ignore(kv.putIfNotBehind(keyOf(id), { version: version * 2n + (Option.isNone(value) ? 1n : 0n), value }, Option.isNone(value) ? tombstoneTtl : ttl));
98
+ const evict = (id, version) => write(id, version, Option.none());
99
+ const set = (id, resource, version) => {
100
+ // Refuse (and evict) what the policy doesn't serve, or what doesn't encode.
101
+ const encoded = serveable(resource) ? encode(resource) : Option.none();
102
+ return write(id, version, encoded);
103
+ };
104
+ const get = (id) => Effect.gen(function* () {
105
+ const stored = yield* Effect.orElseSucceed(kv.get(keyOf(id)), () => Option.none());
106
+ if (Option.isNone(stored) || Option.isNone(stored.value.value))
107
+ return Option.none();
108
+ const resource = decode(stored.value.value.value);
109
+ if (Option.isNone(resource) || !serveable(resource.value)) {
110
+ // Decode or evict, and never serve what the policy now refuses (CACHE-AUTH-005).
111
+ yield* evict(id, stored.value.version / 2n);
112
+ return Option.none();
113
+ }
114
+ return resource;
115
+ });
116
+ return { set, delete: evict, get };
117
+ };
118
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,242 @@
1
+ /**
2
+ * **Ordered collections** (pulpit#193) — positioned members of a parent
3
+ * node: a list's items, an album's media, a byline's authors, a menu's
4
+ * entries.
5
+ *
6
+ * The behavioural spec is website's ordered list items (a reorder appends
7
+ * a parent version) and in-the-dark's list entries (an updatable position
8
+ * beside the chain), with their tests.
9
+ *
10
+ * ## The model (owner decision on pulpit#193)
11
+ *
12
+ * Both models, opt-in per family, over **one store seam**
13
+ * ({@link CollectionStore}), which holds each member's position:
14
+ *
15
+ * - **`mutable`** (the default): the order is state beside the chain, like
16
+ * an edge. A write changes the store and nothing else.
17
+ * - **`versioned`**: every write also appends a parent version through the
18
+ * parent leaf's own `update`, recording the whole order (the member ids,
19
+ * in order) in a declared parent column, so `VERSION-CHAIN-001` covers it
20
+ * and history shows each reorder. After the parent's `restoreVersion`,
21
+ * {@link CollectionOps.resync} replaces the store's order with the one
22
+ * the restored version records, keeping only the members the writer can
23
+ * read, as `append` does.
24
+ *
25
+ * That column holds the id of every member, the ones a reader of the
26
+ * parent may not be able to read included, so it must stay out of the
27
+ * parent's viewer-facing resource (`EDGE-001`): wrap the parent leaf's
28
+ * `toResource` with {@link hideRecordedOrder}. The parent's history
29
+ * (`getVersion`, `compare`) still shows it, behind the write gate
30
+ * (`READ-009`).
31
+ *
32
+ * ## The gates
33
+ *
34
+ * - **Reads** go through the parent's read gate (`get`): a hidden parent is
35
+ * `NodeNotFound`, never `Forbidden` (`SEC-AUTH-ENUM-001`). Each member is
36
+ * read through the member leaf's own gate (`getMany`), and one the viewer
37
+ * can't read is omitted, from the page and from its total, as `EDGE-001`
38
+ * omits an edge with a hidden end.
39
+ * - **Writes** pass the parent's write gate (`authorize`), and a member must
40
+ * be one the writer can read (`NodeNotFound` otherwise).
41
+ * - A collection holds at most {@link MAX_COLLECTION_SIZE} members, so every
42
+ * read and reorder is one bounded read (`SEC-INPUT-003`).
43
+ *
44
+ * @copyright Thomas Foster
45
+ * @module pulpit/collections
46
+ * @since 0.1.0
47
+ */
48
+ import * as Effect from "effect/Effect";
49
+ import * as Schema from "effect/Schema";
50
+ import { type Forbidden, type LeafRow, type Page, type SubtypeOps, type Unauthorized, type WriteRejected } from "./subtype.ts";
51
+ import { NodeNotFound, type StaleVersion, type VersionChainStore, type VersionConflict } from "./version-chain.ts";
52
+ /**
53
+ * The most members a collection holds (`SEC-INPUT-003`): every read and
54
+ * reorder reads the whole order once, so it is bounded.
55
+ *
56
+ * @since 0.1.0
57
+ */
58
+ export declare const MAX_COLLECTION_SIZE = 1000;
59
+ declare const AlreadyMember_base: Schema.Class<AlreadyMember, Schema.TaggedStruct<"AlreadyMember", {
60
+ readonly parentId: Schema.String;
61
+ readonly memberId: Schema.String;
62
+ }>, import("effect/Cause").YieldableError & {
63
+ readonly brand: unique symbol;
64
+ }>;
65
+ /**
66
+ * The member is already in the collection: the wire's 409.
67
+ *
68
+ * @since 0.1.0
69
+ */
70
+ export declare class AlreadyMember extends AlreadyMember_base {
71
+ }
72
+ declare const NotMember_base: Schema.Class<NotMember, Schema.TaggedStruct<"NotMember", {
73
+ readonly parentId: Schema.String;
74
+ readonly memberId: Schema.String;
75
+ }>, import("effect/Cause").YieldableError & {
76
+ readonly brand: unique symbol;
77
+ }>;
78
+ /**
79
+ * The member is not in the collection: the wire's 404.
80
+ *
81
+ * @since 0.1.0
82
+ */
83
+ export declare class NotMember extends NotMember_base {
84
+ }
85
+ declare const CollectionFull_base: Schema.Class<CollectionFull, Schema.TaggedStruct<"CollectionFull", {
86
+ readonly parentId: Schema.String;
87
+ readonly max: Schema.Finite;
88
+ }>, import("effect/Cause").YieldableError & {
89
+ readonly brand: unique symbol;
90
+ }>;
91
+ /**
92
+ * The collection already holds {@link MAX_COLLECTION_SIZE} members: the
93
+ * wire's 409.
94
+ *
95
+ * @since 0.1.0
96
+ */
97
+ export declare class CollectionFull extends CollectionFull_base {
98
+ }
99
+ /**
100
+ * One member's place in a collection.
101
+ *
102
+ * @since 0.1.0
103
+ */
104
+ export interface CollectionEntry {
105
+ readonly memberId: string;
106
+ readonly position: number;
107
+ }
108
+ /**
109
+ * The membership table an application supplies, keyed
110
+ * `(parent_id, member_id)` with both foreign keys cascading. A store seam:
111
+ * no policy, never opens a transaction, fails only with its `E`. See the
112
+ * package README → Seams.
113
+ *
114
+ * @since 0.1.0
115
+ */
116
+ export interface CollectionStore<E, R> {
117
+ /**
118
+ * The parent's members, by position then member id, at most `limit`
119
+ * (never more than {@link MAX_COLLECTION_SIZE} + 1, so the caller can
120
+ * tell a full collection).
121
+ */
122
+ readonly entries: (parentId: string, limit: number) => Effect.Effect<ReadonlyArray<CollectionEntry>, E, R>;
123
+ /** Replace the parent's whole order with `entries` (positions `0…n-1`), in one statement. */
124
+ readonly replace: (parentId: string, entries: ReadonlyArray<CollectionEntry>) => Effect.Effect<void, E, R>;
125
+ }
126
+ /**
127
+ * How a family keeps its order: beside the chain (`mutable`, the default)
128
+ * or in a parent column, versioned with the parent (`versioned`).
129
+ *
130
+ * @since 0.1.0
131
+ */
132
+ export type CollectionMode<E, R> = {
133
+ readonly kind: "mutable";
134
+ } | {
135
+ readonly kind: "versioned";
136
+ /**
137
+ * The parent column recording the order: the member ids, in order (a
138
+ * `jsonb` array). Keep it out of the parent's resource with
139
+ * {@link hideRecordedOrder} (`EDGE-001`).
140
+ */
141
+ readonly column: string;
142
+ /** The parent leaf's chain store, read (ungated) by `resync` for the restored order. */
143
+ readonly chain: VersionChainStore<E, R>;
144
+ };
145
+ /**
146
+ * One family's collection: the parent and member leaves' gated operations,
147
+ * the store and the mode.
148
+ *
149
+ * @since 0.1.0
150
+ */
151
+ export interface CollectionDefinition<Parent, Member, E, R> {
152
+ /** The family's name, traced as `${type}.<operation>`. */
153
+ readonly type: string;
154
+ /** The parent leaf: `get` gates a read, `authorize` a write, `update` records a versioned order. */
155
+ readonly parent: Pick<SubtypeOps<Parent, E, R>, "get" | "authorize" | "update">;
156
+ /** The member leaf's gated batch read. */
157
+ readonly members: Pick<SubtypeOps<Member, E, R>, "getMany">;
158
+ readonly store: CollectionStore<E, R>;
159
+ /** The order's model; `mutable` when omitted. */
160
+ readonly mode?: CollectionMode<E, R> | undefined;
161
+ }
162
+ /**
163
+ * One member as a read returns it: its id, its position among the members
164
+ * the viewer can see (from 0), and the member resource.
165
+ *
166
+ * @since 0.1.0
167
+ */
168
+ export interface CollectionMember<Member> {
169
+ readonly memberId: string;
170
+ readonly index: number;
171
+ readonly resource: Member;
172
+ }
173
+ type WriteFailure = Unauthorized | Forbidden | NodeNotFound | WriteRejected | StaleVersion | VersionConflict;
174
+ /**
175
+ * The operations {@link defineCollection} produces.
176
+ *
177
+ * @since 0.1.0
178
+ */
179
+ export interface CollectionOps<Member, E, R> {
180
+ /**
181
+ * One page of the members the viewer can read, in order, with their
182
+ * total: the parent's read gate first (`NodeNotFound`), each member
183
+ * through its own leaf's, a hidden one omitted. The limit is clamped to
184
+ * `[1, MAX_PAGE_LIMIT]`.
185
+ */
186
+ readonly list: (parentId: string, request: {
187
+ readonly limit: number;
188
+ readonly offset: number;
189
+ }) => Effect.Effect<Page<CollectionMember<Member>>, NodeNotFound | E, R>;
190
+ /**
191
+ * Add `memberId` at the end. Fails the parent's write gate, `NodeNotFound`
192
+ * for a member the writer can't read, {@link AlreadyMember} and
193
+ * {@link CollectionFull}; on a versioned family, the parent `update`'s
194
+ * failures too.
195
+ */
196
+ readonly append: (parentId: string, memberId: string) => Effect.Effect<void, WriteFailure | AlreadyMember | CollectionFull | E, R>;
197
+ /**
198
+ * Move `memberId` to `index` (clamped to the order's bounds), shifting the
199
+ * members between. Fails the parent's write gate and {@link NotMember}.
200
+ */
201
+ readonly move: (parentId: string, memberId: string, index: number) => Effect.Effect<void, WriteFailure | NotMember | E, R>;
202
+ /** Remove `memberId`. Fails the parent's write gate and {@link NotMember}. */
203
+ readonly remove: (parentId: string, memberId: string) => Effect.Effect<void, WriteFailure | NotMember | E, R>;
204
+ /**
205
+ * A versioned family's store order replaced by the order the parent's
206
+ * head records (after the parent's `restoreVersion`), behind the parent's
207
+ * write gate, keeping only the members the writer can read (as `append`
208
+ * checks). A mutable family, or a head recording no order, leaves the
209
+ * store as it is.
210
+ */
211
+ readonly resync: (parentId: string) => Effect.Effect<void, Unauthorized | Forbidden | NodeNotFound | E, R>;
212
+ }
213
+ /**
214
+ * Wrap a parent leaf's `toResource` so the row it maps no longer carries a
215
+ * versioned collection's order `column`: the recorded order names every
216
+ * member, the ones the viewer can't read included, so it must never reach
217
+ * a reader of the parent (`EDGE-001`). The chain still records it, so
218
+ * {@link CollectionOps.resync} reads it back.
219
+ *
220
+ * @example
221
+ * ```ts
222
+ * import { deepStrictEqual } from "node:assert";
223
+ * import { hideRecordedOrder } from "@thomasfosterau/pulpit/collections";
224
+ *
225
+ * const toResource = hideRecordedOrder("memberOrder", (row) => row);
226
+ * const row = { id: "l-1", version: 2n, action: "update", title: "Ferns", memberOrder: ["i-1"] } as const;
227
+ * deepStrictEqual(toResource(row), { id: "l-1", version: 2n, action: "update", title: "Ferns" });
228
+ * ```
229
+ *
230
+ * @since 0.1.0
231
+ */
232
+ export declare const hideRecordedOrder: <Resource>(column: string, toResource: (row: LeafRow) => Resource) => ((row: LeafRow) => Resource);
233
+ /**
234
+ * Bind one family's collection operations. Every operation is traced as
235
+ * `${type}.<operation>`, and the writes run in the caller's transaction (a
236
+ * versioned family's store write and parent append commit together).
237
+ *
238
+ * @since 0.1.0
239
+ */
240
+ export declare const defineCollection: <Parent, Member, E, R>(definition: CollectionDefinition<Parent, Member, E, R>) => CollectionOps<Member, E, R>;
241
+ export {};
242
+ //# sourceMappingURL=collections.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collections.d.ts","sourceRoot":"","sources":["../src/collections.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,215 @@
1
+ /**
2
+ * **Ordered collections** (pulpit#193) — positioned members of a parent
3
+ * node: a list's items, an album's media, a byline's authors, a menu's
4
+ * entries.
5
+ *
6
+ * The behavioural spec is website's ordered list items (a reorder appends
7
+ * a parent version) and in-the-dark's list entries (an updatable position
8
+ * beside the chain), with their tests.
9
+ *
10
+ * ## The model (owner decision on pulpit#193)
11
+ *
12
+ * Both models, opt-in per family, over **one store seam**
13
+ * ({@link CollectionStore}), which holds each member's position:
14
+ *
15
+ * - **`mutable`** (the default): the order is state beside the chain, like
16
+ * an edge. A write changes the store and nothing else.
17
+ * - **`versioned`**: every write also appends a parent version through the
18
+ * parent leaf's own `update`, recording the whole order (the member ids,
19
+ * in order) in a declared parent column, so `VERSION-CHAIN-001` covers it
20
+ * and history shows each reorder. After the parent's `restoreVersion`,
21
+ * {@link CollectionOps.resync} replaces the store's order with the one
22
+ * the restored version records, keeping only the members the writer can
23
+ * read, as `append` does.
24
+ *
25
+ * That column holds the id of every member, the ones a reader of the
26
+ * parent may not be able to read included, so it must stay out of the
27
+ * parent's viewer-facing resource (`EDGE-001`): wrap the parent leaf's
28
+ * `toResource` with {@link hideRecordedOrder}. The parent's history
29
+ * (`getVersion`, `compare`) still shows it, behind the write gate
30
+ * (`READ-009`).
31
+ *
32
+ * ## The gates
33
+ *
34
+ * - **Reads** go through the parent's read gate (`get`): a hidden parent is
35
+ * `NodeNotFound`, never `Forbidden` (`SEC-AUTH-ENUM-001`). Each member is
36
+ * read through the member leaf's own gate (`getMany`), and one the viewer
37
+ * can't read is omitted, from the page and from its total, as `EDGE-001`
38
+ * omits an edge with a hidden end.
39
+ * - **Writes** pass the parent's write gate (`authorize`), and a member must
40
+ * be one the writer can read (`NodeNotFound` otherwise).
41
+ * - A collection holds at most {@link MAX_COLLECTION_SIZE} members, so every
42
+ * read and reorder is one bounded read (`SEC-INPUT-003`).
43
+ *
44
+ * @copyright Thomas Foster
45
+ * @module pulpit/collections
46
+ * @since 0.1.0
47
+ */
48
+ import * as Arr from "effect/Array";
49
+ import * as Effect from "effect/Effect";
50
+ import * as HashMap from "effect/HashMap";
51
+ import * as Option from "effect/Option";
52
+ import * as Order from "effect/Order";
53
+ import * as Predicate from "effect/Predicate";
54
+ import * as Schema from "effect/Schema";
55
+ import { byCodePoint } from "./internal/list-order.js";
56
+ import { MAX_PAGE_LIMIT, } from "./subtype.js";
57
+ import { NodeNotFound, } from "./version-chain.js";
58
+ /**
59
+ * The most members a collection holds (`SEC-INPUT-003`): every read and
60
+ * reorder reads the whole order once, so it is bounded.
61
+ *
62
+ * @since 0.1.0
63
+ */
64
+ export const MAX_COLLECTION_SIZE = MAX_PAGE_LIMIT;
65
+ // ── Errors ────────────────────────────────────────────────────────────
66
+ /**
67
+ * The member is already in the collection: the wire's 409.
68
+ *
69
+ * @since 0.1.0
70
+ */
71
+ export class AlreadyMember extends Schema.TaggedError()("AlreadyMember", {
72
+ parentId: Schema.String,
73
+ memberId: Schema.String,
74
+ }) {
75
+ }
76
+ /**
77
+ * The member is not in the collection: the wire's 404.
78
+ *
79
+ * @since 0.1.0
80
+ */
81
+ export class NotMember extends Schema.TaggedError()("NotMember", {
82
+ parentId: Schema.String,
83
+ memberId: Schema.String,
84
+ }) {
85
+ }
86
+ /**
87
+ * The collection already holds {@link MAX_COLLECTION_SIZE} members: the
88
+ * wire's 409.
89
+ *
90
+ * @since 0.1.0
91
+ */
92
+ export class CollectionFull extends Schema.TaggedError()("CollectionFull", {
93
+ parentId: Schema.String,
94
+ max: Schema.Finite,
95
+ }) {
96
+ }
97
+ /**
98
+ * Wrap a parent leaf's `toResource` so the row it maps no longer carries a
99
+ * versioned collection's order `column`: the recorded order names every
100
+ * member, the ones the viewer can't read included, so it must never reach
101
+ * a reader of the parent (`EDGE-001`). The chain still records it, so
102
+ * {@link CollectionOps.resync} reads it back.
103
+ *
104
+ * @example
105
+ * ```ts
106
+ * import { deepStrictEqual } from "node:assert";
107
+ * import { hideRecordedOrder } from "@thomasfosterau/pulpit/collections";
108
+ *
109
+ * const toResource = hideRecordedOrder("memberOrder", (row) => row);
110
+ * const row = { id: "l-1", version: 2n, action: "update", title: "Ferns", memberOrder: ["i-1"] } as const;
111
+ * deepStrictEqual(toResource(row), { id: "l-1", version: 2n, action: "update", title: "Ferns" });
112
+ * ```
113
+ *
114
+ * @since 0.1.0
115
+ */
116
+ export const hideRecordedOrder = (column, toResource) => (row) => {
117
+ const { [column]: _recorded, ...rest } = row;
118
+ return toResource({ ...rest, id: row.id, version: row.version, action: row.action });
119
+ };
120
+ /** The order with positions `0…n-1`. */
121
+ const renumber = (ids) => Arr.map(ids, (memberId, position) => ({ memberId, position }));
122
+ /** By position, then member id: the store's total order. */
123
+ const entryOrder = Order.combine(Order.mapInput(Order.Number, (entry) => entry.position), Order.mapInput(byCodePoint, (entry) => entry.memberId));
124
+ /**
125
+ * Bind one family's collection operations. Every operation is traced as
126
+ * `${type}.<operation>`, and the writes run in the caller's transaction (a
127
+ * versioned family's store write and parent append commit together).
128
+ *
129
+ * @since 0.1.0
130
+ */
131
+ export const defineCollection = (definition) => {
132
+ const { type, parent, members, store } = definition;
133
+ const mode = definition.mode ?? { kind: "mutable" };
134
+ /** The whole order, as ids (one bounded read). */
135
+ const orderOf = (parentId) => Effect.map(store.entries(parentId, MAX_COLLECTION_SIZE + 1), (entries) => Arr.map(Arr.sort(entries, entryOrder), (entry) => entry.memberId));
136
+ /** Write the new order to the store and, on a versioned family, to the parent's chain. */
137
+ const commit = (parentId, ids) => Effect.gen(function* () {
138
+ yield* store.replace(parentId, renumber(ids));
139
+ if (mode.kind === "versioned") {
140
+ yield* parent.update({ id: parentId, patch: { [mode.column]: [...ids] } });
141
+ }
142
+ });
143
+ const list = Effect.fn(`${type}.list`)(function* (parentId, request) {
144
+ // A parent the viewer can't read is missing to them (SEC-AUTH-ENUM-001).
145
+ yield* parent.get(parentId);
146
+ const ids = Arr.take(yield* orderOf(parentId), MAX_COLLECTION_SIZE);
147
+ const readable = Arr.isReadonlyArrayNonEmpty(ids)
148
+ ? yield* members.getMany(ids)
149
+ : HashMap.empty();
150
+ const shown = Arr.getSomes(Arr.map(ids, (memberId) => Option.map(HashMap.get(readable, memberId), (resource) => ({ memberId, resource }))));
151
+ const limit = Math.max(1, Math.min(Math.trunc(request.limit), MAX_PAGE_LIMIT));
152
+ const offset = Math.max(0, Math.trunc(request.offset));
153
+ return {
154
+ rows: Arr.map(shown.slice(offset, offset + limit), (member, at) => ({
155
+ ...member,
156
+ index: offset + at,
157
+ })),
158
+ total: shown.length,
159
+ };
160
+ });
161
+ const append = Effect.fn(`${type}.append`)(function* (parentId, memberId) {
162
+ yield* parent.authorize(parentId);
163
+ const readable = yield* members.getMany([memberId]);
164
+ if (!HashMap.has(readable, memberId)) {
165
+ return yield* new NodeNotFound({ type, id: memberId });
166
+ }
167
+ const ids = yield* orderOf(parentId);
168
+ if (ids.includes(memberId))
169
+ return yield* new AlreadyMember({ parentId, memberId });
170
+ if (ids.length >= MAX_COLLECTION_SIZE) {
171
+ return yield* new CollectionFull({ parentId, max: MAX_COLLECTION_SIZE });
172
+ }
173
+ yield* commit(parentId, [...ids, memberId]);
174
+ });
175
+ const move = Effect.fn(`${type}.move`)(function* (parentId, memberId, index) {
176
+ yield* parent.authorize(parentId);
177
+ const ids = yield* orderOf(parentId);
178
+ if (!ids.includes(memberId))
179
+ return yield* new NotMember({ parentId, memberId });
180
+ const rest = Arr.filter(ids, (id) => id !== memberId);
181
+ const at = Math.max(0, Math.min(Number.isFinite(index) ? Math.trunc(index) : 0, rest.length));
182
+ yield* commit(parentId, [...rest.slice(0, at), memberId, ...rest.slice(at)]);
183
+ });
184
+ const remove = Effect.fn(`${type}.remove`)(function* (parentId, memberId) {
185
+ yield* parent.authorize(parentId);
186
+ const ids = yield* orderOf(parentId);
187
+ if (!ids.includes(memberId))
188
+ return yield* new NotMember({ parentId, memberId });
189
+ yield* commit(parentId, Arr.filter(ids, (id) => id !== memberId));
190
+ });
191
+ const resync = Effect.fn(`${type}.resync`)(function* (parentId) {
192
+ yield* parent.authorize(parentId);
193
+ if (mode.kind !== "versioned")
194
+ return;
195
+ const head = yield* mode.chain.head(parentId);
196
+ if (Option.isNone(head))
197
+ return;
198
+ const snapshot = yield* mode.chain.snapshot(parentId, head.value.version);
199
+ const recorded = Option.flatMap(snapshot, (version) => {
200
+ const value = version.state[mode.column] ?? null;
201
+ return Arr.isArray(value) ? Option.some(value) : Option.none();
202
+ });
203
+ if (Option.isNone(recorded))
204
+ return;
205
+ const ids = Arr.take(Arr.dedupe(Arr.filter(recorded.value, Predicate.isString)), MAX_COLLECTION_SIZE);
206
+ // Gate parity with `append`: the column is writable through the parent's
207
+ // own `update`, so keep only the members the writer can read.
208
+ const readable = Arr.isReadonlyArrayNonEmpty(ids)
209
+ ? yield* members.getMany(ids)
210
+ : HashMap.empty();
211
+ yield* store.replace(parentId, renumber(Arr.filter(ids, (id) => HashMap.has(readable, id))));
212
+ });
213
+ return { list, append, move, remove, resync };
214
+ };
215
+ //# sourceMappingURL=collections.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collections.js","sourceRoot":"","sources":["../src/collections.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,KAAK,GAAG,MAAM,cAAc,CAAC;AACpC,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,OAAO,MAAM,gBAAgB,CAAC;AAC1C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,KAAK,KAAK,MAAM,cAAc,CAAC;AACtC,OAAO,KAAK,SAAS,MAAM,kBAAkB,CAAC;AAC9C,OAAO,KAAK,MAAM,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EACL,cAAc,GAOf,MAAM,cAAc,CAAC;AACtB,OAAO,EACL,YAAY,GAIb,MAAM,oBAAoB,CAAC;AAE5B;;;;;GAKG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,cAAc,CAAC;AAElD,yEAAyE;AAEzE;;;;GAIG;AACH,MAAM,OAAO,aAAc,SAAQ,MAAM,CAAC,WAAW,EAGlD,CAAC,eAAe,EAAE;IACnB,QAAQ,EAAE,MAAM,CAAC,MAAM;IACvB,QAAQ,EAAE,MAAM,CAAC,MAAM;CACxB,CAAC;CAAG;AAEL;;;;GAIG;AACH,MAAM,OAAO,SAAU,SAAQ,MAAM,CAAC,WAAW,EAAgD,CAC/F,WAAW,EACX;IACE,QAAQ,EAAE,MAAM,CAAC,MAAM;IACvB,QAAQ,EAAE,MAAM,CAAC,MAAM;CACxB,CACF;CAAG;AAEJ;;;;;GAKG;AACH,MAAM,OAAO,cAAe,SAAQ,MAAM,CAAC,WAAW,EAGnD,CAAC,gBAAgB,EAAE;IACpB,QAAQ,EAAE,MAAM,CAAC,MAAM;IACvB,GAAG,EAAE,MAAM,CAAC,MAAM;CACnB,CAAC;CAAG;AAwJL;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAC5B,CACE,MAAc,EACd,UAAsC,EACR,EAAE,CAClC,CAAC,GAAG,EAAE,EAAE;IACN,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,GAAG,GAAG,CAAC;IAC7C,OAAO,UAAU,CAAC,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;AACvF,CAAC,CAAC;AAEJ,wCAAwC;AACxC,MAAM,QAAQ,GAAG,CAAC,GAA0B,EAAkC,EAAE,CAC9E,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;AAEjE,4DAA4D;AAC5D,MAAM,UAAU,GAAiC,KAAK,CAAC,OAAO,CAC5D,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,KAAsB,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,EACxE,KAAK,CAAC,QAAQ,CAAC,WAAW,EAAE,CAAC,KAAsB,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CACxE,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAC9B,UAAsD,EACzB,EAAE;IAC/B,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,UAAU,CAAC;IACpD,MAAM,IAAI,GAAyB,UAAU,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC;IAE1E,kDAAkD;IAClD,MAAM,OAAO,GAAG,CAAC,QAAgB,EAAE,EAAE,CACnC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,mBAAmB,GAAG,CAAC,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,CACvE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAClE,CAAC;IAEJ,0FAA0F;IAC1F,MAAM,MAAM,GAAG,CAAC,QAAgB,EAAE,GAA0B,EAAE,EAAE,CAC9D,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;QAClB,KAAK,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;QAC9C,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW,EAAE,CAAC;YAC9B,KAAK,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;QAC7E,CAAC;IACH,CAAC,CAAC,CAAC;IAEL,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,CAAC,QAAQ,CAAC,EAC9C,QAAgB,EAChB,OAA4D;QAE5D,yEAAyE;QACzE,KAAK,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QAC5B,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,mBAAmB,CAAC,CAAC;QACpE,MAAM,QAAQ,GAAG,GAAG,CAAC,uBAAuB,CAAC,GAAG,CAAC;YAC/C,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC;YAC7B,CAAC,CAAC,OAAO,CAAC,KAAK,EAAkB,CAAC;QACpC,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,CACxB,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,QAAQ,EAAE,EAAE,CACxB,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC,CACpF,CACF,CAAC;QACF,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,cAAc,CAAC,CAAC,CAAC;QAC/E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QACvD,OAAO;YACL,IAAI,EAAE,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,GAAG,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;gBAClE,GAAG,MAAM;gBACT,KAAK,EAAE,MAAM,GAAG,EAAE;aACnB,CAAC,CAAC;YACH,KAAK,EAAE,KAAK,CAAC,MAAM;SACpB,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,SAAS,CAAC,CAAC,QAAQ,CAAC,EAAE,QAAgB,EAAE,QAAgB;QACtF,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAClC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC;QACpD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,QAAQ,CAAC,EAAE,CAAC;YACrC,OAAO,KAAK,CAAC,CAAC,IAAI,YAAY,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;QACzD,CAAC;QACD,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAC,CAAC,IAAI,aAAa,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;QACpF,IAAI,GAAG,CAAC,MAAM,IAAI,mBAAmB,EAAE,CAAC;YACtC,OAAO,KAAK,CAAC,CAAC,IAAI,cAAc,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,mBAAmB,EAAE,CAAC,CAAC;QAC3E,CAAC;QACD,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,GAAG,GAAG,EAAE,QAAQ,CAAC,CAAC,CAAC;IAC9C,CAAC,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,OAAO,CAAC,CAAC,QAAQ,CAAC,EAC9C,QAAgB,EAChB,QAAgB,EAChB,KAAa;QAEb,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAClC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAC,CAAC,IAAI,SAAS,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;QACjF,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC;QACtD,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC;QAC9F,KAAK,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IAC/E,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,SAAS,CAAC,CAAC,QAAQ,CAAC,EAAE,QAAgB,EAAE,QAAgB;QACtF,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAClC,MAAM,GAAG,GAAG,KAAK,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;QACrC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,OAAO,KAAK,CAAC,CAAC,IAAI,SAAS,CAAC,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;QACjF,KAAK,CAAC,CAAC,MAAM,CACX,QAAQ,EACR,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,KAAK,QAAQ,CAAC,CACzC,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,GAAG,IAAI,SAAS,CAAC,CAAC,QAAQ,CAAC,EAAE,QAAgB;QACpE,KAAK,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;QAClC,IAAI,IAAI,CAAC,IAAI,KAAK,WAAW;YAAE,OAAO;QACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC9C,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC;YAAE,OAAO;QAChC,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC;QAC1E,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC,OAAO,EAAE,EAAE;YACpD,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,CAAC;YACjD,OAAO,GAAG,CAAC,OAAO,CAAU,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QAC1E,CAAC,CAAC,CAAC;QACH,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC;YAAE,OAAO;QACpC,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAClB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC,EAC1D,mBAAmB,CACpB,CAAC;QACF,yEAAyE;QACzE,8DAA8D;QAC9D,MAAM,QAAQ,GAAG,GAAG,CAAC,uBAAuB,CAAC,GAAG,CAAC;YAC/C,CAAC,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC;YAC7B,CAAC,CAAC,OAAO,CAAC,KAAK,EAAkB,CAAC;QACpC,KAAK,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC/F,CAAC,CAAC,CAAC;IAEH,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;AAChD,CAAC,CAAC"}