@aglyn/plugins-marketplace 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +7 -0
- package/package.json +51 -0
- package/src/index.d.ts +19 -0
- package/src/index.js +20 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/artifact-update-dialog.component.d.ts +27 -0
- package/src/lib/components/artifact-update-dialog.component.js +244 -0
- package/src/lib/components/artifact-update-dialog.component.js.map +1 -0
- package/src/lib/components/host-plugins-card.component.d.ts +23 -0
- package/src/lib/components/host-plugins-card.component.js +566 -0
- package/src/lib/components/host-plugins-card.component.js.map +1 -0
- package/src/lib/components/listing-content.component.d.ts +57 -0
- package/src/lib/components/listing-content.component.js +1880 -0
- package/src/lib/components/listing-content.component.js.map +1 -0
- package/src/lib/components/listing-image.component.d.ts +46 -0
- package/src/lib/components/listing-image.component.js +38 -0
- package/src/lib/components/listing-image.component.js.map +1 -0
- package/src/lib/components/listing-reviews.component.d.ts +22 -0
- package/src/lib/components/listing-reviews.component.js +419 -0
- package/src/lib/components/listing-reviews.component.js.map +1 -0
- package/src/lib/components/marketplace-browse.component.d.ts +38 -0
- package/src/lib/components/marketplace-browse.component.js +802 -0
- package/src/lib/components/marketplace-browse.component.js.map +1 -0
- package/src/lib/components/plugin-site-set-panel.component.d.ts +24 -0
- package/src/lib/components/plugin-site-set-panel.component.js +137 -0
- package/src/lib/components/plugin-site-set-panel.component.js.map +1 -0
- package/src/lib/components/plugin-site-set.component.d.ts +43 -0
- package/src/lib/components/plugin-site-set.component.js +425 -0
- package/src/lib/components/plugin-site-set.component.js.map +1 -0
- package/src/lib/components/rating-input.component.d.ts +3 -0
- package/src/lib/components/rating-input.component.js +47 -0
- package/src/lib/components/rating-input.component.js.map +1 -0
- package/src/lib/components/report-target.component.d.ts +36 -0
- package/src/lib/components/report-target.component.js +181 -0
- package/src/lib/components/report-target.component.js.map +1 -0
- package/src/lib/components/uninstall-impact-dialog.component.d.ts +40 -0
- package/src/lib/components/uninstall-impact-dialog.component.js +244 -0
- package/src/lib/components/uninstall-impact-dialog.component.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +8 -0
- package/src/lib/constants/bundle-common.js +9 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/hooks/use-artifact-update.d.ts +41 -0
- package/src/lib/hooks/use-artifact-update.js +169 -0
- package/src/lib/hooks/use-artifact-update.js.map +1 -0
- package/src/lib/hooks/use-marketplace-actions.d.ts +37 -0
- package/src/lib/hooks/use-marketplace-actions.js +523 -0
- package/src/lib/hooks/use-marketplace-actions.js.map +1 -0
- package/src/lib/model/artifact-merge.d.ts +154 -0
- package/src/lib/model/artifact-merge.js +228 -0
- package/src/lib/model/artifact-merge.js.map +1 -0
- package/src/lib/model/index.d.ts +43 -0
- package/src/lib/model/index.js +41 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/marketplace-props.d.ts +57 -0
- package/src/lib/model/marketplace-props.js +441 -0
- package/src/lib/model/marketplace-props.js.map +1 -0
- package/src/lib/model/marketplace.d.ts +833 -0
- package/src/lib/model/marketplace.js +929 -0
- package/src/lib/model/marketplace.js.map +1 -0
- package/src/lib/model/rating-field.d.ts +24 -0
- package/src/lib/model/rating-field.js +33 -0
- package/src/lib/model/rating-field.js.map +1 -0
- package/src/lib/plugin.d.ts +33 -0
- package/src/lib/plugin.js +86 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server/billing-webhook.d.ts +25 -0
- package/src/lib/server/billing-webhook.js +1062 -0
- package/src/lib/server/billing-webhook.js.map +1 -0
- package/src/lib/server/checkout.d.ts +76 -0
- package/src/lib/server/checkout.js +477 -0
- package/src/lib/server/checkout.js.map +1 -0
- package/src/lib/server/connect.d.ts +24 -0
- package/src/lib/server/connect.js +205 -0
- package/src/lib/server/connect.js.map +1 -0
- package/src/lib/server/install-dataset-schema.d.ts +32 -0
- package/src/lib/server/install-dataset-schema.js +255 -0
- package/src/lib/server/install-dataset-schema.js.map +1 -0
- package/src/lib/server/install-email-starter.d.ts +65 -0
- package/src/lib/server/install-email-starter.js +279 -0
- package/src/lib/server/install-email-starter.js.map +1 -0
- package/src/lib/server/install-email-template.d.ts +34 -0
- package/src/lib/server/install-email-template.js +252 -0
- package/src/lib/server/install-email-template.js.map +1 -0
- package/src/lib/server/install-layout.d.ts +29 -0
- package/src/lib/server/install-layout.js +323 -0
- package/src/lib/server/install-layout.js.map +1 -0
- package/src/lib/server/install-pin-counts.d.ts +173 -0
- package/src/lib/server/install-pin-counts.js +248 -0
- package/src/lib/server/install-pin-counts.js.map +1 -0
- package/src/lib/server/install-plugin.d.ts +18 -0
- package/src/lib/server/install-plugin.js +405 -0
- package/src/lib/server/install-plugin.js.map +1 -0
- package/src/lib/server/install-template.d.ts +37 -0
- package/src/lib/server/install-template.js +348 -0
- package/src/lib/server/install-template.js.map +1 -0
- package/src/lib/server/install-theme.d.ts +42 -0
- package/src/lib/server/install-theme.js +313 -0
- package/src/lib/server/install-theme.js.map +1 -0
- package/src/lib/server/install.d.ts +27 -0
- package/src/lib/server/install.js +276 -0
- package/src/lib/server/install.js.map +1 -0
- package/src/lib/server/listing-versions.d.ts +26 -0
- package/src/lib/server/listing-versions.js +378 -0
- package/src/lib/server/listing-versions.js.map +1 -0
- package/src/lib/server/preview-image.d.ts +37 -0
- package/src/lib/server/preview-image.js +216 -0
- package/src/lib/server/preview-image.js.map +1 -0
- package/src/lib/server/provenance.d.ts +100 -0
- package/src/lib/server/provenance.js +118 -0
- package/src/lib/server/provenance.js.map +1 -0
- package/src/lib/server/publish-dataset-schema.d.ts +31 -0
- package/src/lib/server/publish-dataset-schema.js +202 -0
- package/src/lib/server/publish-dataset-schema.js.map +1 -0
- package/src/lib/server/publish-email-starter.d.ts +44 -0
- package/src/lib/server/publish-email-starter.js +231 -0
- package/src/lib/server/publish-email-starter.js.map +1 -0
- package/src/lib/server/publish-email-template.d.ts +33 -0
- package/src/lib/server/publish-email-template.js +211 -0
- package/src/lib/server/publish-email-template.js.map +1 -0
- package/src/lib/server/publish-layout.d.ts +33 -0
- package/src/lib/server/publish-layout.js +243 -0
- package/src/lib/server/publish-layout.js.map +1 -0
- package/src/lib/server/publish-plugin.d.ts +18 -0
- package/src/lib/server/publish-plugin.js +485 -0
- package/src/lib/server/publish-plugin.js.map +1 -0
- package/src/lib/server/publish-preconditions.d.ts +86 -0
- package/src/lib/server/publish-preconditions.js +160 -0
- package/src/lib/server/publish-preconditions.js.map +1 -0
- package/src/lib/server/publish-template.d.ts +27 -0
- package/src/lib/server/publish-template.js +215 -0
- package/src/lib/server/publish-template.js.map +1 -0
- package/src/lib/server/publish-theme.d.ts +38 -0
- package/src/lib/server/publish-theme.js +186 -0
- package/src/lib/server/publish-theme.js.map +1 -0
- package/src/lib/server/publish.d.ts +26 -0
- package/src/lib/server/publish.js +204 -0
- package/src/lib/server/publish.js.map +1 -0
- package/src/lib/server/publisher-profile-save.d.ts +30 -0
- package/src/lib/server/publisher-profile-save.js +226 -0
- package/src/lib/server/publisher-profile-save.js.map +1 -0
- package/src/lib/server/publisher-profile.d.ts +86 -0
- package/src/lib/server/publisher-profile.js +116 -0
- package/src/lib/server/publisher-profile.js.map +1 -0
- package/src/lib/server/purchase-entitlement.d.ts +83 -0
- package/src/lib/server/purchase-entitlement.js +118 -0
- package/src/lib/server/purchase-entitlement.js.map +1 -0
- package/src/lib/server/report.d.ts +32 -0
- package/src/lib/server/report.js +101 -0
- package/src/lib/server/report.js.map +1 -0
- package/src/lib/server/reviews.d.ts +41 -0
- package/src/lib/server/reviews.js +240 -0
- package/src/lib/server/reviews.js.map +1 -0
- package/src/lib/server/update-artifact.d.ts +19 -0
- package/src/lib/server/update-artifact.js +569 -0
- package/src/lib/server/update-artifact.js.map +1 -0
- package/src/lib/server/verification-request.d.ts +31 -0
- package/src/lib/server/verification-request.js +173 -0
- package/src/lib/server/verification-request.js.map +1 -0
- package/src/lib/server/version-stats.d.ts +202 -0
- package/src/lib/server/version-stats.js +305 -0
- package/src/lib/server/version-stats.js.map +1 -0
- package/src/lib/server.d.ts +30 -0
- package/src/lib/server.js +95 -0
- package/src/lib/server.js.map +1 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* Install counts derived from the pins, which are the only ground truth
|
|
18
|
+
* (AGL-1419).
|
|
19
|
+
*
|
|
20
|
+
* AGL-1418 made the page internally consistent and said so honestly: the
|
|
21
|
+
* stored numbers were still wrong, because both stored levels are
|
|
22
|
+
* accumulators and an accumulator can only fail downwards from a missed
|
|
23
|
+
* increment — or upwards forever from a pin that disappeared without one. Six
|
|
24
|
+
* of eight production listings disagreed with their real pin counts;
|
|
25
|
+
* `z6glT_UDAQ` advertised `3 active` against two live pins, and nothing in the
|
|
26
|
+
* product could ever have noticed.
|
|
27
|
+
*
|
|
28
|
+
* Nothing decrements when a pin dies out of band, and there are three ways for
|
|
29
|
+
* that to happen: `recursiveDelete` on a tenant erase (`erase.ts` never names
|
|
30
|
+
* `installs`, it just sweeps the subtree), the console's client-side host-pin
|
|
31
|
+
* delete in the promote-to-org flow, and any cascade. So the derived count is
|
|
32
|
+
* not a nicety — it is the only thing that can bring a count back DOWN.
|
|
33
|
+
*
|
|
34
|
+
* ## Why a collection group here, when three other places refuse one
|
|
35
|
+
*
|
|
36
|
+
* `reviews.ts` and `listing-content.component.tsx` both avoid
|
|
37
|
+
* `collectionGroup('installs')`, and both are right to: they ask "which of MY
|
|
38
|
+
* org's sites has this pinned", and a cross-tenant scan to answer a
|
|
39
|
+
* single-org question is unbounded work plus pins the caller must never see.
|
|
40
|
+
*
|
|
41
|
+
* This asks the opposite question — "how many pins exist anywhere" — which is
|
|
42
|
+
* cross-tenant by definition, and answers it with `count()`, which returns an
|
|
43
|
+
* integer and never a document. No pin, no path, no tenant identity leaves
|
|
44
|
+
* this function. It runs in the Admin SDK on a server route, so it is not a
|
|
45
|
+
* rules question either.
|
|
46
|
+
*
|
|
47
|
+
* ## Cost
|
|
48
|
+
*
|
|
49
|
+
* Firestore bills an aggregation at roughly one document read per 1000 index
|
|
50
|
+
* entries scanned, not one per document, so a listing with 500 installs costs
|
|
51
|
+
* ~1 read rather than 500. That is the single biggest lever here, and it is
|
|
52
|
+
* why counting pins on a buyer-facing page is affordable at all.
|
|
53
|
+
*
|
|
54
|
+
* On top of it, `verifiedLivePins` only issues the aggregation when the stored
|
|
55
|
+
* copy is stale, and writes the answer back — so the ordinary request pays
|
|
56
|
+
* ZERO extra reads and serves a number that was derived from the pins.
|
|
57
|
+
*/ /** How long a derived count stays good without re-deriving it. */ export const PIN_VERIFY_TTL_MS = 15 * 60 * 1000;
|
|
58
|
+
/**
|
|
59
|
+
* How long one process stops re-issuing an aggregation that failed.
|
|
60
|
+
*
|
|
61
|
+
* In-process on purpose. It exists to stop an error storm when the index is
|
|
62
|
+
* missing, not to remember anything — a cold instance SHOULD try again and log
|
|
63
|
+
* again, because the message names the index and is how anyone finds out.
|
|
64
|
+
*/ export const PIN_COUNT_BACKOFF_MS = 5 * 60 * 1000;
|
|
65
|
+
/** Retried aggregations are pointless while the index is still missing. */ const failedUntilMs = new Map();
|
|
66
|
+
/** One aggregation per listing per process, however many requests arrive. */ const inFlight = new Map();
|
|
67
|
+
/** Exported for tests: a module-level cache outlives a `jest` test otherwise. */ export function resetPinCountBackoff() {
|
|
68
|
+
failedUntilMs.clear();
|
|
69
|
+
inFlight.clear();
|
|
70
|
+
}
|
|
71
|
+
const isMissingIndex = (error)=>{
|
|
72
|
+
var _ref;
|
|
73
|
+
const code = error == null ? void 0 : error.code;
|
|
74
|
+
const message = String((_ref = error == null ? void 0 : error.message) != null ? _ref : '');
|
|
75
|
+
return code === 9 || code === 'failed-precondition' || /FAILED_PRECONDITION|requires an index|requires a COLLECTION_GROUP/i.test(message);
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* Turns a failed aggregation into `null`, LOUDLY — and never into `0`.
|
|
79
|
+
*
|
|
80
|
+
* This is the whole safety property of the module. A missing composite index
|
|
81
|
+
* surfaces as `FAILED_PRECONDITION`, which every other counter path in this
|
|
82
|
+
* codebase swallows with `.catch(() => undefined)`; swallowed to a zero here
|
|
83
|
+
* it would write "no installs" over every listing on the platform in a single
|
|
84
|
+
* TTL window, and the write-back would make it durable. So the failure value
|
|
85
|
+
* is a distinct one that means "not counted" and that no caller can mistake
|
|
86
|
+
* for a count.
|
|
87
|
+
*/ function reportUncountable(listingId, what, error) {
|
|
88
|
+
if (isMissingIndex(error)) {
|
|
89
|
+
var _ref;
|
|
90
|
+
console.error(`[marketplace] install pin ${what} for ${listingId} could not run: ` + 'the Firestore index is MISSING. Install counts are falling back to ' + 'the stored accumulators, which drift. Deploy the `installs` entries ' + 'in cloud/firebase-firestore.indexes.json ' + '(firebase deploy --only firestore:indexes --config cloud/firebase.json). ' + String((_ref = error == null ? void 0 : error.message) != null ? _ref : error));
|
|
91
|
+
} else {
|
|
92
|
+
console.error(`[marketplace] install pin ${what} for ${listingId} failed`, error);
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Live pins for one listing, or `null` when the count could not be taken.
|
|
98
|
+
*
|
|
99
|
+
* Needs a `COLLECTION_GROUP_ASC` single-field index on `installs.listingId`
|
|
100
|
+
* (a `fieldOverrides` entry — Firestore auto-creates single-field indexes at
|
|
101
|
+
* COLLECTION scope only, never at COLLECTION_GROUP scope).
|
|
102
|
+
*/ export async function countLivePins(firestore, listingId) {
|
|
103
|
+
try {
|
|
104
|
+
var _ref;
|
|
105
|
+
var _snapshot_data;
|
|
106
|
+
const snapshot = await firestore.collectionGroup('installs').where('listingId', '==', listingId).count().get();
|
|
107
|
+
const count = Number((_ref = (_snapshot_data = snapshot.data()) == null ? void 0 : _snapshot_data.count) != null ? _ref : Number.NaN);
|
|
108
|
+
return Number.isFinite(count) && count >= 0 ? Math.floor(count) : null;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return reportUncountable(listingId, 'count', error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Live pins per version, or `null` when the split could not be taken.
|
|
115
|
+
*
|
|
116
|
+
* One aggregation per version, which is bounded by the caller's `limit(20)` on
|
|
117
|
+
* the version history — and paid once per TTL window, not once per request.
|
|
118
|
+
* A composite `listingId ASC, version ASC` collection-group index covers it.
|
|
119
|
+
*
|
|
120
|
+
* `null` and "every version zero" are different answers and the caller treats
|
|
121
|
+
* them differently, so a single failing member fails the whole split rather
|
|
122
|
+
* than reporting a hole as a zero.
|
|
123
|
+
*/ export async function countLivePinsByVersion(firestore, listingId, versionIds) {
|
|
124
|
+
const wanted = [
|
|
125
|
+
...new Set(versionIds.filter(Boolean))
|
|
126
|
+
];
|
|
127
|
+
if (!wanted.length) return new Map();
|
|
128
|
+
try {
|
|
129
|
+
const counts = await Promise.all(wanted.map(async (version)=>{
|
|
130
|
+
var _ref;
|
|
131
|
+
var _snapshot_data;
|
|
132
|
+
const snapshot = await firestore.collectionGroup('installs').where('listingId', '==', listingId).where('version', '==', version).count().get();
|
|
133
|
+
const count = Number((_ref = (_snapshot_data = snapshot.data()) == null ? void 0 : _snapshot_data.count) != null ? _ref : Number.NaN);
|
|
134
|
+
if (!Number.isFinite(count) || count < 0) throw new Error('bad count');
|
|
135
|
+
return [
|
|
136
|
+
version,
|
|
137
|
+
Math.floor(count)
|
|
138
|
+
];
|
|
139
|
+
}));
|
|
140
|
+
return new Map(counts);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
return reportUncountable(listingId, 'per-version count', error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The count the page should print, derived from pins and cached in the listing.
|
|
147
|
+
*
|
|
148
|
+
* Returns `null` when there is no verified answer — a copied artifact, a stale
|
|
149
|
+
* cache the aggregation could not refresh, or a listing whose index is not
|
|
150
|
+
* deployed yet. `null` means "fall back to the stored accumulators", which is
|
|
151
|
+
* exactly AGL-1418's behaviour, so a missing index degrades to the previous
|
|
152
|
+
* release rather than to zeros.
|
|
153
|
+
*
|
|
154
|
+
* Read cost per request:
|
|
155
|
+
*
|
|
156
|
+
* * **Copied artifact** — 0. They install by copying and hold no pin, so a
|
|
157
|
+
* derived count would be a derived zero over a real number.
|
|
158
|
+
* * **Cache hit** — 0. The listing document was already read by the route.
|
|
159
|
+
* * **Cache miss** — 1 aggregation (~1 read per 1000 pins), plus one more per
|
|
160
|
+
* version only when the listing has more than one version, plus 1 write.
|
|
161
|
+
*
|
|
162
|
+
* The cache goes stale two ways. The TTL covers silent drift — a pin swept by
|
|
163
|
+
* a tenant erase, which decrements nothing. The `pinnedActiveInstalls`
|
|
164
|
+
* comparison covers real installs: every install and uninstall route moves
|
|
165
|
+
* `activeInstalls`, which breaks the pair immediately, so a genuine install
|
|
166
|
+
* shows on the next request instead of up to `PIN_VERIFY_TTL_MS` later. No
|
|
167
|
+
* install route had to learn about any of this.
|
|
168
|
+
*/ export async function verifiedLivePins(input) {
|
|
169
|
+
var _input_listingId, _input_nowMs, _input_listing, _listing_activeInstalls, _listing_pinsVerifiedAtMs, _failedUntilMs_get;
|
|
170
|
+
// Only plugins hold a pin. Every other artifact type installs by copying
|
|
171
|
+
// itself into the site, so `collectionGroup('installs')` finds nothing for
|
|
172
|
+
// it — and a derived 0 written back over a real `activeInstalls` would be
|
|
173
|
+
// this fix causing the exact class of damage it exists to repair.
|
|
174
|
+
if (input.artifactType !== 'plugin') return null;
|
|
175
|
+
const listingId = String((_input_listingId = input.listingId) != null ? _input_listingId : '');
|
|
176
|
+
if (!listingId) return null;
|
|
177
|
+
const nowMs = (_input_nowMs = input.nowMs) != null ? _input_nowMs : Date.now();
|
|
178
|
+
const listing = (_input_listing = input.listing) != null ? _input_listing : {};
|
|
179
|
+
const storedActive = Number((_listing_activeInstalls = listing['activeInstalls']) != null ? _listing_activeInstalls : 0);
|
|
180
|
+
const pinnedActive = listing['pinnedActiveInstalls'];
|
|
181
|
+
const verifiedAtMs = Number((_listing_pinsVerifiedAtMs = listing['pinsVerifiedAtMs']) != null ? _listing_pinsVerifiedAtMs : 0);
|
|
182
|
+
const fresh = typeof pinnedActive === 'number' && Number.isFinite(pinnedActive) && pinnedActive === storedActive && nowMs - verifiedAtMs < PIN_VERIFY_TTL_MS && nowMs >= verifiedAtMs;
|
|
183
|
+
if (fresh) {
|
|
184
|
+
const split = listing['pinnedVersionInstalls'];
|
|
185
|
+
return {
|
|
186
|
+
activeInstalls: pinnedActive,
|
|
187
|
+
byVersion: split && typeof split === 'object' ? new Map(Object.entries(split).map(([version, value])=>[
|
|
188
|
+
version,
|
|
189
|
+
Number(value != null ? value : 0)
|
|
190
|
+
])) : null
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const blockedUntil = (_failedUntilMs_get = failedUntilMs.get(listingId)) != null ? _failedUntilMs_get : 0;
|
|
194
|
+
if (nowMs < blockedUntil) return null;
|
|
195
|
+
const pending = inFlight.get(listingId);
|
|
196
|
+
if (pending) return pending;
|
|
197
|
+
const work = (async ()=>{
|
|
198
|
+
var _input_versionIds, _listing_installCount;
|
|
199
|
+
const total = await countLivePins(input.firestore, listingId);
|
|
200
|
+
if (total == null) {
|
|
201
|
+
failedUntilMs.set(listingId, nowMs + PIN_COUNT_BACKOFF_MS);
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
failedUntilMs.delete(listingId);
|
|
205
|
+
// One version needs no split: everything live is on it, which
|
|
206
|
+
// `reconcileInstallTallies` already proves rather than guesses. Skipping
|
|
207
|
+
// the query is the difference between 1 aggregation and 2 for the shape
|
|
208
|
+
// most listings actually have.
|
|
209
|
+
const versionIds = (_input_versionIds = input.versionIds) != null ? _input_versionIds : [];
|
|
210
|
+
const byVersion = versionIds.length > 1 ? await countLivePinsByVersion(input.firestore, listingId, versionIds) : null;
|
|
211
|
+
// Write the derived value back over the accumulator, which is what turns
|
|
212
|
+
// the stored counter into a self-healing cache instead of an independent
|
|
213
|
+
// number that only ever drifts further. Never on the critical path: a
|
|
214
|
+
// count that cannot be written must not fail the page.
|
|
215
|
+
//
|
|
216
|
+
// `lastUpdateTime` is the guard against the one race that matters — an
|
|
217
|
+
// install landing between the aggregation and this write, whose
|
|
218
|
+
// `increment()` an absolute value would erase. The precondition fails the
|
|
219
|
+
// write instead, and the next request re-derives.
|
|
220
|
+
const patch = {
|
|
221
|
+
activeInstalls: total,
|
|
222
|
+
pinnedActiveInstalls: total,
|
|
223
|
+
pinsVerifiedAtMs: nowMs
|
|
224
|
+
};
|
|
225
|
+
// All-time can never be below live: a pin that exists now is an install
|
|
226
|
+
// that landed. It stays an accumulator otherwise — an uninstall leaves no
|
|
227
|
+
// trace, so the pins cannot tell us how many there have ever been.
|
|
228
|
+
if (Number((_listing_installCount = listing['installCount']) != null ? _listing_installCount : 0) < total) patch['installCount'] = total;
|
|
229
|
+
if (byVersion) {
|
|
230
|
+
patch['pinnedVersionInstalls'] = Object.fromEntries(byVersion);
|
|
231
|
+
}
|
|
232
|
+
await input.listingRef.update(patch, input.updateTime ? {
|
|
233
|
+
lastUpdateTime: input.updateTime
|
|
234
|
+
} : undefined).catch(()=>undefined);
|
|
235
|
+
return {
|
|
236
|
+
activeInstalls: total,
|
|
237
|
+
byVersion
|
|
238
|
+
};
|
|
239
|
+
})();
|
|
240
|
+
inFlight.set(listingId, work);
|
|
241
|
+
try {
|
|
242
|
+
return await work;
|
|
243
|
+
} finally{
|
|
244
|
+
inFlight.delete(listingId);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
//# sourceMappingURL=install-pin-counts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/marketplace/src/lib/server/install-pin-counts.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { MarketplaceArtifactType } from '../model/marketplace'\nimport type { LivePinTally } from './version-stats'\n\n/**\n * Install counts derived from the pins, which are the only ground truth\n * (AGL-1419).\n *\n * AGL-1418 made the page internally consistent and said so honestly: the\n * stored numbers were still wrong, because both stored levels are\n * accumulators and an accumulator can only fail downwards from a missed\n * increment — or upwards forever from a pin that disappeared without one. Six\n * of eight production listings disagreed with their real pin counts;\n * `z6glT_UDAQ` advertised `3 active` against two live pins, and nothing in the\n * product could ever have noticed.\n *\n * Nothing decrements when a pin dies out of band, and there are three ways for\n * that to happen: `recursiveDelete` on a tenant erase (`erase.ts` never names\n * `installs`, it just sweeps the subtree), the console's client-side host-pin\n * delete in the promote-to-org flow, and any cascade. So the derived count is\n * not a nicety — it is the only thing that can bring a count back DOWN.\n *\n * ## Why a collection group here, when three other places refuse one\n *\n * `reviews.ts` and `listing-content.component.tsx` both avoid\n * `collectionGroup('installs')`, and both are right to: they ask \"which of MY\n * org's sites has this pinned\", and a cross-tenant scan to answer a\n * single-org question is unbounded work plus pins the caller must never see.\n *\n * This asks the opposite question — \"how many pins exist anywhere\" — which is\n * cross-tenant by definition, and answers it with `count()`, which returns an\n * integer and never a document. No pin, no path, no tenant identity leaves\n * this function. It runs in the Admin SDK on a server route, so it is not a\n * rules question either.\n *\n * ## Cost\n *\n * Firestore bills an aggregation at roughly one document read per 1000 index\n * entries scanned, not one per document, so a listing with 500 installs costs\n * ~1 read rather than 500. That is the single biggest lever here, and it is\n * why counting pins on a buyer-facing page is affordable at all.\n *\n * On top of it, `verifiedLivePins` only issues the aggregation when the stored\n * copy is stale, and writes the answer back — so the ordinary request pays\n * ZERO extra reads and serves a number that was derived from the pins.\n */\n\n/** How long a derived count stays good without re-deriving it. */\nexport const PIN_VERIFY_TTL_MS = 15 * 60 * 1000\n\n/**\n * How long one process stops re-issuing an aggregation that failed.\n *\n * In-process on purpose. It exists to stop an error storm when the index is\n * missing, not to remember anything — a cold instance SHOULD try again and log\n * again, because the message names the index and is how anyone finds out.\n */\nexport const PIN_COUNT_BACKOFF_MS = 5 * 60 * 1000\n\n/** The listing fields this module maintains, on top of the two counters. */\nexport interface PinVerificationFields {\n /**\n * The last count derived from the pins. Kept beside `activeInstalls` rather\n * than instead of it so an accumulator write by an install or uninstall\n * route makes the pair disagree, which is what marks the cache stale — the\n * write paths invalidate this without knowing it exists.\n */\n pinnedActiveInstalls?: number | null\n /** The derived per-version split, so a cache hit serves the same breakdown. */\n pinnedVersionInstalls?: Record<string, number> | null\n pinsVerifiedAtMs?: number | null\n}\n\ninterface CountableFirestore {\n collectionGroup: (id: string) => {\n where: (\n field: string,\n op: '==',\n value: unknown,\n ) => {\n where: (\n field: string,\n op: '==',\n value: unknown,\n ) => { count: () => { get: () => Promise<{ data: () => { count?: unknown } }> } }\n count: () => { get: () => Promise<{ data: () => { count?: unknown } }> }\n }\n }\n}\n\n/** Retried aggregations are pointless while the index is still missing. */\nconst failedUntilMs = new Map<string, number>()\n/** One aggregation per listing per process, however many requests arrive. */\nconst inFlight = new Map<string, Promise<LivePinTally | null>>()\n\n/** Exported for tests: a module-level cache outlives a `jest` test otherwise. */\nexport function resetPinCountBackoff(): void {\n failedUntilMs.clear()\n inFlight.clear()\n}\n\nconst isMissingIndex = (error: unknown): boolean => {\n const code = (error as { code?: unknown })?.code\n const message = String((error as { message?: unknown })?.message ?? '')\n return (\n code === 9 ||\n code === 'failed-precondition' ||\n /FAILED_PRECONDITION|requires an index|requires a COLLECTION_GROUP/i.test(\n message,\n )\n )\n}\n\n/**\n * Turns a failed aggregation into `null`, LOUDLY — and never into `0`.\n *\n * This is the whole safety property of the module. A missing composite index\n * surfaces as `FAILED_PRECONDITION`, which every other counter path in this\n * codebase swallows with `.catch(() => undefined)`; swallowed to a zero here\n * it would write \"no installs\" over every listing on the platform in a single\n * TTL window, and the write-back would make it durable. So the failure value\n * is a distinct one that means \"not counted\" and that no caller can mistake\n * for a count.\n */\nfunction reportUncountable(listingId: string, what: string, error: unknown): null {\n if (isMissingIndex(error)) {\n console.error(\n `[marketplace] install pin ${what} for ${listingId} could not run: ` +\n 'the Firestore index is MISSING. Install counts are falling back to ' +\n 'the stored accumulators, which drift. Deploy the `installs` entries ' +\n 'in cloud/firebase-firestore.indexes.json ' +\n '(firebase deploy --only firestore:indexes --config cloud/firebase.json). ' +\n String((error as { message?: unknown })?.message ?? error),\n )\n } else {\n console.error(\n `[marketplace] install pin ${what} for ${listingId} failed`,\n error,\n )\n }\n return null\n}\n\n/**\n * Live pins for one listing, or `null` when the count could not be taken.\n *\n * Needs a `COLLECTION_GROUP_ASC` single-field index on `installs.listingId`\n * (a `fieldOverrides` entry — Firestore auto-creates single-field indexes at\n * COLLECTION scope only, never at COLLECTION_GROUP scope).\n */\nexport async function countLivePins(\n firestore: CountableFirestore,\n listingId: string,\n): Promise<number | null> {\n try {\n const snapshot = await firestore\n .collectionGroup('installs')\n .where('listingId', '==', listingId)\n .count()\n .get()\n const count = Number(snapshot.data()?.count ?? Number.NaN)\n return Number.isFinite(count) && count >= 0 ? Math.floor(count) : null\n } catch (error) {\n return reportUncountable(listingId, 'count', error)\n }\n}\n\n/**\n * Live pins per version, or `null` when the split could not be taken.\n *\n * One aggregation per version, which is bounded by the caller's `limit(20)` on\n * the version history — and paid once per TTL window, not once per request.\n * A composite `listingId ASC, version ASC` collection-group index covers it.\n *\n * `null` and \"every version zero\" are different answers and the caller treats\n * them differently, so a single failing member fails the whole split rather\n * than reporting a hole as a zero.\n */\nexport async function countLivePinsByVersion(\n firestore: CountableFirestore,\n listingId: string,\n versionIds: string[],\n): Promise<Map<string, number> | null> {\n const wanted = [...new Set(versionIds.filter(Boolean))]\n if (!wanted.length) return new Map()\n try {\n const counts = await Promise.all(\n wanted.map(async (version) => {\n const snapshot = await firestore\n .collectionGroup('installs')\n .where('listingId', '==', listingId)\n .where('version', '==', version)\n .count()\n .get()\n const count = Number(snapshot.data()?.count ?? Number.NaN)\n if (!Number.isFinite(count) || count < 0) throw new Error('bad count')\n return [version, Math.floor(count)] as const\n }),\n )\n return new Map(counts)\n } catch (error) {\n return reportUncountable(listingId, 'per-version count', error)\n }\n}\n\nexport interface VerifyLivePinsInput {\n firestore: CountableFirestore\n listingRef: {\n update: (\n data: Record<string, unknown>,\n precondition?: { lastUpdateTime?: unknown },\n ) => Promise<unknown>\n }\n listingId: string\n /** The listing document as already read by the route. Never re-read here. */\n listing: Record<string, unknown> | undefined | null\n artifactType: MarketplaceArtifactType | string | null | undefined\n /** Version ids in the history, for the per-version split. */\n versionIds?: string[]\n /**\n * `snapshot.updateTime`, so the write-back cannot clobber an install that\n * landed between the read and the write. Omit to write unconditionally.\n */\n updateTime?: unknown\n nowMs?: number\n}\n\n/**\n * The count the page should print, derived from pins and cached in the listing.\n *\n * Returns `null` when there is no verified answer — a copied artifact, a stale\n * cache the aggregation could not refresh, or a listing whose index is not\n * deployed yet. `null` means \"fall back to the stored accumulators\", which is\n * exactly AGL-1418's behaviour, so a missing index degrades to the previous\n * release rather than to zeros.\n *\n * Read cost per request:\n *\n * * **Copied artifact** — 0. They install by copying and hold no pin, so a\n * derived count would be a derived zero over a real number.\n * * **Cache hit** — 0. The listing document was already read by the route.\n * * **Cache miss** — 1 aggregation (~1 read per 1000 pins), plus one more per\n * version only when the listing has more than one version, plus 1 write.\n *\n * The cache goes stale two ways. The TTL covers silent drift — a pin swept by\n * a tenant erase, which decrements nothing. The `pinnedActiveInstalls`\n * comparison covers real installs: every install and uninstall route moves\n * `activeInstalls`, which breaks the pair immediately, so a genuine install\n * shows on the next request instead of up to `PIN_VERIFY_TTL_MS` later. No\n * install route had to learn about any of this.\n */\nexport async function verifiedLivePins(\n input: VerifyLivePinsInput,\n): Promise<LivePinTally | null> {\n // Only plugins hold a pin. Every other artifact type installs by copying\n // itself into the site, so `collectionGroup('installs')` finds nothing for\n // it — and a derived 0 written back over a real `activeInstalls` would be\n // this fix causing the exact class of damage it exists to repair.\n if (input.artifactType !== 'plugin') return null\n const listingId = String(input.listingId ?? '')\n if (!listingId) return null\n\n const nowMs = input.nowMs ?? Date.now()\n const listing = input.listing ?? {}\n const storedActive = Number(listing['activeInstalls'] ?? 0)\n const pinnedActive = listing['pinnedActiveInstalls']\n const verifiedAtMs = Number(listing['pinsVerifiedAtMs'] ?? 0)\n const fresh =\n typeof pinnedActive === 'number' &&\n Number.isFinite(pinnedActive) &&\n pinnedActive === storedActive &&\n nowMs - verifiedAtMs < PIN_VERIFY_TTL_MS &&\n nowMs >= verifiedAtMs\n if (fresh) {\n const split = listing['pinnedVersionInstalls']\n return {\n activeInstalls: pinnedActive,\n byVersion:\n split && typeof split === 'object'\n ? new Map(\n Object.entries(split as Record<string, unknown>).map(\n ([version, value]) => [version, Number(value ?? 0)],\n ),\n )\n : null,\n }\n }\n\n const blockedUntil = failedUntilMs.get(listingId) ?? 0\n if (nowMs < blockedUntil) return null\n\n const pending = inFlight.get(listingId)\n if (pending) return pending\n const work = (async (): Promise<LivePinTally | null> => {\n const total = await countLivePins(input.firestore, listingId)\n if (total == null) {\n failedUntilMs.set(listingId, nowMs + PIN_COUNT_BACKOFF_MS)\n return null\n }\n failedUntilMs.delete(listingId)\n // One version needs no split: everything live is on it, which\n // `reconcileInstallTallies` already proves rather than guesses. Skipping\n // the query is the difference between 1 aggregation and 2 for the shape\n // most listings actually have.\n const versionIds = input.versionIds ?? []\n const byVersion =\n versionIds.length > 1\n ? await countLivePinsByVersion(input.firestore, listingId, versionIds)\n : null\n\n // Write the derived value back over the accumulator, which is what turns\n // the stored counter into a self-healing cache instead of an independent\n // number that only ever drifts further. Never on the critical path: a\n // count that cannot be written must not fail the page.\n //\n // `lastUpdateTime` is the guard against the one race that matters — an\n // install landing between the aggregation and this write, whose\n // `increment()` an absolute value would erase. The precondition fails the\n // write instead, and the next request re-derives.\n const patch: Record<string, unknown> = {\n activeInstalls: total,\n pinnedActiveInstalls: total,\n pinsVerifiedAtMs: nowMs,\n }\n // All-time can never be below live: a pin that exists now is an install\n // that landed. It stays an accumulator otherwise — an uninstall leaves no\n // trace, so the pins cannot tell us how many there have ever been.\n if (Number(listing['installCount'] ?? 0) < total) patch['installCount'] = total\n if (byVersion) {\n patch['pinnedVersionInstalls'] = Object.fromEntries(byVersion)\n }\n await input.listingRef\n .update(\n patch,\n input.updateTime ? { lastUpdateTime: input.updateTime } : undefined,\n )\n .catch(() => undefined)\n return { activeInstalls: total, byVersion }\n })()\n inFlight.set(listingId, work)\n try {\n return await work\n } finally {\n inFlight.delete(listingId)\n }\n}\n"],"names":["PIN_VERIFY_TTL_MS","PIN_COUNT_BACKOFF_MS","failedUntilMs","Map","inFlight","resetPinCountBackoff","clear","isMissingIndex","error","code","message","String","test","reportUncountable","listingId","what","console","countLivePins","firestore","snapshot","collectionGroup","where","count","get","Number","data","NaN","isFinite","Math","floor","countLivePinsByVersion","versionIds","wanted","Set","filter","Boolean","length","counts","Promise","all","map","version","Error","verifiedLivePins","input","listing","artifactType","nowMs","Date","now","storedActive","pinnedActive","verifiedAtMs","fresh","split","activeInstalls","byVersion","Object","entries","value","blockedUntil","pending","work","total","set","delete","patch","pinnedActiveInstalls","pinsVerifiedAtMs","fromEntries","listingRef","update","updateTime","lastUpdateTime","undefined","catch"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
18
|
+
export declare const installPluginHandler: PluginApiHandler;
|