@comms-id/asx 0.4.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/browser-profile.d.ts +21 -0
- package/dist/browser-profile.js +115 -0
- package/dist/client-replies.d.ts +161 -0
- package/dist/client-replies.js +54 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.js +179 -0
- package/dist/collection.d.ts +555 -0
- package/dist/collection.js +194 -0
- package/dist/company-events.d.ts +2 -0
- package/dist/company-events.js +48 -0
- package/dist/company-record.d.ts +4 -0
- package/dist/company-record.js +58 -0
- package/dist/company-stream-client.d.ts +4 -0
- package/dist/company-stream-client.js +139 -0
- package/dist/company-stream.d.ts +46 -0
- package/dist/company-stream.js +117 -0
- package/dist/descriptor.d.ts +2 -2
- package/dist/descriptor.js +2 -2
- package/dist/directory-search.d.ts +2 -0
- package/dist/directory-search.js +33 -0
- package/dist/enrichment.d.ts +75 -0
- package/dist/enrichment.js +114 -0
- package/dist/generated/fixtures.js +2 -2
- package/dist/generated/operations.js +23 -4
- package/dist/generated/types.d.ts +2 -1
- package/dist/http.d.ts +1 -0
- package/dist/http.js +49 -0
- package/dist/live.d.ts +56 -0
- package/dist/live.js +129 -0
- package/dist/model.d.ts +112 -0
- package/dist/model.js +86 -0
- package/dist/parse.d.ts +4 -0
- package/dist/parse.js +130 -0
- package/dist/publication.d.ts +25 -0
- package/dist/publication.js +89 -0
- package/dist/refresh-coordinator.d.ts +96 -0
- package/dist/refresh-coordinator.js +194 -0
- package/dist/refresh.d.ts +126 -0
- package/dist/refresh.js +153 -0
- package/dist/runtime/http.js +8 -1
- package/dist/search-service.d.ts +16 -0
- package/dist/search-service.js +64 -0
- package/dist/service.d.ts +35 -0
- package/dist/service.js +99 -0
- package/dist/snapshot.d.ts +295 -0
- package/dist/snapshot.js +131 -0
- package/dist/source-record.d.ts +63 -0
- package/dist/source-record.js +91 -0
- package/package.json +1 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { failure } from "./model.js";
|
|
2
|
+
export function suggestDirectory(entries, query, limit) {
|
|
3
|
+
if (typeof query !== "string" ||
|
|
4
|
+
query.length > 200 ||
|
|
5
|
+
!Number.isInteger(limit) ||
|
|
6
|
+
limit < 1 ||
|
|
7
|
+
limit > 10)
|
|
8
|
+
throw failure("INVALID_INPUT");
|
|
9
|
+
const normalized = query.trim().toUpperCase();
|
|
10
|
+
if (normalized.length < 2)
|
|
11
|
+
return [];
|
|
12
|
+
const tokens = normalized.split(/\s+/);
|
|
13
|
+
return entries
|
|
14
|
+
.map((directory) => {
|
|
15
|
+
const name = directory.companyName.toUpperCase();
|
|
16
|
+
const score = directory.ticker === normalized
|
|
17
|
+
? 0
|
|
18
|
+
: directory.ticker.startsWith(normalized)
|
|
19
|
+
? 1
|
|
20
|
+
: name.startsWith(normalized)
|
|
21
|
+
? 2
|
|
22
|
+
: tokens.every((token) => name.includes(token))
|
|
23
|
+
? 3
|
|
24
|
+
: 4;
|
|
25
|
+
return { directory, score };
|
|
26
|
+
})
|
|
27
|
+
.filter(({ score }) => score < 4)
|
|
28
|
+
.sort((a, b) => a.score - b.score ||
|
|
29
|
+
a.directory.companyName.localeCompare(b.directory.companyName, "en") ||
|
|
30
|
+
a.directory.ticker.localeCompare(b.directory.ticker, "en"))
|
|
31
|
+
.slice(0, limit)
|
|
32
|
+
.map(({ directory }) => structuredClone(directory));
|
|
33
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { Company } from "./model.js";
|
|
3
|
+
export declare function validAbn(abn: string): boolean;
|
|
4
|
+
export declare const Evidence: Schema.Struct<{
|
|
5
|
+
readonly url: Schema.String;
|
|
6
|
+
readonly sha256: Schema.String;
|
|
7
|
+
readonly observedAt: Schema.String;
|
|
8
|
+
readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
|
|
9
|
+
}>;
|
|
10
|
+
export interface Evidence extends Schema.Schema.Type<typeof Evidence> {
|
|
11
|
+
}
|
|
12
|
+
export declare const ConfirmedEnrichment: Schema.Struct<{
|
|
13
|
+
readonly ticker: Schema.String;
|
|
14
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
15
|
+
readonly abn: Schema.String;
|
|
16
|
+
readonly acn: Schema.NullOr<Schema.String>;
|
|
17
|
+
readonly legalName: Schema.NonEmptyString;
|
|
18
|
+
readonly identityEvidence: Schema.Struct<{
|
|
19
|
+
readonly url: Schema.String;
|
|
20
|
+
readonly sha256: Schema.String;
|
|
21
|
+
readonly observedAt: Schema.String;
|
|
22
|
+
readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
|
|
23
|
+
}>;
|
|
24
|
+
readonly abrEvidence: Schema.Struct<{
|
|
25
|
+
readonly source: Schema.Literals<readonly ["abr-live", "abr-snapshot"]>;
|
|
26
|
+
readonly observedAt: Schema.String;
|
|
27
|
+
}>;
|
|
28
|
+
readonly website: Schema.NullOr<Schema.Struct<{
|
|
29
|
+
readonly url: Schema.String;
|
|
30
|
+
readonly evidence: Schema.Struct<{
|
|
31
|
+
readonly url: Schema.String;
|
|
32
|
+
readonly sha256: Schema.String;
|
|
33
|
+
readonly observedAt: Schema.String;
|
|
34
|
+
readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
|
|
35
|
+
}>;
|
|
36
|
+
}>>;
|
|
37
|
+
}>;
|
|
38
|
+
export interface ConfirmedEnrichment extends Schema.Schema.Type<typeof ConfirmedEnrichment> {
|
|
39
|
+
}
|
|
40
|
+
export declare const EnrichmentResult: Schema.Union<readonly [Schema.Struct<{
|
|
41
|
+
readonly status: Schema.Literal<"confirmed">;
|
|
42
|
+
readonly enrichment: Schema.Struct<{
|
|
43
|
+
readonly ticker: Schema.String;
|
|
44
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
45
|
+
readonly abn: Schema.String;
|
|
46
|
+
readonly acn: Schema.NullOr<Schema.String>;
|
|
47
|
+
readonly legalName: Schema.NonEmptyString;
|
|
48
|
+
readonly identityEvidence: Schema.Struct<{
|
|
49
|
+
readonly url: Schema.String;
|
|
50
|
+
readonly sha256: Schema.String;
|
|
51
|
+
readonly observedAt: Schema.String;
|
|
52
|
+
readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
|
|
53
|
+
}>;
|
|
54
|
+
readonly abrEvidence: Schema.Struct<{
|
|
55
|
+
readonly source: Schema.Literals<readonly ["abr-live", "abr-snapshot"]>;
|
|
56
|
+
readonly observedAt: Schema.String;
|
|
57
|
+
}>;
|
|
58
|
+
readonly website: Schema.NullOr<Schema.Struct<{
|
|
59
|
+
readonly url: Schema.String;
|
|
60
|
+
readonly evidence: Schema.Struct<{
|
|
61
|
+
readonly url: Schema.String;
|
|
62
|
+
readonly sha256: Schema.String;
|
|
63
|
+
readonly observedAt: Schema.String;
|
|
64
|
+
readonly kind: Schema.Literals<readonly ["issuer-filing", "issuer-website"]>;
|
|
65
|
+
}>;
|
|
66
|
+
}>>;
|
|
67
|
+
}>;
|
|
68
|
+
}>, Schema.Struct<{
|
|
69
|
+
readonly status: Schema.Literal<"unresolved">;
|
|
70
|
+
readonly reason: Schema.Literals<readonly ["NO_EVIDENCE", "AMBIGUOUS", "INVALID_EVIDENCE", "IDENTITY_MISMATCH"]>;
|
|
71
|
+
}>]>;
|
|
72
|
+
export type EnrichmentResult = typeof EnrichmentResult.Type;
|
|
73
|
+
/** Inputs are accepted primary-source observations from trusted collectors, never public request data. */
|
|
74
|
+
export declare function confirmEnrichment(companyInput: unknown, issuerCandidates: unknown, abrInput: unknown): EnrichmentResult;
|
|
75
|
+
export declare function validEnrichment(value: ConfirmedEnrichment, company: Company): boolean;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { Result, Schema } from "effect";
|
|
2
|
+
import { Company, isTimestamp, Timestamp, Ticker } from "./model.js";
|
|
3
|
+
// ABR's published modulus-89 rule; checksum validity alone never proves issuer identity.
|
|
4
|
+
export function validAbn(abn) {
|
|
5
|
+
if (!/^\d{11}$/.test(abn))
|
|
6
|
+
return false;
|
|
7
|
+
const weights = [10, 1, 3, 5, 7, 9, 11, 13, 15, 17, 19];
|
|
8
|
+
return (Array.from(abn).reduce((sum, digit, index) => sum + (Number(digit) - (index === 0 ? 1 : 0)) * weights[index], 0) %
|
|
9
|
+
89 ===
|
|
10
|
+
0);
|
|
11
|
+
}
|
|
12
|
+
const ABN = Schema.String.check(Schema.makeFilter(validAbn));
|
|
13
|
+
const ACN = Schema.NullOr(Schema.String.check(Schema.isPattern(/^\d{9}$/)));
|
|
14
|
+
const HttpsUrl = Schema.String.check(Schema.makeFilter((value) => {
|
|
15
|
+
try {
|
|
16
|
+
const url = new URL(value);
|
|
17
|
+
return (url.protocol === "https:" &&
|
|
18
|
+
!url.username &&
|
|
19
|
+
!url.password &&
|
|
20
|
+
!!url.hostname &&
|
|
21
|
+
value.length <= 2048);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}));
|
|
27
|
+
export const Evidence = Schema.Struct({
|
|
28
|
+
url: HttpsUrl,
|
|
29
|
+
sha256: Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/)),
|
|
30
|
+
observedAt: Timestamp,
|
|
31
|
+
kind: Schema.Literals(["issuer-filing", "issuer-website"]),
|
|
32
|
+
});
|
|
33
|
+
export const ConfirmedEnrichment = Schema.Struct({
|
|
34
|
+
ticker: Ticker,
|
|
35
|
+
sourceId: Schema.NonEmptyString,
|
|
36
|
+
abn: ABN,
|
|
37
|
+
acn: ACN,
|
|
38
|
+
legalName: Schema.NonEmptyString,
|
|
39
|
+
identityEvidence: Evidence,
|
|
40
|
+
abrEvidence: Schema.Struct({
|
|
41
|
+
source: Schema.Literals(["abr-live", "abr-snapshot"]),
|
|
42
|
+
observedAt: Timestamp,
|
|
43
|
+
}),
|
|
44
|
+
website: Schema.NullOr(Schema.Struct({ url: HttpsUrl, evidence: Evidence })),
|
|
45
|
+
});
|
|
46
|
+
const IssuerEvidence = Schema.Struct({
|
|
47
|
+
ticker: Ticker,
|
|
48
|
+
sourceId: Schema.NonEmptyString,
|
|
49
|
+
issuerName: Schema.NonEmptyString,
|
|
50
|
+
abn: ABN,
|
|
51
|
+
acn: ACN,
|
|
52
|
+
evidence: Evidence,
|
|
53
|
+
website: Schema.NullOr(Schema.Struct({ url: HttpsUrl, evidence: Evidence })),
|
|
54
|
+
});
|
|
55
|
+
const AbrEvidence = Schema.Struct({
|
|
56
|
+
abn: ABN,
|
|
57
|
+
acn: ACN,
|
|
58
|
+
legalName: Schema.NonEmptyString,
|
|
59
|
+
source: Schema.Literals(["abr-live", "abr-snapshot"]),
|
|
60
|
+
observedAt: Timestamp,
|
|
61
|
+
});
|
|
62
|
+
export const EnrichmentResult = Schema.Union([
|
|
63
|
+
Schema.Struct({ status: Schema.Literal("confirmed"), enrichment: ConfirmedEnrichment }),
|
|
64
|
+
Schema.Struct({
|
|
65
|
+
status: Schema.Literal("unresolved"),
|
|
66
|
+
reason: Schema.Literals(["NO_EVIDENCE", "AMBIGUOUS", "INVALID_EVIDENCE", "IDENTITY_MISMATCH"]),
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
const nameKey = (value) => value.trim().toUpperCase().replace(/\s+/g, " ");
|
|
70
|
+
/** Inputs are accepted primary-source observations from trusted collectors, never public request data. */
|
|
71
|
+
export function confirmEnrichment(companyInput, issuerCandidates, abrInput) {
|
|
72
|
+
const company = Schema.decodeUnknownResult(Company)(companyInput);
|
|
73
|
+
const candidates = Schema.decodeUnknownResult(Schema.Array(IssuerEvidence))(issuerCandidates);
|
|
74
|
+
const abr = Schema.decodeUnknownResult(AbrEvidence)(abrInput);
|
|
75
|
+
if (Result.isFailure(company) || Result.isFailure(candidates) || Result.isFailure(abr))
|
|
76
|
+
return { status: "unresolved", reason: "INVALID_EVIDENCE" };
|
|
77
|
+
if (candidates.success.length === 0)
|
|
78
|
+
return { status: "unresolved", reason: "NO_EVIDENCE" };
|
|
79
|
+
if (candidates.success.length !== 1)
|
|
80
|
+
return { status: "unresolved", reason: "AMBIGUOUS" };
|
|
81
|
+
const issuer = candidates.success[0];
|
|
82
|
+
if (!isTimestamp(issuer.evidence.observedAt) ||
|
|
83
|
+
!isTimestamp(abr.success.observedAt) ||
|
|
84
|
+
(issuer.website && !isTimestamp(issuer.website.evidence.observedAt)))
|
|
85
|
+
return { status: "unresolved", reason: "INVALID_EVIDENCE" };
|
|
86
|
+
if (issuer.sourceId !== company.success.sourceId ||
|
|
87
|
+
issuer.ticker !== company.success.ticker ||
|
|
88
|
+
nameKey(issuer.issuerName) !== nameKey(company.success.companyName) ||
|
|
89
|
+
nameKey(issuer.issuerName) !== nameKey(abr.success.legalName) ||
|
|
90
|
+
issuer.abn !== abr.success.abn ||
|
|
91
|
+
(issuer.acn !== null && issuer.acn !== abr.success.acn))
|
|
92
|
+
return { status: "unresolved", reason: "IDENTITY_MISMATCH" };
|
|
93
|
+
return {
|
|
94
|
+
status: "confirmed",
|
|
95
|
+
enrichment: {
|
|
96
|
+
ticker: issuer.ticker,
|
|
97
|
+
sourceId: issuer.sourceId,
|
|
98
|
+
abn: issuer.abn,
|
|
99
|
+
acn: abr.success.acn,
|
|
100
|
+
legalName: abr.success.legalName,
|
|
101
|
+
identityEvidence: issuer.evidence,
|
|
102
|
+
abrEvidence: { source: abr.success.source, observedAt: abr.success.observedAt },
|
|
103
|
+
website: issuer.website,
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
export function validEnrichment(value, company) {
|
|
108
|
+
return (value.sourceId === company.sourceId &&
|
|
109
|
+
value.ticker === company.ticker &&
|
|
110
|
+
nameKey(value.legalName) === nameKey(company.companyName) &&
|
|
111
|
+
isTimestamp(value.identityEvidence.observedAt) &&
|
|
112
|
+
isTimestamp(value.abrEvidence.observedAt) &&
|
|
113
|
+
(value.website === null || isTimestamp(value.website.evidence.observedAt)));
|
|
114
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Every reply is the example in the contract.
|
|
3
3
|
/** LOCAL FIXTURES: one canned reply for each operation path. Fake data. */
|
|
4
4
|
export const fixtures = {
|
|
5
|
-
"/asx/
|
|
5
|
+
"/asx/v2/lookup": {
|
|
6
6
|
"status": "found",
|
|
7
7
|
"source": "live",
|
|
8
8
|
"observation": {
|
|
@@ -45,7 +45,7 @@ export const fixtures = {
|
|
|
45
45
|
"lastChangedAt": "2026-10-03T01:29:55.678Z"
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
|
-
"/asx/
|
|
48
|
+
"/asx/v2/suggest": {
|
|
49
49
|
"source": "live",
|
|
50
50
|
"directoryObservedAt": "2026-10-03T00:00:00.000Z",
|
|
51
51
|
"suggestions": []
|
|
@@ -4,7 +4,7 @@ export const operations = {
|
|
|
4
4
|
lookup: {
|
|
5
5
|
name: "lookup",
|
|
6
6
|
method: "POST",
|
|
7
|
-
path: "/asx/
|
|
7
|
+
path: "/asx/v2/lookup",
|
|
8
8
|
safeToRetry: true,
|
|
9
9
|
request: {
|
|
10
10
|
"t": "obj",
|
|
@@ -452,7 +452,7 @@ export const operations = {
|
|
|
452
452
|
"abn": {
|
|
453
453
|
"t": "str"
|
|
454
454
|
},
|
|
455
|
-
"
|
|
455
|
+
"asicNumber": {
|
|
456
456
|
"t": "any_of",
|
|
457
457
|
"of": [
|
|
458
458
|
{
|
|
@@ -464,6 +464,24 @@ export const operations = {
|
|
|
464
464
|
}
|
|
465
465
|
]
|
|
466
466
|
},
|
|
467
|
+
"asicNumberType": {
|
|
468
|
+
"t": "any_of",
|
|
469
|
+
"of": [
|
|
470
|
+
{
|
|
471
|
+
"t": "str",
|
|
472
|
+
"values": [
|
|
473
|
+
"ACN",
|
|
474
|
+
"ARBN",
|
|
475
|
+
"ARSN",
|
|
476
|
+
"ARFN",
|
|
477
|
+
"undetermined"
|
|
478
|
+
]
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
"t": "null"
|
|
482
|
+
}
|
|
483
|
+
]
|
|
484
|
+
},
|
|
467
485
|
"legalName": {
|
|
468
486
|
"t": "str",
|
|
469
487
|
"min": 1
|
|
@@ -575,7 +593,8 @@ export const operations = {
|
|
|
575
593
|
"ticker",
|
|
576
594
|
"sourceId",
|
|
577
595
|
"abn",
|
|
578
|
-
"
|
|
596
|
+
"asicNumber",
|
|
597
|
+
"asicNumberType",
|
|
579
598
|
"legalName",
|
|
580
599
|
"identityEvidence",
|
|
581
600
|
"abrEvidence",
|
|
@@ -650,7 +669,7 @@ export const operations = {
|
|
|
650
669
|
suggest: {
|
|
651
670
|
name: "suggest",
|
|
652
671
|
method: "POST",
|
|
653
|
-
path: "/asx/
|
|
672
|
+
path: "/asx/v2/suggest",
|
|
654
673
|
safeToRetry: true,
|
|
655
674
|
request: {
|
|
656
675
|
"t": "obj",
|
|
@@ -70,7 +70,8 @@ export type LookupResponse = {
|
|
|
70
70
|
readonly ticker: string;
|
|
71
71
|
readonly sourceId: string;
|
|
72
72
|
readonly abn: string;
|
|
73
|
-
readonly
|
|
73
|
+
readonly asicNumber: string | null;
|
|
74
|
+
readonly asicNumberType: "ACN" | "ARBN" | "ARSN" | "ARFN" | "undetermined" | null;
|
|
74
75
|
readonly legalName: string;
|
|
75
76
|
readonly identityEvidence: {
|
|
76
77
|
readonly url: string;
|
package/dist/http.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function boundedText(response: Response, limit: number, signal: AbortSignal): Promise<string>;
|
package/dist/http.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { failure } from "./model.js";
|
|
2
|
+
export async function boundedText(response, limit, signal) {
|
|
3
|
+
if (!response.body)
|
|
4
|
+
throw failure("PARSE_FAILED");
|
|
5
|
+
const reader = response.body.getReader();
|
|
6
|
+
let length = 0;
|
|
7
|
+
let body = "";
|
|
8
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: false });
|
|
9
|
+
const decode = (bytes) => {
|
|
10
|
+
try {
|
|
11
|
+
return decoder.decode(bytes, { stream: bytes !== undefined });
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
throw failure("PARSE_FAILED");
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const abort = () => {
|
|
18
|
+
void reader.cancel().catch(() => undefined);
|
|
19
|
+
};
|
|
20
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
21
|
+
try {
|
|
22
|
+
while (true) {
|
|
23
|
+
if (signal.aborted)
|
|
24
|
+
throw failure("CANCELLED");
|
|
25
|
+
const chunk = await reader.read();
|
|
26
|
+
if (chunk.done)
|
|
27
|
+
break;
|
|
28
|
+
const bytes = chunk.value;
|
|
29
|
+
if (!(bytes instanceof Uint8Array))
|
|
30
|
+
throw failure("PARSE_FAILED");
|
|
31
|
+
length += bytes.byteLength;
|
|
32
|
+
if (length > limit)
|
|
33
|
+
throw failure("PARSE_FAILED");
|
|
34
|
+
body += decode(bytes);
|
|
35
|
+
}
|
|
36
|
+
if (signal.aborted)
|
|
37
|
+
throw failure("CANCELLED");
|
|
38
|
+
return body + decode();
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
signal.removeEventListener("abort", abort);
|
|
42
|
+
try {
|
|
43
|
+
await reader.cancel();
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
reader.releaseLock();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/live.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { type DirectoryEntry } from "./model.js";
|
|
3
|
+
export interface AsxLiveConfig {
|
|
4
|
+
/** Host transport owns network routing; must honour AbortSignal. */
|
|
5
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
6
|
+
readonly timeoutMs?: number;
|
|
7
|
+
}
|
|
8
|
+
/** No persistence, source composition, background work or network call at construction. */
|
|
9
|
+
export declare function createAsxLive(config?: AsxLiveConfig): {
|
|
10
|
+
source: (input: string, signal?: AbortSignal) => Promise<import("./source-record.js").SourceObservation>;
|
|
11
|
+
lookup: (ticker: string, signal?: AbortSignal) => Promise<{
|
|
12
|
+
readonly status: "found";
|
|
13
|
+
readonly observation: {
|
|
14
|
+
readonly company: {
|
|
15
|
+
readonly raw?: {
|
|
16
|
+
readonly [x: string]: Schema.Json;
|
|
17
|
+
} | undefined;
|
|
18
|
+
readonly ticker: string;
|
|
19
|
+
readonly sourceId: string;
|
|
20
|
+
readonly companyName: string;
|
|
21
|
+
readonly address: string | null;
|
|
22
|
+
readonly tel: string | null;
|
|
23
|
+
readonly fax: string | null;
|
|
24
|
+
readonly website: string | null;
|
|
25
|
+
readonly description: string | null;
|
|
26
|
+
readonly issueType: string | null;
|
|
27
|
+
readonly foreignExempt: boolean | null;
|
|
28
|
+
readonly people: readonly {
|
|
29
|
+
readonly sourceName: string;
|
|
30
|
+
readonly displayName: string;
|
|
31
|
+
readonly title: string;
|
|
32
|
+
readonly sourceCategory: "directors" | "secretaries";
|
|
33
|
+
}[];
|
|
34
|
+
};
|
|
35
|
+
readonly startedAt: string;
|
|
36
|
+
readonly observedAt: string;
|
|
37
|
+
readonly lastChangedAt: string;
|
|
38
|
+
};
|
|
39
|
+
} | {
|
|
40
|
+
readonly status: "missing";
|
|
41
|
+
readonly startedAt: string;
|
|
42
|
+
readonly observedAt: string;
|
|
43
|
+
readonly raw?: {
|
|
44
|
+
readonly [x: string]: Schema.Json;
|
|
45
|
+
} | undefined;
|
|
46
|
+
}>;
|
|
47
|
+
directory: (signal?: AbortSignal) => Promise<{
|
|
48
|
+
entries: readonly DirectoryEntry[];
|
|
49
|
+
observedAt: string;
|
|
50
|
+
}>;
|
|
51
|
+
suggest: (query: string, limit?: number, signal?: AbortSignal) => Promise<{
|
|
52
|
+
source: "live";
|
|
53
|
+
directoryObservedAt: string;
|
|
54
|
+
suggestions: readonly DirectoryEntry[];
|
|
55
|
+
}>;
|
|
56
|
+
};
|
package/dist/live.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { suggestDirectory } from "./directory-search.js";
|
|
2
|
+
import { Effect, Result, Schema } from "effect";
|
|
3
|
+
import { boundedText } from "./http.js";
|
|
4
|
+
import { failure, Failure } from "./model.js";
|
|
5
|
+
import { normalizeTicker, parseCompany, parseDirectory } from "./parse.js";
|
|
6
|
+
import { ASX_COMPANY_BASE, asxSourceObservation } from "./source-record.js";
|
|
7
|
+
const BASE = ASX_COMPANY_BASE;
|
|
8
|
+
const Missing = Schema.Struct({
|
|
9
|
+
error: Schema.Struct({
|
|
10
|
+
code: Schema.Literal(400),
|
|
11
|
+
message: Schema.Literal("Bad Request: Symbol not found"),
|
|
12
|
+
}),
|
|
13
|
+
});
|
|
14
|
+
function parseJson(text) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(text);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
throw failure("PARSE_FAILED");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const request = Effect.fn("ASX.http")(function* (transport, path, limit) {
|
|
23
|
+
return yield* Effect.tryPromise({
|
|
24
|
+
try: async (signal) => {
|
|
25
|
+
const response = await transport(`${BASE}${path}`, {
|
|
26
|
+
signal,
|
|
27
|
+
redirect: "manual",
|
|
28
|
+
headers: { Accept: path === "directory/file" ? "text/csv" : "application/json" },
|
|
29
|
+
});
|
|
30
|
+
if (response.status === 401 || response.status === 403) {
|
|
31
|
+
await response.body?.cancel();
|
|
32
|
+
throw failure("ACCESS_DENIED");
|
|
33
|
+
}
|
|
34
|
+
if (response.status === 429) {
|
|
35
|
+
await response.body?.cancel();
|
|
36
|
+
throw failure("RATE_LIMITED");
|
|
37
|
+
}
|
|
38
|
+
if (response.status >= 500 || response.status === 408) {
|
|
39
|
+
await response.body?.cancel();
|
|
40
|
+
throw failure("UPSTREAM_UNAVAILABLE");
|
|
41
|
+
}
|
|
42
|
+
if (!response.ok && response.status !== 400) {
|
|
43
|
+
await response.body?.cancel();
|
|
44
|
+
throw failure("UPSTREAM_REJECTED");
|
|
45
|
+
}
|
|
46
|
+
const body = await boundedText(response, response.ok ? limit : 4096, signal);
|
|
47
|
+
return { status: response.status, body };
|
|
48
|
+
},
|
|
49
|
+
catch: (error) => Schema.is(Failure)(error) ? failure(error.code) : failure("TRANSPORT_FAILED"),
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
function run(program, timeoutMs, signal) {
|
|
53
|
+
return Effect.runPromise(Effect.result(program.pipe(Effect.timeoutOrElse({
|
|
54
|
+
duration: timeoutMs,
|
|
55
|
+
orElse: () => Effect.fail(failure("TIMEOUT")),
|
|
56
|
+
}))), { signal })
|
|
57
|
+
.then((result) => {
|
|
58
|
+
if (Result.isFailure(result))
|
|
59
|
+
throw result.failure;
|
|
60
|
+
return result.success;
|
|
61
|
+
})
|
|
62
|
+
.catch((error) => {
|
|
63
|
+
if (signal?.aborted)
|
|
64
|
+
throw failure("CANCELLED");
|
|
65
|
+
throw Schema.is(Failure)(error) ? failure(error.code) : failure("INTERNAL_ERROR");
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** No persistence, source composition, background work or network call at construction. */
|
|
69
|
+
export function createAsxLive(config = {}) {
|
|
70
|
+
const timeoutMs = config.timeoutMs ?? 3000;
|
|
71
|
+
if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60_000)
|
|
72
|
+
throw failure("CONFIGURATION_ERROR");
|
|
73
|
+
const transport = config.fetch ?? globalThis.fetch;
|
|
74
|
+
const lookup = Effect.fn("ASX.profile")(function* (input) {
|
|
75
|
+
const ticker = yield* Effect.try({
|
|
76
|
+
try: () => normalizeTicker(input),
|
|
77
|
+
catch: () => failure("INVALID_INPUT"),
|
|
78
|
+
});
|
|
79
|
+
const startedAt = new Date().toISOString();
|
|
80
|
+
const response = yield* request(transport, `${ticker}/about`, 1024 * 1024);
|
|
81
|
+
const payload = yield* Effect.try({
|
|
82
|
+
try: () => parseJson(response.body),
|
|
83
|
+
catch: () => failure("PARSE_FAILED"),
|
|
84
|
+
});
|
|
85
|
+
const observedAt = new Date().toISOString();
|
|
86
|
+
if (response.status === 400) {
|
|
87
|
+
if (Schema.is(Missing)(payload) && Schema.is(Schema.JsonObject)(payload))
|
|
88
|
+
return { status: "missing", startedAt, observedAt, raw: structuredClone(payload) };
|
|
89
|
+
return yield* Effect.fail(failure("UPSTREAM_REJECTED"));
|
|
90
|
+
}
|
|
91
|
+
const company = yield* Effect.try({
|
|
92
|
+
try: () => parseCompany(ticker, payload),
|
|
93
|
+
catch: (error) => (Schema.is(Failure)(error) ? failure(error.code) : failure("PARSE_FAILED")),
|
|
94
|
+
});
|
|
95
|
+
return {
|
|
96
|
+
status: "found",
|
|
97
|
+
observation: { company, startedAt, observedAt, lastChangedAt: observedAt },
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
const directory = Effect.fn("ASX.directory")(function* () {
|
|
101
|
+
const response = yield* request(transport, "directory/file", 4 * 1024 * 1024);
|
|
102
|
+
if (response.status !== 200)
|
|
103
|
+
return yield* Effect.fail(failure("UPSTREAM_REJECTED"));
|
|
104
|
+
const entries = yield* Effect.try({
|
|
105
|
+
try: () => parseDirectory(response.body),
|
|
106
|
+
catch: () => failure("PARSE_FAILED"),
|
|
107
|
+
});
|
|
108
|
+
return { entries, observedAt: new Date().toISOString() };
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
source: async (input, signal) => {
|
|
112
|
+
const ticker = normalizeTicker(input);
|
|
113
|
+
const result = await run(lookup(ticker), timeoutMs, signal);
|
|
114
|
+
return asxSourceObservation(ticker, result);
|
|
115
|
+
},
|
|
116
|
+
lookup: (ticker, signal) => run(lookup(ticker), timeoutMs, signal),
|
|
117
|
+
directory: (signal) => run(directory(), timeoutMs, signal),
|
|
118
|
+
suggest: async (query, limit = 8, signal) => {
|
|
119
|
+
// Validate before acquiring the directory.
|
|
120
|
+
suggestDirectory([], query, limit);
|
|
121
|
+
const value = await run(directory(), timeoutMs, signal);
|
|
122
|
+
return {
|
|
123
|
+
source: "live",
|
|
124
|
+
directoryObservedAt: value.observedAt,
|
|
125
|
+
suggestions: suggestDirectory(value.entries, query, limit),
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
export declare const Ticker: Schema.String;
|
|
3
|
+
export declare const Person: Schema.Struct<{
|
|
4
|
+
readonly sourceName: Schema.String;
|
|
5
|
+
readonly displayName: Schema.String;
|
|
6
|
+
readonly title: Schema.String;
|
|
7
|
+
readonly sourceCategory: Schema.Literals<readonly ["directors", "secretaries"]>;
|
|
8
|
+
}>;
|
|
9
|
+
export interface Person extends Schema.Schema.Type<typeof Person> {
|
|
10
|
+
}
|
|
11
|
+
export declare const Company: Schema.Struct<{
|
|
12
|
+
/** Complete public ASX profile response, including fields unknown to this package version. */
|
|
13
|
+
readonly raw: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>>;
|
|
14
|
+
readonly ticker: Schema.String;
|
|
15
|
+
readonly sourceId: Schema.String;
|
|
16
|
+
readonly companyName: Schema.String;
|
|
17
|
+
readonly address: Schema.NullOr<Schema.String>;
|
|
18
|
+
readonly tel: Schema.NullOr<Schema.String>;
|
|
19
|
+
readonly fax: Schema.NullOr<Schema.String>;
|
|
20
|
+
readonly website: Schema.NullOr<Schema.String>;
|
|
21
|
+
readonly description: Schema.NullOr<Schema.String>;
|
|
22
|
+
readonly issueType: Schema.NullOr<Schema.String>;
|
|
23
|
+
readonly foreignExempt: Schema.NullOr<Schema.Boolean>;
|
|
24
|
+
readonly people: Schema.$Array<Schema.Struct<{
|
|
25
|
+
readonly sourceName: Schema.String;
|
|
26
|
+
readonly displayName: Schema.String;
|
|
27
|
+
readonly title: Schema.String;
|
|
28
|
+
readonly sourceCategory: Schema.Literals<readonly ["directors", "secretaries"]>;
|
|
29
|
+
}>>;
|
|
30
|
+
}>;
|
|
31
|
+
export interface Company extends Schema.Schema.Type<typeof Company> {
|
|
32
|
+
}
|
|
33
|
+
export declare const DirectoryEntry: Schema.Struct<{
|
|
34
|
+
readonly ticker: Schema.String;
|
|
35
|
+
readonly companyName: Schema.String;
|
|
36
|
+
readonly industryGroup: Schema.NullOr<Schema.String>;
|
|
37
|
+
readonly listingDate: Schema.NullOr<Schema.String>;
|
|
38
|
+
readonly marketCapitalisation: Schema.NullOr<Schema.Number>;
|
|
39
|
+
}>;
|
|
40
|
+
export interface DirectoryEntry extends Schema.Schema.Type<typeof DirectoryEntry> {
|
|
41
|
+
}
|
|
42
|
+
export declare const Failure: Schema.Struct<{
|
|
43
|
+
readonly _tag: Schema.Literal<"AsxError">;
|
|
44
|
+
readonly code: Schema.Literals<readonly ["INVALID_INPUT", "PARSE_FAILED", "SOURCE_MISMATCH", "TRANSPORT_FAILED", "TIMEOUT", "RATE_LIMITED", "UPSTREAM_UNAVAILABLE", "CONFIGURATION_ERROR", "SNAPSHOT_UNAVAILABLE", "CANCELLED", "INTERNAL_ERROR", "ACCESS_DENIED", "UPSTREAM_REJECTED", "PERSISTENCE_FAILED", "PUBLICATION_CONFLICT", "COVERAGE_REJECTED"]>;
|
|
45
|
+
}>;
|
|
46
|
+
export type AsxError = typeof Failure.Type;
|
|
47
|
+
export declare const failure: (code: AsxError["code"]) => AsxError;
|
|
48
|
+
export declare const isFallbackEligible: (code: AsxError["code"]) => boolean;
|
|
49
|
+
export declare const Timestamp: Schema.String;
|
|
50
|
+
export declare const Observation: Schema.Struct<{
|
|
51
|
+
readonly company: Schema.Struct<{
|
|
52
|
+
/** Complete public ASX profile response, including fields unknown to this package version. */
|
|
53
|
+
readonly raw: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>>;
|
|
54
|
+
readonly ticker: Schema.String;
|
|
55
|
+
readonly sourceId: Schema.String;
|
|
56
|
+
readonly companyName: Schema.String;
|
|
57
|
+
readonly address: Schema.NullOr<Schema.String>;
|
|
58
|
+
readonly tel: Schema.NullOr<Schema.String>;
|
|
59
|
+
readonly fax: Schema.NullOr<Schema.String>;
|
|
60
|
+
readonly website: Schema.NullOr<Schema.String>;
|
|
61
|
+
readonly description: Schema.NullOr<Schema.String>;
|
|
62
|
+
readonly issueType: Schema.NullOr<Schema.String>;
|
|
63
|
+
readonly foreignExempt: Schema.NullOr<Schema.Boolean>;
|
|
64
|
+
readonly people: Schema.$Array<Schema.Struct<{
|
|
65
|
+
readonly sourceName: Schema.String;
|
|
66
|
+
readonly displayName: Schema.String;
|
|
67
|
+
readonly title: Schema.String;
|
|
68
|
+
readonly sourceCategory: Schema.Literals<readonly ["directors", "secretaries"]>;
|
|
69
|
+
}>>;
|
|
70
|
+
}>;
|
|
71
|
+
readonly startedAt: Schema.String;
|
|
72
|
+
readonly observedAt: Schema.String;
|
|
73
|
+
readonly lastChangedAt: Schema.String;
|
|
74
|
+
}>;
|
|
75
|
+
export interface Observation extends Schema.Schema.Type<typeof Observation> {
|
|
76
|
+
}
|
|
77
|
+
export declare const LiveOutcome: Schema.Union<readonly [Schema.Struct<{
|
|
78
|
+
readonly status: Schema.Literal<"found">;
|
|
79
|
+
readonly observation: Schema.Struct<{
|
|
80
|
+
readonly company: Schema.Struct<{
|
|
81
|
+
/** Complete public ASX profile response, including fields unknown to this package version. */
|
|
82
|
+
readonly raw: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>>;
|
|
83
|
+
readonly ticker: Schema.String;
|
|
84
|
+
readonly sourceId: Schema.String;
|
|
85
|
+
readonly companyName: Schema.String;
|
|
86
|
+
readonly address: Schema.NullOr<Schema.String>;
|
|
87
|
+
readonly tel: Schema.NullOr<Schema.String>;
|
|
88
|
+
readonly fax: Schema.NullOr<Schema.String>;
|
|
89
|
+
readonly website: Schema.NullOr<Schema.String>;
|
|
90
|
+
readonly description: Schema.NullOr<Schema.String>;
|
|
91
|
+
readonly issueType: Schema.NullOr<Schema.String>;
|
|
92
|
+
readonly foreignExempt: Schema.NullOr<Schema.Boolean>;
|
|
93
|
+
readonly people: Schema.$Array<Schema.Struct<{
|
|
94
|
+
readonly sourceName: Schema.String;
|
|
95
|
+
readonly displayName: Schema.String;
|
|
96
|
+
readonly title: Schema.String;
|
|
97
|
+
readonly sourceCategory: Schema.Literals<readonly ["directors", "secretaries"]>;
|
|
98
|
+
}>>;
|
|
99
|
+
}>;
|
|
100
|
+
readonly startedAt: Schema.String;
|
|
101
|
+
readonly observedAt: Schema.String;
|
|
102
|
+
readonly lastChangedAt: Schema.String;
|
|
103
|
+
}>;
|
|
104
|
+
}>, Schema.Struct<{
|
|
105
|
+
readonly status: Schema.Literal<"missing">;
|
|
106
|
+
readonly startedAt: Schema.String;
|
|
107
|
+
readonly observedAt: Schema.String;
|
|
108
|
+
readonly raw: Schema.optionalKey<Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>>;
|
|
109
|
+
}>]>;
|
|
110
|
+
export type LiveOutcome = typeof LiveOutcome.Type;
|
|
111
|
+
export declare function isTimestamp(value: string): boolean;
|
|
112
|
+
export declare function validObservation(value: Observation): boolean;
|