@kensio/yulin 1.21.8 → 1.21.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-properties.d.ts +1 -1
  2. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-properties.js +6 -3
  3. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-names.d.ts +30 -0
  4. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-names.js +91 -0
  5. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-rules.d.ts +7 -12
  6. package/dist/service/lambda/cfn/event-source-mapping/sim-cfn-lambda-event-source-mapping-property-rules.js +12 -92
  7. package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-input.d.ts +2 -0
  8. package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-input.js +5 -0
  9. package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping-refusals.js +0 -10
  10. package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping.handler.d.ts +2 -0
  11. package/dist/service/lambda/command/event-source-mapping/create-event-source-mapping.handler.js +2 -0
  12. package/dist/service/lambda/command/event-source-mapping/sim-lambda-event-source-mapping-commands.d.ts +2 -0
  13. package/dist/service/lambda/destination/sim-lambda-destination-targets.d.ts +2 -1
  14. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-shard-poller.d.ts +0 -4
  15. package/dist/service/lambda/event-source/poll/kinesis/sim-lambda-kinesis-shard-poller.js +11 -16
  16. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event-source-poller.d.ts +1 -6
  17. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-event-source-poller.js +9 -27
  18. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-polling.d.ts +19 -0
  19. package/dist/service/lambda/event-source/poll/sim-lambda-dynamodb-stream-polling.js +27 -0
  20. package/dist/service/lambda/event-source/poll/sim-lambda-event-source-function.d.ts +1 -1
  21. package/dist/service/lambda/event-source/poll/sim-lambda-event-source-function.js +3 -1
  22. package/dist/service/lambda/event-source/poll/sim-lambda-stream-batch-age.d.ts +13 -0
  23. package/dist/service/lambda/event-source/poll/sim-lambda-stream-batch-age.js +27 -0
  24. package/dist/service/lambda/event-source/poll/sim-lambda-stream-batch-info.d.ts +15 -0
  25. package/dist/service/lambda/event-source/poll/sim-lambda-stream-batch-info.js +26 -0
  26. package/dist/service/lambda/event-source/poll/sim-lambda-stream-batch-outcome.d.ts +1 -0
  27. package/dist/service/lambda/event-source/poll/sim-lambda-stream-batch-outcome.js +7 -1
  28. package/dist/service/lambda/event-source/poll/sim-lambda-stream-batch-processing.d.ts +17 -0
  29. package/dist/service/lambda/event-source/poll/sim-lambda-stream-batch-processing.js +12 -0
  30. package/dist/service/lambda/event-source/poll/sim-lambda-stream-bisect.d.ts +53 -0
  31. package/dist/service/lambda/event-source/poll/sim-lambda-stream-bisect.js +79 -0
  32. package/dist/service/lambda/event-source/poll/sim-lambda-stream-cursor.d.ts +15 -0
  33. package/dist/service/lambda/event-source/poll/sim-lambda-stream-cursor.js +28 -0
  34. package/dist/service/lambda/event-source/poll/sim-lambda-stream-expiry.d.ts +19 -0
  35. package/dist/service/lambda/event-source/poll/sim-lambda-stream-expiry.js +23 -0
  36. package/dist/service/lambda/event-source/poll/sim-lambda-stream-failure-context.d.ts +3 -0
  37. package/dist/service/lambda/event-source/poll/sim-lambda-stream-failure-context.js +19 -0
  38. package/dist/service/lambda/event-source/poll/sim-lambda-stream-failure-destination.d.ts +15 -0
  39. package/dist/service/lambda/event-source/poll/sim-lambda-stream-failure-destination.js +38 -0
  40. package/dist/service/lambda/event-source/poll/sim-lambda-stream-failure-record.d.ts +41 -0
  41. package/dist/service/lambda/event-source/poll/sim-lambda-stream-failure-record.js +17 -0
  42. package/dist/service/lambda/event-source/poll/sim-lambda-stream-progress-state.d.ts +23 -0
  43. package/dist/service/lambda/event-source/poll/sim-lambda-stream-progress-state.js +42 -0
  44. package/dist/service/lambda/event-source/poll/sim-lambda-stream-progress.d.ts +21 -38
  45. package/dist/service/lambda/event-source/poll/sim-lambda-stream-progress.js +45 -63
  46. package/dist/service/lambda/event-source/poll/sim-lambda-stream-retry.d.ts +4 -39
  47. package/dist/service/lambda/event-source/poll/sim-lambda-stream-retry.js +12 -64
  48. package/dist/service/lambda/event-source/sim-lambda-event-source-mapping.d.ts +10 -3
  49. package/dist/service/lambda/event-source/sim-lambda-event-source-mapping.js +12 -3
  50. package/dist/service/lambda/event-source/sim-lambda-event-source-retry-limits.d.ts +8 -7
  51. package/dist/service/lambda/event-source/sim-lambda-event-source-retry-limits.js +6 -3
  52. package/dist/service/lambda/event-source/sim-lambda-stream-destination-config.d.ts +7 -0
  53. package/dist/service/lambda/event-source/sim-lambda-stream-destination-config.js +38 -0
  54. package/dist/service/lambda/event-source/sim-lambda-stream-retry-limits.d.ts +15 -7
  55. package/dist/service/lambda/event-source/sim-lambda-stream-retry-limits.js +15 -7
  56. package/dist/service/lambda/event-source/stream/kinesis/sim-kinesis-event-source-streams.js +1 -0
  57. package/dist/service/lambda/event-source/stream/sim-dynamodb-event-source-stream-reader.js +1 -0
  58. package/dist/service/lambda/event-source/stream/sim-dynamodb-event-source-stream-shard.d.ts +1 -0
  59. package/dist/service/lambda/event-source/stream/sim-dynamodb-event-source-stream-shard.js +7 -0
  60. package/dist/service/lambda/event-source/stream/sim-lambda-event-source-streams.d.ts +1 -0
  61. package/dist/service/lambda/index.d.ts +1 -0
  62. package/dist/service/lambda/sim-lambda-commands.js +1 -0
  63. package/docs/services/lambda/README.md +111 -11
  64. package/package.json +1 -1
@@ -23,17 +23,23 @@ const maximumRecordAgeLimit = 604_800;
23
23
  const defaultAttemptLimit = 5;
24
24
  const millisecondsPerSecond = 1000;
25
25
  /**
26
- * When a stream mapping stops delivering a batch its function keeps failing.
26
+ * What a stream mapping does with a batch its function keeps failing.
27
27
  *
28
- * The two limits are one thing rather than two, because they end the same
29
- * lifecycle: a record leaves the mapping when it has had its retries or when it
30
- * is too old to be worth another, whichever comes first. Both are held as
31
- * Lambda states them, with `-1` for no limit, so a Get or a List reports back
32
- * exactly what was asked for.
28
+ * The three settings are one thing rather than three, because they run the same
29
+ * lifecycle. A failing batch is split around the record that broke it where the
30
+ * mapping was asked to bisect, and a record leaves the mapping when it has had
31
+ * its retries or when it is too old to be worth another, whichever comes first.
32
+ * The limits are held as Lambda states them, with `-1` for no limit, so a Get
33
+ * or a List reports back exactly what was asked for.
33
34
  */
34
35
  export class SimLambdaStreamRetryLimits {
35
36
  maximumRetryAttempts;
36
37
  maximumRecordAgeInSeconds;
38
+ /**
39
+ * Whether a batch the function threw on is split in half and delivered again
40
+ * as two batches.
41
+ */
42
+ bisectBatchOnFunctionError;
37
43
  constructor(input = {}) {
38
44
  this.maximumRetryAttempts = checkedLimit({
39
45
  value: input.maximumRetryAttempts,
@@ -47,14 +53,16 @@ export class SimLambdaStreamRetryLimits {
47
53
  maximum: maximumRecordAgeLimit,
48
54
  unit: "seconds",
49
55
  });
56
+ this.bisectBatchOnFunctionError = input.bisectBatchOnFunctionError ?? false;
50
57
  }
51
58
  /**
52
- * The two limits as a Get, a List or a create response reports them.
59
+ * The three settings as a Get, a List or a create response reports them.
53
60
  */
54
61
  configuration() {
55
62
  return {
56
63
  MaximumRetryAttempts: this.maximumRetryAttempts,
57
64
  MaximumRecordAgeInSeconds: this.maximumRecordAgeInSeconds,
65
+ BisectBatchOnFunctionError: this.bisectBatchOnFunctionError,
58
66
  };
59
67
  }
60
68
  /**
@@ -40,6 +40,7 @@ export class SimKinesisEventSourceStreams {
40
40
  }, { caller });
41
41
  return {
42
42
  records: output.Records,
43
+ shardId: request.shardId,
43
44
  next: { kind: "iterator", shardIterator: output.NextShardIterator },
44
45
  drained: false,
45
46
  };
@@ -29,6 +29,7 @@ export class SimDynamoDbEventSourceStreamReader {
29
29
  const next = output.NextShardIterator;
30
30
  return {
31
31
  records: output.Records ?? [],
32
+ shardId: await this.shard.shardId(request),
32
33
  next: simDynamoDbEventSourceNextPosition(next, position),
33
34
  drained: next === undefined,
34
35
  };
@@ -23,5 +23,6 @@ export declare class SimDynamoDbEventSourceStreamShard {
23
23
  * is the shard.
24
24
  */
25
25
  iteratorFor(request: SimLambdaEventSourceStreamRequest, position: SimLambdaEventSourceStreamNamedPosition): Promise<string>;
26
+ shardId(request: SimLambdaEventSourceStreamRequest): Promise<string>;
26
27
  private describe;
27
28
  }
@@ -49,6 +49,13 @@ export class SimDynamoDbEventSourceStreamShard {
49
49
  }
50
50
  return iterator.ShardIterator;
51
51
  }
52
+ async shardId(request) {
53
+ const description = await this.describe(request);
54
+ const shardId = description.Shards?.[0]?.ShardId;
55
+ if (shardId === undefined)
56
+ refuse(request, "reports no shard to read records from");
57
+ return shardId;
58
+ }
52
59
  async describe(request) {
53
60
  const { caller, streamArn } = request;
54
61
  const described = await this.commands.describeStream({ input: { StreamArn: streamArn } }, { caller });
@@ -84,6 +84,7 @@ export interface SimLambdaEventSourceStreamReadRequest extends SimLambdaEventSou
84
84
  export interface SimLambdaEventSourceStreamProgressBatch {
85
85
  readonly next: SimLambdaEventSourceStreamPosition;
86
86
  readonly drained: boolean;
87
+ readonly shardId?: string;
87
88
  }
88
89
  /**
89
90
  * What one read of a DynamoDB stream came back with.
@@ -16,3 +16,4 @@ export type { SimLambdaDynamoDbAttributeValue, SimLambdaDynamoDbImage, } from ".
16
16
  export { SimLambdaStreamCascadeError } from "./event-source/stream/sim-lambda-stream-cascade.error.js";
17
17
  export type { SimLambdaDestinationRecord, SimLambdaDestinationRequestContext, SimLambdaDestinationResponseContext, } from "./destination/sim-lambda-destination-record.js";
18
18
  export type { SimLambdaInvocationCondition } from "./function/event-invoke/sim-lambda-event-invoke-config.js";
19
+ export type { SimLambdaStreamFailureRecord, SimLambdaStreamFailureCondition, } from "./event-source/poll/sim-lambda-stream-failure-record.js";
@@ -113,6 +113,7 @@ export class SimLambdaCommands {
113
113
  background,
114
114
  });
115
115
  this.eventSourceMappings = new SimLambdaEventSourceMappingCommands({
116
+ destinations,
116
117
  accountRegionScope,
117
118
  pollers: new SimLambdaEventSourcePollers({
118
119
  functions: this.functionLookup,
@@ -1904,7 +1904,7 @@ with more steps.
1904
1904
  `MaximumRetryAttempts` and `MaximumRecordAgeInSeconds` govern the same failed-batch lifecycle, and a
1905
1905
  stream mapping keeps both. `MaximumRetryAttempts: 0` makes one delivery and no retries, and a
1906
1906
  positive value allows that many retries after the first delivery. `MaximumRecordAgeInSeconds`
1907
- discards a record once the next attempt would fall past that age. Lambda's `-1` means no limit
1907
+ discards expired records before each invocation, including the first poll. Lambda's `-1` means no limit
1908
1908
  (that is what a mapping naming neither reports back).
1909
1909
 
1910
1910
  ```typescript sim-lambda-stream-retry-limits
@@ -2033,6 +2033,93 @@ discarded, and the mapping reads on from behind it.
2033
2033
  10,000 retries, `-1` to 604,800 seconds) is a `ValidationException`. A queue mapping takes neither,
2034
2034
  because a message the handler never takes is left to the queue's own redrive policy.
2035
2035
 
2036
+ ### Splitting a failed batch around the record that broke it
2037
+
2038
+ `BisectBatchOnFunctionError` splits a batch the handler threw on in half, and delivers each half as
2039
+ its own batch. A half that fails is split again, down to a single record. That is how the records
2040
+ beside a poison record get through, and how the poison record ends up delivered on its own and
2041
+ discarded on its own.
2042
+
2043
+ ```json
2044
+ {
2045
+ "StartingPosition": "TRIM_HORIZON",
2046
+ "BatchSize": 100,
2047
+ "MaximumRetryAttempts": 10,
2048
+ "BisectBatchOnFunctionError": true
2049
+ }
2050
+ ```
2051
+
2052
+ A batch of four whose third record the handler cannot take is delivered as `1, 2, 3, 4`, then
2053
+ `1, 2`, then `3, 4`, then `3` on its own, and finally `4`. The record that broke every batch it was
2054
+ in leaves the mapping under the retry and record-age limits, and reaches the
2055
+ [failure destination](#sending-discarded-stream-batches-to-a-destination) naming one record rather
2056
+ than four.
2057
+
2058
+ Splitting puts the retry count back to the start, so a batch always reaches a single record before
2059
+ the count decides anything. Once it is down to one record, the limits count as they do for any other
2060
+ failing batch. A batch the handler reported partial failures on is left whole, because the report
2061
+ already says which record to go back to.
2062
+
2063
+ The halves carry the batch that failed and nothing else. A record written while the splitting is
2064
+ going on waits behind it and is delivered as part of the batch after it, and the mapping reads at its
2065
+ own batch size again once the whole of the split batch is through.
2066
+
2067
+ `GetEventSourceMapping` and `ListEventSourceMappings` report the setting, and
2068
+ `AWS::Lambda::EventSourceMapping` takes it in a template. Both stream sources have it. A queue
2069
+ mapping is refused for naming it, the way real Lambda refuses one.
2070
+
2071
+ ### Sending discarded stream batches to a destination
2072
+
2073
+ DynamoDB Streams and Kinesis mappings accept `DestinationConfig.OnFailure` with a standard SQS
2074
+ queue or SNS topic ARN. Supply it to `CreateEventSourceMapping` or the
2075
+ `AWS::Lambda::EventSourceMapping` resource alongside the retry and record-age limits:
2076
+
2077
+ ```json
2078
+ {
2079
+ "MaximumRetryAttempts": 10,
2080
+ "MaximumRecordAgeInSeconds": 3600,
2081
+ "DestinationConfig": {
2082
+ "OnFailure": {
2083
+ "Destination": "arn:aws:sqs:eu-west-2:111111111111:stream-failures"
2084
+ }
2085
+ }
2086
+ }
2087
+ ```
2088
+
2089
+ CDK's `DynamoEventSource` and `KinesisEventSource` accept `onFailure: new SqsDlq(queue)` or
2090
+ `onFailure: new SnsDlq(topic)`. Their synthesized destination configuration is passed through to
2091
+ the simulated mapping. Create, Get, List and Delete responses preserve that configuration.
2092
+
2093
+ The function's execution role needs `sqs:SendMessage` on the queue or `sns:Publish` on the topic.
2094
+ Delivery goes through the simulated destination service, so its IAM checks and SNS subscriptions
2095
+ apply. An IAM denial or a missing destination rejects `backgroundTasksComplete()` or the clock
2096
+ advance that exhausted the batch. The discarded records have already advanced the checkpoint.
2097
+ Yulin attempts destination delivery once and does not retry a failed send.
2098
+
2099
+ The JSON message follows the AWS
2100
+ [DynamoDB Streams](https://docs.aws.amazon.com/lambda/latest/dg/services-dynamodb-errors.html) and
2101
+ [Kinesis](https://docs.aws.amazon.com/lambda/latest/dg/kinesis-on-failure-destination.html)
2102
+ notification formats. Import `SimLambdaStreamFailureRecord` from `@kensio/yulin/lambda` to type it.
2103
+
2104
+ - `version` is `"1.0"` and `timestamp` is the simulated discard time in ISO 8601 format.
2105
+ - `requestContext` contains a generated `requestId`, the mapping's `functionArn`,
2106
+ `approximateInvokeCount`, and either `RetryAttemptsExhausted` or `RecordAgeExceeded` as `condition`.
2107
+ - `responseContext` reports `statusCode: 200` and `executedVersion`. A handler error adds
2108
+ `functionError: "Unhandled"`. A valid partial-batch failure response leaves that field out.
2109
+ Records discarded before any invocation omit `responseContext`.
2110
+ - `DDBStreamBatchInfo` or `KinesisBatchInfo` identifies the stream ARN, shard ID, first and last
2111
+ sequence numbers, first and last arrival times, and batch size. Arrival times are ISO 8601 strings.
2112
+
2113
+ The original record payloads are absent. A successful batch produces no notification. After a
2114
+ partial-batch response, the notification covers the discarded suffix starting at the failed
2115
+ checkpoint. When only an older prefix expires, its notification excludes the younger records.
2116
+ Records already expired on their first poll report zero invocations. Request IDs are generated for
2117
+ the notification and are not correlated with the handler's invocation context.
2118
+
2119
+ S3 destinations, FIFO queues or topics, and `OnSuccess` are refused. SQS source mappings cannot
2120
+ have this destination configuration. The simulator's existing five-retry cap for mappings with
2121
+ neither limit also produces a failure notification when it discards a batch.
2122
+
2036
2123
  ### Reporting individual record failures
2037
2124
 
2038
2125
  A stream mapping with `FunctionResponseTypes: ["ReportBatchItemFailures"]` reads the handler's
@@ -2272,9 +2359,15 @@ this, and deploys without hand-editing. The grant CDK writes alongside it is an
2272
2359
  the three stream actions on the stream ARN and `dynamodb:ListStreams` on every stream, exactly what
2273
2360
  the mapping's execution-role check is looking for.
2274
2361
 
2275
- The properties a non-default `DynamoEventSource` adds are refused by name. Those are
2276
- `FilterCriteria`, `ParallelizationFactor`, `BisectBatchOnFunctionError`, `TumblingWindowInSeconds`
2277
- and `DestinationConfig`.
2362
+ `bisectBatchOnError` is simulated, and is covered under
2363
+ [Splitting a failed batch around the record that broke it](#splitting-a-failed-batch-around-the-record-that-broke-it).
2364
+ The other properties a non-default `DynamoEventSource` adds are recorded rather than acted on. Those
2365
+ are `FilterCriteria`, `ParallelizationFactor` and `TumblingWindowInSeconds`.
2366
+ The mapping deploys, delivers every record whole and unfiltered, and each property it was created
2367
+ without is listed in
2368
+ [`stack.ignoredProperties`](https://yulinsim.dev/services/cloudformation/#properties-a-resource-was-created-without "Properties a Resource was created without")
2369
+ with what the mapping does in its place. `CreateEventSourceMapping` still refuses the same
2370
+ properties, since a caller naming one is asking for behaviour by hand.
2278
2371
 
2279
2372
  A hand-written template or a SAM application usually gives the function the AWS managed policy
2280
2373
  `AWSLambdaDynamoDBExecutionRole` instead. Simulated IAM has no model for managed policy ARNs, so
@@ -4154,8 +4247,10 @@ Current documented limitations:
4154
4247
  - Throttling does not drive a retry. A retry follows a handler that threw or ran out of time, and
4155
4248
  the `MaximumEventAgeInSeconds` a config carries is measured from when the invocation was
4156
4249
  accepted.
4157
- - `DestinationConfig` on an event source mapping is left out. It is a different mechanism from the
4158
- asynchronous invocation destinations above.
4250
+ - Stream mappings support standard SQS and SNS `DestinationConfig.OnFailure` destinations.
4251
+ S3 destinations and `OnSuccess` are refused. Destination delivery is attempted once. A delivery
4252
+ failure rejects the background task after the discarded records have advanced the checkpoint.
4253
+ Destination delivery retries and `DestinationDeliveryFailures` metrics are not simulated.
4159
4254
  - A settings change takes effect at once. Real Lambda reports `LastUpdateStatus: "InProgress"` while
4160
4255
  it rolls the change out, and neither that member nor the wait it implies is simulated.
4161
4256
  - A cross-account grant is only half of what admits a call. The caller's own Account has to allow
@@ -4244,11 +4339,12 @@ Current documented limitations:
4244
4339
  function, and `S3ObjectVersion` on `UpdateFunctionCode`. A versioned location loads the object as
4245
4340
  it stands.
4246
4341
  - SQS queues, DynamoDB streams and Kinesis streams are the only event sources. Kafka, DocumentDB and
4247
- Kinesis enhanced fan-out consumers are refused outright, and so are `FilterCriteria`,
4248
- `ScalingConfig`, `DestinationConfig`, `BisectBatchOnFunctionError`, `ParallelizationFactor`,
4249
- `TumblingWindowInSeconds` and the other mapping inputs this simulation has no behaviour for. An
4250
- `AWS::Lambda::EventSourceMapping` carrying `Tags` is the exception, and deploys with the tags
4251
- dropped and the property recorded.
4342
+ Kinesis enhanced fan-out consumers are refused outright. `CreateEventSourceMapping` also refuses
4343
+ `FilterCriteria`, `ScalingConfig`, `ParallelizationFactor`, `TumblingWindowInSeconds` and the
4344
+ other inputs this simulation has no behaviour for. An
4345
+ `AWS::Lambda::EventSourceMapping` naming any of them deploys instead, and records each one against
4346
+ the Resource (see
4347
+ [Properties a Resource was created without](https://yulinsim.dev/services/cloudformation/#properties-a-resource-was-created-without "Properties a Resource was created without")).
4252
4348
  - A failed stream batch waits 1, 2, 4, 8 and 16 seconds between attempts, where AWS documents no
4253
4349
  delay. That is deliberate. A delay of zero falls due at the instant the clock already reads, and a
4254
4350
  handler that always throws would leave `advanceBy` with work falling due forever. A mapping that
@@ -4256,6 +4352,10 @@ Current documented limitations:
4256
4352
  discards the batch, where AWS goes on until the records age out a day later. A batch item failure
4257
4353
  report counts against the same retries, and never starts them again for the records it rewound
4258
4354
  to.
4355
+ - Splitting a batch under `BisectBatchOnFunctionError` puts the retry count back to the start, where
4356
+ AWS counts a bisected batch's deliveries against the same quota. Without that, the simulator's own
4357
+ cap of five attempts would discard a batch of a hundred long before it was down to one record. The
4358
+ splitting still ends on its own, because a batch halves at every step.
4259
4359
  - A handler writing into the table whose stream invoked it is refused with
4260
4360
  `SimLambdaStreamCascadeError` rather than being delivered its own writes forever. Real Lambda runs
4261
4361
  that loop.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kensio/yulin",
3
- "version": "1.21.8",
3
+ "version": "1.21.10",
4
4
  "description": "AWS system behaviour simulation for isolated unit testing",
5
5
  "repository": "https://github.com/KensioSoftware/yulin",
6
6
  "homepage": "https://yulinsim.dev/",