@rotorsoft/act-pg 0.22.0 → 0.24.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/dist/.tsbuildinfo +1 -1
- package/dist/@types/postgres-store.d.ts +66 -3
- package/dist/@types/postgres-store.d.ts.map +1 -1
- package/dist/index.cjs +248 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +248 -28
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BlockedLease, Committed, EventMeta, Lease, Message, NotifyDisposer,
|
|
1
|
+
import type { BlockedLease, Committed, EventMeta, Lease, Message, NotifyDisposer, Query, QueryStatsOptions, QueryStreams, QueryStreamsResult, Schema, Schemas, Store, StoreNotification, StreamFilter, StreamPosition, StreamStats } from "@rotorsoft/act";
|
|
2
2
|
import pg from "pg";
|
|
3
3
|
type Config = Readonly<{
|
|
4
4
|
schema: string;
|
|
@@ -275,7 +275,30 @@ export declare class PostgresStore implements Store {
|
|
|
275
275
|
* @param streams - Stream names to reset.
|
|
276
276
|
* @returns Count of streams that were actually reset.
|
|
277
277
|
*/
|
|
278
|
-
|
|
278
|
+
/**
|
|
279
|
+
* Translate a {@link StreamFilter} to a `WHERE` clause fragment and
|
|
280
|
+
* the corresponding parameter values. The fragment never starts with
|
|
281
|
+
* `WHERE` — callers compose it with any other predicates they need.
|
|
282
|
+
* Returns an always-true clause (`true`) when the filter is empty.
|
|
283
|
+
*/
|
|
284
|
+
private _filterClause;
|
|
285
|
+
reset(input: string[] | StreamFilter): Promise<number>;
|
|
286
|
+
/**
|
|
287
|
+
* Clear blocked flag (and retry / error / lease state) on streams
|
|
288
|
+
* without touching the `at` watermark. `blocked = true` is always
|
|
289
|
+
* applied, so the return count reflects only streams that were
|
|
290
|
+
* actually flipped — already-unblocked rows, unknown streams, and
|
|
291
|
+
* filter matches that aren't blocked are silently skipped.
|
|
292
|
+
*
|
|
293
|
+
* `retry = -1` matches the InMemoryStore convention: claim() bumps
|
|
294
|
+
* retry on every acquisition, so storing -1 means the first claim
|
|
295
|
+
* after unblock returns retry=0 ("first attempt"). Storing 0 would
|
|
296
|
+
* mis-report the post-recovery attempt as a continuation of the
|
|
297
|
+
* failed sequence. See {@link Store.unblock}.
|
|
298
|
+
*
|
|
299
|
+
* @returns Count of streams that were actually flipped (were blocked).
|
|
300
|
+
*/
|
|
301
|
+
unblock(input: string[] | StreamFilter): Promise<number>;
|
|
279
302
|
/**
|
|
280
303
|
* Bulk-update priority of streams matching `filter` (ACT-102).
|
|
281
304
|
*
|
|
@@ -290,7 +313,7 @@ export declare class PostgresStore implements Store {
|
|
|
290
313
|
*
|
|
291
314
|
* @returns Count of streams whose priority changed.
|
|
292
315
|
*/
|
|
293
|
-
prioritize(filter:
|
|
316
|
+
prioritize(filter: StreamFilter, priority: number): Promise<number>;
|
|
294
317
|
/**
|
|
295
318
|
* Streams subscription positions to a callback, ordered by stream name,
|
|
296
319
|
* along with the highest event id in the store.
|
|
@@ -302,6 +325,46 @@ export declare class PostgresStore implements Store {
|
|
|
302
325
|
* @returns `maxEventId` and the `count` of positions emitted.
|
|
303
326
|
*/
|
|
304
327
|
query_streams(callback: (position: StreamPosition) => void, query?: QueryStreams): Promise<QueryStreamsResult>;
|
|
328
|
+
/**
|
|
329
|
+
* Per-stream aggregated stats — see {@link Store.query_stats}.
|
|
330
|
+
*
|
|
331
|
+
* Two code paths chosen by the requested stats:
|
|
332
|
+
*
|
|
333
|
+
* - **Heads-only path** (no `count`, no `names`): one or two
|
|
334
|
+
* `SELECT DISTINCT ON (stream) ... ORDER BY stream, version DESC|ASC`
|
|
335
|
+
* queries, executed in parallel when `tail: true`. The
|
|
336
|
+
* `(stream, version)` unique index gives index-only access — K rows
|
|
337
|
+
* touched per query (K = matched streams), not N (events).
|
|
338
|
+
* Ordering by `version` (not `id`) is equivalent within a stream
|
|
339
|
+
* (versions are monotonic per stream and events are committed
|
|
340
|
+
* sequentially) and is the column actually indexed.
|
|
341
|
+
*
|
|
342
|
+
* - **Full-scan path** (`count` or `names` set): one CTE materializes
|
|
343
|
+
* the filtered events, then `GROUP BY stream, name` →
|
|
344
|
+
* `jsonb_object_agg(name, n)` for the `names` map plus per-stream
|
|
345
|
+
* `COUNT(*)` for `count`. Heads (and `tails` when requested) come
|
|
346
|
+
* from `DISTINCT ON` over the same CTE — they ride free on the
|
|
347
|
+
* already-paid scan.
|
|
348
|
+
*
|
|
349
|
+
* The stream universe is derived from the events table: filter form
|
|
350
|
+
* matches event-bearing streams (not subscription rows). When the
|
|
351
|
+
* filter sets `source` or `blocked`, the events table is joined
|
|
352
|
+
* against the streams subscription table since those concepts only
|
|
353
|
+
* exist for subscribed streams.
|
|
354
|
+
*/
|
|
355
|
+
query_stats<E extends Schemas>(input: string[] | Pick<StreamFilter, "stream" | "stream_exact">, options?: QueryStatsOptions<E>): Promise<Map<string, StreamStats<E>>>;
|
|
356
|
+
/**
|
|
357
|
+
* Cheap path: index-only DISTINCT ON for the head per stream, plus an
|
|
358
|
+
* optional second query (in parallel) for the tail. K rows touched
|
|
359
|
+
* per query, not N events.
|
|
360
|
+
*/
|
|
361
|
+
private _queryStatsHeadsOnly;
|
|
362
|
+
/**
|
|
363
|
+
* Full-scan path: one CTE-based query computes the per-stream
|
|
364
|
+
* `COUNT(*)` and `jsonb_object_agg(name, n)` map alongside the head
|
|
365
|
+
* (and tail when requested). All extras share the single events scan.
|
|
366
|
+
*/
|
|
367
|
+
private _queryStatsFullScan;
|
|
305
368
|
/**
|
|
306
369
|
* Implementation of the optional `Store.notify` hook. Bound onto
|
|
307
370
|
* `this.notify` in the constructor when `config.notify === true`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/postgres-store.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,SAAS,EACT,KAAK,EAEL,OAAO,EACP,cAAc,EACd,
|
|
1
|
+
{"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/postgres-store.ts"],"names":[],"mappings":"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"}
|
package/dist/index.cjs
CHANGED
|
@@ -632,14 +632,89 @@ var PostgresStore = class {
|
|
|
632
632
|
* @param streams - Stream names to reset.
|
|
633
633
|
* @returns Count of streams that were actually reset.
|
|
634
634
|
*/
|
|
635
|
-
|
|
636
|
-
|
|
635
|
+
/**
|
|
636
|
+
* Translate a {@link StreamFilter} to a `WHERE` clause fragment and
|
|
637
|
+
* the corresponding parameter values. The fragment never starts with
|
|
638
|
+
* `WHERE` — callers compose it with any other predicates they need.
|
|
639
|
+
* Returns an always-true clause (`true`) when the filter is empty.
|
|
640
|
+
*/
|
|
641
|
+
_filterClause(filter, start) {
|
|
642
|
+
const conditions = [];
|
|
643
|
+
const values = [];
|
|
644
|
+
if (filter.stream !== void 0) {
|
|
645
|
+
values.push(filter.stream);
|
|
646
|
+
conditions.push(
|
|
647
|
+
filter.stream_exact ? `stream = $${start + values.length - 1}` : `stream ~ $${start + values.length - 1}`
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
if (filter.source !== void 0) {
|
|
651
|
+
conditions.push(`source IS NOT NULL`);
|
|
652
|
+
values.push(filter.source);
|
|
653
|
+
conditions.push(
|
|
654
|
+
filter.source_exact ? `source = $${start + values.length - 1}` : `source ~ $${start + values.length - 1}`
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
if (filter.blocked !== void 0) {
|
|
658
|
+
values.push(filter.blocked);
|
|
659
|
+
conditions.push(`blocked = $${start + values.length - 1}`);
|
|
660
|
+
}
|
|
661
|
+
return {
|
|
662
|
+
clause: conditions.length ? conditions.join(" AND ") : "TRUE",
|
|
663
|
+
values
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
async reset(input) {
|
|
667
|
+
const setClause = `SET at = -1, retry = 0, blocked = false, error = NULL,
|
|
668
|
+
leased_by = NULL, leased_until = NULL`;
|
|
669
|
+
if (Array.isArray(input)) {
|
|
670
|
+
if (!input.length) return 0;
|
|
671
|
+
const { rowCount: rowCount2 } = await this._pool.query(
|
|
672
|
+
`UPDATE ${this._fqs} ${setClause} WHERE stream = ANY($1)`,
|
|
673
|
+
[input]
|
|
674
|
+
);
|
|
675
|
+
return rowCount2 ?? 0;
|
|
676
|
+
}
|
|
677
|
+
const { clause, values } = this._filterClause(input, 1);
|
|
678
|
+
const { rowCount } = await this._pool.query(
|
|
679
|
+
`UPDATE ${this._fqs} ${setClause} WHERE ${clause}`,
|
|
680
|
+
values
|
|
681
|
+
);
|
|
682
|
+
return rowCount ?? 0;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Clear blocked flag (and retry / error / lease state) on streams
|
|
686
|
+
* without touching the `at` watermark. `blocked = true` is always
|
|
687
|
+
* applied, so the return count reflects only streams that were
|
|
688
|
+
* actually flipped — already-unblocked rows, unknown streams, and
|
|
689
|
+
* filter matches that aren't blocked are silently skipped.
|
|
690
|
+
*
|
|
691
|
+
* `retry = -1` matches the InMemoryStore convention: claim() bumps
|
|
692
|
+
* retry on every acquisition, so storing -1 means the first claim
|
|
693
|
+
* after unblock returns retry=0 ("first attempt"). Storing 0 would
|
|
694
|
+
* mis-report the post-recovery attempt as a continuation of the
|
|
695
|
+
* failed sequence. See {@link Store.unblock}.
|
|
696
|
+
*
|
|
697
|
+
* @returns Count of streams that were actually flipped (were blocked).
|
|
698
|
+
*/
|
|
699
|
+
async unblock(input) {
|
|
700
|
+
const setClause = `SET retry = -1, blocked = false, error = NULL,
|
|
701
|
+
leased_by = NULL, leased_until = NULL`;
|
|
702
|
+
if (Array.isArray(input)) {
|
|
703
|
+
if (!input.length) return 0;
|
|
704
|
+
const { rowCount: rowCount2 } = await this._pool.query(
|
|
705
|
+
`UPDATE ${this._fqs} ${setClause}
|
|
706
|
+
WHERE stream = ANY($1) AND blocked = true`,
|
|
707
|
+
[input]
|
|
708
|
+
);
|
|
709
|
+
return rowCount2 ?? 0;
|
|
710
|
+
}
|
|
711
|
+
const { clause, values } = this._filterClause(
|
|
712
|
+
{ ...input, blocked: true },
|
|
713
|
+
1
|
|
714
|
+
);
|
|
637
715
|
const { rowCount } = await this._pool.query(
|
|
638
|
-
`UPDATE ${this._fqs}
|
|
639
|
-
|
|
640
|
-
leased_by = NULL, leased_until = NULL
|
|
641
|
-
WHERE stream = ANY($1)`,
|
|
642
|
-
[streams]
|
|
716
|
+
`UPDATE ${this._fqs} ${setClause} WHERE ${clause}`,
|
|
717
|
+
values
|
|
643
718
|
);
|
|
644
719
|
return rowCount ?? 0;
|
|
645
720
|
}
|
|
@@ -658,27 +733,10 @@ var PostgresStore = class {
|
|
|
658
733
|
* @returns Count of streams whose priority changed.
|
|
659
734
|
*/
|
|
660
735
|
async prioritize(filter, priority) {
|
|
661
|
-
const
|
|
662
|
-
const
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
conditions.push(
|
|
666
|
-
filter.stream_exact ? `stream = $${values.length}` : `stream ~ $${values.length}`
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
if (filter.source !== void 0) {
|
|
670
|
-
conditions.push(`source IS NOT NULL`);
|
|
671
|
-
values.push(filter.source);
|
|
672
|
-
conditions.push(
|
|
673
|
-
filter.source_exact ? `source = $${values.length}` : `source ~ $${values.length}`
|
|
674
|
-
);
|
|
675
|
-
}
|
|
676
|
-
if (filter.blocked !== void 0) {
|
|
677
|
-
values.push(filter.blocked);
|
|
678
|
-
conditions.push(`blocked = $${values.length}`);
|
|
679
|
-
}
|
|
680
|
-
const sql = `UPDATE ${this._fqs} SET priority = $1 WHERE ${conditions.join(" AND ")}`;
|
|
681
|
-
const { rowCount } = await this._pool.query(sql, values);
|
|
736
|
+
const { clause, values } = this._filterClause(filter, 2);
|
|
737
|
+
const sql = `UPDATE ${this._fqs} SET priority = $1
|
|
738
|
+
WHERE priority <> $1 AND ${clause}`;
|
|
739
|
+
const { rowCount } = await this._pool.query(sql, [priority, ...values]);
|
|
682
740
|
return rowCount ?? 0;
|
|
683
741
|
}
|
|
684
742
|
/**
|
|
@@ -748,6 +806,168 @@ var PostgresStore = class {
|
|
|
748
806
|
client.release();
|
|
749
807
|
}
|
|
750
808
|
}
|
|
809
|
+
/**
|
|
810
|
+
* Per-stream aggregated stats — see {@link Store.query_stats}.
|
|
811
|
+
*
|
|
812
|
+
* Two code paths chosen by the requested stats:
|
|
813
|
+
*
|
|
814
|
+
* - **Heads-only path** (no `count`, no `names`): one or two
|
|
815
|
+
* `SELECT DISTINCT ON (stream) ... ORDER BY stream, version DESC|ASC`
|
|
816
|
+
* queries, executed in parallel when `tail: true`. The
|
|
817
|
+
* `(stream, version)` unique index gives index-only access — K rows
|
|
818
|
+
* touched per query (K = matched streams), not N (events).
|
|
819
|
+
* Ordering by `version` (not `id`) is equivalent within a stream
|
|
820
|
+
* (versions are monotonic per stream and events are committed
|
|
821
|
+
* sequentially) and is the column actually indexed.
|
|
822
|
+
*
|
|
823
|
+
* - **Full-scan path** (`count` or `names` set): one CTE materializes
|
|
824
|
+
* the filtered events, then `GROUP BY stream, name` →
|
|
825
|
+
* `jsonb_object_agg(name, n)` for the `names` map plus per-stream
|
|
826
|
+
* `COUNT(*)` for `count`. Heads (and `tails` when requested) come
|
|
827
|
+
* from `DISTINCT ON` over the same CTE — they ride free on the
|
|
828
|
+
* already-paid scan.
|
|
829
|
+
*
|
|
830
|
+
* The stream universe is derived from the events table: filter form
|
|
831
|
+
* matches event-bearing streams (not subscription rows). When the
|
|
832
|
+
* filter sets `source` or `blocked`, the events table is joined
|
|
833
|
+
* against the streams subscription table since those concepts only
|
|
834
|
+
* exist for subscribed streams.
|
|
835
|
+
*/
|
|
836
|
+
async query_stats(input, options) {
|
|
837
|
+
const exclude = options?.exclude ?? [];
|
|
838
|
+
const wantTail = options?.tail ?? false;
|
|
839
|
+
const wantCount = options?.count ?? false;
|
|
840
|
+
const wantNames = options?.names ?? false;
|
|
841
|
+
const before = options?.before;
|
|
842
|
+
const fullScan = wantCount || wantNames;
|
|
843
|
+
if (Array.isArray(input) && input.length === 0) {
|
|
844
|
+
return /* @__PURE__ */ new Map();
|
|
845
|
+
}
|
|
846
|
+
const where = [];
|
|
847
|
+
const params = [];
|
|
848
|
+
if (Array.isArray(input)) {
|
|
849
|
+
params.push(input);
|
|
850
|
+
where.push(`e.stream = ANY($${params.length})`);
|
|
851
|
+
} else if (input.stream !== void 0) {
|
|
852
|
+
params.push(input.stream);
|
|
853
|
+
where.push(
|
|
854
|
+
input.stream_exact ? `e.stream = $${params.length}` : `e.stream ~ $${params.length}`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
if (exclude.length) {
|
|
858
|
+
params.push(exclude);
|
|
859
|
+
where.push(`e.name <> ALL($${params.length})`);
|
|
860
|
+
}
|
|
861
|
+
if (before !== void 0) {
|
|
862
|
+
params.push(before);
|
|
863
|
+
where.push(`e.id < $${params.length}`);
|
|
864
|
+
}
|
|
865
|
+
const fromClause = `${this._fqt} e`;
|
|
866
|
+
const whereClause = `WHERE ${where.length ? where.join(" AND ") : "TRUE"}`;
|
|
867
|
+
return fullScan ? this._queryStatsFullScan(
|
|
868
|
+
fromClause,
|
|
869
|
+
whereClause,
|
|
870
|
+
params,
|
|
871
|
+
wantTail,
|
|
872
|
+
wantCount,
|
|
873
|
+
wantNames
|
|
874
|
+
) : this._queryStatsHeadsOnly(fromClause, whereClause, params, wantTail);
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* Cheap path: index-only DISTINCT ON for the head per stream, plus an
|
|
878
|
+
* optional second query (in parallel) for the tail. K rows touched
|
|
879
|
+
* per query, not N events.
|
|
880
|
+
*/
|
|
881
|
+
async _queryStatsHeadsOnly(fromClause, whereClause, params, wantTail) {
|
|
882
|
+
const cols = `e.id, e.stream, e.version, e.name, e.data, e.created, e.meta`;
|
|
883
|
+
const headSql = `SELECT DISTINCT ON (e.stream) ${cols} FROM ${fromClause} ${whereClause} ORDER BY e.stream, e.version DESC`;
|
|
884
|
+
const tailSql = wantTail ? `SELECT DISTINCT ON (e.stream) ${cols} FROM ${fromClause} ${whereClause} ORDER BY e.stream, e.version ASC` : null;
|
|
885
|
+
const [headRes, tailRes] = await Promise.all([
|
|
886
|
+
this._pool.query(headSql, params),
|
|
887
|
+
tailSql ? this._pool.query(tailSql, params) : Promise.resolve(null)
|
|
888
|
+
]);
|
|
889
|
+
const out = /* @__PURE__ */ new Map();
|
|
890
|
+
for (const row of headRes.rows) {
|
|
891
|
+
out.set(row.stream, { head: row });
|
|
892
|
+
}
|
|
893
|
+
if (tailRes) {
|
|
894
|
+
for (const row of tailRes.rows) {
|
|
895
|
+
out.get(row.stream).tail = row;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return out;
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Full-scan path: one CTE-based query computes the per-stream
|
|
902
|
+
* `COUNT(*)` and `jsonb_object_agg(name, n)` map alongside the head
|
|
903
|
+
* (and tail when requested). All extras share the single events scan.
|
|
904
|
+
*/
|
|
905
|
+
async _queryStatsFullScan(fromClause, whereClause, params, wantTail, wantCount, wantNames) {
|
|
906
|
+
const tailCte = wantTail ? `, tails AS (SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version ASC)` : "";
|
|
907
|
+
const tailJoin = wantTail ? `LEFT JOIN tails t ON t.stream = h.stream` : "";
|
|
908
|
+
const tailCols = wantTail ? `, t.id AS t_id, t.stream AS t_stream, t.version AS t_version,
|
|
909
|
+
t.name AS t_name, t.data AS t_data, t.created AS t_created, t.meta AS t_meta` : "";
|
|
910
|
+
const sql = `
|
|
911
|
+
WITH ef AS (
|
|
912
|
+
SELECT e.id, e.stream, e.version, e.name, e.data, e.created, e.meta
|
|
913
|
+
FROM ${fromClause}
|
|
914
|
+
${whereClause}
|
|
915
|
+
),
|
|
916
|
+
agg AS (
|
|
917
|
+
SELECT stream,
|
|
918
|
+
SUM(n)::int AS cnt,
|
|
919
|
+
jsonb_object_agg(name, n) AS names
|
|
920
|
+
FROM (
|
|
921
|
+
SELECT stream, name, COUNT(*)::int AS n
|
|
922
|
+
FROM ef
|
|
923
|
+
GROUP BY stream, name
|
|
924
|
+
) t
|
|
925
|
+
GROUP BY stream
|
|
926
|
+
),
|
|
927
|
+
heads AS (
|
|
928
|
+
SELECT DISTINCT ON (stream) * FROM ef ORDER BY stream, version DESC
|
|
929
|
+
)
|
|
930
|
+
${tailCte}
|
|
931
|
+
SELECT
|
|
932
|
+
h.id, h.stream, h.version, h.name, h.data, h.created, h.meta,
|
|
933
|
+
a.cnt AS agg_count,
|
|
934
|
+
a.names AS agg_names
|
|
935
|
+
${tailCols}
|
|
936
|
+
FROM heads h
|
|
937
|
+
LEFT JOIN agg a ON a.stream = h.stream
|
|
938
|
+
${tailJoin}
|
|
939
|
+
`;
|
|
940
|
+
const res = await this._pool.query(sql, params);
|
|
941
|
+
const out = /* @__PURE__ */ new Map();
|
|
942
|
+
for (const row of res.rows) {
|
|
943
|
+
const stats = {
|
|
944
|
+
head: {
|
|
945
|
+
id: row.id,
|
|
946
|
+
stream: row.stream,
|
|
947
|
+
version: row.version,
|
|
948
|
+
name: row.name,
|
|
949
|
+
data: row.data,
|
|
950
|
+
created: row.created,
|
|
951
|
+
meta: row.meta
|
|
952
|
+
}
|
|
953
|
+
};
|
|
954
|
+
if (wantTail && row.t_id !== void 0 && row.t_id !== null) {
|
|
955
|
+
stats.tail = {
|
|
956
|
+
id: row.t_id,
|
|
957
|
+
stream: row.t_stream,
|
|
958
|
+
version: row.t_version,
|
|
959
|
+
name: row.t_name,
|
|
960
|
+
data: row.t_data,
|
|
961
|
+
created: row.t_created,
|
|
962
|
+
meta: row.t_meta
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
if (wantCount) stats.count = row.agg_count;
|
|
966
|
+
if (wantNames) stats.names = row.agg_names;
|
|
967
|
+
out.set(row.stream, stats);
|
|
968
|
+
}
|
|
969
|
+
return out;
|
|
970
|
+
}
|
|
751
971
|
/**
|
|
752
972
|
* Implementation of the optional `Store.notify` hook. Bound onto
|
|
753
973
|
* `this.notify` in the constructor when `config.notify === true`,
|